/** * multi-agent — spawn, list, kill, invoke agents from within agent tool calls. * * Ported from cagataycali/agi-diy agi.html. * * The registry is set at app init by useAgents() via `setAgentRegistry()`. * Tools read/write through that registry. */ import { tool } from '@strands-agents/sdk' import { z } from 'zod' import type { UseAgentsReturn } from '../hooks/useAgents' /** Registry ref set by App.tsx after useAgents() mounts. */ let registry: UseAgentsReturn | null = null export function setAgentRegistry(r: UseAgentsReturn | null): void { registry = r } function ensureRegistry(): UseAgentsReturn { if (!registry) throw new Error('Multi-agent registry not initialized yet') return registry } function ok(data: Record) { return JSON.stringify({ status: 'success', ...data }) } function fail(e: unknown) { return JSON.stringify({ status: 'error', error: (e as Error).message || String(e) }) } export const spawnAgentTool = tool({ name: 'spawn_agent', description: 'Create a new agent in this tab with its own provider/model/system prompt. Returns the new agent id. ' + 'By DEFAULT the new agent INHERITS the parent\'s full tool set and live dynamic context (mesh peers, activity, time, etc.). ' + 'Restrict tools with `tool_filter: ["tool_name", ...]`. Use `prompt_mode: "replace"` to start from a blank base prompt.', inputSchema: z.object({ id: z.string().describe('Unique agent id (e.g. researcher, coder, critic)'), provider: z.enum(['anthropic', 'openai', 'google', 'bedrock']).optional().describe('Model provider (default: inherit parent)'), model: z.string().optional().describe('Model id (default: inherit parent)'), systemPrompt: z.string().describe('System prompt defining this agent\'s role/behavior. In append mode (default) this is added to the careless base identity.'), maxTokens: z.number().optional(), tool_filter: z.array(z.string()).optional().describe( 'Optional list of tool NAMES to expose to this sub-agent. Omit to inherit ALL parent tools. ' + 'Example: ["web_search", "fetch_url", "remember"] for a focused researcher.' ), prompt_mode: z.enum(['append', 'replace']).optional().describe( '"append" (default): sub-agent inherits careless base prompt + live dynamic context, ' + 'with your systemPrompt appended as "## Sub-agent role". ' + '"replace": use systemPrompt verbatim with no base prompt (strict/minimal personas).' ), }), callback: async (input) => { try { const r = ensureRegistry() if (r.slots.has(input.id)) return fail(new Error(`agent '${input.id}' already exists`)) const slot = await r.spawn({ id: input.id, provider: (input.provider as any) || 'bedrock', model: input.model, systemPrompt: input.systemPrompt, maxTokens: input.maxTokens, tool_filter: input.tool_filter, prompt_mode: input.prompt_mode, }) return ok({ id: slot.id, provider: slot.provider, color: slot.color, total_agents: r.slots.size + 1, inherited_tools: !slot.tool_filter ? 'all (inherited from parent)' : `${slot.tool_filter.length} filtered`, prompt_mode: slot.prompt_mode, }) } catch (e) { return fail(e) } }, }) export const killAgentTool = tool({ name: 'kill_agent', description: 'Remove an agent and its message history. Cannot be undone.', inputSchema: z.object({ id: z.string(), confirm: z.boolean().describe('Must be true'), }), callback: async (input) => { try { if (!input.confirm) return fail(new Error('pass confirm=true to kill')) const r = ensureRegistry() if (!r.slots.has(input.id)) return fail(new Error(`agent '${input.id}' not found`)) await r.kill(input.id) return ok({ killed: input.id, remaining: r.slots.size - 1 }) } catch (e) { return fail(e) } }, }) export const listAgentsTool = tool({ name: 'list_agents', description: 'List all agents in this tab with their status, provider, and message counts.', inputSchema: z.object({}), callback: async () => { try { const r = ensureRegistry() const agents = [...r.slots.values()].map(s => ({ id: s.id, provider: s.provider, model: s.model, status: s.status, color: s.color, messages: s.messages.length, is_active: s.id === r.activeId, })) return ok({ count: agents.length, active_id: r.activeId, agents }) } catch (e) { return fail(e) } }, }) export const invokeAgentTool = tool({ name: 'invoke_agent', description: 'Send a message to another agent in this tab and await their response. Use list_agents first to see who\'s available.', inputSchema: z.object({ target_id: z.string().describe('Agent id to invoke'), message: z.string().describe('Message to send'), }), callback: async (input) => { try { const r = ensureRegistry() if (!r.slots.has(input.target_id)) return fail(new Error(`agent '${input.target_id}' not found`)) const response = await r.send(input.target_id, input.message) return ok({ from: input.target_id, response: response.slice(0, 4000), // truncate very long responses truncated: response.length > 4000, length: response.length, }) } catch (e) { return fail(e) } }, }) export const broadcastToAgentsTool = tool({ name: 'broadcast_to_agents', description: 'Send a message to ALL other agents in this tab in parallel. Returns their responses keyed by id.', inputSchema: z.object({ message: z.string(), exclude_self: z.boolean().optional().describe('Exclude the sender from broadcast (default: true — but sender id not known from tool context, so all agents receive)'), }), callback: async (input) => { try { const r = ensureRegistry() if (r.slots.size === 0) return fail(new Error('no agents to broadcast to')) const results = await r.broadcast(input.message) const responses: Record = {} for (const [id, value] of results) { if (value instanceof Error) responses[id] = { error: value.message } else responses[id] = { response: String(value).slice(0, 2000) } } return ok({ broadcast_to: results.size, responses }) } catch (e) { return fail(e) } }, }) export const updateAgentTool = tool({ name: 'update_agent', description: 'Update an agent\'s system prompt, provider, model, tool filter, or prompt mode at runtime. Runtime rebuilds on next invoke.', inputSchema: z.object({ id: z.string(), systemPrompt: z.string().optional(), provider: z.enum(['anthropic', 'openai', 'google', 'bedrock']).optional(), model: z.string().optional(), maxTokens: z.number().optional(), tool_filter: z.array(z.string()).optional().describe('Replace the sub-agent\'s tool-name filter. Pass [] to disable tools; omit to leave unchanged.'), prompt_mode: z.enum(['append', 'replace']).optional(), }), callback: async (input) => { try { const r = ensureRegistry() if (!r.slots.has(input.id)) return fail(new Error(`agent '${input.id}' not found`)) const { id, ...patch } = input await r.update(id, patch as any) return ok({ updated: id, patch: Object.keys(patch) }) } catch (e) { return fail(e) } }, }) export const MULTI_AGENT_TOOLS = [ spawnAgentTool, killAgentTool, listAgentsTool, invokeAgentTool, broadcastToAgentsTool, updateAgentTool, ]