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
8 changes: 4 additions & 4 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
".": "1.5.0",
"core": "1.5.0",
"dev": "1.5.0",
"integrations": "1.5.0"
".": "1.6.0",
"core": "1.6.0",
"dev": "1.6.0",
"integrations": "1.6.0"
}
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,19 @@ dependency.

## Quick Start

Define an agent:
Set up authentication. Get an API key from
[Google AI Studio](https://aistudio.google.com/app/apikey) and put it in a
`.env` file next to your agent:

```bash
echo "GOOGLE_GENAI_API_KEY=your-api-key-here" > .env
```

> Using Vertex AI instead? Set `GOOGLE_GENAI_USE_VERTEXAI=1`,
> `GOOGLE_CLOUD_PROJECT`, and `GOOGLE_CLOUD_LOCATION` in place of the API key,
> and authenticate with `gcloud auth application-default login`.

Define an agent in `agent.ts`:

```typescript
import {LlmAgent, GOOGLE_SEARCH} from '@google/adk';
Expand All @@ -90,12 +102,16 @@ Run from your agent project directory:

```bash
# Interactive CLI
npx adk run agent.ts
npx @google/adk-devtools run agent.ts

# Web UI
npx adk web
npx @google/adk-devtools web
```

> Always name the package. If `@google/adk-devtools` is not installed, bare
> `npx adk` silently downloads and runs an unrelated `adk` package from the
> public registry.

The `adk web` command launches a development UI for testing and debugging
agents:

Expand Down
33 changes: 33 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## [1.6.0](https://github.com/google/adk-js/compare/adk-v1.5.0...adk-v1.6.0) (2026-08-05)


### Features

* Add bearerTokenUserBuilder for A2A authentication (Part 1/2) ([#562](https://github.com/google/adk-js/issues/562)) ([f077722](https://github.com/google/adk-js/commit/f077722d54528c609c95fed51019fc013093a8e5))
* CLI-level A2A authenticator for the dev server and Cloud Run deploy (Part 2/2) ([#559](https://github.com/google/adk-js/issues/559)) ([0c0bba2](https://github.com/google/adk-js/commit/0c0bba2cc4639ff89e4fa517ace25e34babb64b0))
* Support ttl and expireTime session-expiration options in VertexAiSessionService.createSession ([#561](https://github.com/google/adk-js/issues/561)) ([b390217](https://github.com/google/adk-js/commit/b390217e65dc69af85373a8eac79f0bc3baae165))
* **tools:** add getUserChoiceTool and requestInputTool for parity with adk-python ([#506](https://github.com/google/adk-js/issues/506)) ([03abf76](https://github.com/google/adk-js/commit/03abf761e97ef2c2902e776886b1a8d2a653a527))
* Validate tool callback response types and prevent state event pollution ([#505](https://github.com/google/adk-js/issues/505)) ([b99f21b](https://github.com/google/adk-js/commit/b99f21b8a549d9b4e1255ae4233d9f691eeb9fe3))


### Bug Fixes

* **a2a:** stop restoring event branch from A2A peer metadata ([#606](https://github.com/google/adk-js/issues/606)) ([0000925](https://github.com/google/adk-js/commit/00009258373f8adc213171cf664dee2a21c19417))
* accept the derived allowedTools alias in skill frontmatter validation ([#560](https://github.com/google/adk-js/issues/560)) ([fcfd043](https://github.com/google/adk-js/commit/fcfd04363ebbbea03e7a7c32d11f05358fdb4a4e))
* **artifacts:** isolate in-memory composite keys ([#576](https://github.com/google/adk-js/issues/576)) ([693a1d7](https://github.com/google/adk-js/commit/693a1d7959f68a7e578312c5c88d9ad70c283cd1))
* **core:** fall back to node:crypto so randomUUID cannot throw on Node ([#599](https://github.com/google/adk-js/issues/599)) ([3dc1b20](https://github.com/google/adk-js/commit/3dc1b2012cac86dbed249e0b3d17e16f1d0bd791))
* **core:** use a cryptographically secure source for randomUUID ([#577](https://github.com/google/adk-js/issues/577)) ([81bcc8d](https://github.com/google/adk-js/commit/81bcc8dd4784d1d94e64b599f9d092a99333539d))
* detect PowerShell 7+ (pwsh) in the UnsafeLocalCodeExecutor SHELL branch ([#568](https://github.com/google/adk-js/issues/568)) ([ce0e474](https://github.com/google/adk-js/commit/ce0e474bb5ab931894bf379588e357617ce0b5a1))
* fail fast when a Vertex AI Express Mode API key cannot be used ([#563](https://github.com/google/adk-js/issues/563)) ([e1112c6](https://github.com/google/adk-js/commit/e1112c6df905853ad99d84f6490d12e4dbbbfd22))
* gate the set_model_response workaround on canUseOutputSchemaWithTools (adk-python parity) ([#580](https://github.com/google/adk-js/issues/580)) ([5b65ee1](https://github.com/google/adk-js/commit/5b65ee109083002263f1e30593035e9778add996))
* parse JSON bodies only in the toA2a server (drop express.urlencoded) ([#558](https://github.com/google/adk-js/issues/558)) ([605b469](https://github.com/google/adk-js/commit/605b46980cabd2a8928e08d3fc3669a1de13c24c))
* pass -NoProfile to spawned PowerShell and /D to cmd.exe in UnsafeLocalCodeExecutor ([#566](https://github.com/google/adk-js/issues/566)) ([b56761b](https://github.com/google/adk-js/commit/b56761bfb0d9b0d139cc95ba1a6d1803732507dd))
* reject zip-slip entries and non-bare skill names when loading zipped skills (adk-python parity) ([#584](https://github.com/google/adk-js/issues/584)) ([4fe80b0](https://github.com/google/adk-js/commit/4fe80b0ef7e5df1803c76e8681cd9740fcf6ebf5))
* **runner:** persist events returned by onEventCallback ([#575](https://github.com/google/adk-js/issues/575)) ([c4c5582](https://github.com/google/adk-js/commit/c4c55829394cb4f15fa13f54235a9a77c5417e54))
* **security:** prevent prototype pollution via untrusted map keys ([#619](https://github.com/google/adk-js/issues/619)) ([2c07ad3](https://github.com/google/adk-js/commit/2c07ad3741cd84788d0b30a793587d5dd4b46106))
* surface root-cause MCP session errors instead of swallowing them ([#527](https://github.com/google/adk-js/issues/527)) ([13d7304](https://github.com/google/adk-js/commit/13d7304a0611fe8f1dc00a2e5de9dc3ac4d63943))
* treat @google/genai ApiError 404 as session not found in VertexAiSessionService ([#567](https://github.com/google/adk-js/issues/567)) ([5331c77](https://github.com/google/adk-js/commit/5331c771cde1c3af8ef1d87e44d41161c9919721))
* unsafe A2A peer-supplied transferToAgent metadata ([#596](https://github.com/google/adk-js/issues/596)) ([d3f250e](https://github.com/google/adk-js/commit/d3f250e876d0a76f4d09b3439e84e7dbd1fc32ec))
* **utils:** fix sibling-directory escape in materializeFiles path check ([#603](https://github.com/google/adk-js/issues/603)) ([868ca1f](https://github.com/google/adk-js/commit/868ca1f373a175c7fe2c788b167f05a58e6eed2e))
* zip-slip blacklist bypass in isDangerousZipEntryName ([#621](https://github.com/google/adk-js/issues/621)) ([7bc05f6](https://github.com/google/adk-js/commit/7bc05f6156e3e121663acee2c6476e953a626b61))

## [1.5.0](https://github.com/google/adk-js/compare/adk-v1.4.0...adk-v1.5.0) (2026-07-29)


Expand Down
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@google/adk",
"version": "1.5.0",
"version": "1.6.0",
"description": "Google ADK JS",
"author": "Google",
"license": "Apache-2.0",
Expand Down
68 changes: 58 additions & 10 deletions core/src/agents/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,36 @@
*/

import {State} from '../sessions/state.js';
import type {WorkflowInstructionScope} from './invocation_context.js';
import {ReadonlyContext} from './readonly_context.js';

const ARTIFACT_PREFIX = 'artifact.';

/** Matches a `{Class.field}` workflow placeholder key (dotted identifier pair). */
const WORKFLOW_FIELD_KEY = /^[A-Za-z_]\w*\.[A-Za-z_]\w*$/;

/** Matches a `<Class.field from source_node>` workflow placeholder. */
const SOURCE_NODE_PLACEHOLDER =
/<\s*[A-Za-z_]\w*\.([A-Za-z_]\w*)\s+from\s+([A-Za-z_]\w*)\s*>/g;

/**
* Resolves `<Class.field from source_node>` placeholders against a workflow
* scope (predecessor outputs by node name). Synchronous; unresolved placeholders
* are left untouched. Mirrors Python's source-node-qualified data selection.
*/
function resolveSourceNodePlaceholders(
template: string,
scope: WorkflowInstructionScope,
): string {
return template.replace(SOURCE_NODE_PLACEHOLDER, (raw, field, nodeName) => {
const out = scope.outputsByNode?.[nodeName];
if (out && typeof out === 'object' && field in (out as object)) {
return formatValue((out as Record<string, unknown>)[field], false);
}
return raw;
});
}

/**
* Resolves a single key from the context (state or artifact).
*/
Expand Down Expand Up @@ -39,19 +65,30 @@ async function resolveKey(
}

// Step 3: Handle state variable injection.
if (!isValidStateName(key)) {
return rawMatch;
}

if (key in invocationContext.session.state) {
return formatValue(invocationContext.session.state[key], false);
if (isValidStateName(key)) {
if (key in invocationContext.session.state) {
return formatValue(invocationContext.session.state[key], false);
}
if (isOptional) {
return '';
}
throw new Error(`Context variable not found: \`${key}\`.`);
}

