Skip to content

Commit 1e78819

Browse files
bchapuisclaude
andcommitted
Add Google Search grounding toggle to Gemini nodes
Enable Google Search as a built-in tool on all Gemini text generation and agent nodes via a hidden boolean toggle. When enabled, responses are grounded in current web results. Grounding metadata (search queries, sources, citations) is exposed as a new hidden output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ca8ffc0 commit 1e78819

10 files changed

Lines changed: 149 additions & 13 deletions

apps/api/src/durable-objects/agent-runner.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface AgentRunRequest {
6565
tools: ToolReference[];
6666
/** Enable code mode for multi-tool orchestration */
6767
codeMode?: boolean;
68+
/** Enable Google Search grounding (Gemini only) */
69+
googleSearch?: boolean;
6870
/** Organization ID for credential access (integrations, secrets) */
6971
organizationId: string;
7072
}
@@ -194,6 +196,9 @@ export class AgentRunner extends DurableObject<Bindings> {
194196
? `Context:\n${body.context}\n\nRequest:\n${body.input}`
195197
: body.input;
196198

199+
// Build built-in Gemini tools (only effective for google provider)
200+
const geminiBuiltInTools = this.buildGeminiBuiltInTools(body);
201+
197202
// Run the agent loop
198203
const result = await runAgentLoop({
199204
userMessage,
@@ -205,7 +210,8 @@ export class AgentRunner extends DurableObject<Bindings> {
205210
body.model,
206211
body.instructions,
207212
messages,
208-
tools
213+
tools,
214+
geminiBuiltInTools
209215
),
210216
onStepComplete: async (state) => {
211217
this.ctx.storage.sql.exec(
@@ -342,6 +348,8 @@ export class AgentRunner extends DurableObject<Bindings> {
342348
? `Context:\n${body.context}\n\nRequest:\n${body.input}`
343349
: body.input;
344350

351+
const geminiBuiltInTools = this.buildGeminiBuiltInTools(body);
352+
345353
const result = await runAgentLoop({
346354
userMessage,
347355
tools: toolDefinitions,
@@ -352,7 +360,8 @@ export class AgentRunner extends DurableObject<Bindings> {
352360
body.model,
353361
body.instructions,
354362
messages,
355-
tools
363+
tools,
364+
geminiBuiltInTools
356365
),
357366
onStepComplete: async (state) => {
358367
this.ctx.storage.sql.exec(
@@ -453,6 +462,16 @@ export class AgentRunner extends DurableObject<Bindings> {
453462
});
454463
}
455464

465+
// ── Built-in Gemini tools ─────────────────────────────────────────────
466+
467+
private buildGeminiBuiltInTools(
468+
body: AgentRunRequest
469+
): Record<string, unknown>[] {
470+
const tools: Record<string, unknown>[] = [];
471+
if (body.googleSearch) tools.push({ googleSearch: {} });
472+
return tools;
473+
}
474+
456475
// ── Code Mode wrapping ─────────────────────────────────────────────────
457476

458477
/**
@@ -507,13 +526,20 @@ export class AgentRunner extends DurableObject<Bindings> {
507526
model: string,
508527
instructions: string,
509528
messages: AgentMessage[],
510-
tools: ToolDefinition[]
529+
tools: ToolDefinition[],
530+
builtInTools?: Record<string, unknown>[]
511531
): Promise<LLMResponse> {
512532
switch (provider) {
513533
case "anthropic":
514534
return this.callAnthropic(model, instructions, messages, tools);
515535
case "google":
516-
return this.callGoogle(model, instructions, messages, tools);
536+
return this.callGoogle(
537+
model,
538+
instructions,
539+
messages,
540+
tools,
541+
builtInTools
542+
);
517543
case "openai":
518544
return this.callOpenAI(model, instructions, messages, tools);
519545
case "workers-ai":
@@ -616,7 +642,8 @@ export class AgentRunner extends DurableObject<Bindings> {
616642
model: string,
617643
instructions: string,
618644
messages: AgentMessage[],
619-
tools: ToolDefinition[]
645+
tools: ToolDefinition[],
646+
builtInTools?: Record<string, unknown>[]
620647
): Promise<LLMResponse> {
621648
const ai = new GoogleGenAI({
622649
apiKey: "gateway-managed",
@@ -668,8 +695,12 @@ export class AgentRunner extends DurableObject<Bindings> {
668695
}));
669696

670697
const config: Record<string, unknown> = {};
698+
const allTools: Record<string, unknown>[] = [...(builtInTools ?? [])];
671699
if (functionDeclarations.length > 0) {
672-
config.tools = [{ functionDeclarations }];
700+
allTools.push({ functionDeclarations });
701+
}
702+
if (allTools.length > 0) {
703+
config.tools = allTools;
673704
}
674705

675706
const response = await ai.models.generateContent({

packages/runtime/src/nodes/agent/agent-gemini-2-5-flash-node.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { NodeType } from "@dafthunk/types";
22

3-
import { BaseAgentNode, buildAgentNodeType } from "./base-agent-node";
3+
import {
4+
BaseAgentNode,
5+
buildAgentNodeType,
6+
GEMINI_BUILTIN_TOOL_INPUTS,
7+
} from "./base-agent-node";
48

59
export class AgentGemini25FlashNode extends BaseAgentNode {
610
// https://ai.google.dev/pricing
@@ -18,5 +22,6 @@ export class AgentGemini25FlashNode extends BaseAgentNode {
1822
tags: ["AI", "Agent", "Google", "Gemini"],
1923
documentation:
2024
"This node runs a multi-turn agent loop using Gemini 2.5 Flash. The agent calls the LLM, executes tool calls, and iterates until the task is complete or the step limit is reached.",
25+
extraInputs: GEMINI_BUILTIN_TOOL_INPUTS,
2126
});
2227
}

packages/runtime/src/nodes/agent/agent-gemini-3-1-pro-node.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { NodeType } from "@dafthunk/types";
22

3-
import { BaseAgentNode, buildAgentNodeType } from "./base-agent-node";
3+
import {
4+
BaseAgentNode,
5+
buildAgentNodeType,
6+
GEMINI_BUILTIN_TOOL_INPUTS,
7+
} from "./base-agent-node";
48

59
export class AgentGemini31ProNode extends BaseAgentNode {
610
// https://ai.google.dev/pricing
@@ -18,5 +22,6 @@ export class AgentGemini31ProNode extends BaseAgentNode {
1822
tags: ["AI", "Agent", "Google", "Gemini"],
1923
documentation:
2024
"This node runs a multi-turn agent loop using Gemini 3.1 Pro. The agent calls the LLM, executes tool calls, and iterates until the task is complete or the step limit is reached.",
25+
extraInputs: GEMINI_BUILTIN_TOOL_INPUTS,
2126
});
2227
}

packages/runtime/src/nodes/agent/agent-gemini-3-flash-node.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { NodeType } from "@dafthunk/types";
22

3-
import { BaseAgentNode, buildAgentNodeType } from "./base-agent-node";
3+
import {
4+
BaseAgentNode,
5+
buildAgentNodeType,
6+
GEMINI_BUILTIN_TOOL_INPUTS,
7+
} from "./base-agent-node";
48

59
export class AgentGemini3FlashNode extends BaseAgentNode {
610
// https://ai.google.dev/pricing
@@ -18,5 +22,6 @@ export class AgentGemini3FlashNode extends BaseAgentNode {
1822
tags: ["AI", "Agent", "Google", "Gemini"],
1923
documentation:
2024
"This node runs a multi-turn agent loop using Gemini 3 Flash. The agent calls the LLM, executes tool calls, and iterates until the task is complete or the step limit is reached.",
25+
extraInputs: GEMINI_BUILTIN_TOOL_INPUTS,
2126
});
2227
}

packages/runtime/src/nodes/agent/base-agent-node.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ const AGENT_INPUTS: NodeType["inputs"] = [
6363
},
6464
];
6565

66+
/** Extra inputs for Gemini agent nodes — Google built-in tools */
67+
export const GEMINI_BUILTIN_TOOL_INPUTS: NodeType["inputs"] = [
68+
{
69+
name: "googleSearch",
70+
type: "boolean",
71+
description:
72+
"Enable Google Search to ground responses in current web results",
73+
required: false,
74+
hidden: true,
75+
},
76+
];
77+
6678
/** Standard outputs shared by all agent nodes */
6779
const AGENT_OUTPUTS: NodeType["outputs"] = [
6880
{
@@ -106,6 +118,7 @@ export function buildAgentNodeType(meta: {
106118
description: string;
107119
tags: string[];
108120
documentation: string;
121+
extraInputs?: NodeType["inputs"];
109122
}): NodeType {
110123
return {
111124
id: meta.id,
@@ -117,7 +130,7 @@ export function buildAgentNodeType(meta: {
117130
documentation: meta.documentation,
118131
usage: 1,
119132
functionCalling: true,
120-
inputs: AGENT_INPUTS,
133+
inputs: [...AGENT_INPUTS, ...(meta.extraInputs ?? [])],
121134
outputs: AGENT_OUTPUTS,
122135
};
123136
}
@@ -179,7 +192,7 @@ export abstract class BaseAgentNode extends ExecutableNode {
179192
config: AgentNodeConfig
180193
): Promise<NodeExecution> {
181194
try {
182-
const { instructions, input, max_steps, tools, code_mode } =
195+
const { instructions, input, max_steps, tools, code_mode, googleSearch } =
183196
context.inputs;
184197
const agentContext = context.inputs.context as string | undefined;
185198

@@ -212,6 +225,7 @@ export abstract class BaseAgentNode extends ExecutableNode {
212225
maxSteps: max_steps ?? 10,
213226
tools: tools ?? [],
214227
codeMode: code_mode ?? false,
228+
googleSearch: googleSearch ?? false,
215229
organizationId: context.organizationId,
216230
}),
217231
});
@@ -253,7 +267,7 @@ export abstract class BaseAgentNode extends ExecutableNode {
253267
config: AgentNodeConfig
254268
): Promise<NodeExecution> {
255269
try {
256-
const { instructions, input, max_steps, tools, code_mode } =
270+
const { instructions, input, max_steps, tools, code_mode, googleSearch } =
257271
context.inputs;
258272
const agentContext = context.inputs.context as string | undefined;
259273

@@ -283,6 +297,7 @@ export abstract class BaseAgentNode extends ExecutableNode {
283297
maxSteps: max_steps ?? 10,
284298
tools: tools ?? [],
285299
codeMode: code_mode ?? false,
300+
googleSearch: googleSearch ?? false,
286301
organizationId: context.organizationId,
287302
}),
288303
});

packages/runtime/src/nodes/gemini/execute-gemini-model.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function executeGeminiModel(
4040
maxOutputTokens,
4141
thinking_budget,
4242
tools,
43+
googleSearch,
4344
} = context.inputs;
4445

4546
if (!input) {
@@ -69,13 +70,25 @@ export async function executeGeminiModel(
6970
context
7071
);
7172

73+
// Build built-in tools array
74+
const builtInTools: Record<string, unknown>[] = [];
75+
if (googleSearch) builtInTools.push({ googleSearch: {} });
76+
7277
let response: any;
7378
const executedToolCalls: any[] = [];
7479
let intermediaryMessages: any[] = [];
7580

81+
// Merge function declarations with built-in tools
82+
const allTools: Record<string, unknown>[] = [...builtInTools];
7683
if (functionDeclarations.length > 0) {
77-
genConfig.tools = [{ functionDeclarations }];
84+
allTools.push({ functionDeclarations });
85+
}
7886

87+
if (allTools.length > 0) {
88+
genConfig.tools = allTools;
89+
}
90+
91+
if (functionDeclarations.length > 0) {
7992
response = await ai.models.generateContent({
8093
model: config.modelId,
8194
contents: [{ text: input }],
@@ -152,6 +165,7 @@ export async function executeGeminiModel(
152165
const usageMetadata = response.usageMetadata;
153166
const promptFeedback = response.promptFeedback;
154167
const finishReason = candidate?.finishReason;
168+
const groundingMetadata = candidate?.groundingMetadata;
155169

156170
const usage = calculateTokenUsage(
157171
usageMetadata?.promptTokenCount ?? 0,
@@ -166,6 +180,7 @@ export async function executeGeminiModel(
166180
...(usageMetadata && { usage_metadata: usageMetadata }),
167181
...(promptFeedback && { prompt_feedback: promptFeedback }),
168182
...(finishReason && { finish_reason: finishReason }),
183+
...(groundingMetadata && { grounding_metadata: groundingMetadata }),
169184
...(executedToolCalls.length > 0
170185
? { tool_calls: executedToolCalls }
171186
: {}),

packages/runtime/src/nodes/gemini/gemini-2-5-flash-node.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ export class Gemini25FlashNode extends ExecutableNode {
6666
hidden: true,
6767
value: [] as any,
6868
},
69+
{
70+
name: "googleSearch",
71+
type: "boolean",
72+
description:
73+
"Enable Google Search to ground responses in current web results",
74+
required: false,
75+
hidden: true,
76+
},
6977
],
7078
outputs: [
7179
{
@@ -98,6 +106,13 @@ export class Gemini25FlashNode extends ExecutableNode {
98106
"Reason why the generation finished (STOP, MAX_TOKENS, etc.)",
99107
hidden: true,
100108
},
109+
{
110+
name: "grounding_metadata",
111+
type: "json",
112+
description:
113+
"Grounding metadata with search queries, sources, and citations from Google Search or Maps",
114+
hidden: true,
115+
},
101116
{
102117
name: "tool_calls",
103118
type: "json",

packages/runtime/src/nodes/gemini/gemini-2-5-pro-node.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ export class Gemini25ProNode extends ExecutableNode {
6666
hidden: true,
6767
value: [] as any,
6868
},
69+
{
70+
name: "googleSearch",
71+
type: "boolean",
72+
description:
73+
"Enable Google Search to ground responses in current web results",
74+
required: false,
75+
hidden: true,
76+
},
6977
],
7078
outputs: [
7179
{
@@ -98,6 +106,13 @@ export class Gemini25ProNode extends ExecutableNode {
98106
"Reason why the generation finished (STOP, MAX_TOKENS, etc.)",
99107
hidden: true,
100108
},
109+
{
110+
name: "grounding_metadata",
111+
type: "json",
112+
description:
113+
"Grounding metadata with search queries, sources, and citations from Google Search or Maps",
114+
hidden: true,
115+
},
101116
{
102117
name: "tool_calls",
103118
type: "json",

packages/runtime/src/nodes/gemini/gemini-3-1-pro-node.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ export class Gemini31ProNode extends ExecutableNode {
6666
hidden: true,
6767
value: [] as any,
6868
},
69+
{
70+
name: "googleSearch",
71+
type: "boolean",
72+
description:
73+
"Enable Google Search to ground responses in current web results",
74+
required: false,
75+
hidden: true,
76+
},
6977
],
7078
outputs: [
7179
{
@@ -98,6 +106,13 @@ export class Gemini31ProNode extends ExecutableNode {
98106
"Reason why the generation finished (STOP, MAX_TOKENS, etc.)",
99107
hidden: true,
100108
},
109+
{
110+
name: "grounding_metadata",
111+
type: "json",
112+
description:
113+
"Grounding metadata with search queries, sources, and citations from Google Search or Maps",
114+
hidden: true,
115+
},
101116
{
102117
name: "tool_calls",
103118
type: "json",

0 commit comments

Comments
 (0)