Skip to content
Closed
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()]),
);
}
3 changes: 3 additions & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ 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 {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 +255,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';
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