feat(FN-4244): add no-task heartbeat procedure override
Merged FN-4244: adds an override path for the no-task heartbeat procedure in the engine, allowing agents to execute a custom procedure when they have no active task. The fix commit corrects the initial implementation, with test coverage for procedure source selection and updated documentation in `do Fusion-Task-Id: FN-4244 Fusion-Task-Lineage: 6bf1bfe4-de2c-4467-93dc-d90231a53eb2
This commit is contained in:
@@ -855,6 +855,57 @@ describe("executeHeartbeat", () => {
|
||||
expect(executionPrompt).toContain(HEARTBEAT_NO_TASK_PROCEDURE);
|
||||
});
|
||||
|
||||
it("no-task run overrides a seeded task-scoped heartbeatProcedurePath in the assembled prompt", async () => {
|
||||
const tmpRoot = mkdtempSync(join(tmpdir(), "fn-hb-no-task-procedure-"));
|
||||
try {
|
||||
writeFileSync(join(tmpRoot, "HEARTBEAT.md"), HEARTBEAT_PROCEDURE, "utf-8");
|
||||
|
||||
const store = createStoreWithAgentForExec({
|
||||
taskId: undefined,
|
||||
soul: "I am a coordinator",
|
||||
heartbeatProcedurePath: "HEARTBEAT.md",
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: tmpRoot });
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0];
|
||||
expect(executionPrompt).toBeDefined();
|
||||
expect(executionPrompt).not.toContain("fn_task_log");
|
||||
expect(executionPrompt).not.toContain("fn_task_document_write");
|
||||
expect(executionPrompt).not.toContain("do not re-read PROMPT.md to advance it");
|
||||
expect(executionPrompt).toContain("Implementation-scope discovery");
|
||||
|
||||
const savedRun = await store.getRunDetail("agent-001", result.id);
|
||||
expect(savedRun?.heartbeatProcedureSource).toBe("default-no-task-override");
|
||||
} finally {
|
||||
rmSync(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("no-task run without a custom heartbeatProcedurePath still uses the ambient procedure", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0];
|
||||
expect(executionPrompt).toBeDefined();
|
||||
expect(executionPrompt).not.toContain("fn_task_log");
|
||||
expect(executionPrompt).not.toContain("fn_task_document_write");
|
||||
expect(executionPrompt).not.toContain("do not re-read PROMPT.md to advance it");
|
||||
expect(executionPrompt).toContain("Implementation-scope discovery");
|
||||
|
||||
const savedRun = await store.getRunDetail("agent-001", result.id);
|
||||
expect(savedRun?.heartbeatProcedureSource).toBe("default");
|
||||
});
|
||||
|
||||
it("task-scoped run receives HEARTBEAT_SYSTEM_PROMPT as system prompt", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
|
||||
const mockSession = createMockAgentSession();
|
||||
@@ -1305,6 +1356,9 @@ describe("executeHeartbeat", () => {
|
||||
expect(executionPrompt).not.toContain(HEARTBEAT_PROCEDURE);
|
||||
// Wake Delta still rendered.
|
||||
expect(executionPrompt).toContain("## Wake Delta");
|
||||
|
||||
const savedRun = await store.getRunDetail("agent-001", result.id);
|
||||
expect(savedRun?.heartbeatProcedureSource).toBe("custom");
|
||||
} finally {
|
||||
rmSync(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -2163,8 +2163,19 @@ export class HeartbeatMonitor {
|
||||
// existing instructionsPath/instructionsText reload contract) so an
|
||||
// operator can iterate on procedure text without restarting agents.
|
||||
const customProcedure = await resolveAgentHeartbeatProcedure(agent, rootDir);
|
||||
const heartbeatProcedureText = customProcedure
|
||||
?? (isNoTaskRun ? HEARTBEAT_NO_TASK_PROCEDURE : HEARTBEAT_PROCEDURE);
|
||||
const customProcedureConfigured = Boolean(customProcedure);
|
||||
const shouldOverrideCustomProcedureForNoTaskRun = isNoTaskRun && customProcedureConfigured;
|
||||
if (shouldOverrideCustomProcedureForNoTaskRun) {
|
||||
heartbeatLog.log(
|
||||
`Agent ${agentId} no-task heartbeat bypassed configured heartbeatProcedurePath and used HEARTBEAT_NO_TASK_PROCEDURE to keep prompt guidance aligned with ambient tools`,
|
||||
);
|
||||
}
|
||||
const heartbeatProcedureText = shouldOverrideCustomProcedureForNoTaskRun
|
||||
? HEARTBEAT_NO_TASK_PROCEDURE
|
||||
: (customProcedure ?? (isNoTaskRun ? HEARTBEAT_NO_TASK_PROCEDURE : HEARTBEAT_PROCEDURE));
|
||||
const heartbeatProcedureSource = shouldOverrideCustomProcedureForNoTaskRun
|
||||
? "default-no-task-override"
|
||||
: (customProcedure ? "custom" : "default");
|
||||
const reportsHealthSection = await this.buildReportsHealthSection(agent.id, this.store);
|
||||
|
||||
if (isNoTaskRun) {
|
||||
@@ -2360,7 +2371,7 @@ export class HeartbeatMonitor {
|
||||
...run,
|
||||
systemPrompt: truncatePrompt(systemPromptFinal, 100_000),
|
||||
executionPrompt: truncatePrompt(executionPrompt, 100_000),
|
||||
heartbeatProcedureSource: customProcedure ? "custom" : "default",
|
||||
heartbeatProcedureSource,
|
||||
};
|
||||
await this.store.saveRun(runWithPrompts);
|
||||
// Update local run reference so completeRun merges correctly
|
||||
|
||||
Reference in New Issue
Block a user