if (isOptional) {
return '';
// Step 4: Workflow — resolve `{Class.field}` from the current node input.
const scope = invocationContext.workflowInstructionScope;
if (scope && WORKFLOW_FIELD_KEY.test(key)) {
const field = key.slice(key.indexOf('.') + 1);
const input = scope.input;
if (input && typeof input === 'object' && field in (input as object)) {
return formatValue((input as Record<string, unknown>)[field], false);
}
if (isOptional) {
return '';
}
}

throw new Error(`Context variable not found: \`${key}\`.`);
return rawMatch;
}

/**
Expand Down Expand Up @@ -115,6 +152,14 @@ export async function injectSessionState(
template: string,
readonlyContext: ReadonlyContext,
): Promise<string> {
// Workflow: first resolve `<Class.field from source_node>` placeholders, and
// enable `{Class.field}` resolution below. Both are no-ops (placeholders left
// untouched) for ordinary agents, which have no workflow scope.
const scope = readonlyContext.invocationContext.workflowInstructionScope;
if (scope) {
template = resolveSourceNodePlaceholders(template, scope);
}

const pattern = /\{+[^{}]*}+/g;
const matches = Array.from(template.matchAll(pattern));

Expand All @@ -130,7 +175,10 @@ export async function injectSessionState(
if (isOptional) {
key = key.slice(0, -1);
}
const isValid = key.startsWith(ARTIFACT_PREFIX) || isValidStateName(key);
const isValid =
key.startsWith(ARTIFACT_PREFIX) ||
isValidStateName(key) ||
(!!scope && WORKFLOW_FIELD_KEY.test(key));
return {
raw,
key,
Expand Down
56 changes: 56 additions & 0 deletions core/src/agents/invocation_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,32 @@ import {Content} from '@google/genai';

import {SessionArtifactService} from '../artifacts/session_artifact_service.js';
import {BaseCredentialService} from '../auth/credential_service/base_credential_service.js';
import {Event} from '../events/event.js';
import {BaseMemoryService} from '../memory/base_memory_service.js';
import {PluginManager} from '../plugins/plugin_manager.js';
import {BaseSessionService} from '../sessions/base_session_service.js';
import {Session} from '../sessions/session.js';
import {AsyncQueue} from '../utils/async_queue.js';
import {randomUUID} from '../utils/env_aware_utils.js';

import {ActiveStreamingTool} from './active_streaming_tool.js';
import {BaseAgent} from './base_agent.js';
import {RunConfig} from './run_config.js';
import {TranscriptionEntry} from './transcription_entry.js';

/**
* Workflow: data exposed to `{Class.field}` and `<Class.field from source_node>`
* instruction placeholders when an LlmAgent runs as a workflow node. Populated by
* `LLMAgentWrapper`; absent for ordinary (non-workflow) agent runs, in which case
* those placeholders are left untouched.
*/
export interface WorkflowInstructionScope {
/** The current node's input, exposing fields for `{Class.field}`. */
input?: unknown;
/** Predecessor node outputs keyed by node name, for `<Class.field from node>`. */
outputsByNode?: Record<string, unknown>;
}

