mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Disable timer heartbeats by default for new agents
This commit is contained in:
parent
5640d29ab0
commit
844b061267
8 changed files with 151 additions and 22 deletions
|
|
@ -213,6 +213,80 @@ describe("agent permission routes", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes direct agent creation to disable timer heartbeats by default", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
type: "board",
|
||||||
|
userId: "board-user",
|
||||||
|
source: "local_implicit",
|
||||||
|
isInstanceAdmin: true,
|
||||||
|
companyIds: [companyId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/api/companies/${companyId}/agents`)
|
||||||
|
.send({
|
||||||
|
name: "Builder",
|
||||||
|
role: "engineer",
|
||||||
|
adapterType: "process",
|
||||||
|
adapterConfig: {},
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeat: {
|
||||||
|
intervalSec: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||||
|
companyId,
|
||||||
|
expect.objectContaining({
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeat: {
|
||||||
|
enabled: false,
|
||||||
|
intervalSec: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes hire requests to disable timer heartbeats by default", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
type: "board",
|
||||||
|
userId: "board-user",
|
||||||
|
source: "local_implicit",
|
||||||
|
isInstanceAdmin: true,
|
||||||
|
companyIds: [companyId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/api/companies/${companyId}/agent-hires`)
|
||||||
|
.send({
|
||||||
|
name: "Builder",
|
||||||
|
role: "engineer",
|
||||||
|
adapterType: "process",
|
||||||
|
adapterConfig: {},
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeat: {
|
||||||
|
intervalSec: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(mockAgentService.create).toHaveBeenCalledWith(
|
||||||
|
companyId,
|
||||||
|
expect.objectContaining({
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeat: {
|
||||||
|
enabled: false,
|
||||||
|
intervalSec: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("exposes explicit task assignment access on agent detail", async () => {
|
it("exposes explicit task assignment access on agent detail", async () => {
|
||||||
mockAccessService.listPrincipalGrants.mockResolvedValue([
|
mockAccessService.listPrincipalGrants.mockResolvedValue([
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -449,11 +449,25 @@ export function agentRoutes(db: Db) {
|
||||||
function parseSchedulerHeartbeatPolicy(runtimeConfig: unknown) {
|
function parseSchedulerHeartbeatPolicy(runtimeConfig: unknown) {
|
||||||
const heartbeat = asRecord(asRecord(runtimeConfig)?.heartbeat) ?? {};
|
const heartbeat = asRecord(asRecord(runtimeConfig)?.heartbeat) ?? {};
|
||||||
return {
|
return {
|
||||||
enabled: parseBooleanLike(heartbeat.enabled) ?? true,
|
enabled: parseBooleanLike(heartbeat.enabled) ?? false,
|
||||||
intervalSec: Math.max(0, parseNumberLike(heartbeat.intervalSec) ?? 0),
|
intervalSec: Math.max(0, parseNumberLike(heartbeat.intervalSec) ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeNewAgentRuntimeConfig(runtimeConfig: unknown): Record<string, unknown> {
|
||||||
|
const parsedRuntimeConfig = asRecord(runtimeConfig);
|
||||||
|
const normalizedRuntimeConfig = parsedRuntimeConfig ? { ...parsedRuntimeConfig } : {};
|
||||||
|
const parsedHeartbeat = asRecord(normalizedRuntimeConfig.heartbeat);
|
||||||
|
const heartbeat = parsedHeartbeat ? { ...parsedHeartbeat } : {};
|
||||||
|
|
||||||
|
if (parseBooleanLike(heartbeat.enabled) == null) {
|
||||||
|
heartbeat.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedRuntimeConfig.heartbeat = heartbeat;
|
||||||
|
return normalizedRuntimeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
function generateEd25519PrivateKeyPem(): string {
|
function generateEd25519PrivateKeyPem(): string {
|
||||||
const { privateKey } = generateKeyPairSync("ed25519");
|
const { privateKey } = generateKeyPairSync("ed25519");
|
||||||
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
||||||
|
|
@ -1308,6 +1322,7 @@ export function agentRoutes(db: Db) {
|
||||||
const normalizedHireInput = {
|
const normalizedHireInput = {
|
||||||
...hireInput,
|
...hireInput,
|
||||||
adapterConfig: normalizedAdapterConfig,
|
adapterConfig: normalizedAdapterConfig,
|
||||||
|
runtimeConfig: normalizeNewAgentRuntimeConfig(hireInput.runtimeConfig),
|
||||||
};
|
};
|
||||||
|
|
||||||
const company = await db
|
const company = await db
|
||||||
|
|
@ -1474,6 +1489,7 @@ export function agentRoutes(db: Db) {
|
||||||
const createdAgent = await svc.create(companyId, {
|
const createdAgent = await svc.create(companyId, {
|
||||||
...createInput,
|
...createInput,
|
||||||
adapterConfig: normalizedAdapterConfig,
|
adapterConfig: normalizedAdapterConfig,
|
||||||
|
runtimeConfig: normalizeNewAgentRuntimeConfig(createInput.runtimeConfig),
|
||||||
status: "idle",
|
status: "idle",
|
||||||
spentMonthlyCents: 0,
|
spentMonthlyCents: 0,
|
||||||
lastHeartbeatAt: null,
|
lastHeartbeatAt: null,
|
||||||
|
|
|
||||||
|
|
@ -2159,7 +2159,7 @@ export function heartbeatService(db: Db) {
|
||||||
const heartbeat = parseObject(runtimeConfig.heartbeat);
|
const heartbeat = parseObject(runtimeConfig.heartbeat);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: asBoolean(heartbeat.enabled, true),
|
enabled: asBoolean(heartbeat.enabled, false),
|
||||||
intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
|
intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
|
||||||
wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
|
wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
|
||||||
maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
|
maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
|
||||||
|
|
|
||||||
|
|
@ -923,14 +923,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||||
<ToggleWithNumber
|
<ToggleWithNumber
|
||||||
label="Heartbeat on interval"
|
label="Heartbeat on interval"
|
||||||
hint={help.heartbeatInterval}
|
hint={help.heartbeatInterval}
|
||||||
checked={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
checked={eff("heartbeat", "enabled", heartbeat.enabled === true)}
|
||||||
onCheckedChange={(v) => mark("heartbeat", "enabled", v)}
|
onCheckedChange={(v) => mark("heartbeat", "enabled", v)}
|
||||||
number={eff("heartbeat", "intervalSec", Number(heartbeat.intervalSec ?? 300))}
|
number={eff("heartbeat", "intervalSec", Number(heartbeat.intervalSec ?? 300))}
|
||||||
onNumberChange={(v) => mark("heartbeat", "intervalSec", v)}
|
onNumberChange={(v) => mark("heartbeat", "intervalSec", v)}
|
||||||
numberLabel="sec"
|
numberLabel="sec"
|
||||||
numberPrefix="Run heartbeat every"
|
numberPrefix="Run heartbeat every"
|
||||||
numberHint={help.intervalSec}
|
numberHint={help.intervalSec}
|
||||||
showNumber={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
showNumber={eff("heartbeat", "enabled", heartbeat.enabled === true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
buildOnboardingProjectPayload,
|
buildOnboardingProjectPayload,
|
||||||
selectDefaultCompanyGoalId
|
selectDefaultCompanyGoalId
|
||||||
} from "../lib/onboarding-launch";
|
} from "../lib/onboarding-launch";
|
||||||
|
import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||||
DEFAULT_CODEX_LOCAL_MODEL
|
DEFAULT_CODEX_LOCAL_MODEL
|
||||||
|
|
@ -460,15 +461,7 @@ export function OnboardingWizard() {
|
||||||
role: "ceo",
|
role: "ceo",
|
||||||
adapterType,
|
adapterType,
|
||||||
adapterConfig: buildAdapterConfig(),
|
adapterConfig: buildAdapterConfig(),
|
||||||
runtimeConfig: {
|
runtimeConfig: buildNewAgentRuntimeConfig()
|
||||||
heartbeat: {
|
|
||||||
enabled: true,
|
|
||||||
intervalSec: 3600,
|
|
||||||
wakeOnDemand: true,
|
|
||||||
cooldownSec: 10,
|
|
||||||
maxConcurrentRuns: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
setCreatedAgentId(agent.id);
|
setCreatedAgentId(agent.id);
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
|
|
|
||||||
34
ui/src/lib/new-agent-runtime-config.test.ts
Normal file
34
ui/src/lib/new-agent-runtime-config.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// @vitest-environment node
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildNewAgentRuntimeConfig } from "./new-agent-runtime-config";
|
||||||
|
|
||||||
|
describe("buildNewAgentRuntimeConfig", () => {
|
||||||
|
it("defaults new agents to no timer heartbeat", () => {
|
||||||
|
expect(buildNewAgentRuntimeConfig()).toEqual({
|
||||||
|
heartbeat: {
|
||||||
|
enabled: false,
|
||||||
|
intervalSec: 300,
|
||||||
|
wakeOnDemand: true,
|
||||||
|
cooldownSec: 10,
|
||||||
|
maxConcurrentRuns: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves explicit heartbeat settings", () => {
|
||||||
|
expect(
|
||||||
|
buildNewAgentRuntimeConfig({
|
||||||
|
heartbeatEnabled: true,
|
||||||
|
intervalSec: 3600,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
intervalSec: 3600,
|
||||||
|
wakeOnDemand: true,
|
||||||
|
cooldownSec: 10,
|
||||||
|
maxConcurrentRuns: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
16
ui/src/lib/new-agent-runtime-config.ts
Normal file
16
ui/src/lib/new-agent-runtime-config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defaultCreateValues } from "../components/agent-config-defaults";
|
||||||
|
|
||||||
|
export function buildNewAgentRuntimeConfig(input?: {
|
||||||
|
heartbeatEnabled?: boolean;
|
||||||
|
intervalSec?: number;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
heartbeat: {
|
||||||
|
enabled: input?.heartbeatEnabled ?? defaultCreateValues.heartbeatEnabled,
|
||||||
|
intervalSec: input?.intervalSec ?? defaultCreateValues.intervalSec,
|
||||||
|
wakeOnDemand: true,
|
||||||
|
cooldownSec: 10,
|
||||||
|
maxConcurrentRuns: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import { getUIAdapter, listUIAdapters } from "../adapters";
|
||||||
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
||||||
import { isValidAdapterType } from "../adapters/metadata";
|
import { isValidAdapterType } from "../adapters/metadata";
|
||||||
import { ReportsToPicker } from "../components/ReportsToPicker";
|
import { ReportsToPicker } from "../components/ReportsToPicker";
|
||||||
|
import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||||
DEFAULT_CODEX_LOCAL_MODEL,
|
DEFAULT_CODEX_LOCAL_MODEL,
|
||||||
|
|
@ -175,15 +176,10 @@ export function NewAgent() {
|
||||||
...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}),
|
...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}),
|
||||||
adapterType: configValues.adapterType,
|
adapterType: configValues.adapterType,
|
||||||
adapterConfig: buildAdapterConfig(),
|
adapterConfig: buildAdapterConfig(),
|
||||||
runtimeConfig: {
|
runtimeConfig: buildNewAgentRuntimeConfig({
|
||||||
heartbeat: {
|
heartbeatEnabled: configValues.heartbeatEnabled,
|
||||||
enabled: configValues.heartbeatEnabled,
|
intervalSec: configValues.intervalSec,
|
||||||
intervalSec: configValues.intervalSec,
|
}),
|
||||||
wakeOnDemand: true,
|
|
||||||
cooldownSec: 10,
|
|
||||||
maxConcurrentRuns: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
budgetMonthlyCents: 0,
|
budgetMonthlyCents: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue