feat(FN-1272): add task document tools for triage and executor
- Add task_document_write and task_document_read factories with schemas, revision-aware responses, and error handling in agent-tools - Wire document tools into executor and triage sessions and update prompts to persist and reuse planning artifacts across runs - Export document tool factories and parameter schemas from engine public entrypoints while keeping executor re-export compatibility - Add comprehensive tests covering write/read success paths, empty/not-found cases, and store failure handling
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* The parameter schemas are canonical here — executor.ts imports and reuses them.
|
||||
*/
|
||||
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import type { TaskDocument, TaskDocumentCreateInput, TaskStore } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import type { AgentReflectionService } from "./agent-reflection.js";
|
||||
@@ -26,6 +26,20 @@ export const taskLogParams = Type.Object({
|
||||
outcome: Type.Optional(Type.String({ description: "Result or consequence (optional)" })),
|
||||
});
|
||||
|
||||
export const taskDocumentWriteParams = Type.Object({
|
||||
key: Type.String({
|
||||
description: "Document key (e.g., 'plan', 'notes', 'research'). Alphanumeric, hyphens, underscores, 1-64 chars.",
|
||||
}),
|
||||
content: Type.String({ description: "Document content to store" }),
|
||||
author: Type.Optional(Type.String({ description: "Who is writing (default: 'agent')" })),
|
||||
});
|
||||
|
||||
export const taskDocumentReadParams = Type.Object({
|
||||
key: Type.Optional(
|
||||
Type.String({ description: "Document key to read. Omit to list all documents for this task." }),
|
||||
),
|
||||
});
|
||||
|
||||
export const reflectOnPerformanceParams = Type.Object({
|
||||
focus_area: Type.Optional(
|
||||
Type.String({ description: "Optional focus area for reflection (e.g., 'code quality', 'speed', 'testing')" }),
|
||||
@@ -93,6 +107,117 @@ export function createTaskLogTool(store: TaskStore, taskId: string): ToolDefinit
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `task_document_write` tool that stores a named task document.
|
||||
*
|
||||
* @param store - TaskStore for task document persistence
|
||||
* @param taskId - The task ID to write documents against
|
||||
* @returns ToolDefinition for the `task_document_write` tool
|
||||
*/
|
||||
export function createTaskDocumentWriteTool(store: TaskStore, taskId: string): ToolDefinition {
|
||||
return {
|
||||
name: "task_document_write",
|
||||
label: "Write Document",
|
||||
description:
|
||||
"Save a named document for this task (for example plan, notes, or research). " +
|
||||
"Each write creates a new revision so you can update documents over time.",
|
||||
parameters: taskDocumentWriteParams,
|
||||
execute: async (_id: string, params: Static<typeof taskDocumentWriteParams>) => {
|
||||
const input: TaskDocumentCreateInput = {
|
||||
key: params.key,
|
||||
content: params.content,
|
||||
author: params.author || "agent",
|
||||
};
|
||||
|
||||
try {
|
||||
const document: TaskDocument = await store.upsertTaskDocument(taskId, input);
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `Saved document "${document.key}" (revision ${document.revision}).`,
|
||||
}],
|
||||
details: {},
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `ERROR: Failed to save document "${params.key}": ${err.message}`,
|
||||
}],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `task_document_read` tool that reads task-scoped documents.
|
||||
*
|
||||
* @param store - TaskStore for task document reads
|
||||
* @param taskId - The task ID to read documents from
|
||||
* @returns ToolDefinition for the `task_document_read` tool
|
||||
*/
|
||||
export function createTaskDocumentReadTool(store: TaskStore, taskId: string): ToolDefinition {
|
||||
return {
|
||||
name: "task_document_read",
|
||||
label: "Read Document",
|
||||
description:
|
||||
"Read a named document for this task, or list all documents when no key is provided.",
|
||||
parameters: taskDocumentReadParams,
|
||||
execute: async (_id: string, params: Static<typeof taskDocumentReadParams>) => {
|
||||
try {
|
||||
if (params.key) {
|
||||
const document: TaskDocument | null = await store.getTaskDocument(taskId, params.key);
|
||||
if (!document) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Document "${params.key}" not found.` }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text:
|
||||
`Document: ${document.key}\n` +
|
||||
`Revision: ${document.revision}\n` +
|
||||
`Updated: ${document.updatedAt}\n\n` +
|
||||
document.content,
|
||||
}],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
const documents: TaskDocument[] = await store.getTaskDocuments(taskId);
|
||||
if (documents.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "No documents found for this task." }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
const lines = documents.map((doc) => `- ${doc.key} (revision ${doc.revision}, updated ${doc.updatedAt})`);
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `Task documents:\n${lines.join("\n")}`,
|
||||
}],
|
||||
details: {},
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `ERROR: Failed to read task documents: ${err.message}`,
|
||||
}],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `reflect_on_performance` tool that asks the reflection service to
|
||||
* analyze recent agent performance and return actionable insights.
|
||||
|
||||
Reference in New Issue
Block a user