/**
* The parameters for creating an invocation context.
*/
Expand All @@ -38,6 +53,9 @@ export interface InvocationContextParams {
activeStreamingTools?: Record<string, ActiveStreamingTool>;
pluginManager: PluginManager;
abortSignal?: AbortSignal;
workflowInstructionScope?: WorkflowInstructionScope;
/** Nesting depth of node-as-tool executions; used to bound recursion. */
nodeToolDepth?: number;
}

/**
Expand Down Expand Up @@ -185,6 +203,28 @@ export class InvocationContext {

readonly abortSignal?: AbortSignal;

/**
* An optional channel into which a running tool can push events to be
* interleaved into the agent's output stream. Set by the LLM flow around tool
* execution so a {@link NodeTool} (running a node/workflow) can surface the
* node's intermediate and interrupt events. Cleared once tools finish.
*/
eventQueue?: AsyncQueue<Event>;

/**
* Workflow: field-resolution scope for `{Class.field}` /
* `<Class.field from node>` instruction placeholders (set by
* `LLMAgentWrapper`).
*/
workflowInstructionScope?: WorkflowInstructionScope;

/**
* Nesting depth of node-as-tool ({@link NodeTool}) executions in this
* invocation. Incremented each time a node runs as a tool (via a depth+1
* clone), so `NodeTool` can bound `node -> tool -> node` recursion.
*/
readonly nodeToolDepth: number;

