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
61 changes: 61 additions & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,12 @@ export type {
VertexAiSearchToolParams,
} from './tools/vertex_ai_search_tool.js';
export {VertexRagRetrievalTool} from './tools/vertex_rag_retrieval_tool.js';
export {AsyncQueue} from './utils/async_queue.js';
export {getClientLabels, runWithClientLabel} from './utils/client_labels.js';
export {LogLevel, getLogger, setLogLevel, setLogger} from './utils/logger.js';
export type {Logger} from './utils/logger.js';
export {isGemini2OrAbove, isGemini3xFlashLive} from './utils/model_name.js';
export type {SchemaLike} from './utils/schema.js';
export {zodObjectToSchema} from './utils/simple_zod_to_json.js';
export {Task} from './utils/task.js';
export type {TaskExecutable} from './utils/task.js';
Expand Down Expand Up @@ -331,6 +333,65 @@ export {
createRestApiTool,
} from './tools/openapi_tool/rest_api_tool.js';

// Workflow (parity port of google/adk-python `google/adk/workflow`). Named
// explicitly (not `export *`) so the top-level surface stays intentional and
// collisions are compile errors; keep this in sync with `./workflow/index.js`.
export {
BaseNode,
BranchPath,
DEFAULT_ROUTE,
Edge,
FunctionNode,
Graph,
JoinNode,
NodeContext,
NodeStatus,
NodeTimeoutError,
ParallelWorker,
RequestInput,
START,
ToolNode,
Workflow,
WorkflowAgent,
WorkflowNode,
commonPrefixOf,
createNodeState,
createSubBranch,
isNodeState,
isRequestInput,
node,
normalizeRetryExceptions,
prepareRetryConfig,
} from './workflow/index.js';
export type {
BaseNodeConfig,
BuildNodeOptions,
ChainElement,
DynamicEntry,
EdgeItem,
ErrorClass,
FunctionNodeConfig,
FunctionNodeHandler,
FunctionNodeResult,
NodeContextOptions,
NodeLike,
NodeOptions,
NodeResult,
NodeState,
ParallelWorkerConfig,
PreparedRetryConfig,
RequestInputParams,
RetryConfig,
RouteValue,
RoutingMap,
RunNodeOptions,
ScheduleDynamicNode,
ScheduleDynamicNodeOptions,
ToolNodeConfig,
WorkflowAgentConfig,
WorkflowConfig,
} from './workflow/index.js';

export * from './apps/app.js';
export * from './artifacts/base_artifact_service.js';
export * from './features/feature_registry.js';
Expand Down
13 changes: 8 additions & 5 deletions core/src/events/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,12 @@ const PRESERVE_KEYS_CAMEL_CASE = [
'customMetadata',
'content.parts.functionCall.args',
'content.parts.functionResponse.response',
// Workflow: arbitrary node output and checkpointed node state carry
// user-defined keys that must survive round-trips verbatim (a node's original
// input is stashed under `actions.agentState` for HITL resume).
// Workflow: arbitrary node output, emitted route(s), and checkpointed node
// state carry user-defined keys that must survive round-trips verbatim (a
// node's original input is stashed under `actions.agentState` for HITL
// resume, and rehydration reads `output`/`route`/`actions.agentState` back).
'output',
'route',
'actions.agentState',
];

Expand All @@ -389,9 +391,10 @@ const PRESERVE_KEYS_SNAKE_CASE = [
'custom_metadata',
'content.parts.function_call.args',
'content.parts.function_response.response',
// Workflow: arbitrary node output and checkpointed node state (see the
// camelCase list above).
// Workflow: arbitrary node output, emitted route(s), and checkpointed node
// state (see the camelCase list above).
'output',
'route',
'actions.agent_state',
];

