diff options
| author | Adam Malczewski <[email protected]> | 2026-05-28 22:51:47 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-28 22:51:47 +0900 |
| commit | d6609efd4e14101e77fb35a98ce597a32816862d (patch) | |
| tree | 09ea404ce0a780ca6b8c380fdd93ad1ae9960986 /packages/core/src/tools/summon.ts | |
| parent | 2eeabc95b78f6624c187e1e3892f9413266b4b9a (diff) | |
| download | dispatch-d6609efd4e14101e77fb35a98ce597a32816862d.tar.gz dispatch-d6609efd4e14101e77fb35a98ce597a32816862d.zip | |
fix(core): normalize tool schemas for Anthropic, add toolChoice=auto; feat(summon): agent definition support; docs: cc/ research findings
- registry.ts: add normalizeForAnthropic() to strip , additionalProperties, default, nullable from zodToJsonSchema output so Anthropic doesn't silently reject tool definitions
- agent.ts: add toolChoice=auto for Claude OAuth to prevent Opus thinking forever without calling tools
- summon.ts: add agentSlug parameter, build agents catalog in description, add toAvailableAgents helper
- agent-manager.ts: wire agent definition loading into spawnChildAgent, agent model fallback
- loader.ts: export loadAgent, expandAgentToolNames, getAgentDirPaths; add getAgentDirPaths for permission gate
- agent.ts: auto-allow read-only tools in agent definition directories
- packaging/PKGBUILD: exclude ARM64 prebuilds from x86_64 package
- cc/: research findings on Claude Opus tool calling issues
- tests: loader tests, summon tool tests
Diffstat (limited to 'packages/core/src/tools/summon.ts')
| -rw-r--r-- | packages/core/src/tools/summon.ts | 127 |
1 files changed, 122 insertions, 5 deletions
diff --git a/packages/core/src/tools/summon.ts b/packages/core/src/tools/summon.ts index 22ab35b..ed4b080 100644 --- a/packages/core/src/tools/summon.ts +++ b/packages/core/src/tools/summon.ts @@ -1,17 +1,90 @@ import { z } from "zod"; -import type { ToolDefinition } from "../types/index.js"; +import type { AgentDefinition, ToolDefinition } from "../types/index.js"; export interface SummonCallbacks { - spawn(options: { task: string; tools: string[]; workingDirectory?: string }): Promise<string>; + spawn(options: { + task: string; + tools: string[]; + workingDirectory?: string; + /** + * Optional slug of an `AgentDefinition` (loaded from + * `~/.config/dispatch/agents/` or `<projectDir>/.dispatch/agents/`) + * to use as the basis for the spawned child. When provided, + * the definition's tools, models, and cwd override the + * `tools` and `workingDirectory` parameters passed alongside. + */ + agentSlug?: string; + }): Promise<string>; getResult( agentId: string, ): Promise<{ status: "done"; result: string } | { status: "error"; error: string }>; } +/** + * Summary of an agent definition surfaced to the calling LLM in the + * summon tool's description. The shape is intentionally minimal — full + * TOML inspection is done by reading the definition file directly, + * which all agents are allowed to do by default. + */ +export interface AvailableAgent { + slug: string; + name: string; + description: string; + /** Filesystem path of the TOML the agent can read for full details. */ + path: string; +} + +/** + * Build the prose paragraph that lists available agent definitions plus + * the disk locations where they live, injected into the summon tool's + * description. + * + * Returns the empty string when no agents are visible — keeps the + * description compact for environments where no definitions exist yet. + */ +function buildAgentsCatalog(agents: AvailableAgent[], agentDirs: string[]): string { + const lines: string[] = []; + lines.push(""); + lines.push("Agent definitions live on disk and can be inspected with read_file/list_files:"); + for (const d of agentDirs) { + lines.push(` - ${d}`); + } + if (agents.length === 0) { + lines.push(""); + lines.push("No agent definitions are currently defined."); + return lines.join("\n"); + } + lines.push(""); + lines.push("To summon a specific agent, pass its slug as the 'agent' parameter."); + lines.push("When 'agent' is set, the child inherits that definition's tools, models,"); + lines.push("and working directory; the 'tools' parameter is ignored."); + lines.push(""); + lines.push("Available agents:"); + for (const a of agents) { + const desc = a.description ? ` — ${a.description}` : ""; + lines.push(` - ${a.slug}: ${a.name}${desc}`); + } + return lines.join("\n"); +} + +/** + * Factory for the `summon` tool. Accepts a snapshot of agent definitions + * available at the time the tool is registered so the LLM's view of + * which agents exist matches what `spawnChildAgent` can actually load. + * + * `agentDirs` is the list of filesystem paths the catalog references in + * its description; this is information-only — the runtime resolves + * slugs through `loadAgent` independently. + */ export function createSummonTool( defaultWorkingDirectory: string, callbacks: SummonCallbacks, + availableAgents: AvailableAgent[] = [], + agentDirs: string[] = [], ): ToolDefinition { + const catalog = buildAgentsCatalog(availableAgents, agentDirs); + const agentSlugs = availableAgents.map((a) => a.slug); + return { name: "summon", description: [ @@ -38,7 +111,8 @@ export function createSummonTool( " - web_search: Search the web", " - youtube_transcribe: Fetch YouTube video transcripts", "", - "If tools is omitted, the child gets read_file, list_files, and todo only (read-only by default).", + "If tools is omitted (and no 'agent' is specified), the child gets read_file, list_files, and todo only (read-only by default).", + catalog, ].join("\n"), parameters: z.object({ task: z @@ -46,6 +120,21 @@ export function createSummonTool( .describe( "Detailed instructions for the child agent. Be specific about what it should do and what it should return.", ), + agent: z + .string() + .optional() + .describe( + [ + "Slug of an agent definition to use as the basis for the child agent.", + "When provided, the child inherits the definition's tools, models, and", + "working directory; the 'tools' parameter is ignored. Inspect the agent", + "directories listed above to discover which slugs are available and what", + "each one does.", + agentSlugs.length > 0 ? `Available slugs: ${agentSlugs.join(", ")}.` : "", + ] + .filter(Boolean) + .join(" "), + ), tools: z .array( z.enum([ @@ -62,13 +151,13 @@ export function createSummonTool( ) .optional() .describe( - 'Tool names to give the child. Defaults to ["read_file", "list_files", "todo"]. Include "summon" and "retrieve" to allow nesting.', + 'Tool names to give the child. Defaults to ["read_file", "list_files", "todo"]. Include "summon" and "retrieve" to allow nesting. Ignored when "agent" is set.', ), working_directory: z .string() .optional() .describe( - "Absolute path for the child to work in. Defaults to the current working directory.", + "Absolute path for the child to work in. Defaults to the current working directory. When 'agent' is set and its definition has a cwd, that takes precedence.", ), background: z .boolean() @@ -79,6 +168,7 @@ export function createSummonTool( }), execute: async (args: Record<string, unknown>): Promise<string> => { const task = args.task as string; + const agentSlug = args.agent as string | undefined; const tools = (args.tools as string[] | undefined) ?? ["read_file", "list_files", "todo"]; const workingDirectory = (args.working_directory as string | undefined) ?? defaultWorkingDirectory; @@ -89,6 +179,7 @@ export function createSummonTool( task, tools, workingDirectory, + ...(agentSlug ? { agentSlug } : {}), }); if (!background) { @@ -113,3 +204,29 @@ export function createSummonTool( }, }; } + +/** + * Build the `AvailableAgent[]` projection from a list of full + * `AgentDefinition` records. Each entry's `path` is derived from the + * scope+slug so the agent can `read_file(path)` directly. + */ +export function toAvailableAgents( + defs: AgentDefinition[], + globalDir: string, + projectDir: string | null, +): AvailableAgent[] { + return defs.map((d) => { + const baseDir = + d.scope === "global" + ? globalDir + : projectDir + ? `${projectDir.replace(/\/$/, "")}/.dispatch/agents` + : globalDir; + return { + slug: d.slug, + name: d.name, + description: d.description, + path: `${baseDir}/${d.slug}.toml`, + }; + }); +} |
