From fb97d4cb72d0a90dde102b7001603716ee6e4c3b Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 22 May 2026 00:19:14 +0900 Subject: 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 --- packages/core/src/index.ts | 74 +++++++++++++++++++----------- packages/core/src/tools/registry.ts | 9 ++-- packages/core/src/tools/retrieve.ts | 41 +++++++++++++++++ packages/core/src/tools/summon.ts | 88 ++++++++++++++++++++++++++++++++++++ packages/core/src/tools/task-list.ts | 15 ++---- packages/core/src/types/index.ts | 3 +- 6 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 packages/core/src/tools/retrieve.ts create mode 100644 packages/core/src/tools/summon.ts (limited to 'packages/core/src') 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> = {}; 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); - }, }); - // 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; } 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): Promise => { + const agentId = args.agent_id as string; + + try { + const outcome = await callbacks.getResult(agentId); + if (outcome.status === "done") { + return ["", outcome.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; +} + +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): Promise => { + 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 ────────────────────────────────────────────────── -- cgit v1.2.3