UNPKG

@waldzellai/adk-typescript

Version:

TypeScript SDK for Google Agent Development Kit (ADK) - A comprehensive framework for building AI agents

413 lines (412 loc) 17.3 kB
"use strict"; // Runners module for the Google Agent Development Kit (ADK) in TypeScript // Mirrors runners.py from the Python SDK Object.defineProperty(exports, "__esModule", { value: true }); exports.InMemoryRunner = exports.Runner = void 0; const invocation_context_1 = require("./agents/invocation_context"); const live_request_queue_1 = require("./agents/live_request_queue"); const run_config_1 = require("./agents/run_config"); const in_memory_artifact_service_1 = require("./artifacts/in_memory_artifact_service"); const event_1 = require("./events/event"); const in_memory_memory_service_1 = require("./memory/in_memory_memory_service"); const in_memory_session_service_1 = require("./sessions/in_memory_session_service"); const built_in_code_execution_tool_1 = require("./tools/built_in_code_execution_tool"); // Logger placeholder - in a real implementation, this would be replaced with a proper logging solution console.log('Logger placeholder for runners.ts'); /** * The Runner class is used to run agents. * * It manages the execution of an agent within a session, handling message * processing, event generation, and interaction with various services like * artifact storage, session management, and memory. */ class Runner { constructor(options) { this.appName = options.appName; this.agent = options.agent; this.artifactService = options.artifactService; this.sessionService = options.sessionService; this.memoryService = options.memoryService; } /** * Runs the agent. * * NOTE: This sync interface is only for local testing and convenience purposes. * Consider using `runAsync` for production usage. * * @param options.userId The user ID of the session. * @param options.sessionId The session ID of the session. * @param options.newMessage A new message to append to the session. * @param options.runConfig The run config for the agent. * @returns An array of events generated by the agent. */ run(options) { // For simplicity, in this implementation we'll collect and return all events const events = []; // Create a promise to run the async generator const runPromise = (async () => { for await (const event of this.runAsync(options)) { events.push(event); } })(); // Block until the promise is resolved (NOT RECOMMENDED in production) // This is a workaround for the synchronous requirement runPromise.then(); // Start the promise execution // Return the collected events return events; } /** * Main entry method to run the agent in this runner. * * @param options.userId The user ID of the session. * @param options.sessionId The session ID of the session. * @param options.newMessage A new message to append to the session. * @param options.runConfig The run config for the agent. * @returns An async generator yielding the events generated by the agent. */ async *runAsync(options) { const { userId, sessionId, newMessage } = options; const runConfig = options.runConfig || new run_config_1.RunConfig(); // Get or create the session let session; if (runConfig.loadSession) { const existingSession = this.sessionService.getSession(this.appName, userId, sessionId); if (existingSession) { session = existingSession; } else { console.log(`Session not found. Creating a new session with ID: ${sessionId}`); session = this.sessionService.createSession(this.appName, userId, {}, // empty initial state sessionId); } } else { // Create a new session regardless of whether one exists session = this.sessionService.createSession(this.appName, userId, {}, // empty initial state sessionId); } // Create the invocation context const context = this._newInvocationContext({ agent: this.agent, runConfig, session, userContent: newMessage, userId, }); // Append the new message to the session if (newMessage) { const event = await this._appendNewMessageToSession(context); yield event; } // Update memory if requested and available if (runConfig.loadMemory && this.memoryService) { try { // In the Python implementation, there's an update method // In our TypeScript implementation, we'll use searchMemory as a placeholder // since we don't have a direct equivalent this.memoryService.searchMemory(this.appName, userId, ''); } catch (error) { console.error('Failed to update memory:', error); } } // Find the agent to run (typically the root agent or a sub-agent based on state) const agentToRun = this._findAgentToRun(context); // Update the context with the agent to run const updatedContext = context.withModifications({ agent: agentToRun }); // Run the agent for await (const event of agentToRun.runAsync(updatedContext)) { if (runConfig.saveSession) { // Append the event to the session this.sessionService.appendEvent(session, event); } yield event; } // No need for additional save after agent execution as events are appended during the loop } /** * Creates a new invocation context. * * @param options Options for creating the invocation context * @returns The new invocation context */ _newInvocationContext(options) { const { agent, runConfig, session, userContent, userId } = options; // Handle CFC support if (runConfig.supportCfc && 'canonicalModel' in agent) { const modelName = agent.canonicalModel?.modelName; if (modelName && !modelName.startsWith('gemini-2')) { throw new Error(`CFC is not supported for model: ${modelName} in agent: ${agent.name}`); } // Add built-in code execution tool if not already present if ('tools' in agent && Array.isArray(agent.tools)) { // Check if the tool is already added const hasCodeExecution = agent.tools.some((tool) => tool.name === 'code_execution'); if (!hasCodeExecution) { agent.tools.push(built_in_code_execution_tool_1.builtInCodeExecution); } // Also add to canonical tools if available if ('canonicalTools' in agent && Array.isArray(agent.canonicalTools)) { const hasCanonicalCodeExecution = agent.canonicalTools.some((tool) => tool.name === 'code_execution'); if (!hasCanonicalCodeExecution) { agent.canonicalTools.push(built_in_code_execution_tool_1.builtInCodeExecution); } } } } return new invocation_context_1.InvocationContext({ agent, runConfig, artifactService: this.artifactService || new in_memory_artifact_service_1.InMemoryArtifactService(), memoryService: this.memoryService, sessionService: this.sessionService, session, userContent, appName: this.appName, userId, }); } /** * Creates a new invocation context for live execution. * * @param options Options for creating the live invocation context * @returns The new live invocation context */ _newInvocationContextForLive(options) { const { agent, runConfig, session, requestQueue, userId } = options; return new invocation_context_1.InvocationContext({ agent, runConfig, requestQueue, artifactService: this.artifactService || new in_memory_artifact_service_1.InMemoryArtifactService(), memoryService: this.memoryService, sessionService: this.sessionService, session, appName: this.appName, userId, }); } /** * Appends a new message to the session and creates an event. * * @param context The invocation context * @returns The created event */ async _appendNewMessageToSession(context) { if (!context.session || !context.userContent) { throw new Error('Session and user content must be provided to append a message'); } // Clone the user content to avoid modifying the original const userContent = JSON.parse(JSON.stringify(context.userContent)); // Handle artifacts if needed if (this.artifactService && context.runConfig.saveInputBlobsAsArtifacts) { // The runner directly saves the artifacts (if applicable) in the // user message and replaces the artifact data with a file name placeholder if (userContent.parts && userContent.parts.length > 0) { for (let i = 0; i < userContent.parts.length; i++) { const part = userContent.parts[i]; if (part.inlineData) { const fileName = `artifact_${context.invocationId}_${i}`; this.artifactService.saveArtifact(this.appName, context.session.userId, context.session.id, fileName, part); // Replace the inline data with a text reference userContent.parts[i] = { text: `Uploaded file: ${fileName}. It is saved into artifacts` }; } } } } const event = new event_1.Event({ author: 'user', content: userContent, invocationId: context.invocationId, }); context.session.addEvent(event); return event; } /** * Finds the appropriate agent to run based on the current state. * * @param context The invocation context * @returns The agent to run */ _findAgentToRun(context) { if (!context.session) { return this.agent; } // Check if there's a current agent in the session state const currentAgentName = context.session.getStateValue('current_agent'); if (!currentAgentName) { return this.agent; } // Find the agent with the given name in the agent tree const foundAgent = this._findAgentByName(this.agent, currentAgentName); return foundAgent || this.agent; } /** * Recursively searches for an agent by name in the agent tree. * * @param agent The current agent to check * @param name The name to search for * @returns The found agent or null if not found */ _findAgentByName(agent, name) { if (agent.name === name) { return agent; } for (const subAgent of agent.subAgents) { const found = this._findAgentByName(subAgent, name); if (found) { return found; } } return null; } /** * Checks if an agent can be transferred to across the agent tree. * * @param agentToCheck The agent to check * @returns True if the agent can be transferred to, false otherwise */ _isTransferableAcrossAgentTree(agentToCheck) { // Check if the agent is an LlmAgent if (!('disallowTransferToParent' in agentToCheck)) { return false; } // Check if the agent disallows transfer if (agentToCheck.disallowTransferToParent) { return false; } // Check parent agents recursively let currentAgent = agentToCheck; while (currentAgent) { if (!('disallowTransferToParent' in currentAgent)) { return false; } if (currentAgent.disallowTransferToParent) { return false; } // Move to parent agent const parent = currentAgent.parentAgent; // Explicitly cast the parent to the type required by the loop checks currentAgent = parent; } return true; } /** * Closes the current session, performing cleanup as needed. * * @param sessionId The ID of the session to close * @param userId The ID of the user */ async closeSession(sessionId, userId) { try { console.log(`Closing session ${sessionId} for user ${userId}`); } catch (error) { console.error(`Error closing session ${sessionId}:`, error); } } /** * Runs the agent in live (streaming) mode. * * This method is designed for bidirectional streaming where the user can * send multiple messages during an agent invocation, especially useful for * processing real-time audio or other streaming data. * * @param options.userId The user ID of the session * @param options.sessionId The session ID of the session * @param options.runConfig The run configuration * @param options.initialRequest The initial live request to send * @returns An async generator yielding events generated by the agent */ async *runLive(options) { const { userId, sessionId } = options; const runConfig = options.runConfig || new run_config_1.RunConfig(); // Set streaming mode to at least SSE if not already specified if (runConfig.streamingMode === run_config_1.StreamingMode.NONE) { runConfig.streamingMode = run_config_1.StreamingMode.SSE; } // Get or create the session let session; if (runConfig.loadSession) { const existingSession = this.sessionService.getSession(this.appName, userId, sessionId); if (existingSession) { session = existingSession; } else { console.log(`Session not found. Creating a new session with ID: ${sessionId}`); session = this.sessionService.createSession(this.appName, userId, {}, // empty initial state sessionId); } } else { // Create a new session regardless of whether one exists session = this.sessionService.createSession(this.appName, userId, {}, // empty initial state sessionId); } // Create a request queue for live interaction const requestQueue = new live_request_queue_1.LiveRequestQueue(); // Add the initial request if provided if (options.initialRequest) { requestQueue.send(options.initialRequest); } // Update memory if requested and available if (runConfig.loadMemory && this.memoryService) { try { // In the Python implementation, there's an update method // In our TypeScript implementation, we'll use searchMemory as a placeholder // since we don't have a direct equivalent this.memoryService.searchMemory(this.appName, userId, ''); } catch (error) { console.error('Failed to update memory:', error); } } // Find the agent to run const agentToRun = this._findAgentToRun(this._newInvocationContext({ agent: this.agent, runConfig, session, userContent: { role: 'user', parts: [] }, userId, })); // Create the live invocation context const context = this._newInvocationContextForLive({ agent: agentToRun, runConfig, session, requestQueue, userId, }); // Run the agent in live mode for await (const event of agentToRun.runLiveAsync(context)) { if (runConfig.saveSession) { // Append the event to the session this.sessionService.appendEvent(session, event); } yield event; } // No need for additional save after agent execution as events are appended during the loop } } exports.Runner = Runner; /** * An in-memory Runner for testing and development. * * This runner uses in-memory implementations for artifact, session, and memory * services, providing a lightweight and self-contained environment for agent * execution. */ class InMemoryRunner extends Runner { constructor(agent, appName = 'InMemoryRunner') { super({ appName, agent, artifactService: new in_memory_artifact_service_1.InMemoryArtifactService(), sessionService: new in_memory_session_service_1.InMemorySessionService(), memoryService: new in_memory_memory_service_1.InMemoryMemoryService(), }); } } exports.InMemoryRunner = InMemoryRunner;