Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions core/src/agents/llm_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ import {BaseContextCompactor} from '../context/base_context_compactor.js';
import {InvocationContext} from './invocation_context.js';
import {AGENT_TRANSFER_LLM_REQUEST_PROCESSOR} from './processors/agent_transfer_llm_request_processor.js';
import {BASIC_LLM_REQUEST_PROCESSOR} from './processors/basic_llm_request_processor.js';
import {CODE_EXECUTION_REQUEST_PROCESSOR} from './processors/code_execution_request_processor.js';
import {
CODE_EXECUTION_REQUEST_PROCESSOR,
CODE_EXECUTION_RESPONSE_PROCESSOR,
} from './processors/code_execution_request_processor.js';
import {CONTENT_REQUEST_PROCESSOR} from './processors/content_request_processor.js';
import {ContextCompactorRequestProcessor} from './processors/context_compactor_request_processor.js';
import {IDENTITY_LLM_REQUEST_PROCESSOR} from './processors/identity_llm_request_processor.js';
Expand Down Expand Up @@ -431,7 +434,10 @@ export class LlmAgent extends BaseAgent<LlmAgentConfig> {
}
}

this.responseProcessors = config.responseProcessors ?? [];
// Orders matter, don't change. Append new processors to the end
this.responseProcessors = config.responseProcessors ?? [
CODE_EXECUTION_RESPONSE_PROCESSOR,
];

// Preserve the agent transfer behavior.
const agentTransferDisabled =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ export class CodeExecutionResponseProcessor implements BaseLlmResponseProcessor
}

/**
* The exported response processor instance.
* The exported code execution response processor instance.
*/
export const responseProcessor = new CodeExecutionResponseProcessor();
export const CODE_EXECUTION_RESPONSE_PROCESSOR =
new CodeExecutionResponseProcessor();

/**
* Pre-processes the user message by adding the user message to the execution
Expand Down
120 changes: 119 additions & 1 deletion core/test/agents/llm_agent_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@

import {
AUTH_PREPROCESSOR,
BaseCodeExecutor,
BaseLlm,
BaseLlmConnection,
BaseLlmRequestProcessor,
BaseLlmResponseProcessor,
BasePlugin,
BaseTool,
CodeExecutionInput,
CodeExecutionResult,
CONTENT_REQUEST_PROCESSOR,
Context,
ContextCompactorRequestProcessor,
createEvent,
createSession,
Event,
ExecuteCodeParams,
InMemoryArtifactService,
InvocationContext,
LlmAgent,
LlmRequest,
Expand All @@ -26,10 +32,12 @@ import {
Session,
ToolProcessLlmRequest,
} from '@google/adk';
import {Content, Schema, Type} from '@google/genai';
import {Content, Outcome, Schema, Type} from '@google/genai';
import {beforeEach, describe, expect, it} from 'vitest';
import {z as z3} from 'zod/v3';
import {z as z4} from 'zod/v4';
import {CODE_EXECUTION_RESPONSE_PROCESSOR} from '../../src/agents/processors/code_execution_request_processor.js';
import {ScopedArtifactService} from '../../src/artifacts/scoped_artifact_service.js';

class MockLlmConnection implements BaseLlmConnection {
sendHistory(_history: Content[]): Promise<void> {
Expand Down Expand Up @@ -839,3 +847,113 @@ describe('LlmAgent Default Request Processors', () => {
expect(authIndex).toBeLessThan(contentIndex);
});
});

class RecordingCodeExecutor extends BaseCodeExecutor {
readonly calls: CodeExecutionInput[] = [];

async executeCode(params: ExecuteCodeParams): Promise<CodeExecutionResult> {
this.calls.push(params.codeExecutionInput);
return {stdout: 'hello\n', stderr: '', outputFiles: []};
}
}

/** A mock LLM that returns one queued response per call. */
class SequencedMockLlm extends BaseLlm {
constructor(private readonly responses: LlmResponse[]) {
super({model: 'sequenced-mock-llm'});
}

async *generateContentAsync(
_request: LlmRequest,
): AsyncGenerator<LlmResponse, void, void> {
const response = this.responses.shift();
if (response) {
yield response;
}
}

async connect(_llmRequest: LlmRequest): Promise<BaseLlmConnection> {
return new MockLlmConnection();
}
}

