summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 00:19:14 +0900
committerAdam Malczewski <[email protected]>2026-05-22 00:19:14 +0900
commitfb97d4cb72d0a90dde102b7001603716ee6e4c3b (patch)
tree55c6e8b56b3395008523ab94c16c4f526083d846 /packages/core/src
parent7884709e3b2adb1b65c1c086257e0300eed51cee (diff)
downloaddispatch-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.ts74
-rw-r--r--packages/core/src/tools/registry.ts9
-rw-r--r--packages/core/src/tools/retrieve.ts41
-rw-r--r--packages/core/src/tools/summon.ts88
-rw-r--r--packages/core/src/tools/task-list.ts15
-rw-r--r--packages/core/src/types/index.ts3
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 ──────────────────────────────────────────────────