UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

376 lines 14.7 kB
import { createToolFunction, ensembleRequest } from '../index.js'; import { v4 as uuid } from 'uuid'; export const agentToolCache = new Map(); export function exportAgent(agent, model) { const agentExport = typeof agent.export === 'function' ? agent.export() : { agent_id: agent.agent_id, name: agent.name, model: agent.model, modelClass: agent.modelClass, parent_id: agent.parent_id, cwd: agent.cwd, }; if (model) agentExport.model = model; return agentExport; } export function getAgentSpecificTools(agent_id) { const tools = []; if (agentToolCache.has(agent_id)) { tools.push(...agentToolCache.get(agent_id)); } return tools; } export function cloneAgent(agent) { const copy = Object.create(Object.getPrototypeOf(agent)); Object.entries(agent).forEach(([key, value]) => { if (typeof value === 'function') { copy[key] = value; } else if (key === 'parent' && value instanceof Agent) { copy[key] = value; } else if (Array.isArray(value)) { copy[key] = [...value]; } else if (value && typeof value === 'object') { copy[key] = { ...value }; } else { copy[key] = value; } }); return copy; } export class Agent { agent_id; name; description; instructions; parent_id; workers; tools; model; modelClass; modelSettings; intelligence; maxToolCalls; maxToolCallRoundsPerTurn; verifier; maxVerificationAttempts; args; jsonSchema; historyThread; cwd; modelScores; disabledModels; onToolCall; processToolCall; onToolResult; onToolError; onRequest; onResponse; onThinking; onToolEvent; params; processParams; constructor(definition) { if (!definition || typeof definition !== 'object') { throw new Error(`Agent constructor expects an AgentDefinition object, but received: ${typeof definition}`); } this.agent_id = definition.agent_id || uuid(); this.name = (definition.name || 'Agent').replaceAll(' ', '_'); this.description = definition.description; this.instructions = definition.instructions; this.tools = definition.tools || []; this.model = definition.model; this.modelClass = definition.modelClass; this.jsonSchema = definition.jsonSchema; this.params = definition.params; this.modelSettings = definition.modelSettings || {}; this.maxToolCalls = definition.maxToolCalls ?? 200; this.maxToolCallRoundsPerTurn = definition.maxToolCallRoundsPerTurn; this.maxVerificationAttempts = definition.maxVerificationAttempts ?? 2; this.processParams = definition.processParams; this.historyThread = definition.historyThread; this.cwd = definition.cwd; if (definition.verifier) { this.verifier = new Agent({ ...definition.verifier, verifier: undefined, }); this.verifier.parent_id = this.agent_id; } this.onToolCall = definition.onToolCall; this.onToolResult = definition.onToolResult; this.onRequest = definition.onRequest; this.onThinking = definition.onThinking; this.onResponse = definition.onResponse; this.onToolEvent = definition.onToolEvent; if (this.jsonSchema) { if (!this.modelSettings) this.modelSettings = {}; this.modelSettings.json_schema = this.jsonSchema; } if (definition.workers) { this.workers = definition.workers.map((createAgentFn) => { return () => { const agent = createAgentFn(); agent.parent_id = this.agent_id; return agent; }; }); this.tools = this.tools.concat(this.workers.map((createAgentFn) => { const agent = createAgentFn(); agent.parent_id = this.agent_id; if (this.onToolEvent) { agent.onToolEvent = this.onToolEvent; } return agent.asTool(); })); } } asTool() { let description = `An agent called ${this.name}.\n\n${this.description}`; if (this.tools) { description += `\n\n${this.name} has access to the following tools:\n`; this.tools.forEach(tool => { description += `- ${tool.definition.function.name}\n`; }); description += '\nUse this as a guide when to call the agent, but let the agent decide which tools to use.'; } return createToolFunction(async (...args) => { const agent = cloneAgent(this); agent.agent_id = uuid(); agent.parent_id = this.parent_id; if (this.onToolEvent) { agent.onToolEvent = this.onToolEvent; } if (agent.processParams) { let paramsObj; if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) { paramsObj = args[0]; } else { paramsObj = {}; const paramKeys = Object.keys(agent.params || {}); paramKeys.forEach((key, idx) => { if (idx < args.length) paramsObj[key] = args[idx]; }); } const { prompt, intelligence } = await agent.processParams(agent, paramsObj); return runAgentTool(agent, prompt, intelligence); } let task = typeof args[0] === 'string' ? args[0] : ''; let context = typeof args[1] === 'string' ? args[1] : undefined; let warnings = typeof args[2] === 'string' ? args[2] : undefined; let goal = typeof args[3] === 'string' ? args[3] : undefined; let intelligence = args[4]; if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) { const params = args[0]; task = params.task || task; context = params.context || context; warnings = params.warnings || warnings; goal = params.goal || goal; intelligence = params.intelligence || intelligence; } let prompt = `**Task:** ${task}`; if (context) { prompt += `\n\n**Context:** ${context}`; } if (warnings) { prompt += `\n\n**Warnings:** ${warnings}`; } if (goal) { prompt += `\n\n**Goal:** ${goal}`; } return runAgentTool(agent, prompt, intelligence); }, description, this.params || { task: { type: 'string', description: `What should ${this.name} work on? Generally you should leave the way the task is performed up to the agent unless the agent previously failed. Agents are expected to work mostly autonomously.`, }, context: { type: 'string', description: `What else might the ${this.name} need to know? Explain why you are asking for this - summarize the task you were given or the project you are working on. Please make it comprehensive. A couple of paragraphs is ideal.`, optional: true, }, warnings: { type: 'string', description: `Is there anything the ${this.name} should avoid or be aware of? You can leave this as a blank string if there's nothing obvious.`, optional: true, }, goal: { type: 'string', description: `This is the final goal/output or result you expect from the task. Try to focus on the overall goal and allow the ${this.name} to make it's own decisions on how to get there. One sentence is ideal.`, optional: true, }, intelligence: { type: 'string', description: `What level of intelligence do you recommend for this task? - low: (under 90 IQ) Mini model used. - standard: (90 - 110 IQ) - high: (110+ IQ) Reasoning used.`, enum: ['low', 'standard', 'high'], optional: true, }, }, undefined, this.name); } async getTools() { const combinedTools = new Map(); const baseTools = this.tools && this.tools.length > 0 ? this.tools : []; for (const tool of baseTools) { if (tool && tool.definition && tool.definition.function && tool.definition.function.name) { const clonedTool = { ...tool }; clonedTool.definition = { ...tool.definition }; clonedTool.definition.function = { ...tool.definition.function, }; clonedTool.definition.function.parameters = { ...tool.definition.function.parameters, }; clonedTool.definition.function.parameters.properties = { ...tool.definition.function.parameters.properties, }; await this.processDynamicToolParameters(clonedTool); combinedTools.set(clonedTool.definition.function.name, clonedTool); } else { console.warn('[Agent.getTools] Encountered a base tool with missing definition or name:', tool); } } if (this.agent_id) { const cachedAgentTools = getAgentSpecificTools(this.agent_id); for (const tool of cachedAgentTools) { if (tool && tool.definition && tool.definition.function && tool.definition.function.name) { const clonedTool = { ...tool }; clonedTool.definition = { ...tool.definition }; clonedTool.definition.function = { ...tool.definition.function, }; clonedTool.definition.function.parameters = { ...tool.definition.function.parameters, }; clonedTool.definition.function.parameters.properties = { ...tool.definition.function.parameters.properties, }; await this.processDynamicToolParameters(clonedTool); combinedTools.set(clonedTool.definition.function.name, clonedTool); } else { console.warn('[Agent.getTools] Encountered a cached tool with missing definition or name:', tool); } } } return Array.from(combinedTools.values()); } async processDynamicToolParameters(tool) { const properties = tool.definition.function.parameters.properties; for (const paramName in properties) { const param = properties[paramName]; if (typeof param.description === 'function') { param.description = param.description(); } if (typeof param.enum === 'function') { param.enum = await param.enum(); } if (param.properties) { for (const nestedParamName in param.properties) { const nestedParam = param.properties[nestedParamName]; if (typeof nestedParam.description === 'function') { nestedParam.description = nestedParam.description(); } if (typeof nestedParam.enum === 'function') { nestedParam.enum = await nestedParam.enum(); } } } if (param.items) { const items = param.items; if (typeof items.description === 'function') { items.description = items.description(); } if (typeof items.enum === 'function') { items.enum = await items.enum(); } } } } export() { const agentExport = { agent_id: this.agent_id, name: this.name, }; if (this.model) { agentExport.model = this.model; } if (this.modelClass) { agentExport.modelClass = this.modelClass; } if (this.parent_id) { agentExport.parent_id = this.parent_id; } if (this.cwd) { agentExport.cwd = this.cwd; } return agentExport; } } async function runAgentTool(agent, prompt, intelligence) { agent.intelligence = intelligence || undefined; const modelClass = agent.modelClass || 'standard'; switch (agent.intelligence) { case 'low': if (['standard'].includes(modelClass)) { agent.modelClass = 'mini'; } if (['code', 'reasoning'].includes(modelClass)) { agent.modelClass = 'standard'; } break; case 'standard': break; case 'high': if (['mini'].includes(modelClass)) { agent.modelClass = 'standard'; } if (['standard'].includes(modelClass)) { agent.modelClass = 'reasoning'; } break; } const messages = []; messages.push({ type: 'message', role: 'user', content: prompt, }); try { const parentOnToolEvent = agent.onToolEvent; const stream = ensembleRequest(messages, agent); let fullResponse = ''; for await (const event of stream) { if (parentOnToolEvent) { await parentOnToolEvent(event); } if (event.type === 'message_complete' && 'content' in event) { fullResponse = event.content; } } return fullResponse; } catch (error) { console.error(`Error in ${agent.name}: ${error}`); return `Error in ${agent.name}: ${error}`; } } export async function getToolsFromAgent(agent) { if (agent && typeof agent.getTools === 'function') { return await agent.getTools(); } return agent?.tools || []; } //# sourceMappingURL=agent.js.map