adk-typescript
Version:
TypeScript port of Google's Agent Development Kit (ADK)
446 lines (445 loc) • 18.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InMemoryRunner = exports.Runner = void 0;
const uuid_1 = require("uuid");
const InvocationContext_1 = require("./agents/InvocationContext");
const LlmAgent_1 = require("./agents/LlmAgent");
const LiveRequestQueue_1 = require("./agents/LiveRequestQueue");
const RunConfig_1 = require("./agents/RunConfig");
const InMemoryArtifactService_1 = require("./artifacts/InMemoryArtifactService");
const Event_1 = require("./events/Event");
const InMemoryMemoryService_1 = require("./memory/InMemoryMemoryService");
const InMemorySessionService_1 = require("./sessions/InMemorySessionService");
const telemetry_1 = require("./telemetry");
const BuiltInCodeExecutionTool_1 = require("./tools/BuiltInCodeExecutionTool");
// Logger is a placeholder - implement with proper logging library
const logger = {
warning: (message, ...args) => console.warn(message, ...args),
info: (message, ...args) => console.info(message, ...args),
error: (message, ...args) => console.error(message, ...args)
};
/**
* 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 {
/**
* Initializes the Runner.
*
* @param params The parameters for the runner.
* @param params.appName The application name of the runner.
* @param params.agent The root agent to run.
* @param params.artifactService The artifact service for the runner.
* @param params.sessionService The session service for the runner.
* @param params.memoryService The memory service for the runner.
*/
constructor(params) {
this.appName = params.appName;
this.agent = params.agent;
this.artifactService = params.artifactService;
this.sessionService = params.sessionService;
this.memoryService = params.memoryService;
}
/**
* Runs the agent.
*
* NOTE: This sync interface is only for local testing and convenience purpose.
* Consider using `runAsync` for production usage.
*
* @param params The parameters for the run.
* @param params.userId The user ID of the session.
* @param params.sessionId The session ID of the session.
* @param params.newMessage A new message to append to the session.
* @param params.runConfig The run config for the agent.
* @returns A generator that yields the events generated by the agent.
*/
async *run(params) {
const { userId, sessionId, newMessage, runConfig = new RunConfig_1.RunConfig() } = params;
// In JavaScript, we can just use the async generator directly
for await (const event of this.runAsync({
userId,
sessionId,
newMessage,
runConfig
})) {
yield event;
}
}
/**
* Main entry method to run the agent in this runner.
*
* @param params The parameters for the run.
* @param params.userId The user ID of the session.
* @param params.sessionId The session ID of the session.
* @param params.newMessage A new message to append to the session.
* @param params.runConfig The run config for the agent.
* @returns An async generator that yields events generated by the agent.
*/
async *runAsync(params) {
const { userId, sessionId, newMessage, runConfig = new RunConfig_1.RunConfig() } = params;
const span = telemetry_1.tracer.startAsCurrentSpan('invocation');
try {
const session = await this.sessionService.getSession({
appName: this.appName,
userId: userId,
sessionId: sessionId
});
if (!session) {
throw new Error(`Session not found: ${sessionId}`);
}
const invocationContext = this._newInvocationContext({
session: session,
newMessage,
runConfig
});
const rootAgent = this.agent;
if (newMessage) {
await this._appendNewMessageToSession({
session: session,
newMessage,
invocationContext,
saveInputBlobsAsArtifacts: runConfig.saveInputBlobsAsArtifacts
});
}
invocationContext.agent = this._findAgentToRun(session, rootAgent);
// Use invoke method which is guaranteed to exist
for await (const event of invocationContext.agent.invoke(invocationContext)) {
if (!event.partial) {
await this.sessionService.appendEvent({
session: session,
event: event
});
}
yield event;
}
}
finally {
span.end();
}
}
/**
* Appends a new message to a session.
*
* @param params The parameters for appending a message.
* @param params.session The session to append to.
* @param params.newMessage The message to append.
* @param params.invocationContext The invocation context.
* @param params.saveInputBlobsAsArtifacts Whether to save input blobs as artifacts.
* @private
*/
async _appendNewMessageToSession(params) {
const { session, newMessage, invocationContext, saveInputBlobsAsArtifacts = false } = params;
if (this.artifactService && saveInputBlobsAsArtifacts) {
// The runner directly saves the artifacts (if applicable) in the
// user message and replaces the artifact data with a file name
// placeholder.
for (let i = 0; i < newMessage.parts.length; i++) {
const part = newMessage.parts[i];
if (!part.inlineData) {
continue;
}
const fileName = `artifact_${invocationContext.invocationId}_${i}`;
try {
const saveResult = this.artifactService.saveArtifact({
appName: this.appName,
userId: session.userId,
sessionId: session.id,
filename: fileName,
artifact: part
});
// Handle both synchronous and asynchronous cases
if (saveResult instanceof Promise) {
await saveResult;
}
newMessage.parts[i] = {
text: `Uploaded file: ${fileName}. It is saved into artifacts`
};
}
catch (error) {
console.error('Error saving artifact:', error);
// Continue with the message even if saving artifact fails
}
}
}
// Appends only. We do not yield the event because it's not from the model.
const event = new Event_1.Event({
invocationId: invocationContext.invocationId,
author: 'user',
content: newMessage
});
await this.sessionService.appendEvent({
session: session,
event: event
});
}
/**
* Runs the agent in live mode.
*
* @param params The parameters for live mode.
* @param params.session The session to use.
* @param params.liveRequestQueue The queue for live requests.
* @param params.runConfig The run config for the agent.
* @returns An async generator of events.
*
* @experimental This feature is **experimental** and its API or behavior may change
* in future releases.
*/
async *runLive(params) {
const { session, liveRequestQueue, runConfig = new RunConfig_1.RunConfig() } = params;
// TODO: right now, only works for a single audio agent without FC.
const invocationContext = this._newInvocationContextForLive({
session: session,
liveRequestQueue,
runConfig
});
const rootAgent = this.agent;
invocationContext.agent = this._findAgentToRun(session, rootAgent);
invocationContext.activeStreamingTools = new Map();
// Get tools from the agent, assuming it has a canonicalTools property
const tools = invocationContext.agent.canonicalTools || [];
// Initialize streaming tools that rely on LiveRequestQueue
for (const tool of tools) {
// Check if tool requires LiveRequestQueue
// This is a bit complex to port directly - in TypeScript we would need
// to use reflection or decorator metadata to check parameter types
if (tool.usesLiveQueue) {
if (!invocationContext.activeStreamingTools) {
invocationContext.activeStreamingTools = new Map();
}
// Create an object with appropriate structure for ActiveStreamingTool
// Based on inspected constructor, ActiveStreamingTool expects name, args, id
const streamObj = {
name: tool.name,
args: {},
id: (0, uuid_1.v4)(),
stream: new LiveRequestQueue_1.LiveRequestQueue()
};
// Add to map
invocationContext.activeStreamingTools.set(tool.name,
// Using any type assertion since we may not have the correct constructor
// signature but we know the object structure should work
streamObj);
}
}
// Set invocation context to live mode
invocationContext.live = true;
// Use invoke method which will call the appropriate implementation based on live flag
for await (const event of invocationContext.agent.invoke(invocationContext)) {
await this.sessionService.appendEvent({
session: session,
event: event
});
yield event;
}
}
/**
* Closes a session and adds it to the memory service.
*
* @param session The session to close.
* @experimental This feature is **experimental** and its API or behavior may change
* in future releases.
*/
async closeSession(session) {
if (this.memoryService) {
await this.memoryService.addSessionToMemory(session);
}
await this.sessionService.closeSession({
session: session
});
}
/**
* Finds the agent to run to continue the session.
*
* A qualified agent must be either of:
* - The root agent;
* - An LlmAgent who replied last and is capable to transfer to any other agent
* in the agent hierarchy.
*
* @param session The session to find the agent for.
* @param rootAgent The root agent of the runner.
* @returns The agent of the last message in the session or the root agent.
* @private
*/
_findAgentToRun(session, rootAgent) {
// Make sure session has required properties
if (!session.agents) {
session.agents = new Map();
}
// Make sure the root agent is registered in the session
if (!session.agents.has(rootAgent.name)) {
session.agents.set(rootAgent.name, rootAgent);
}
// Add getAgent method if not present
if (!session.getAgent) {
session.getAgent = function (name) {
return this.agents.get(name);
};
}
// Filter for non-user events and process them in reverse order
const nonUserEvents = session.events.filter(e => e.author !== 'user');
for (let i = nonUserEvents.length - 1; i >= 0; i--) {
const event = nonUserEvents[i];
if (event.author === rootAgent.name) {
// Found root agent
return rootAgent;
}
const agent = rootAgent.findSubAgent(event.author);
if (!agent) {
// Agent not found, continue looking
logger.warning(`Event from an unknown agent: ${event.author}, event id: ${event.id}`);
continue;
}
if (this._isTransferableAcrossAgentTree(agent)) {
return agent;
}
}
// Falls back to root agent if no suitable agents are found in the session
return rootAgent;
}
/**
* Whether the agent to run can transfer to any other agent in the agent tree.
*
* This typically means all agent_to_run's parent through root agent can
* transfer to their parent_agent.
*
* @param agentToRun The agent to check for transferability.
* @returns True if the agent can transfer, False otherwise.
* @private
*/
_isTransferableAcrossAgentTree(agentToRun) {
let agent = agentToRun;
while (agent) {
if (!(agent instanceof LlmAgent_1.LlmAgent)) {
// Only LLM-based Agent can provide agent transfer capability
return false;
}
const llmAgent = agent;
// Check if transfers to peers are allowed - in TypeScript we use disallowTransferToPeers
if (llmAgent.disallowTransferToPeers === true) {
return false;
}
agent = agent.parentAgent;
}
return true;
}
/**
* Creates a new invocation context.
*
* @param params The parameters for the invocation context.
* @param params.session The session for the context.
* @param params.newMessage The new message for the context.
* @param params.liveRequestQueue The live request queue for the context.
* @param params.runConfig The run config for the context.
* @returns The new invocation context.
* @private
*/
_newInvocationContext(params) {
const { session, newMessage, liveRequestQueue, runConfig = new RunConfig_1.RunConfig() } = params;
const invocationId = (0, uuid_1.v4)();
// Get the LLM model to use
let llm;
if (this.agent instanceof LlmAgent_1.LlmAgent) {
try {
llm = this.agent.canonicalModel;
}
catch (error) {
console.error('Error getting canonicalModel:', error);
}
}
if (runConfig.supportCfc && this.agent instanceof LlmAgent_1.LlmAgent) {
// Get the agent's model name using canonicalModel
const llmAgent = this.agent;
const modelName = llmAgent.canonicalModel.model || 'unknown';
if (!modelName.startsWith('gemini-2')) {
throw new Error(`CFC is not supported for model: ${modelName} in agent: ${this.agent.name}`);
}
// Check if built-in code execution tool is already included
const tools = llmAgent.canonicalTools || [];
const hasCodeExecutionTool = tools.some((tool) => tool instanceof BuiltInCodeExecutionTool_1.BuiltInCodeExecutionTool);
if (!hasCodeExecutionTool) {
llmAgent.canonicalTools.push(new BuiltInCodeExecutionTool_1.BuiltInCodeExecutionTool());
}
}
// Create context with the model set
const context = new InvocationContext_1.InvocationContext({
artifactService: this.artifactService,
sessionService: this.sessionService,
memoryService: this.memoryService,
invocationId: invocationId,
agent: this.agent,
session: session,
userContent: newMessage,
liveRequestQueue: liveRequestQueue,
runConfig: runConfig,
llm: llm
});
// Debug log if model is missing
if (!context.llm) {
console.warn(`No LLM model set in invocation context for agent: ${this.agent.name}`);
}
return context;
}
/**
* Creates a new invocation context for live multi-agent.
*
* @param params The parameters for the invocation context.
* @param params.session The session for the context.
* @param params.liveRequestQueue The live request queue for the context.
* @param params.runConfig The run config for the context.
* @returns The new invocation context.
* @private
*/
_newInvocationContextForLive(params) {
const { session, liveRequestQueue, runConfig = new RunConfig_1.RunConfig() } = params;
// For live multi-agent, we need model's text transcription as context for
// next agent
if (this.agent.subAgents && this.agent.subAgents.length > 0 && liveRequestQueue) {
if (!runConfig.responseModalities || runConfig.responseModalities.length === 0) {
// default
runConfig.responseModalities = ['AUDIO'];
if (!runConfig.outputAudioTranscription) {
runConfig.outputAudioTranscription = {};
}
}
else if (!runConfig.responseModalities.includes('TEXT')) {
if (!runConfig.outputAudioTranscription) {
runConfig.outputAudioTranscription = {};
}
}
}
return this._newInvocationContext({
session,
liveRequestQueue,
runConfig
});
}
}
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 {
/**
* Initializes the InMemoryRunner.
*
* @param agent The root agent to run.
* @param appName The application name of the runner. Defaults to 'InMemoryRunner'.
*/
constructor(agent, appName = 'InMemoryRunner') {
super({
appName: appName,
agent: agent,
artifactService: new InMemoryArtifactService_1.InMemoryArtifactService(),
sessionService: new InMemorySessionService_1.InMemorySessionService(),
memoryService: new InMemoryMemoryService_1.InMemoryMemoryService()
});
}
}
exports.InMemoryRunner = InMemoryRunner;