describe('LlmAgent Default Response Processors', () => {
it('includes CODE_EXECUTION_RESPONSE_PROCESSOR when a codeExecutor is set', () => {
const agent = new LlmAgent({
name: 'test_agent',
model: new MockLlm(null),
codeExecutor: new RecordingCodeExecutor(),
});
expect(agent.responseProcessors).toContain(
CODE_EXECUTION_RESPONSE_PROCESSOR,
);
});

it('includes CODE_EXECUTION_RESPONSE_PROCESSOR when no codeExecutor is set', () => {
const agent = new LlmAgent({name: 'test_agent'});
expect(agent.responseProcessors).toContain(
CODE_EXECUTION_RESPONSE_PROCESSOR,
);
});

it('uses caller-supplied responseProcessors verbatim, including an empty list', () => {
const agent = new LlmAgent({name: 'test_agent', responseProcessors: []});
expect(agent.responseProcessors).toHaveLength(0);
});

it('executes a model-emitted code block with only codeExecutor configured', async () => {
const executor = new RecordingCodeExecutor();
const agent = new LlmAgent({
name: 'test_agent',
model: new SequencedMockLlm([
{
content: {
role: 'model',
parts: [
{text: 'Here is the code:\n```python\nprint("hello")\n```'},
],
},
},
{content: {role: 'model', parts: [{text: 'Execution finished.'}]}},
]),
codeExecutor: executor,
});

const appName = 'test_app';
const userId = 'test_user';
const sessionId = 'sess_123';
const invocationContext = new InvocationContext({
invocationId: 'inv_123',
session: createSession({id: sessionId, appName, userId, events: []}),
agent,
pluginManager: new PluginManager(),
artifactService: new ScopedArtifactService(
new InMemoryArtifactService(),
appName,
userId,
sessionId,
),
});

const events: Event[] = [];
for await (const event of agent.runAsync(invocationContext)) {
events.push(event);
}

expect(executor.calls).toHaveLength(1);
expect(executor.calls[0].code).toBe('print("hello")');

const resultParts = events.flatMap(
(e) => e.content?.parts?.filter((p) => p.codeExecutionResult) ?? [],
);
expect(resultParts).toHaveLength(1);
expect(resultParts[0].codeExecutionResult?.outcome).toBe(
Outcome.OUTCOME_OK,
);
expect(resultParts[0].text).toContain('hello');

expect(events.at(-1)?.content?.parts?.[0]?.text).toBe(
'Execution finished.',
);
});
});
160 changes: 103 additions & 57 deletions tests/integration/agents/agent_with_sandbox_executor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,100 +5,104 @@
*/

import {Client} from '@google-cloud/vertexai';
import type {Event} from '@google/adk';
import {AgentEngineSandboxCodeExecutor, LlmAgent} from '@google/adk';
import {responseProcessor} from '@google/adk/agents/processors/code_execution_request_processor.js';
import {FinishReason} from '@google/genai';
import {CODE_EXECUTION_RESPONSE_PROCESSOR} from '@google/adk/agents/processors/code_execution_request_processor.js';
import {FinishReason, Outcome} from '@google/genai';
import {describe, expect, it, vi} from 'vitest';
import {
createRunner,
GeminiWithMockResponses,
RawGenerateContentResponse,
} from '../test_case_utils.js';

