Skip to content
Open
2 changes: 2 additions & 0 deletions .github/workflows/cross-language-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:

- name: Use Node.js
uses: actions/setup-node@v6
with:
cache: npm

- name: Setup Go
uses: actions/setup-go@v5
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ web runtimes.

## 🚀 Installation

> **Prerequisite:** ADK for TypeScript requires a current Node.js LTS release.

```bash
npm install @google/adk
npm install -D @google/adk-devtools
Expand Down
50 changes: 45 additions & 5 deletions core/src/agents/routed_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ export interface RoutedAgentConfig extends BaseAgentConfig {
* Routing is strictly limited to the agents passed in the config.
*
* @remarks
* The inherited {@link BaseAgent.clone} does not support `RoutedAgent`: the
* constructor derives its routing targets from `config.agents` (not
* `subAgents`), so a rebuilt clone re-reads the already-parented originals and
* throws. Cloning a `RoutedAgent` is tracked as a follow-up to
* google/adk-js#534.
* Cloning is supported: {@link RoutedAgent.clone} deep-clones the routing
* targets from `config.agents` and re-parents the fresh copies onto the clone,
* so the clone is a detached root that routes identically to the original while
* leaving the original agent tree untouched.
*/
@experimental
export class RoutedAgent extends BaseAgent<RoutedAgentConfig> {
Expand Down Expand Up @@ -94,6 +93,30 @@ export class RoutedAgent extends BaseAgent<RoutedAgentConfig> {
this.router = config.router;
}

/**
* Creates a detached copy of this routed agent.
*
* The inherited {@link BaseAgent.clone} only deep-clones `subAgents`, but a
* `RoutedAgent` derives its routing targets from `config.agents`. Rebuilding
* through the base clone alone would re-read the already-parented original
* agents and throw "already has a parent agent". This override deep-clones the
* routing targets (unless the caller overrides `agents`) so the rebuilt
* constructor re-parents fresh, detached copies. The array-vs-record shape
* and, for records, the keys are preserved so the router keeps selecting the
* same targets.
*
* @param overrides Config fields to override on the clone. Overriding
* `parentAgent` is rejected by the base implementation.
* @returns A new detached `RoutedAgent` of the same concrete class.
*/
override clone(overrides?: Partial<RoutedAgentConfig>): this {
const nextOverrides: Partial<RoutedAgentConfig> = {...overrides};
if (!('agents' in nextOverrides)) {
nextOverrides.agents = cloneRoutingTargets(this.config.agents);
}
return super.clone(nextOverrides);
}

/**
* Runs the selected agent via text-based conversation.
*/
Expand All @@ -116,3 +139,20 @@ export class RoutedAgent extends BaseAgent<RoutedAgentConfig> {
);
}
}

/**
* Deep-clones a RoutedAgent's routing targets, preserving whether they were
* supplied as an array or a keyed record so the rebuilt constructor derives the
* same routing map. Each clone is detached (no parent), so the constructor can
* re-parent it without conflict.
*/
function cloneRoutingTargets(
agents: Readonly<Record<string, BaseAgent>> | BaseAgent[],
): Readonly<Record<string, BaseAgent>> | BaseAgent[] {
if (Array.isArray(agents)) {
return agents.map((agent) => agent.clone());
}
return Object.fromEntries(
Object.entries(agents).map(([key, agent]) => [key, agent.clone()]),
);
}
7 changes: 7 additions & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ export type {
export {BaseToolset, isBaseToolset} from './tools/base_toolset.js';
export type {ToolPredicate} from './tools/base_toolset.js';
export {ConsolidateContextTool} from './tools/consolidate_context_tool.js';
export {
ENTERPRISE_WEB_SEARCH,
EnterpriseWebSearchTool,
} from './tools/enterprise_web_search_tool.js';
export {ExampleTool} from './tools/example_tool.js';
export {EXIT_LOOP, ExitLoopTool} from './tools/exit_loop_tool.js';
export {FunctionTool, isFunctionTool} from './tools/function_tool.js';
export type {
Expand All @@ -254,6 +259,8 @@ export {
LoadArtifactsTool,
} from './tools/load_artifacts_tool.js';
export {LOAD_MEMORY, LoadMemoryTool} from './tools/load_memory_tool.js';
export {LOAD_WEB_PAGE, loadWebPage} from './tools/load_web_page.js';
export type {LoadWebPageOptions} from './tools/load_web_page.js';
export {LongRunningFunctionTool} from './tools/long_running_tool.js';
export {
PRELOAD_MEMORY,
Expand Down
1 change: 1 addition & 0 deletions core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export {RunSkillScriptTool} from './tools/skill/run_skill_script_tool.js';
export * from './integrations/agent_registry/agent_registry.js';
export * from './telemetry/google_cloud.js';
export * from './telemetry/setup.js';
export * from './tools/mcp/load_mcp_resource_tool.js';
export * from './tools/mcp/mcp_session_manager.js';
export * from './tools/mcp/mcp_tool.js';
export * from './tools/mcp/mcp_toolset.js';
84 changes: 84 additions & 0 deletions core/src/tools/enterprise_web_search_tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {GenerateContentConfig} from '@google/genai';

import {LlmRequest} from '../models/llm_request.js';
import {
isGemini1Model,
isGeminiModel,
isGeminiModelIdCheckDisabled,
} from '../utils/model_name.js';

import {BaseTool, ToolProcessLlmRequest} from './base_tool.js';

/**
* Appends the Enterprise Web Search built-in tool to the LLM request when the
* target model supports it.
*
* NOTE: This is NOT Vertex AI Search (formerly "Enterprise Search"). See
* https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/web-grounding-enterprise
*/
export function applyEnterpriseWebSearch(llmRequest: LlmRequest): void {
if (!llmRequest.model) {
return;
}

const modelCheckDisabled = isGeminiModelIdCheckDisabled();
llmRequest.config = llmRequest.config || ({} as GenerateContentConfig);
llmRequest.config.tools = llmRequest.config.tools || [];

if (isGeminiModel(llmRequest.model) || modelCheckDisabled) {
if (
isGemini1Model(llmRequest.model) &&
llmRequest.config.tools.length > 0
) {
throw new Error(
'Enterprise Web Search tool cannot be used with other tools in Gemini 1.x.',
);
}

llmRequest.config.tools.push({enterpriseWebSearch: {}});

return;
}

throw new Error(
`Enterprise Web Search tool is not supported for model ${llmRequest.model}`,
);
}

/**
* A Gemini 2+ built-in tool that grounds responses on public web data via
* Vertex AI Search with Enterprise (Sec4) compliance.
*
* This tool operates internally within the model and does not require or
* perform local code execution.
*/
export class EnterpriseWebSearchTool extends BaseTool {
constructor() {
super({
name: 'enterprise_web_search',
description: 'Enterprise Web Search Tool',
});
}

runAsync(): Promise<unknown> {
// This is a built-in tool on server side, it's triggered by setting the
// corresponding request parameters.
return Promise.resolve();
}

override async processLlmRequest({
llmRequest,
}: ToolProcessLlmRequest): Promise<void> {
applyEnterpriseWebSearch(llmRequest);
}
}

/**
* A global instance of {@link EnterpriseWebSearchTool}.
*/
export const ENTERPRISE_WEB_SEARCH = new EnterpriseWebSearchTool();
52 changes: 52 additions & 0 deletions core/src/tools/example_tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {BaseExampleProvider} from '../examples/base_example_provider.js';
import {Example} from '../examples/example.js';
import {buildExampleSi} from '../examples/example_util.js';
import {appendInstructions} from '../models/llm_request.js';

import {
BaseTool,
RunAsyncToolRequest,
ToolProcessLlmRequest,
} from './base_tool.js';

/**
* A tool that adds (few-shot) examples to the LLM request.
*
* This tool is executed for each LLM request and is never called by the model;
* it only mutates the outgoing request by appending few-shot instructions built
* from the latest user query.
*/
export class ExampleTool extends BaseTool {
constructor(readonly examples: Example[] | BaseExampleProvider) {
super({
// Name and description are not used because this tool only changes
// llmRequest.
name: 'example_tool',
description: 'example tool',
});
}

override async runAsync(_request: RunAsyncToolRequest): Promise<unknown> {
// Should not be called by model because it's not declared in LLM tools list.
throw new Error('ExampleTool should not be called by model');
}

override async processLlmRequest({
toolContext,
llmRequest,
}: ToolProcessLlmRequest): Promise<void> {
const parts = toolContext.userContent?.parts;
if (!parts || !parts[0]?.text) {
return;
}
appendInstructions(llmRequest, [
buildExampleSi(this.examples, parts[0].text, llmRequest.model),
]);
}
}
Loading
Loading