FN-6635: expose task document tools to chat agents
Expose task document read/write tools to dashboard chat agents with explicit task targeting. - Add chat-specific fn_task_document_write/read factories that require task_id and reuse task document read behavior. - Wire dashboard chat custom tools to include task document tools when a scoped task store is available. - Cover chat document tool exposure and explicit-task document operations with engine and dashboard tests. - Document chat availability and add a published package changeset. Files changed: .changeset/fn-6635-chat-task-documents.md | 5 + docs/agents.md | 1 + .../cli/skill/fusion/references/engine-tools.md | 4 +- .../dashboard/src/__tests__/chat-manager.test.ts | 78 +++++++++++ packages/dashboard/src/chat.ts | 15 ++- .../src/__tests__/agent-document-tools.test.ts | 108 +++++++++++++++ packages/engine/src/agent-tools.ts | 145 ++++++++++++++++----- packages/engine/src/index.ts | 3 + 8 files changed, 318 insertions(+), 41 deletions(-) Fusion-Task-Id: FN-6635 Fusion-Task-Lineage: 25e701d4-7670-470b-9dea-df49884f7b21
This commit is contained in:
5
.changeset/fn-6635-chat-task-documents.md
Normal file
5
.changeset/fn-6635-chat-task-documents.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Expose task document read/write tools to dashboard chat agents with explicit `task_id` targeting.
|
||||||
@@ -26,6 +26,7 @@ fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]
|
|||||||
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
||||||
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
|
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
|
||||||
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
|
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
|
||||||
|
- Dashboard chat sessions with a scoped task store expose `fn_task_document_write` and `fn_task_document_read`; because chat has no ambient task, both tools require an explicit `task_id`.
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ These tools are **not** part of the user-invokable extension surface. They are i
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `fn_task_create` | triage, executor, heartbeat | Create a follow-up task from within an agent run | `description` (string), `dependencies?` (string[]), `priority?` (`low` \| `normal` \| `high` \| `urgent`), `workflow_id?` (string) |
|
| `fn_task_create` | triage, executor, heartbeat | Create a follow-up task from within an agent run | `description` (string), `dependencies?` (string[]), `priority?` (`low` \| `normal` \| `high` \| `urgent`), `workflow_id?` (string) |
|
||||||
| `fn_task_log` | executor, heartbeat | Write significant task log entries | `message` (string), `outcome?` (string) |
|
| `fn_task_log` | executor, heartbeat | Write significant task log entries | `message` (string), `outcome?` (string) |
|
||||||
| `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) |
|
| `fn_task_document_write` | triage, executor, heartbeat; chat (explicit `task_id`) | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string); chat also requires `task_id` (string) |
|
||||||
| `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) |
|
| `fn_task_document_read` | triage, executor, heartbeat; chat (explicit `task_id`) | Read one task document or list all | `key?` (string); chat also requires `task_id` (string) |
|
||||||
| `fn_goal_list` | triage, executor, heartbeat | List goals with concise citation-ready snippets and active-goal warning details | `status?` (`active` \| `archived` \| `all`) |
|
| `fn_goal_list` | triage, executor, heartbeat | List goals with concise citation-ready snippets and active-goal warning details | `status?` (`active` \| `archived` \| `all`) |
|
||||||
| `fn_goal_show` | triage, executor, heartbeat | Show one goal's full detail on demand, including the full description body | `id` (string) |
|
| `fn_goal_show` | triage, executor, heartbeat | Show one goal's full detail on demand, including the full description body | `id` (string) |
|
||||||
| `fn_workflow_list` | executor | List the project's custom workflows (read-only built-ins plus user definitions) | none |
|
| `fn_workflow_list` | executor | List the project's custom workflows (read-only built-ins plus user definitions) | none |
|
||||||
|
|||||||
@@ -420,6 +420,84 @@ describe("ChatManager.sendMessage", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes fn_task_document_* tools to the chat agent when a task store is present", async () => {
|
||||||
|
let capturedTools: Array<{ name: string; execute?: (...args: any[]) => Promise<any> }> = [];
|
||||||
|
__setCreateFnAgent(async (options: any) => {
|
||||||
|
capturedTools = options.customTools ?? [];
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "ok" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskStore = {
|
||||||
|
upsertTaskDocument: vi.fn().mockResolvedValue({
|
||||||
|
id: "doc-1",
|
||||||
|
taskId: "FN-6635",
|
||||||
|
key: "docs",
|
||||||
|
content: "Saved from chat",
|
||||||
|
revision: 1,
|
||||||
|
author: "chat-agent",
|
||||||
|
createdAt: "2026-06-18T06:51:00.000Z",
|
||||||
|
updatedAt: "2026-06-18T06:51:00.000Z",
|
||||||
|
}),
|
||||||
|
} as any;
|
||||||
|
const chatManager = new ChatManager(
|
||||||
|
mockChatStore as any,
|
||||||
|
"/tmp/test",
|
||||||
|
mockAgentStore as any,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
taskStore,
|
||||||
|
);
|
||||||
|
|
||||||
|
await chatManager.sendMessage("chat-001", "Save this as task docs");
|
||||||
|
|
||||||
|
const names = capturedTools.map((tool) => tool.name);
|
||||||
|
expect(names).toContain("fn_task_document_write");
|
||||||
|
expect(names).toContain("fn_task_document_read");
|
||||||
|
|
||||||
|
const writeTool = capturedTools.find((tool) => tool.name === "fn_task_document_write");
|
||||||
|
const writeResult = await writeTool?.execute?.("call-doc-write", {
|
||||||
|
task_id: "FN-6635",
|
||||||
|
key: "docs",
|
||||||
|
content: "Saved from chat",
|
||||||
|
author: "chat-agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(taskStore.upsertTaskDocument).toHaveBeenCalledWith("FN-6635", {
|
||||||
|
key: "docs",
|
||||||
|
content: "Saved from chat",
|
||||||
|
author: "chat-agent",
|
||||||
|
});
|
||||||
|
expect(writeResult?.content?.[0]?.text).toContain("Saved document \"docs\"");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not expose fn_task_document_* tools when no task store is present", async () => {
|
||||||
|
let capturedTools: Array<{ name: string }> = [];
|
||||||
|
__setCreateFnAgent(async (options: any) => {
|
||||||
|
capturedTools = options.customTools ?? [];
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "ok" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatManager = createChatManager();
|
||||||
|
await chatManager.sendMessage("chat-001", "Try saving docs");
|
||||||
|
|
||||||
|
const names = capturedTools.map((tool) => tool.name);
|
||||||
|
expect(names).not.toContain("fn_task_document_write");
|
||||||
|
expect(names).not.toContain("fn_task_document_read");
|
||||||
|
});
|
||||||
|
|
||||||
it("persists and clears durable in-flight generation snapshots during streaming", async () => {
|
it("persists and clears durable in-flight generation snapshots during streaming", async () => {
|
||||||
let onTextCb: ((delta: string) => void) | undefined;
|
let onTextCb: ((delta: string) => void) | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
createSendMessageTool,
|
createSendMessageTool,
|
||||||
createReadMessagesTool,
|
createReadMessagesTool,
|
||||||
createAskQuestionTool,
|
createAskQuestionTool,
|
||||||
|
createChatTaskDocumentTools,
|
||||||
createWorkflowAuthoringTools,
|
createWorkflowAuthoringTools,
|
||||||
} from "@fusion/engine";
|
} from "@fusion/engine";
|
||||||
import * as engineModule from "@fusion/engine";
|
import * as engineModule from "@fusion/engine";
|
||||||
@@ -827,8 +828,8 @@ export class ChatManager {
|
|||||||
> | undefined,
|
> | undefined,
|
||||||
private messageStore?: MessageStore,
|
private messageStore?: MessageStore,
|
||||||
// Scoped task store for the chat's project — enables workflow-authoring
|
// Scoped task store for the chat's project — enables workflow-authoring
|
||||||
// tools (fn_workflow_*). Optional so existing test/construction sites that
|
// tools (fn_workflow_*) and explicit-task document tools. Optional so
|
||||||
// don't author workflows keep working.
|
// existing test/construction sites that don't author workflows keep working.
|
||||||
private taskStore?: TaskStore,
|
private taskStore?: TaskStore,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -1812,7 +1813,15 @@ export class ChatManager {
|
|||||||
? createWorkflowAuthoringTools(this.taskStore, "", { stripApprovalFlags: true })
|
? createWorkflowAuthoringTools(this.taskStore, "", { stripApprovalFlags: true })
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const customTools = [createAskQuestionTool(), ...messagingTools, ...workflowTools];
|
/*
|
||||||
|
FNXC:ChatAgentTools 2026-06-18-06:51:
|
||||||
|
The dashboard chat lane has no ambient task, so task-document tools must require explicit `task_id` while keeping the canonical `fn_task_document_write` and `fn_task_document_read` names available to chat agents.
|
||||||
|
*/
|
||||||
|
const documentTools = this.taskStore
|
||||||
|
? createChatTaskDocumentTools(this.taskStore)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const customTools = [createAskQuestionTool(), ...messagingTools, ...workflowTools, ...documentTools];
|
||||||
|
|
||||||
const sessionOptions = {
|
const sessionOptions = {
|
||||||
cwd: this.rootDir,
|
cwd: this.rootDir,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { TaskDocument, TaskStore } from "@fusion/core";
|
import type { TaskDocument, TaskStore } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
|
createChatTaskDocumentTools,
|
||||||
createTaskDocumentReadTool,
|
createTaskDocumentReadTool,
|
||||||
createTaskDocumentWriteTool,
|
createTaskDocumentWriteTool,
|
||||||
} from "../agent-tools.js";
|
} from "../agent-tools.js";
|
||||||
@@ -223,6 +224,113 @@ describe("task_document_read tool", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("chat task document tools", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
function findChatTool(name: "fn_task_document_write" | "fn_task_document_read", store: TaskStore) {
|
||||||
|
const tool = createChatTaskDocumentTools(store).find((candidate) => candidate.name === name);
|
||||||
|
expect(tool).toBeDefined();
|
||||||
|
return tool!;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("exposes canonical document tool names for chat agents", () => {
|
||||||
|
const { store } = createMockStore();
|
||||||
|
|
||||||
|
expect(createChatTaskDocumentTools(store).map((tool) => tool.name)).toEqual([
|
||||||
|
"fn_task_document_write",
|
||||||
|
"fn_task_document_read",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes a document to the explicit task_id", async () => {
|
||||||
|
const { store, upsertTaskDocument } = createMockStore();
|
||||||
|
upsertTaskDocument.mockResolvedValue(createMockDocument({ taskId: "FN-2020", key: "plan", revision: 5 }));
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_write", store);
|
||||||
|
const result = await runTool(tool, "call-chat-write", {
|
||||||
|
task_id: "FN-2020",
|
||||||
|
key: "plan",
|
||||||
|
content: "Chat-authored plan",
|
||||||
|
author: "chat-agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(upsertTaskDocument).toHaveBeenCalledWith("FN-2020", {
|
||||||
|
key: "plan",
|
||||||
|
content: "Chat-authored plan",
|
||||||
|
author: "chat-agent",
|
||||||
|
});
|
||||||
|
expect(getText(result)).toContain("Saved document \"plan\"");
|
||||||
|
expect(getText(result)).toContain("revision 5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads a document from the explicit task_id", async () => {
|
||||||
|
const { store, getTaskDocument } = createMockStore();
|
||||||
|
getTaskDocument.mockResolvedValue(createMockDocument({ taskId: "FN-2021", key: "notes", content: "Chat notes" }));
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_read", store);
|
||||||
|
const result = await runTool(tool, "call-chat-read", { task_id: "FN-2021", key: "notes" });
|
||||||
|
|
||||||
|
expect(getTaskDocument).toHaveBeenCalledWith("FN-2021", "notes");
|
||||||
|
expect(getText(result)).toContain("Document: notes");
|
||||||
|
expect(getText(result)).toContain("Chat notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns not found for a missing explicit-task document key", async () => {
|
||||||
|
const { store, getTaskDocument } = createMockStore();
|
||||||
|
getTaskDocument.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_read", store);
|
||||||
|
const result = await runTool(tool, "call-chat-missing", { task_id: "FN-2022", key: "missing" });
|
||||||
|
|
||||||
|
expect(getTaskDocument).toHaveBeenCalledWith("FN-2022", "missing");
|
||||||
|
expect(getText(result)).toContain("Document \"missing\" not found.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists documents for the explicit task_id when key is omitted", async () => {
|
||||||
|
const { store, getTaskDocuments } = createMockStore();
|
||||||
|
getTaskDocuments.mockResolvedValue([
|
||||||
|
createMockDocument({ taskId: "FN-2023", key: "plan", revision: 1 }),
|
||||||
|
createMockDocument({ taskId: "FN-2023", key: "docs", revision: 2 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_read", store);
|
||||||
|
const result = await runTool(tool, "call-chat-list", { task_id: "FN-2023" });
|
||||||
|
|
||||||
|
expect(getTaskDocuments).toHaveBeenCalledWith("FN-2023");
|
||||||
|
expect(getText(result)).toContain("Task documents:");
|
||||||
|
expect(getText(result)).toContain("- plan (revision 1");
|
||||||
|
expect(getText(result)).toContain("- docs (revision 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns clean errors for non-existent explicit task writes", async () => {
|
||||||
|
const { store, upsertTaskDocument } = createMockStore();
|
||||||
|
upsertTaskDocument.mockRejectedValue(new Error("Task FN-404 not found"));
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_write", store);
|
||||||
|
const result = await runTool(tool, "call-chat-write-error", {
|
||||||
|
task_id: "FN-404",
|
||||||
|
key: "plan",
|
||||||
|
content: "No target",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getText(result)).toContain("ERROR: Failed to save document \"plan\" for task FN-404");
|
||||||
|
expect(getText(result)).toContain("Task FN-404 not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns clean errors for non-existent explicit task reads", async () => {
|
||||||
|
const { store, getTaskDocuments } = createMockStore();
|
||||||
|
getTaskDocuments.mockRejectedValue(new Error("Task FN-405 not found"));
|
||||||
|
|
||||||
|
const tool = findChatTool("fn_task_document_read", store);
|
||||||
|
const result = await runTool(tool, "call-chat-read-error", { task_id: "FN-405" });
|
||||||
|
|
||||||
|
expect(getText(result)).toContain("ERROR: Failed to read task documents for task FN-405");
|
||||||
|
expect(getText(result)).toContain("Task FN-405 not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("document tool factory integration", () => {
|
describe("document tool factory integration", () => {
|
||||||
it("uses the provided store instance across write and read tools", async () => {
|
it("uses the provided store instance across write and read tools", async () => {
|
||||||
const { store, upsertTaskDocument, getTaskDocument, getTaskDocuments } = createMockStore();
|
const { store, upsertTaskDocument, getTaskDocument, getTaskDocuments } = createMockStore();
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ export const taskDocumentReadParams = Type.Object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const chatTaskDocumentWriteParams = Type.Object({
|
||||||
|
task_id: Type.String({ description: "Task ID to write the document to (e.g. 'FN-001')." }),
|
||||||
|
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 chatTaskDocumentReadParams = Type.Object({
|
||||||
|
task_id: Type.String({ description: "Task ID to read documents from (e.g. 'FN-001')." }),
|
||||||
|
key: Type.Optional(
|
||||||
|
Type.String({ description: "Document key to read. Omit to list all documents for this task." }),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
export const workflowListParams = Type.Object({});
|
export const workflowListParams = Type.Object({});
|
||||||
|
|
||||||
export const workflowGetParams = Type.Object({
|
export const workflowGetParams = Type.Object({
|
||||||
@@ -1046,58 +1062,115 @@ export function createTaskDocumentReadTool(store: TaskStore, taskId: string): To
|
|||||||
description:
|
description:
|
||||||
"Read a named document for this task, or list all documents when no key is provided.",
|
"Read a named document for this task, or list all documents when no key is provided.",
|
||||||
parameters: taskDocumentReadParams,
|
parameters: taskDocumentReadParams,
|
||||||
execute: async (_id: string, params: Static<typeof taskDocumentReadParams>) => {
|
execute: async (_id: string, params: Static<typeof taskDocumentReadParams>) => readTaskDocuments(store, taskId, params.key),
|
||||||
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: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:ChatAgentTools 2026-06-18-06:51:
|
||||||
|
* Chat sessions do not have an ambient task, but users expect the same `fn_task_document_write` and `fn_task_document_read` names that task-bound lanes expose.
|
||||||
|
* Require an explicit `task_id` here, mirroring no-ambient workflow authoring tools, so FN-6635 chat agents can persist task documents without guessing a target task.
|
||||||
|
*/
|
||||||
|
export function createChatTaskDocumentTools(store: TaskStore): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "fn_task_document_write",
|
||||||
|
label: "Write Document",
|
||||||
|
description:
|
||||||
|
"Save a named document for a task (for example plan, notes, or research). " +
|
||||||
|
"Each write creates a new revision so you can update documents over time. Requires task_id.",
|
||||||
|
parameters: chatTaskDocumentWriteParams,
|
||||||
|
execute: async (_id: string, params: Static<typeof chatTaskDocumentWriteParams>) => {
|
||||||
|
const input: TaskDocumentCreateInput = {
|
||||||
|
key: params.key,
|
||||||
|
content: params.content,
|
||||||
|
author: params.author || "agent",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const document: TaskDocument = await store.upsertTaskDocument(params.task_id, input);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text:
|
text: `Saved document "${document.key}" (revision ${document.revision}).`,
|
||||||
`Document: ${document.key}\n` +
|
}],
|
||||||
`Revision: ${document.revision}\n` +
|
details: {},
|
||||||
`Updated: ${document.updatedAt}\n\n` +
|
};
|
||||||
document.content,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `ERROR: Failed to save document "${params.key}" for task ${params.task_id}: ${err.message}`,
|
||||||
}],
|
}],
|
||||||
details: {},
|
details: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fn_task_document_read",
|
||||||
|
label: "Read Document",
|
||||||
|
description:
|
||||||
|
"Read a named document for a task, or list all documents when no key is provided. Requires task_id.",
|
||||||
|
parameters: chatTaskDocumentReadParams,
|
||||||
|
execute: async (_id: string, params: Static<typeof chatTaskDocumentReadParams>) => (
|
||||||
|
readTaskDocuments(store, params.task_id, params.key)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const documents: TaskDocument[] = await store.getTaskDocuments(taskId);
|
async function readTaskDocuments(store: TaskStore, taskId: string, key?: string) {
|
||||||
if (documents.length === 0) {
|
try {
|
||||||
return {
|
if (key) {
|
||||||
content: [{ type: "text" as const, text: "No documents found for this task." }],
|
const document: TaskDocument | null = await store.getTaskDocument(taskId, key);
|
||||||
details: {},
|
if (!document) {
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = documents.map((doc) => `- ${doc.key} (revision ${doc.revision}, updated ${doc.updatedAt})`);
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{ type: "text" as const, text: `Document "${key}" not found.` }],
|
||||||
type: "text" as const,
|
|
||||||
text: `Task documents:\n${lines.join("\n")}`,
|
|
||||||
}],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text" as const,
|
|
||||||
text: `ERROR: Failed to read task documents: ${err.message}`,
|
|
||||||
}],
|
|
||||||
details: {},
|
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: {},
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `ERROR: Failed to read task documents for task ${taskId}: ${err.message}`,
|
||||||
|
}],
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { reloadExemptTools, addToExemptTools, getExemptToolNames } from "./agent
|
|||||||
export { createFusionAuthStorage } from "./auth-storage.js";
|
export { createFusionAuthStorage } from "./auth-storage.js";
|
||||||
export {
|
export {
|
||||||
createTaskCreateTool,
|
createTaskCreateTool,
|
||||||
|
createChatTaskDocumentTools,
|
||||||
createTaskDocumentReadTool,
|
createTaskDocumentReadTool,
|
||||||
createTaskDocumentWriteTool,
|
createTaskDocumentWriteTool,
|
||||||
createTaskLogTool,
|
createTaskLogTool,
|
||||||
@@ -18,6 +19,8 @@ export {
|
|||||||
createTraitListTool,
|
createTraitListTool,
|
||||||
createWorkflowAuthoringTools,
|
createWorkflowAuthoringTools,
|
||||||
taskCreateParams,
|
taskCreateParams,
|
||||||
|
chatTaskDocumentReadParams,
|
||||||
|
chatTaskDocumentWriteParams,
|
||||||
taskDocumentReadParams,
|
taskDocumentReadParams,
|
||||||
taskDocumentWriteParams,
|
taskDocumentWriteParams,
|
||||||
taskLogParams,
|
taskLogParams,
|
||||||
|
|||||||
Reference in New Issue
Block a user