describe('Agent with AgentEngineSandboxCodeExecutor', () => {
it('executes code generated by the agent', async () => {
const mockResponses: RawGenerateContentResponse[] = [
const MOCK_RESPONSES: RawGenerateContentResponse[] = [
{
candidates: [
{
candidates: [
{
content: {
parts: [
{
text: 'Here is the code to print hello:\n```python\nprint("hello")\n```',
},
],
role: 'model',
content: {
parts: [
{
text: 'Here is the code to print hello:\n```python\nprint("hello")\n```',
},
finishReason: FinishReason.STOP,
},
],
],
role: 'model',
},
finishReason: FinishReason.STOP,
},
],
},
{
candidates: [
{
candidates: [
{
content: {
parts: [{text: 'Execution was successful.'}],
role: 'model',
},
finishReason: FinishReason.STOP,
},
],
content: {
parts: [{text: 'Execution was successful.'}],
role: 'model',
},
finishReason: FinishReason.STOP,
},
];
],
},
];

const mockClient = {
agentEnginesInternal: {
function createMockClient() {
return {
agentEnginesInternal: {
createInternal: vi.fn().mockResolvedValue({
name: 'operations/create-engine-op',
done: true,
response: {
name: 'projects/test-project/locations/us-central1/reasoningEngines/123',
},
}),
sandboxes: {
getInternal: vi.fn().mockResolvedValue({
state: 'STATE_RUNNING',
}),
createInternal: vi.fn().mockResolvedValue({
name: 'operations/create-engine-op',
name: 'operations/create-sandbox-op',
done: true,
response: {
name: 'projects/test-project/locations/us-central1/reasoningEngines/123',
name: 'projects/test-project/locations/us-central1/reasoningEngines/123/sandboxEnvironments/456',
},
}),
sandboxes: {
getInternal: vi.fn().mockResolvedValue({
state: 'STATE_RUNNING',
}),
createInternal: vi.fn().mockResolvedValue({
name: 'operations/create-sandbox-op',
done: true,
response: {
name: 'projects/test-project/locations/us-central1/reasoningEngines/123/sandboxEnvironments/456',
executeCodeInternal: vi.fn().mockResolvedValue({
outputs: [
{
mimeType: 'application/json',
data: Buffer.from(
JSON.stringify({msg_out: 'hello', msg_err: ''}),
).toString('base64'),
},
}),
executeCodeInternal: vi.fn().mockResolvedValue({
outputs: [
{
mimeType: 'application/json',
data: Buffer.from(
JSON.stringify({msg_out: 'hello', msg_err: ''}),
).toString('base64'),
},
],
}),
},
],
}),
},
};
},
};
}

describe('Agent with AgentEngineSandboxCodeExecutor', () => {
it('executes code generated by the agent', async () => {
const mockClient = createMockClient();

const executor = new AgentEngineSandboxCodeExecutor({
projectId: 'test-project',
client: mockClient as unknown as Client,
});

const model = new GeminiWithMockResponses(mockResponses);
const model = new GeminiWithMockResponses(MOCK_RESPONSES);
const agent = new LlmAgent({
model,
name: 'coderAgent',
description: 'An agent that writes and runs code',
instruction: 'Write code to solve the user request.',
codeExecutor: executor,
responseProcessors: [responseProcessor],
responseProcessors: [CODE_EXECUTION_RESPONSE_PROCESSOR],
});

const {run} = await createRunner(agent);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const events: any[] = [];
const events: Event[] = [];
for await (const event of run('Print hello')) {
events.push(event);
}
Expand All @@ -125,4 +129,46 @@ describe('Agent with AgentEngineSandboxCodeExecutor', () => {

expect(hasExecutionResult).toBe(true);
});

it('executes code with no explicit responseProcessors', async () => {
const mockClient = createMockClient();

const executor = new AgentEngineSandboxCodeExecutor({
projectId: 'test-project',
client: mockClient as unknown as Client,
});

const model = new GeminiWithMockResponses(MOCK_RESPONSES);
const agent = new LlmAgent({
model,
name: 'coderAgent',
description: 'An agent that writes and runs code',
instruction: 'Write code to solve the user request.',
codeExecutor: executor,
});

const {run} = await createRunner(agent);

const events: Event[] = [];
for await (const event of run('Print hello')) {
events.push(event);
}

expect(
mockClient.agentEnginesInternal.sandboxes.executeCodeInternal,
).toHaveBeenCalledTimes(1);

const resultParts = events.flatMap(
(e) => e.content?.parts?.filter((p) => p.codeExecutionResult) ?? [],
);
expect(resultParts).toHaveLength(1);
expect(resultParts[0].codeExecutionResult?.outcome).toBe(
Outcome.OUTCOME_OK,
);
expect(resultParts[0].text).toContain('hello');

expect(events.at(-1)?.content?.parts?.[0]?.text).toBe(
'Execution was successful.',
);
});
});
Loading