diff --git a/package.json b/package.json index dfc320c..c4dc0b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/ai-utils", - "version": "1.6.13", + "version": "1.7.0", "description": "AI Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/llm.ts b/src/llm.ts index 2669aef..e6ee40d 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -19,6 +19,13 @@ const PDF_OCR_PAGE_THRESHOLD = 12; // above this many pages, OCR scanned pages i export type AnthropicConfig = {proto: 'anthropic', token: string | string[]}; export type OpenAiConfig = {proto: 'openai', host?: string, token: string | string[]}; +export type AgentRef = { + name: string; + description?: string; + delegate?: boolean; + fn: () => Agent | null | Promise; +} + export type Agent = { name: string; description?: string; @@ -29,7 +36,7 @@ export type Agent = { skills?: Skill[] | null; tools?: AiTool[] | null; mcp?: McpServer[] | null; - agents?: string[] | null; + agents?: AgentRef[] | null; } export type LLMFile = { @@ -106,8 +113,8 @@ export type LLMRequest = { skills?: Skill[]; /** MCP servers to connect and expose as tools */ mcp?: McpServer[]; - /** Subagents exposed as delegatable/wrapped tools */ - agents?: Agent[]; + /** Subagents exposed as delegatable/wrapped tools, resolved lazily via their `fn` */ + agents?: AgentRef[]; /** Attach files to request */ files?: LLMFile[]; /** @internal recursion guard for nested agent delegation */ @@ -265,22 +272,21 @@ class LLM { }; } - private setupAgent(agents: Agent[] = [], allAgents: Agent[], history: LLMMessage[], aborts: ((keep?: boolean) => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] { - return agents.map(a => { - const toolName = `${a.delegate ? '' : 'sub'}agent_${snakeCase(a.name)}`; + private setupAgent(stubs: AgentRef[] = [], history: LLMMessage[], aborts: ((keep?: boolean) => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] { + return stubs.map(stub => { + const toolName = `${stub.delegate ? '' : 'sub'}agent_${snakeCase(stub.name)}`; return { name: toolName, - description: `${a.delegate ? 'Delegate to ' : ''}Subagent: ${a.description || a.name}`, + description: `${stub.delegate ? 'Delegate to ' : ''}Subagent: ${stub.description || stub.name}`, args: clean({ - context: !a.delegate ? {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true} : undefined, + context: !stub.delegate ? {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true} : undefined, instructions: {type: 'string', description: 'Detailed instructions for subagent to complete', required: true}, }), fn: async (args: any, stream: any, ai: any, id?: string) => { if(depth >= MAX_AGENT_DEPTH) return 'Max agent delegation depth exceeded'; - const nested = (a.agents || []) - .map(name => allAgents.find(x => x.name === name)) - .filter((x): x is Agent => !!x && x.name !== a.name); + const a = await stub.fn(); + if(!a) return `Agent "${stub.name}" could not be resolved`; const q = a.delegate ? '' : `${args.instructions}${args.context ? `\n\n${args.context}` : ''}`; @@ -297,7 +303,7 @@ ${a.system}`, mcp: a.mcp || undefined, skills: a.skills || undefined, tools: a.tools || undefined, - agents: nested, + agents: a.agents || [], _agentDepth: depth + 1, } as any); aborts.push(request.abort); @@ -451,7 +457,7 @@ ${a.system}`, // Agents const agents = options.agents || this.ai.options?.llm?.agents; const delegateState: {resp: string | null} = {resp: null}; - if(agents?.length) tools.push(...this.setupAgent(agents, agents, history, nestedAborts, options._agentDepth || 0, delegateState)); + if(agents?.length) tools.push(...this.setupAgent(agents, history, nestedAborts, options._agentDepth || 0, delegateState)); // Memory const mem = MemoryManager.normalize(options.memory);