Expand Down
4 changes: 2 additions & 2 deletions core/src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import {
* A schema accepted by ADK APIs, expressed as a Zod v3 type, a Zod v4 type, or
* a genai `Schema`.
*
* Use {@link parseWithSchema} to validate a value against one, and
* {@link toJsonSchema} to render one as a plain JSON Schema.
* Use `parseWithSchema` to validate a value against one, and `toJsonSchema` to
* render one as a plain JSON Schema.
*/
export type SchemaLike = z3.ZodType | z4.ZodType | Schema;

Expand Down
6 changes: 3 additions & 3 deletions core/src/workflow/base_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface BaseNodeConfig {
* {@link Event}s consumed by the engine.
*/
export abstract class BaseNode<TInput = unknown, TOutput = unknown> {
/** Brand identifying this object as a {@link BaseNode} (see {@link isBaseNode}). */
/** Brand identifying this object as a {@link BaseNode} (see `isBaseNode`). */
readonly [BASE_NODE_SIGNATURE_SYMBOL] = true;

readonly name: string;
Expand Down Expand Up @@ -163,7 +163,7 @@ export abstract class BaseNode<TInput = unknown, TOutput = unknown> {
/**
* Validates node input against `inputSchema` (Content passes through). Only
* enforced for Zod schemas; a genai `Schema` is left unvalidated (see
* {@link parseWithSchema}).
* `parseWithSchema`).
*/
protected validateInput(input: TInput): TInput {
if (isContent(input)) {
Expand All @@ -175,7 +175,7 @@ export abstract class BaseNode<TInput = unknown, TOutput = unknown> {
/**
* Validates node output against `outputSchema` (Content passes through). Only
* enforced for Zod schemas; a genai `Schema` is left unvalidated (see
* {@link parseWithSchema}).
* `parseWithSchema`).
*/
protected validateOutput(output: unknown): unknown {
if (isContent(output)) {
Expand Down
151 changes: 151 additions & 0 deletions core/src/workflow/dynamic_node_scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {BaseNode} from './base_node.js';
import {NodeContext, NodeResult} from './node_context.js';
import {executeChildNode} from './node_runner.js';
import {createNodeState} from './node_state.js';
import {NodeStatus} from './node_status.js';
import {
DynamicNodeRun,
DynamicNodeState,
ScheduleDynamicNode,
ScheduleDynamicNodeOptions,
} from './schedule_dynamic_node.js';
import {
isFastForwardable,
makeFastForwardResult,
reconstructNodeStatesByPath,
} from './utils/rehydration_utils.js';

/**
* Handles `ctx.runNode()` calls for a {@link Workflow} subtree.
*
* Ported (Phase 4 subset) from `google/adk-python`
* `workflow/_dynamic_node_scheduler.py`. Implemented now: fresh execution and
* deduplication of concurrent calls to the same node path. Resumption from
* session events (rehydration + replay interception) is added in Phase 5 at the
* marked hook point.
*/
export class DynamicNodeScheduler implements ScheduleDynamicNode {
/**
* @param state Shared dynamic-node bookkeeping for this workflow subtree.
* @param abortSignal Workflow-scoped cancellation signal, forwarded to each
* dynamic child so a workflow shutting down on error can cancel in-flight
* `ctx.runNode()` children too.
*/
constructor(
private readonly state: DynamicNodeState,
private readonly abortSignal?: AbortSignal,
) {}

async schedule(
ctx: NodeContext,
node: BaseNode,
input: unknown,
options: ScheduleDynamicNodeOptions,
): Promise<NodeContext | NodeResult> {
const name = options.nodeName ?? node.name;
const runId = options.runId;
const nodePath = ctx.nodePath
? `${ctx.nodePath}.${name}@${runId}`
: `${name}@${runId}`;

const existing = this.state.runs.get(nodePath);
if (existing?.task) {
// Deduplicate concurrent calls: await the in-flight task.
return existing.task;
}

// Cross-turn resume: rehydrate this dynamic run from prior session events.
if (!this.state.runs.has(nodePath)) {
const prior = reconstructNodeStatesByPath(ctx.session?.events ?? []).get(
nodePath,
);
if (prior && !node.rerunOnResume && isFastForwardable(prior)) {
// Completed in a prior turn -> return cached output, do not re-execute.
this.state.runs.set(nodePath, {
state: createNodeState({
status: NodeStatus.COMPLETED,
runId,
parentRunId: ctx.runId,
}),
output: prior.output,
});
if (options.useAsOutput) {
ctx.output = prior.output;
ctx.route = prior.route;
}
return makeFastForwardResult(ctx, prior);
}
// Otherwise (waiting/unresolved): resume inputs were already merged into
// ctx.resumeInputs by the Workflow; fall through to a fresh run.
}

return this.runFresh(ctx, node, input, name, runId, nodePath, options);
}

private async runFresh(
ctx: NodeContext,
node: BaseNode,
input: unknown,
name: string,
runId: string,
nodePath: string,
options: ScheduleDynamicNodeOptions,
): Promise<NodeContext> {
const run: DynamicNodeRun = {
state: createNodeState({
status: NodeStatus.RUNNING,
input,
runId,
parentRunId: ctx.runId,
}),
};
this.state.runs.set(nodePath, run);

run.task = executeChildNode({
parent: ctx,
node,
input,
abortSignal: this.abortSignal,
options: {
nodeName: name,
runId,
overrideNodePath: nodePath,
useAsOutput: options.useAsOutput,
useSubBranch: options.useSubBranch,
overrideBranch: options.overrideBranch,
overrideIsolationScope: options.overrideIsolationScope,
},
});

const childCtx = await run.task;
this.recordResult(run, childCtx, node);
return childCtx;
}

private recordResult(
run: DynamicNodeRun,
childCtx: NodeContext,
node: BaseNode,
): void {
if (childCtx.interruptIds.length > 0) {
run.state.status = NodeStatus.WAITING;
run.state.interrupts = [...childCtx.interruptIds];
childCtx.interruptIds.forEach((id) => this.state.interruptIds.add(id));
} else if (
node.waitForOutput &&
childCtx.output === undefined &&
childCtx.route === undefined
) {
run.state.status = NodeStatus.WAITING;
} else {
run.state.status = NodeStatus.COMPLETED;
run.output = childCtx.output;
}
}
}
4 changes: 2 additions & 2 deletions core/src/workflow/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {validateGraph} from './utils/graph_validation.js';
/**
* A unique symbol branding {@link Edge} instances.
*
* {@link isEdge} matches on this brand rather than `instanceof` so an edge built
* `isEdge` matches on this brand rather than `instanceof` so an edge built
* by another copy of adk-js in the same runtime is still recognised (an
* `instanceof` check fails across package copies) — mirroring the
* `Symbol.for('google.adk.*')` brands used across ADK.
Expand Down Expand Up @@ -78,7 +78,7 @@ export type EdgeItem = Edge | ChainElement[];
* Mirrors `google/adk-python` `workflow/_graph.py::Edge`.
*/
export class Edge {
/** Brand identifying this object as an {@link Edge} (see {@link isEdge}). */
/** Brand identifying this object as an {@link Edge} (see `isEdge`). */
readonly [EDGE_SIGNATURE_SYMBOL] = true;

constructor(
Expand Down
75 changes: 75 additions & 0 deletions core/src/workflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* The new ADK workflow module (parity port of `google/adk-python`
* `google/adk/workflow`). Public surface mirrors Python's `__all__`, plus the
* TypeScript-specific `WorkflowAgent` adapter and the types needed to use the
* API from TypeScript.
*/

// --- Core graph / workflow ---
export {Workflow} from './workflow.js';
export type {DynamicEntry, WorkflowConfig} from './workflow.js';
export {WorkflowAgent} from './workflow_agent.js';
export type {WorkflowAgentConfig} from './workflow_agent.js';

// --- Nodes ---
export {BaseNode, START} from './base_node.js';
export type {BaseNodeConfig} from './base_node.js';
export {WorkflowNode, node} from './node.js';
export type {NodeOptions} from './node.js';
export {FunctionNode} from './nodes/function_node.js';
export type {
FunctionNodeConfig,
FunctionNodeHandler,
FunctionNodeResult,
} from './nodes/function_node.js';
export {JoinNode} from './nodes/join_node.js';
export {ParallelWorker} from './nodes/parallel_worker.js';
export type {ParallelWorkerConfig} from './nodes/parallel_worker.js';
export {ToolNode} from './nodes/tool_node.js';
export type {ToolNodeConfig} from './nodes/tool_node.js';
export type {BuildNodeOptions} from './utils/workflow_graph_utils.js';
// LLMAgentWrapper and NodeTool are exported by Part 7 (LLM node).

// --- Graph model ---
export {DEFAULT_ROUTE, Edge, Graph} from './graph.js';
export type {
ChainElement,
EdgeItem,
NodeLike,
RouteValue,
RoutingMap,
} from './graph.js';

// --- Execution context & state ---
export {BranchPath, commonPrefixOf, createSubBranch} from './branch_path.js';
export {NodeContext} from './node_context.js';
export type {NodeContextOptions, NodeResult} from './node_context.js';
export type {RunNodeOptions} from './node_runner.js';
export {createNodeState, isNodeState} from './node_state.js';
export type {NodeState} from './node_state.js';
export {NodeStatus} from './node_status.js';
export type {
ScheduleDynamicNode,
ScheduleDynamicNodeOptions,
} from './schedule_dynamic_node.js';

// --- HITL ---
export {RequestInput, isRequestInput} from './request_input.js';
export type {RequestInputParams} from './request_input.js';

// --- Retry ---
export {normalizeRetryExceptions, prepareRetryConfig} from './retry_config.js';
export type {
ErrorClass,
PreparedRetryConfig,
RetryConfig,
} from './retry_config.js';

// --- Errors ---
export {NodeTimeoutError} from './errors.js';
Loading
Loading