@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
JavaScript
"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;