diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 00:19:14 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 00:19:14 +0900 |
| commit | fb97d4cb72d0a90dde102b7001603716ee6e4c3b (patch) | |
| tree | 55c6e8b56b3395008523ab94c16c4f526083d846 /packages/core/src | |
| parent | 7884709e3b2adb1b65c1c086257e0300eed51cee (diff) | |
| download | dispatch-fb97d4cb72d0a90dde102b7001603716ee6e4c3b.tar.gz dispatch-fb97d4cb72d0a90dde102b7001603716ee6e4c3b.zip | |
feat: agent summoning system, todo improvements, security fixes, double-execution bug fix
- Add summon/retrieve tools for spawning child agents in new tabs
- summon: non-blocking, returns agent_id immediately
- retrieve: blocking, waits for child to finish, returns result
- Child tools are intersected with parent permissions (no privilege escalation)
- Working directory validated to stay within workspace
- Abort controller stops orphaned agents on tab close
- Rename task_list tool to todo with comprehensive usage guidance in system prompt
- Rename PermissionLog.svelte to ToolPermissions.svelte
- Add 'Summon agents' toggle to tool permissions UI
- Redesign TaskListPanel with DaisyUI checkboxes (indeterminate for in-progress)
- Remove 'blocked' status from task system
- Add tab-created WebSocket event for child agent tab visibility
- Add HMR cleanup for WebSocket connections (close stale connections on hot reload)
- Fix ensureAssistantMessage to not throw on closed tabs
- Fix double tool execution: remove execute from AI SDK tool() in registry.ts
(agent.ts already executes tools manually via executeToolWithStreaming)
- Fix all pre-existing test failures (missing mocks, stale API signatures)
- Add debug info to copy button (tab ID, injected skills, all tab IDs)
- Add tab ID and tools to conversation copy output
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/index.ts | 74 | ||||
| -rw-r--r-- | packages/core/src/tools/registry.ts | 9 | ||||
| -rw-r--r-- | packages/core/src/tools/retrieve.ts | 41 | ||||
| -rw-r--r-- | packages/core/src/tools/summon.ts | 88 | ||||
| -rw-r--r-- | packages/core/src/tools/task-list.ts | 15 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 3 |
6 files changed, 187 insertions, 43 deletions
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3b49de9..c5aa81b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,38 +2,58 @@ // Agent & LLM export { Agent } from "./agent/agent.js"; +// Config +export { + configToRuleset, + createConfigWatcher, + loadConfig, + validateConfig, +} from "./config/index.js"; +// Credentials +export * from "./credentials/index.js"; +// Database +export { closeDatabase, getDatabase, getDatabasePath } from "./db/index.js"; +export { + appendMessage, + clearMessagesForTab, + getMessagesForTab, + type MessageRow, + updateMessage, +} from "./db/messages.js"; +export { deleteSetting, getSetting, setSetting } from "./db/settings.js"; +// Tabs & Messages +export { + archiveTab, + createTab, + getTab, + listOpenTabs, + type TabRow, + updateTabModel, + updateTabStatus, + updateTabTitle, +} from "./db/tabs.js"; export { createProvider } from "./llm/provider.js"; - +// Models +export { ModelRegistry } from "./models/index.js"; +export * from "./permission/index.js"; +// Skills +export { + createSkillsWatcher, + getSkillByName, + loadSkills, + parseSkillFile, + resolveSkillsForAgent, +} from "./skills/index.js"; +export { prefix as bashArityPrefix } from "./tools/bash-arity.js"; // Tools export { createListFilesTool } from "./tools/list-files.js"; -export { createRunShellTool } from "./tools/run-shell.js"; -export { analyzeCommand } from "./tools/shell-analyze.js"; -export { prefix as bashArityPrefix } from "./tools/bash-arity.js"; export { createReadFileTool } from "./tools/read-file.js"; export { createToolRegistry } from "./tools/registry.js"; +export { createRetrieveTool, type RetrieveCallbacks } from "./tools/retrieve.js"; +export { createRunShellTool } from "./tools/run-shell.js"; +export { analyzeCommand } from "./tools/shell-analyze.js"; +export { createSummonTool, type SummonCallbacks } from "./tools/summon.js"; +export { createTaskListTool, TaskList } from "./tools/task-list.js"; export { createWriteFileTool } from "./tools/write-file.js"; -export { TaskList, createTaskListTool } from "./tools/task-list.js"; - // Types & Permissions export * from "./types/index.js"; -export * from "./permission/index.js"; - -// Config -export { loadConfig, configToRuleset, validateConfig, createConfigWatcher } from "./config/index.js"; - -// Skills -export { parseSkillFile, loadSkills, resolveSkillsForAgent, getSkillByName, createSkillsWatcher } from "./skills/index.js"; - -// Models -export { ModelRegistry } from "./models/index.js"; - -// Credentials -export * from "./credentials/index.js"; - -// Database -export { getDatabase, closeDatabase, getDatabasePath } from "./db/index.js"; - -// Tabs & Messages -export { type TabRow, createTab, getTab, listOpenTabs, updateTabTitle, updateTabModel, updateTabStatus, archiveTab } from "./db/tabs.js"; -export { type MessageRow, appendMessage, updateMessage, getMessagesForTab, clearMessagesForTab } from "./db/messages.js"; -export { getSetting, setSetting, deleteSetting } from "./db/settings.js"; diff --git a/packages/core/src/tools/registry.ts b/packages/core/src/tools/registry.ts index 4699c93..0c7b110 100644 --- a/packages/core/src/tools/registry.ts +++ b/packages/core/src/tools/registry.ts @@ -18,15 +18,14 @@ export function createToolRegistry(tools: ToolDefinition[]) { const result: Record<string, ReturnType<typeof tool>> = {}; for (const [name, def] of toolMap) { const schema = def.parameters; + // Do NOT pass execute here — agent.ts handles tool execution + // manually via executeToolWithStreaming. Passing execute would + // cause the AI SDK to auto-execute tools AND agent.ts to execute + // them again, resulting in double execution. const t = tool({ description: def.description, parameters: schema instanceof z.ZodObject ? schema : z.object({}), - execute: async (args) => { - return def.execute(args as Record<string, unknown>); - }, }); - // The AI SDK tool() overloads cause type narrowing issues when - // execute is provided. The runtime value is correct. result[name] = t as unknown as ReturnType<typeof tool>; } return result; diff --git a/packages/core/src/tools/retrieve.ts b/packages/core/src/tools/retrieve.ts new file mode 100644 index 0000000..93f4c89 --- /dev/null +++ b/packages/core/src/tools/retrieve.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import type { ToolDefinition } from "../types/index.js"; + +export interface RetrieveCallbacks { + getResult( + agentId: string, + ): Promise<{ status: "done"; result: string } | { status: "error"; error: string }>; +} + +export function createRetrieveTool(callbacks: RetrieveCallbacks): ToolDefinition { + return { + name: "retrieve", + description: [ + "Wait for a child agent to finish and retrieve its result. This tool BLOCKS until the child completes.", + "", + "Pass the agent_id returned by the summon tool. Once the child finishes, its final output is returned.", + "If the child encountered an error, the error message is returned instead.", + "", + "Typical usage:", + ' 1. summon({ task: "...", tools: [...] }) -> get agent_id', + " 2. ... do other work or summon more agents ...", + ' 3. retrieve({ agent_id: "..." }) -> blocks until done, returns result', + ].join("\n"), + parameters: z.object({ + agent_id: z.string().describe("The agent_id returned by a previous summon call."), + }), + execute: async (args: Record<string, unknown>): Promise<string> => { + const agentId = args.agent_id as string; + + try { + const outcome = await callbacks.getResult(agentId); + if (outcome.status === "done") { + return ["<agent_result>", outcome.result, "</agent_result>"].join("\n"); + } + return `Agent error: ${outcome.error}`; + } catch (err) { + return `Error retrieving result: ${err instanceof Error ? err.message : String(err)}`; + } + }, + }; +} diff --git a/packages/core/src/tools/summon.ts b/packages/core/src/tools/summon.ts new file mode 100644 index 0000000..582b871 --- /dev/null +++ b/packages/core/src/tools/summon.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import type { ToolDefinition } from "../types/index.js"; + +export interface SummonCallbacks { + spawn(options: { task: string; tools: string[]; workingDirectory?: string }): Promise<string>; +} + +export function createSummonTool( + defaultWorkingDirectory: string, + callbacks: SummonCallbacks, +): ToolDefinition { + return { + name: "summon", + description: [ + "Spawn a new child agent to work on a task independently. Returns immediately with an agent_id — does NOT wait for the child to finish.", + "", + "The child agent runs in its own tab visible to the user. Use the 'retrieve' tool with the returned agent_id to get the result when needed.", + "", + "Pattern for parallel work:", + " 1. Call summon multiple times to start several agents", + " 2. Do your own work or wait", + " 3. Call retrieve for each agent_id to collect results", + "", + "The 'tools' parameter controls what the child can do. Available tool names:", + " - read_file: Read file contents", + " - list_files: List files and directories", + " - write_file: Write/edit files", + " - run_shell: Execute shell commands", + " - todo: Track work items", + " - summon: Spawn its own child agents (enables nesting)", + " - retrieve: Collect results from its children (required if summon is given)", + "", + "If tools is omitted, the child gets read_file, list_files, and todo only (read-only by default).", + ].join("\n"), + parameters: z.object({ + task: z + .string() + .describe( + "Detailed instructions for the child agent. Be specific about what it should do and what it should return.", + ), + tools: z + .array( + z.enum([ + "read_file", + "list_files", + "write_file", + "run_shell", + "todo", + "summon", + "retrieve", + ]), + ) + .optional() + .describe( + 'Tool names to give the child. Defaults to ["read_file", "list_files", "todo"]. Include "summon" and "retrieve" to allow nesting.', + ), + working_directory: z + .string() + .optional() + .describe( + "Absolute path for the child to work in. Defaults to the current working directory.", + ), + }), + execute: async (args: Record<string, unknown>): Promise<string> => { + const task = args.task as string; + const tools = (args.tools as string[] | undefined) ?? ["read_file", "list_files", "todo"]; + const workingDirectory = + (args.working_directory as string | undefined) ?? defaultWorkingDirectory; + + try { + const agentId = await callbacks.spawn({ + task, + tools, + workingDirectory, + }); + return [ + `Agent spawned successfully.`, + `agent_id: ${agentId}`, + ``, + `The child agent is now working on the task in its own tab.`, + `Use the retrieve tool with this agent_id to get the result when ready.`, + ].join("\n"); + } catch (err) { + return `Error spawning agent: ${err instanceof Error ? err.message : String(err)}`; + } + }, + }; +} diff --git a/packages/core/src/tools/task-list.ts b/packages/core/src/tools/task-list.ts index 0bacdb4..29f1543 100644 --- a/packages/core/src/tools/task-list.ts +++ b/packages/core/src/tools/task-list.ts @@ -60,24 +60,19 @@ export class TaskList { export function createTaskListTool(taskList: TaskList): ToolDefinition { return { - name: "task_list", + name: "todo", description: - "Manages a task list for tracking work items. The agent can add tasks, update their status, list all tasks, or get details on a specific task.", + "Manage a todo list for planning and tracking work. Add items, update their status, list all items, or get details on a specific item.", parameters: z.object({ - action: z - .enum(["add", "update", "list", "get", "remove"]) - .describe("The action to perform"), + action: z.enum(["add", "update", "list", "get", "remove"]).describe("The action to perform"), title: z.string().optional().describe("Task title (required for 'add')"), description: z .string() .optional() .describe("Task description (for 'add', defaults to empty)"), - task_id: z - .string() - .optional() - .describe("Task ID (required for 'update', 'get', 'remove')"), + task_id: z.string().optional().describe("Task ID (required for 'update', 'get', 'remove')"), status: z - .enum(["pending", "in_progress", "done", "blocked"]) + .enum(["pending", "in_progress", "done"]) .optional() .describe("New status (required for 'update')"), }), diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index f5dafbb..bf312ae 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -39,7 +39,8 @@ export type AgentEvent = | { type: "error"; error: string } | { type: "done"; message: ChatMessage } | { type: "task-list-update"; tasks: TaskItem[] } - | { type: "config-reload" }; + | { type: "config-reload" } + | { type: "tab-created"; id: string; title: string }; // ─── Tool Types ────────────────────────────────────────────────── |
