feat(FN-1663): merge fusion/fn-1663
This commit is contained in:
@@ -196,6 +196,61 @@ describe("resolveAgentPrompt", () => {
|
|||||||
const result = resolveAgentPrompt("executor", config);
|
const result = resolveAgentPrompt("executor", config);
|
||||||
expect(result).toContain("attachments");
|
expect(result).toContain("attachments");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Task Document Tool Guidance ─────────────────────────────────────────
|
||||||
|
|
||||||
|
it("built-in executor prompt includes task_document_write guidance", () => {
|
||||||
|
const result = resolveAgentPrompt("executor");
|
||||||
|
expect(result).toContain("task_document_write");
|
||||||
|
expect(result).toContain("Task Documents");
|
||||||
|
expect(result).toContain("Documents tab");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("built-in executor prompt includes task_document_read guidance", () => {
|
||||||
|
const result = resolveAgentPrompt("executor");
|
||||||
|
expect(result).toContain("task_document_read");
|
||||||
|
expect(result).toContain("task documents visible in the dashboard");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("senior-engineer prompt includes task_document_write guidance", () => {
|
||||||
|
const config: AgentPromptsConfig = {
|
||||||
|
roleAssignments: {
|
||||||
|
executor: "senior-engineer",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = resolveAgentPrompt("executor", config);
|
||||||
|
expect(result).toContain("task_document_write");
|
||||||
|
expect(result).toContain("Task Documents");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("senior-engineer prompt includes task_document_read guidance", () => {
|
||||||
|
const config: AgentPromptsConfig = {
|
||||||
|
roleAssignments: {
|
||||||
|
executor: "senior-engineer",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = resolveAgentPrompt("executor", config);
|
||||||
|
expect(result).toContain("task_document_read");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("built-in triage prompt includes task_document_write guidance for planning output", () => {
|
||||||
|
const result = resolveAgentPrompt("triage");
|
||||||
|
expect(result).toContain("task_document_write");
|
||||||
|
expect(result).toContain("planning");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("concise-triage prompt includes task_document_write guidance", () => {
|
||||||
|
const config: AgentPromptsConfig = {
|
||||||
|
roleAssignments: {
|
||||||
|
triage: "concise-triage",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = resolveAgentPrompt("triage", config);
|
||||||
|
expect(result).toContain("task_document_write");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -50,6 +50,22 @@ When creating multiple related tasks, declare dependencies between them:
|
|||||||
|
|
||||||
**Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
|
**Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
|
||||||
|
|
||||||
|
## Task Documents
|
||||||
|
|
||||||
|
You have tools to persist durable work products as task documents visible in the dashboard's Documents tab:
|
||||||
|
|
||||||
|
**Saving work:** \`task_document_write(key="plan", content="...")\` — Save structured notes, plans, research findings, or decision logs. Each write creates a revision so history is preserved. Use descriptive keys like "plan", "notes", "research", "decision-log".
|
||||||
|
|
||||||
|
**Reading work:** \`task_document_read(key="plan")\` — Read a saved document by key. Omit the key to list all documents for this task.
|
||||||
|
|
||||||
|
**When to use task documents:**
|
||||||
|
- Save planning notes or architectural decisions early in the task for downstream continuity
|
||||||
|
- Record research findings or investigation results
|
||||||
|
- Document design decisions and trade-offs
|
||||||
|
- Keep a running log of important choices made during implementation
|
||||||
|
|
||||||
|
Documents persist across sessions and are visible to other agents and humans in the Documents tab.
|
||||||
|
|
||||||
## Cross-model review via review_step tool
|
## Cross-model review via review_step tool
|
||||||
|
|
||||||
You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
|
You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
|
||||||
@@ -166,6 +182,11 @@ Your job: take a rough task description and produce a fully specified PROMPT.md
|
|||||||
## What you produce
|
## What you produce
|
||||||
Write a complete PROMPT.md specification to the given path using the write tool.
|
Write a complete PROMPT.md specification to the given path using the write tool.
|
||||||
|
|
||||||
|
**Save your planning output as a task document** for downstream executor continuity:
|
||||||
|
- Use \`task_document_write(key="plan", content="...")\` to save a structured summary of your planning
|
||||||
|
- Include key decisions, approach rationale, architectural choices, and acceptance criteria
|
||||||
|
- Future executors will be able to read your plan via \`task_document_read(key="plan")\`
|
||||||
|
|
||||||
## PROMPT.md Format
|
## PROMPT.md Format
|
||||||
|
|
||||||
Follow this structure exactly:
|
Follow this structure exactly:
|
||||||
@@ -439,6 +460,22 @@ When creating multiple related tasks, declare dependencies between them:
|
|||||||
|
|
||||||
**Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
|
**Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` — use when you discover mid-execution that another task must be completed first. This will return a warning first — you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
|
||||||
|
|
||||||
|
## Task Documents
|
||||||
|
|
||||||
|
You have tools to persist durable work products as task documents visible in the dashboard's Documents tab:
|
||||||
|
|
||||||
|
**Saving work:** \`task_document_write(key="plan", content="...")\` — Save structured notes, plans, research findings, or decision logs. Each write creates a revision so history is preserved. Use descriptive keys like "plan", "notes", "research", "decision-log".
|
||||||
|
|
||||||
|
**Reading work:** \`task_document_read(key="plan")\` — Read a saved document by key. Omit the key to list all documents for this task.
|
||||||
|
|
||||||
|
**When to use task documents:**
|
||||||
|
- Save planning notes or architectural decisions early in the task for downstream continuity
|
||||||
|
- Record research findings or investigation results
|
||||||
|
- Document design decisions and trade-offs
|
||||||
|
- Keep a running log of important choices made during implementation
|
||||||
|
|
||||||
|
Documents persist across sessions and are visible to other agents and humans in the Documents tab.
|
||||||
|
|
||||||
## Cross-model review via review_step tool
|
## Cross-model review via review_step tool
|
||||||
|
|
||||||
You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
|
You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
|
||||||
@@ -645,6 +682,8 @@ const CONCISE_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn".
|
|||||||
## What you produce
|
## What you produce
|
||||||
Write a PROMPT.md specification to the given path. Be brief and precise — avoid verbosity.
|
Write a PROMPT.md specification to the given path. Be brief and precise — avoid verbosity.
|
||||||
|
|
||||||
|
**Save your planning output as a task document:** Use \`task_document_write(key="plan", content="...")\` to save a structured summary of your planning for downstream executors.
|
||||||
|
|
||||||
## PROMPT.md Format
|
## PROMPT.md Format
|
||||||
|
|
||||||
\`\`\`markdown
|
\`\`\`markdown
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ describe("SettingsModal", () => {
|
|||||||
mockFetchBackups.mockResolvedValue({ backups: [], totalSize: 0 });
|
mockFetchBackups.mockResolvedValue({ backups: [], totalSize: 0 });
|
||||||
mockFetchMemory.mockResolvedValue({ content: "## Existing memory\n- Learned pattern" });
|
mockFetchMemory.mockResolvedValue({ content: "## Existing memory\n- Learned pattern" });
|
||||||
mockSaveMemory.mockResolvedValue({ success: true });
|
mockSaveMemory.mockResolvedValue({ success: true });
|
||||||
mockFetchGlobalConcurrency.mockResolvedValue({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} });
|
mockFetchGlobalConcurrency.mockResolvedValue({ globalMaxConcurrent: 4, currentUsage: 0 });
|
||||||
mockUpdateGlobalConcurrency.mockResolvedValue({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} });
|
mockUpdateGlobalConcurrency.mockResolvedValue({ globalMaxConcurrent: 4, currentUsage: 0 });
|
||||||
|
|
||||||
// jsdom doesn't provide URL.createObjectURL — polyfill it
|
// jsdom doesn't provide URL.createObjectURL — polyfill it
|
||||||
if (!URL.createObjectURL) {
|
if (!URL.createObjectURL) {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ vi.mock("../../api", () => ({
|
|||||||
favoriteModels: [],
|
favoriteModels: [],
|
||||||
})),
|
})),
|
||||||
testNtfyNotification: vi.fn(() => Promise.resolve({ success: true })),
|
testNtfyNotification: vi.fn(() => Promise.resolve({ success: true })),
|
||||||
|
// Global concurrency mocks
|
||||||
|
fetchGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentUsage: 0 })),
|
||||||
|
updateGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentUsage: 0 })),
|
||||||
// Plugin API mocks
|
// Plugin API mocks
|
||||||
fetchPlugins: vi.fn(() => Promise.resolve([])),
|
fetchPlugins: vi.fn(() => Promise.resolve([])),
|
||||||
installPlugin: vi.fn(() => Promise.resolve({ id: "test-plugin", name: "Test Plugin", version: "1.0.0", state: "started" as const, enabled: true, settings: {}, settingsSchema: {} })),
|
installPlugin: vi.fn(() => Promise.resolve({ id: "test-plugin", name: "Test Plugin", version: "1.0.0", state: "started" as const, enabled: true, settings: {}, settingsSchema: {} })),
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ vi.mock("../../api", () => ({
|
|||||||
importSettings: vi.fn(() => Promise.resolve({ success: true })),
|
importSettings: vi.fn(() => Promise.resolve({ success: true })),
|
||||||
fetchMemory: vi.fn(() => Promise.resolve({ memory: "" })),
|
fetchMemory: vi.fn(() => Promise.resolve({ memory: "" })),
|
||||||
saveMemory: vi.fn(() => Promise.resolve({ success: true })),
|
saveMemory: vi.fn(() => Promise.resolve({ success: true })),
|
||||||
fetchGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} })),
|
fetchGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentUsage: 0 })),
|
||||||
updateGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentlyActive: 0, queuedCount: 0, projectsActive: {} })),
|
updateGlobalConcurrency: vi.fn(() => Promise.resolve({ globalMaxConcurrent: 4, currentUsage: 0 })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { fetchSettings } from "../../api";
|
import { fetchSettings } from "../../api";
|
||||||
|
|||||||
@@ -1016,6 +1016,28 @@ describe("HeartbeatMonitor", () => {
|
|||||||
logEntry: vi.fn().mockResolvedValue({}),
|
logEntry: vi.fn().mockResolvedValue({}),
|
||||||
addComment: vi.fn().mockResolvedValue({}),
|
addComment: vi.fn().mockResolvedValue({}),
|
||||||
appendAgentLog: vi.fn().mockResolvedValue(undefined),
|
appendAgentLog: vi.fn().mockResolvedValue(undefined),
|
||||||
|
// Document-related methods for task_document tools
|
||||||
|
upsertTaskDocument: vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-001",
|
||||||
|
key: "test-plan",
|
||||||
|
content: "Test document content",
|
||||||
|
revision: 1,
|
||||||
|
author: "agent",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
getTaskDocument: vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-001",
|
||||||
|
key: "test-plan",
|
||||||
|
content: "Test document content",
|
||||||
|
revision: 1,
|
||||||
|
author: "agent",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
getTaskDocuments: vi.fn().mockResolvedValue([]),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
@@ -1518,10 +1540,48 @@ describe("HeartbeatMonitor", () => {
|
|||||||
expect(callArgs.cwd).toBe("/tmp/test");
|
expect(callArgs.cwd).toBe("/tmp/test");
|
||||||
expect(callArgs.systemPrompt).toBe(HEARTBEAT_SYSTEM_PROMPT);
|
expect(callArgs.systemPrompt).toBe(HEARTBEAT_SYSTEM_PROMPT);
|
||||||
expect(callArgs.tools).toBe("readonly");
|
expect(callArgs.tools).toBe("readonly");
|
||||||
expect(callArgs.customTools).toHaveLength(3);
|
// Tools: task_create, task_log, task_document_write, task_document_read, heartbeat_done
|
||||||
|
expect(callArgs.customTools).toHaveLength(5);
|
||||||
expect(callArgs.customTools![0]!.name).toBe("task_create");
|
expect(callArgs.customTools![0]!.name).toBe("task_create");
|
||||||
expect(callArgs.customTools![1]!.name).toBe("task_log");
|
expect(callArgs.customTools![1]!.name).toBe("task_log");
|
||||||
expect(callArgs.customTools![2]!.name).toBe("heartbeat_done");
|
expect(callArgs.customTools![2]!.name).toBe("task_document_write");
|
||||||
|
expect(callArgs.customTools![3]!.name).toBe("task_document_read");
|
||||||
|
// heartbeat_done is last (terminal tool)
|
||||||
|
expect(callArgs.customTools![4]!.name).toBe("heartbeat_done");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes document tools in heartbeat session", async () => {
|
||||||
|
const store = createStoreWithAgentForExec();
|
||||||
|
const mockSession = createMockAgentSession();
|
||||||
|
mockedCreateKbAgent.mockResolvedValue({
|
||||||
|
session: mockSession as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
|
||||||
|
|
||||||
|
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||||
|
|
||||||
|
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||||
|
const toolNames = callArgs.customTools!.map((t: any) => t.name);
|
||||||
|
expect(toolNames).toContain("task_document_write");
|
||||||
|
expect(toolNames).toContain("task_document_read");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("heartbeat_done is the terminal tool (last in array)", async () => {
|
||||||
|
const store = createStoreWithAgentForExec();
|
||||||
|
const mockSession = createMockAgentSession();
|
||||||
|
mockedCreateKbAgent.mockResolvedValue({
|
||||||
|
session: mockSession as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
|
||||||
|
|
||||||
|
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||||
|
|
||||||
|
const callArgs = mockedCreateKbAgent.mock.calls[0]![0];
|
||||||
|
const toolNames = callArgs.customTools!.map((t: any) => t.name);
|
||||||
|
// heartbeat_done should be last for stable terminal signaling
|
||||||
|
expect(toolNames[toolNames.length - 1]).toBe("heartbeat_done");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls promptWithFallback with task context", async () => {
|
it("calls promptWithFallback with task context", async () => {
|
||||||
@@ -1760,7 +1820,8 @@ describe("HeartbeatMonitor", () => {
|
|||||||
let capturedDoneTool: any;
|
let capturedDoneTool: any;
|
||||||
const mockSession = createMockAgentSession();
|
const mockSession = createMockAgentSession();
|
||||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||||
capturedDoneTool = opts.customTools[2]; // heartbeat_done
|
// heartbeat_done is last in the customTools array (index 4)
|
||||||
|
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
|
||||||
return { session: mockSession as any };
|
return { session: mockSession as any };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1785,7 +1846,7 @@ describe("HeartbeatMonitor", () => {
|
|||||||
let capturedDoneTool: any;
|
let capturedDoneTool: any;
|
||||||
const mockSession = createMockAgentSession();
|
const mockSession = createMockAgentSession();
|
||||||
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
mockedCreateKbAgent.mockImplementation(async (opts: any) => {
|
||||||
capturedDoneTool = opts.customTools[2];
|
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
|
||||||
return { session: mockSession as any };
|
return { session: mockSession as any };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2200,6 +2261,28 @@ describe("HeartbeatMonitor", () => {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
} as unknown as TaskDetail),
|
} as unknown as TaskDetail),
|
||||||
|
// Document-related methods for task_document tools
|
||||||
|
upsertTaskDocument: vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-001",
|
||||||
|
key: "test-plan",
|
||||||
|
content: "Test document content",
|
||||||
|
revision: 1,
|
||||||
|
author: "agent",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
getTaskDocument: vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-001",
|
||||||
|
key: "test-plan",
|
||||||
|
content: "Test document content",
|
||||||
|
revision: 1,
|
||||||
|
author: "agent",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
getTaskDocuments: vi.fn().mockResolvedValue([]),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
@@ -2208,15 +2291,17 @@ describe("HeartbeatMonitor", () => {
|
|||||||
mockTaskStore = createMockTaskStoreForTools();
|
mockTaskStore = createMockTaskStoreForTools();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns task_create and task_log tools", () => {
|
it("returns task_create, task_log, task_document_write, and task_document_read tools", () => {
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||||
|
|
||||||
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||||
|
|
||||||
expect(tools).toHaveLength(2);
|
expect(tools).toHaveLength(4);
|
||||||
expect(tools[0]!.name).toBe("task_create");
|
expect(tools[0]!.name).toBe("task_create");
|
||||||
expect(tools[1]!.name).toBe("task_log");
|
expect(tools[1]!.name).toBe("task_log");
|
||||||
|
expect(tools[2]!.name).toBe("task_document_write");
|
||||||
|
expect(tools[3]!.name).toBe("task_document_read");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("task_create tool creates a task in triage via TaskStore", async () => {
|
it("task_create tool creates a task in triage via TaskStore", async () => {
|
||||||
@@ -2280,6 +2365,72 @@ describe("HeartbeatMonitor", () => {
|
|||||||
// Task was still created
|
// Task was still created
|
||||||
expect(mockTaskStore.createTask).toHaveBeenCalled();
|
expect(mockTaskStore.createTask).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("task_document_write tool persists documents via TaskStore", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||||
|
|
||||||
|
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||||
|
const writeTool = tools.find((t) => t.name === "task_document_write")!;
|
||||||
|
|
||||||
|
const result = await writeTool.execute("call-1", { key: "plan", content: "Implementation plan here" }, undefined as any, undefined as any, undefined as any);
|
||||||
|
|
||||||
|
expect(mockTaskStore.upsertTaskDocument).toHaveBeenCalledWith("FN-001", {
|
||||||
|
key: "plan",
|
||||||
|
content: "Implementation plan here",
|
||||||
|
author: "agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||||
|
expect(responseText).toContain("Saved document");
|
||||||
|
expect(responseText).toContain("plan");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("task_document_read tool reads specific document by key", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
mockTaskStore.getTaskDocument = vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-001",
|
||||||
|
key: "plan",
|
||||||
|
content: "Implementation plan content",
|
||||||
|
revision: 2,
|
||||||
|
author: "agent",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||||
|
|
||||||
|
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||||
|
const readTool = tools.find((t) => t.name === "task_document_read")!;
|
||||||
|
|
||||||
|
const result = await readTool.execute("call-1", { key: "plan" }, undefined as any, undefined as any, undefined as any);
|
||||||
|
|
||||||
|
expect(mockTaskStore.getTaskDocument).toHaveBeenCalledWith("FN-001", "plan");
|
||||||
|
|
||||||
|
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||||
|
expect(responseText).toContain("plan");
|
||||||
|
expect(responseText).toContain("Implementation plan content");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("task_document_read tool lists all documents when key is omitted", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
mockTaskStore.getTaskDocuments = vi.fn().mockResolvedValue([
|
||||||
|
{ id: "doc-1", taskId: "FN-001", key: "plan", content: "", revision: 1, author: "agent", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||||
|
{ id: "doc-2", taskId: "FN-001", key: "notes", content: "", revision: 1, author: "agent", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||||
|
]);
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||||
|
|
||||||
|
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||||
|
const readTool = tools.find((t) => t.name === "task_document_read")!;
|
||||||
|
|
||||||
|
const result = await readTool.execute("call-1", { key: undefined }, undefined as any, undefined as any, undefined as any);
|
||||||
|
|
||||||
|
expect(mockTaskStore.getTaskDocuments).toHaveBeenCalledWith("FN-001");
|
||||||
|
|
||||||
|
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||||
|
expect(responseText).toContain("plan");
|
||||||
|
expect(responseText).toContain("notes");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("completeRun task tracking", () => {
|
describe("completeRun task tracking", () => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, BlockedStateSnapshot, RunMutationContext } from "@fusion/core";
|
import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, BlockedStateSnapshot, RunMutationContext } from "@fusion/core";
|
||||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||||
import { createTaskCreateTool, createTaskLogToolWithContext, taskCreateParams } from "./agent-tools.js";
|
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, taskCreateParams } from "./agent-tools.js";
|
||||||
import { AgentLogger } from "./agent-logger.js";
|
import { AgentLogger } from "./agent-logger.js";
|
||||||
import { heartbeatLog } from "./logger.js";
|
import { heartbeatLog } from "./logger.js";
|
||||||
import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
|
import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
|
||||||
@@ -121,7 +121,7 @@ export function isBlockedStateDuplicate(current: BlockedStateSnapshot, previous:
|
|||||||
/**
|
/**
|
||||||
* System prompt for heartbeat agent sessions.
|
* System prompt for heartbeat agent sessions.
|
||||||
* Instructs the agent to perform a single-pass check on its assigned task
|
* Instructs the agent to perform a single-pass check on its assigned task
|
||||||
* and use `task_create` / `task_log` to record findings or spawn follow-up work.
|
* and use `task_create` / `task_log` / task documents to record findings or spawn follow-up work.
|
||||||
*/
|
*/
|
||||||
export const HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
|
export const HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
|
||||||
|
|
||||||
@@ -129,10 +129,14 @@ Your job:
|
|||||||
1. Check your assigned task — read the description and PROMPT.md if present.
|
1. Check your assigned task — read the description and PROMPT.md if present.
|
||||||
2. Do ONE useful action: analyze, review, create follow-up tasks, or log findings.
|
2. Do ONE useful action: analyze, review, create follow-up tasks, or log findings.
|
||||||
3. Use task_create to spawn follow-up work, task_log to record observations.
|
3. Use task_create to spawn follow-up work, task_log to record observations.
|
||||||
4. Call heartbeat_done when finished with an optional summary of what was accomplished.
|
4. Use task_document_write to save durable findings, plans, or research notes.
|
||||||
|
5. Call heartbeat_done when finished with an optional summary of what was accomplished.
|
||||||
|
|
||||||
Keep work lightweight — this is a single-pass check, not a full implementation run.
|
Keep work lightweight — this is a single-pass check, not a full implementation run.
|
||||||
You have readonly file access plus task_create and task_log tools.`;
|
You have readonly file access plus task_create, task_log, and task_document tools.
|
||||||
|
|
||||||
|
**Task Documents:** Save important findings with task_document_write(key="...", content="...").
|
||||||
|
Documents persist across sessions and are visible in the dashboard's Documents tab.`;
|
||||||
|
|
||||||
/** Parameter schema for the heartbeat_done tool */
|
/** Parameter schema for the heartbeat_done tool */
|
||||||
const heartbeatDoneParams = Type.Object({
|
const heartbeatDoneParams = Type.Object({
|
||||||
@@ -1115,6 +1119,10 @@ export class HeartbeatMonitor {
|
|||||||
// task_log tool (with run context for mutation correlation)
|
// task_log tool (with run context for mutation correlation)
|
||||||
tools.push(createTaskLogToolWithContext(taskStore, taskId, runContext));
|
tools.push(createTaskLogToolWithContext(taskStore, taskId, runContext));
|
||||||
|
|
||||||
|
// Document tools for persisting durable findings
|
||||||
|
tools.push(createTaskDocumentWriteTool(taskStore, taskId));
|
||||||
|
tools.push(createTaskDocumentReadTool(taskStore, taskId));
|
||||||
|
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user