/**
* @param params The parameters for creating an invocation context.
*/
Expand All @@ -203,7 +243,10 @@ export class InvocationContext {
this.activeStreamingTools = params.activeStreamingTools;
this.pluginManager = params.pluginManager;
this.abortSignal = params.abortSignal;
this.workflowInstructionScope = params.workflowInstructionScope;
this.nodeToolDepth = params.nodeToolDepth ?? 0;
// Inherit the parent invocation's cost manager when one is available.

// Child contexts created for sub-agents, agent transfers and loop
// iterations (via createInvocationContext / createBranchCtxForSubAgent)
// carry the parent context's fields over, so reusing its cost manager
Expand Down Expand Up @@ -236,6 +279,19 @@ export class InvocationContext {
incrementLlmCallCount() {
this.invocationCostManager.incrementAndEnforceLlmCallsLimit(this.runConfig);
}

/**
* Returns a copy of this context with `overrides` applied. The spread carries
* every own field over (including the shared cost manager), so the copy keeps
* a single LLM-call counter for the invocation.
*
* Note: this copies own enumerable fields by value — scalar mutable fields
* (e.g. `endInvocation`) are decoupled from the original, while object-valued
* fields (`session`, …) stay shared by reference.
*/
clone(overrides: Partial<InvocationContextParams> = {}): InvocationContext {
return new InvocationContext({...this, ...overrides});
}
}

export function newInvocationContextId(): string {
Expand Down
Loading
Loading