UNPKG

jorel

Version:

A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.

597 lines (596 loc) 27.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JorElAgentManager = void 0; const zod_1 = require("zod"); const agents_1 = require("../agents"); const providers_1 = require("../providers"); const shared_1 = require("../shared"); const tools_1 = require("../tools"); /** * Manages teams of agents for JorEl */ class JorElAgentManager { constructor(_core) { this.delegateToAgentToolName = "ask_agent"; this.transferToAgentToolName = "handover_to_agent"; this._agents = new Map(); /** @internal */ this._defaultAgentId = null; this._core = _core; this.tools = new tools_1.LlmToolKit([ { name: this.delegateToAgentToolName, description: "Ask another agent to handle a task for you", params: zod_1.z.object({ agentName: zod_1.z.string(), taskDescription: zod_1.z.string().describe("The description of the task that you want the agent to handle"), }), executor: "subTask", }, { name: this.transferToAgentToolName, description: "Transfer the conversation to another agent", params: zod_1.z.object({ agentName: zod_1.z.string(), }), executor: "transfer", }, ]); } /** * Change the default agent * @param value */ set defaultAgentId(value) { if (value && !this._agents.has(value)) { throw new Error(`Agent with name ${value} is not registered`); } this._defaultAgentId = value; } /** * Currently registered agents */ get agents() { return Array.from(this._agents.values()); } /** * Get the default agent */ get defaultAgent() { return this._defaultAgentId ? (this._agents.get(this._defaultAgentId) ?? null) : null; } get logger() { return this._core.logger; } /** * Get an agent by name * @param name */ getAgent(name) { return this._agents.get(name) || null; } /** * Add an agent * @param agent Agent instance or definition */ addAgent(agent) { const agentInstance = agent instanceof agents_1.LlmAgent ? agent : new agents_1.LlmAgent(agent, this); if (this._agents.has(agentInstance.name)) { throw new Error(`Agent with name ${agentInstance.name} already exists`); } this._agents.set(agentInstance.name, agentInstance); if (!this._defaultAgentId) { this._defaultAgentId = agentInstance.name; } return agentInstance; } /** * Remove an agent. Will also remove the agent from the allowed delegates of other agents * @param agent */ removeAgent(agent) { const name = agent instanceof agents_1.LlmAgent ? agent.name : agent; this._agents.delete(name); if (this._defaultAgentId === name) { this._defaultAgentId = this._agents.size > 0 ? (this._agents.keys().next().value ?? null) : null; } for (const registeredAgent of this._agents.values()) { registeredAgent.removeDelegate(name); } return this; } /** * Register tools for the agents to use (if allowed) * @param tools */ addTools(tools) { if (tools instanceof tools_1.LlmToolKit) { this.tools.registerTools(tools.tools); if (tools.allowParallelCalls !== undefined) { this.tools.allowParallelCalls = tools.allowParallelCalls; } } else { this.tools.registerTools(tools); } return this; } /** * Hydrate a task definition into a task execution * If a task execution is passed, it will be returned as a copy * @param taskOrDefinition */ hydrateTask(taskOrDefinition) { return taskOrDefinition instanceof agents_1.TaskExecution ? taskOrDefinition.copy : new agents_1.TaskExecution(taskOrDefinition, this); } /** * Create a new task * @param task * @param config */ async createTask(task, config) { const agentId = config?.agent || this._defaultAgentId; if (!agentId) { throw new agents_1.TaskCreationError("No agent specified and no default agent set"); } const agent = agentId ? this.getAgent(agentId) : null; if (!agent) { throw new agents_1.TaskCreationError(`Agent ${agentId} is not registered`); } return agents_1.TaskExecution.fromTask(task, agent.name, this); } /** * Resume a task execution. Will * @param taskOrDefinition * @param env */ async resumeTask(taskOrDefinition, env) { const task = this.hydrateTask(taskOrDefinition); if (task.status === "completed" || task.status === "halted") { return task; } if (!task.activeThread.agent) throw new agents_1.TaskExecutionError(`Agent ${task.activeThread.agentId} not found`, task.id); if (task.status === "pending") { this._core.logger.info("Team", `Starting task with agent ${task.activeThread.agent.name}`); } else { this._core.logger.verbose("Team", `Resuming task on thread '${task.activeThread.id}' with agent ${task.activeThread.agent.name}`); } this._core.logger.silly("Team", `Task`, task.definition); if (task.activeThread.latestMessage.role === "assistant" && task.activeThread.id === "__main__") { task.status = "completed"; this._core.logger.verbose("Team", `Task completed`); return task; } if (task.status !== "running") task.status = "running"; if (task.activeThread.latestMessage.role === "user") { return this.generateAssistantMessage(task, env); } if (task.activeThread.latestMessage.role === "assistant" && task.activeThread.id !== "__main__") { return this.passAssistantResultToMainThread(task); } if (task.activeThread.latestMessage.role === "assistant_with_tools" && task.activeThread.latestMessage.toolCalls.every((toolCall) => toolCall.executionState === "completed" || toolCall.executionState === "error")) { return this.generateAssistantMessage(task, env); } if (task.activeThread.latestMessage.role === "assistant_with_tools") { return this.processToolCalls(task, env); } return task.halt("invalidState"); } /** * Execute a task to completion, or until stop condition is met (limit, approval, failure) * @param taskOrDefinition * @param env */ async executeTask(taskOrDefinition, env) { let task = this.hydrateTask(taskOrDefinition); let iterations = 0; const { limits } = env; while (true) { iterations++; if (task.status === "completed" || task.status === "halted") { return task; } if (limits?.maxGenerations && task.stats.generations >= limits.maxGenerations) { this._core.logger.warn("Team", `Max generations reached`); return task.halt("maxGenerations"); } if (limits?.maxDelegations && task.stats.delegations >= limits.maxDelegations) { this._core.logger.warn("Team", `Max delegations reached`); return task.halt("maxDelegations"); } if (iterations >= (limits?.maxIterations ?? 10)) { this._core.logger.warn("Team", `Max iterations reached`); return task.halt("maxIterations"); } task = await this.resumeTask(task, { context: env?.context, secureContext: env?.secureContext, }); } } /** * Generate an assistant message (either in response to a user message or tool call results) * @param task * @param env * @internal */ async generateAssistantMessage(task, env) { if (!task.activeThread.agent) throw new agents_1.TaskExecutionError(`Agent ${task.activeThread.agentId} not found`, task.id); const allowedToolNames = task.activeThread.agent.allowedToolNames; if (task.activeThread.agent.availableDelegateAgents.length > 0) allowedToolNames.push(this.delegateToAgentToolName); if (task.activeThread.agent.availableTransferAgents.length > 0) allowedToolNames.push(this.transferToAgentToolName); const response = await this._core.generate([(0, providers_1.generateSystemMessage)(task.activeThread.agent.systemMessage), ...task.activeThread.messages], { tools: this.tools.withAllowedToolsOnly(allowedToolNames), model: task.activeThread.agent.model ?? undefined, context: env?.context, secureContext: env?.secureContext, temperature: task.activeThread.agent.temperature ?? undefined, json: task.activeThread.agent.responseType === "json", }); task.activeThread.addEvent({ eventType: "generation", timestamp: new Date().getTime(), messageId: response.id, action: `Agent ${task.activeThread.agent.name} generated ${response.role} message based on ${task.activeThread.latestMessage.role} message`, model: response.meta.model, tokenUsage: { input: response.meta.inputTokens ?? null, output: response.meta.outputTokens ?? null, }, }); this._core.logger.verbose("Team", `Completed generation step (generate response to user message)`); task.activeThread.addMessage(response); task.stats.generations++; return task; } /** * Pass the result of an assistant message with tools back to the parent thread * @param task * @internal */ async passAssistantResultToMainThread(task) { if (task.activeThread.isMain) throw new agents_1.TaskExecutionError("Cannot return to other thread from main thread", task.id); const latestMessage = task.activeThread.latestMessage; if (!task.activeThread.agent) throw new agents_1.TaskExecutionError(`Agent ${task.activeThread.agentId} not found`, task.id); const parentThread = task.activeThread.parentThreadId ? task.threads[task.activeThread.parentThreadId] : null; if (!parentThread) throw new agents_1.TaskExecutionError(`Parent thread ${task.activeThread.parentThreadId} not found`, task.id); const originatingMessageIndex = parentThread.messages.findIndex((m) => m.role === "assistant_with_tools" && m.toolCalls.some((tc) => tc.id === task.activeThread.parentToolCallId)); if (originatingMessageIndex === -1) { throw new agents_1.TaskExecutionError("Unable to return to parent thread. Originating tool call not found", task.id); } const message = parentThread.messages[originatingMessageIndex]; parentThread.messages[originatingMessageIndex] = { ...message, role: "assistant_with_tools", toolCalls: message.toolCalls.map((tc) => tc.id === task.activeThread.parentToolCallId ? { id: tc.id, request: tc.request, approvalState: tc.approvalState, executionState: "completed", result: { conversationId: task.activeThread.id, message: latestMessage.content, }, } : tc), }; task.activeThread.addEvent({ eventType: "threadChange", timestamp: new Date().getTime(), targetThreadId: parentThread.id, action: `Agent ${task.activeThread.agent.name} returned execution to agent ${parentThread.agent?.name ?? "unknown"} (${parentThread.isMain ? "Main" : "Sub"} thread)`, messageId: latestMessage.id ?? "-", }); this._core.logger.info("Team", `Returning answer from "${task.activeThread.agent.name}" to "${parentThread.agent?.name ?? "unknown"}"`); task.activeThreadId = parentThread.id; this._core.logger.verbose("Team", `Changing active thread to parent thread ${parentThread.id}`); return task; } /** * Process tool calls in the assistant_with_tools message * @param task * @param env * @internal */ async processToolCalls(task, env) { const latestMessage = task.activeThread.latestMessage; const processedToolCalls = []; if (latestMessage.role !== "assistant_with_tools") { throw new agents_1.TaskExecutionError("Expected assistant_with_tools message", task.id); } if (!task.activeThread.agent) { throw new agents_1.TaskExecutionError(`Agent ${task.activeThread.agentId} not found`, task.id); } const allowedToolNames = task.activeThread.agent.allowedToolNames; if (task.activeThread.agent.availableDelegateAgents.length > 0) allowedToolNames.push(this.delegateToAgentToolName); if (task.activeThread.agent.availableTransferAgents.length > 0) allowedToolNames.push(this.transferToAgentToolName); const tools = this.tools.withAllowedToolsOnly(allowedToolNames); const preValidation = tools.classifyToolCalls(latestMessage.toolCalls); if (preValidation === "approvalPending") { task.halt("approvalRequired"); this._core.logger.verbose("Team", `Approval required for pending tool call`); return task; } if (preValidation === "missingExecutor") { throw new agents_1.TaskExecutionError("Missing executor for pending tool call", task.id); } let continueProcessing = true; for (let i = 0; i < latestMessage.toolCalls.length; i++) { const toolCall = latestMessage.toolCalls[i]; if (toolCall.executionState === "completed" || toolCall.executionState === "error") { processedToolCalls.push(toolCall); continue; } if (!continueProcessing) { processedToolCalls.push(toolCall); continue; } const tool = tools.getTool(toolCall.request.function.name); if (!tool) { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "toolNotFound", lastAttempt: new Date(), message: `Tool not found: ${toolCall.request.function.name}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.activeThread.addEvent({ eventType: "toolUse", timestamp: new Date().getTime(), messageId: toolCall.id, action: `Agent ${task.activeThread.agent.name} tried using tool ${toolCall.request.function.name}`, toolId: toolCall.request.function.name, toolArguments: toolCall.request.function.arguments, toolResult: null, toolError: `Tool not found: ${toolCall.request.function.name}`, }); continue; } if (tool.type === "functionDefinition") { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "toolNotExecutable", lastAttempt: new Date(), message: `Tool not executable: ${tool.name}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.activeThread.addEvent({ eventType: "toolUse", timestamp: new Date().getTime(), messageId: toolCall.id, action: `Agent ${task.activeThread.agent.name} tried using tool ${tool.name}`, toolId: toolCall.request.function.name, toolArguments: toolCall.request.function.arguments, toolResult: null, toolError: `Tool not executable: ${tool.name}`, }); continue; } if (tool.type === "function") { const result = await tools.processToolCall(toolCall, { context: env?.context, secureContext: env?.secureContext, // retryFailed: env?.retryFailed, }); processedToolCalls.push(result.toolCall); task.activeThread.addEvent({ eventType: "toolUse", timestamp: new Date().getTime(), messageId: toolCall.id, action: `Agent ${task.activeThread.agent.name} used tool ${tool.name}`, toolId: toolCall.request.function.name, toolArguments: toolCall.request.function.arguments, toolResult: result.toolCall.result, toolError: result?.toolCall.error?.message ?? null, }); if (!result.handled) { this._core.logger.warn("Team", `[Warning]: Tool call not handled: ${tool.name}`); } continue; } if (tool.type === "subTask") { if (!toolCall.request.function.arguments) { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "missingArguments", lastAttempt: new Date(), message: `No arguments provided for tool call ${tool.name}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } if (typeof toolCall.request.function.arguments !== "object" || !("agentName" in toolCall.request.function.arguments) || typeof toolCall.request.function.arguments.agentName !== "string" || toolCall.request.function.arguments.agentName === "" || !("taskDescription" in toolCall.request.function.arguments) || typeof toolCall.request.function.arguments.taskDescription !== "string" || toolCall.request.function.arguments.taskDescription === "") { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "invalidArguments", lastAttempt: new Date(), message: `Invalid arguments provided for tool call ${tool.name} - agentName and taskDescription must be a non-empty string`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } const agentName = toolCall.request.function.arguments.agentName; const taskDescription = toolCall.request.function.arguments.taskDescription; const delegate = task.activeThread.agent.getDelegate(agentName); if (!delegate) { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "delegateNotAvailable", lastAttempt: new Date(), message: `Agent ${task.activeThread.agent.name} is not allowed to delegate to ${agentName}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } const subThreadId = (0, shared_1.generateUniqueId)(); task.threads[subThreadId] = new agents_1.TaskExecutionThread({ id: subThreadId, agentId: delegate.name, messages: [await (0, providers_1.generateUserMessage)(taskDescription)], parentThreadId: task.activeThread.id, parentToolCallId: toolCall.id, events: [], modified: false, }, this); processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "inProgress", result: { message: `Task delegated to ${delegate.name}`, conversationId: subThreadId, }, error: null, }); task.activeThread.addEvent({ eventType: "delegation", timestamp: new Date().getTime(), messageId: toolCall.id, action: `Agent ${task.activeThread.agent.name} delegated to ${delegate.name}`, delegateToAgentName: delegate.name, }); task.activeThreadId = subThreadId; task.stats.delegations++; continueProcessing = false; continue; } if (tool.type === "transfer") { if (!toolCall.request.function.arguments) { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "missingArguments", lastAttempt: new Date(), message: `No arguments provided for tool call ${tool.name}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } if (typeof toolCall.request.function.arguments !== "object" || !("agentName" in toolCall.request.function.arguments) || typeof toolCall.request.function.arguments.agentName !== "string" || toolCall.request.function.arguments.agentName === "") { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "invalidArguments", lastAttempt: new Date(), message: `Invalid arguments provided for tool call ${tool.name} - agentName is required and must be a non-empty string`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } const agentName = toolCall.request.function.arguments.agentName; const delegate = task.activeThread.agent.getDelegate(agentName, "transfer"); if (!delegate) { processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "error", result: null, error: { type: "delegateNotAvailable", lastAttempt: new Date(), message: `Agent ${task.activeThread.agent.name} is not allowed to transfer to ${agentName}`, numberOfAttempts: 1, // toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, }, }); task.modified = true; continue; } processedToolCalls.push({ id: toolCall.id, request: toolCall.request, approvalState: toolCall.approvalState, executionState: "completed", result: { message: `Transfer from ${task.activeThread.agent.name} to ${delegate.name} successful`, }, error: null, }); task.activeThread.addEvent({ eventType: "transfer", timestamp: new Date().getTime(), messageId: toolCall.id, action: `Agent ${task.activeThread.agent.name} transferred to ${delegate.name}`, fromAgentName: task.activeThread.agent.name, toAgentName: delegate.name, }); task.activeThread.agentId = delegate.name; continueProcessing = false; } } latestMessage.toolCalls = processedToolCalls; this._core.logger.verbose("Team", `Completed assistant with tools step`); return task; } } exports.JorElAgentManager = JorElAgentManager;