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
32 changes: 28 additions & 4 deletions core/src/agents/llm_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {GenerateContentConfig, Schema} from '@google/genai';
import {context, trace} from '@opentelemetry/api';
import {FunctionTool} from '../tools/function_tool.js';

import {z as z3} from 'zod/v3';
import {z as z4} from 'zod/v4';
Expand Down Expand Up @@ -743,7 +744,23 @@ export class LlmAgent extends BaseAgent {
}
// TODO - b/425992518: check if tool preprocessors can be simplified.
// Run pre-processors for tools.
for (const toolUnion of this.tools) {
const allTools = [...this.tools];
if (this.outputSchema && allTools.length > 0) {
const setModelResponseTool = new FunctionTool({
name: 'set_model_response',
description:
'Call this tool to submit your final response conforming to the output schema. Use this tool only when you have collected all the information and are ready to return the final answer.',
parameters: this.outputSchema,
execute: async (args, toolContext) => {
if (toolContext) {
toolContext.actions.skipSummarization = true;
}
return JSON.stringify(args);
},
});
allTools.push(setModelResponseTool);
}
for (const toolUnion of allTools) {
const toolContext = new Context({invocationContext});

// process all tools from this tool union
Expand All @@ -758,7 +775,8 @@ export class LlmAgent extends BaseAgent {
// The allowedTools set is populated by request processors.
return (
!llmRequest.allowedTools ||
llmRequest.allowedTools.includes(tool.name)
llmRequest.allowedTools.includes(tool.name) ||
tool.name === 'set_model_response'
);
});

Expand Down Expand Up @@ -880,8 +898,14 @@ export class LlmAgent extends BaseAgent {

if (mergedEvent.content) {
const functionCalls = getFunctionCalls(mergedEvent);
if (functionCalls?.length) {
// TODO - b/425992518: rename topopulate if missing.
const setModelResponseCall = functionCalls.find(
(call) => call.name === 'set_model_response',
);
if (setModelResponseCall) {
const args = setModelResponseCall.args;
mergedEvent.content.parts = [{text: JSON.stringify(args)}];
mergedEvent.actions.skipSummarization = true;
} else if (functionCalls && functionCalls.length) {
populateClientFunctionCallId(mergedEvent);
// TODO - b/425992518: hacky, transaction log, simplify.
// Long running is a property of tool in registry.
Expand Down
2 changes: 1 addition & 1 deletion core/src/agents/processors/basic_llm_request_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class BasicLlmRequestProcessor extends BaseLlmRequestProcessor {
llmRequest.model = agent.canonicalModel.model;

llmRequest.config = {...(agent.generateContentConfig ?? {})};
if (agent.outputSchema) {
if (agent.outputSchema && (!agent.tools || agent.tools.length === 0)) {
setOutputSchema(llmRequest, agent.outputSchema);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export class InstructionsLlmRequestProcessor extends BaseLlmRequestProcessor {
}
appendInstructions(llmRequest, [instructionWithState]);
}

if (agent.outputSchema && agent.tools && agent.tools.length > 0) {
appendInstructions(llmRequest, [
'To output the final result, you must call the "set_model_response" function with the appropriate values. Do not output anything else.',
]);
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions core/test/agents/processors/basic_llm_request_processor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BaseLlm,
BaseLlmConnection,
createSession,
FunctionTool,
InvocationContext,
LlmAgent,
LLMRegistry,
Expand Down Expand Up @@ -168,6 +169,34 @@ describe('BasicLlmRequestProcessor', () => {
expect(llmRequest.config?.responseMimeType).toBe('application/json');
});

it('should not set outputSchema in config when agent has outputSchema and tools', async () => {
const outputSchema = {
type: 'object' as const,
properties: {
answer: {type: 'string' as const},
},
};
const agent = new LlmAgent({
name: 'test_agent',
model: 'test-basic-processor-model',
outputSchema,
tools: [
new FunctionTool({
name: 'some_tool',
description: 'A test tool',
execute: () => 'result',
}),
],
});
const invocationContext = createMockInvocationContext(agent);
const llmRequest = makeLlmRequest();

await runProcessor(invocationContext, llmRequest);

expect(llmRequest.config?.responseSchema).toBeUndefined();
expect(llmRequest.config?.responseMimeType).toBeUndefined();
});

it('should populate liveConnectConfig from runConfig', async () => {
const agent = new LlmAgent({
name: 'test_agent',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

import {
BaseAgent,
createSession,
FunctionTool,
InvocationContext,
LlmAgent,
LlmRequest,
PluginManager,
ReadonlyContext,
createSession,
} from '@google/adk';
import {describe, expect, it} from 'vitest';
import {INSTRUCTIONS_LLM_REQUEST_PROCESSOR} from '../../../src/agents/processors/instructions_llm_request_processor.js';
Expand Down Expand Up @@ -159,4 +160,44 @@ describe('InstructionsLlmRequestProcessor', () => {
'Global instruction\n\nLocal instruction',
);
});

it('should append set_model_response instruction when outputSchema and tools are present', async () => {
const outputSchema = {
type: 'object' as const,
properties: {
answer: {type: 'string' as const},
},
};
const agent = new LlmAgent({
name: 'test_agent',
model: 'gemini-2.5-flash',
instruction: 'Base instruction',
outputSchema,
tools: [
new FunctionTool({
name: 'some_tool',
description: 'A test tool',
execute: () => 'result',
}),
],
});

const invocationContext = createMockInvocationContext(agent);
const llmRequest: LlmRequest = {
contents: [],
toolsDict: {},
liveConnectConfig: {},
};

for await (const _ of INSTRUCTIONS_LLM_REQUEST_PROCESSOR.runAsync(
invocationContext,
llmRequest,
)) {
// intentionally empty
}

expect(llmRequest.config?.systemInstruction).toContain(
'To output the final result, you must call the "set_model_response" function with the appropriate values. Do not output anything else.',
);
});
});