From 3ec2f24f3df415e18ca60c94d6ec36040801376b Mon Sep 17 00:00:00 2001 From: kalenkevich Date: Fri, 21 Aug 2026 13:26:34 -0700 Subject: [PATCH 1/6] Add TypeScript tabs to the graph workflow pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five /graphs/ pages documented graph workflows for Python and Go only, so a TypeScript reader had to infer the API from the Python tab — which does not translate: TypeScript has no `@node` decorator, schemas are Zod objects rather than pydantic models, state is written through `ctx.state` instead of returned on an event, and a user-facing message is the event's `content` rather than a `message` field. Every section that has a Python tab now has a TypeScript tab before the Go one, backed by 26 snippet files under examples/typescript/snippets/graphs/. The snippets are ported from the runnable samples in adk-js (samples/workflows/), which already map 1:1 to these section anchors, and they all type-check against the adk-js workflow API. The tabs also call out the behaviours that are easy to get wrong and have no Python equivalent: `ctx.runNode()` resolves to a node result rather than the output, and does not throw when a child interrupts; a second event carrying `output` silently overwrites the first; `LlmAgent.inputSchema` is not the node's input contract inside a graph. --- docs/graphs/data-handling.md | 130 ++++++++++++++++- docs/graphs/dynamic.md | 135 +++++++++++++++++- docs/graphs/human-input.md | 74 +++++++++- docs/graphs/index.md | 29 +++- docs/graphs/routes.md | 113 ++++++++++++++- .../graphs/data-handling/node_output.ts | 52 +++++++ .../graphs/data-handling/routing_output.ts | 61 ++++++++ .../snippets/graphs/data-handling/schemas.ts | 126 ++++++++++++++++ .../graphs/data-handling/session_state.ts | 58 ++++++++ .../graphs/data-handling/structured_access.ts | 77 ++++++++++ .../graphs/data-handling/structured_output.ts | 48 +++++++ .../graphs/data-handling/user_message.ts | 54 +++++++ .../snippets/graphs/dynamic/custom_run_ids.ts | 67 +++++++++ .../snippets/graphs/dynamic/data_handling.ts | 59 ++++++++ .../snippets/graphs/dynamic/get_started.ts | 40 ++++++ .../snippets/graphs/dynamic/human_input.ts | 64 +++++++++ .../snippets/graphs/dynamic/loop_route.ts | 86 +++++++++++ .../snippets/graphs/dynamic/nodes.ts | 58 ++++++++ .../snippets/graphs/dynamic/parallel_route.ts | 65 +++++++++ .../snippets/graphs/dynamic/sequence_route.ts | 68 +++++++++ .../graphs/human-input/get_started.ts | 46 ++++++ .../graphs/human-input/initial_prompt.ts | 67 +++++++++ .../graphs/human-input/payload_and_schema.ts | 99 +++++++++++++ .../snippets/graphs/index/get_started.ts | 85 +++++++++++ .../snippets/graphs/index/process_pipeline.ts | 89 ++++++++++++ .../typescript/snippets/graphs/package.json | 30 ++++ .../snippets/graphs/routes/branches.ts | 69 +++++++++ .../snippets/graphs/routes/fan_out_join.ts | 61 ++++++++ .../snippets/graphs/routes/function_node.ts | 51 +++++++ .../snippets/graphs/routes/loop_escalation.ts | 81 +++++++++++ .../snippets/graphs/routes/nested_workflow.ts | 87 +++++++++++ .../snippets/graphs/routes/sequence.ts | 42 ++++++ .../typescript/snippets/graphs/tsconfig.json | 23 +++ 33 files changed, 2289 insertions(+), 5 deletions(-) create mode 100644 examples/typescript/snippets/graphs/data-handling/node_output.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/routing_output.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/schemas.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/session_state.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/structured_access.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/structured_output.ts create mode 100644 examples/typescript/snippets/graphs/data-handling/user_message.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/data_handling.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/get_started.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/human_input.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/loop_route.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/nodes.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/parallel_route.ts create mode 100644 examples/typescript/snippets/graphs/dynamic/sequence_route.ts create mode 100644 examples/typescript/snippets/graphs/human-input/get_started.ts create mode 100644 examples/typescript/snippets/graphs/human-input/initial_prompt.ts create mode 100644 examples/typescript/snippets/graphs/human-input/payload_and_schema.ts create mode 100644 examples/typescript/snippets/graphs/index/get_started.ts create mode 100644 examples/typescript/snippets/graphs/index/process_pipeline.ts create mode 100644 examples/typescript/snippets/graphs/package.json create mode 100644 examples/typescript/snippets/graphs/routes/branches.ts create mode 100644 examples/typescript/snippets/graphs/routes/fan_out_join.ts create mode 100644 examples/typescript/snippets/graphs/routes/function_node.ts create mode 100644 examples/typescript/snippets/graphs/routes/loop_escalation.ts create mode 100644 examples/typescript/snippets/graphs/routes/nested_workflow.ts create mode 100644 examples/typescript/snippets/graphs/routes/sequence.ts create mode 100644 examples/typescript/snippets/graphs/tsconfig.json diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md index ff02684041..559801a795 100644 --- a/docs/graphs/data-handling.md +++ b/docs/graphs/data-handling.md @@ -1,7 +1,7 @@ # Data handling for agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0 + Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Structuring and managing data between agents and graph-based nodes is critical @@ -28,6 +28,31 @@ receives it as its typed input. - **`state`**: Data automatically persisted across nodes via ***Events*** throughout an ADK session. +=== "TypeScript" + + In ADK TypeScript v2.0.0, nodes exchange data through events. The key + fields for node data handling are: + + - **`output`**: the value handed to the next node. Return it bare and + it is boxed into an event for you, or set it explicitly with + `createEvent({output})`. + - **`content`**: a user-facing message. The runtime renders it, and + the graph does *not* forward it to the next node. + - **`route`**: the routing key(s) that select which conditional edge + to follow. + + Session state is separate from the event: a node reads and writes it + through `ctx.state`, and the accumulated delta is attached to that + node's events. State keys may carry a prefix that controls their + lifetime and scope: + + | Prefix | Scope | + |---|---| + | `app:` | Shared across all users and sessions for the app | + | `user:` | Tied to the user, shared across their sessions | + | `temp:` | Discarded after the current invocation ends | + | *(none)* | Persists for the lifetime of the session | + === "Go" In ADK Go v2.0.0, the data-passing mechanism depends on which agent style @@ -89,6 +114,23 @@ Each step in a workflow produces output for its successor. ***return*** or ***yield*** command without a parameter passes a `None` value to the next node. +=== "TypeScript" + + There are three equivalent ways to produce a node's output: return a + bare value, return `createEvent({output})`, or yield events from an + async generator when you want to stream progress alongside the result. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/node_output.ts:node-output" + ``` + + !!! warning "Caution: emit `output` from one event per execution" + + Nothing enforces this, so getting it wrong is silent. A node may + yield any number of events carrying `output`, each overwrites the + last, and the successor receives only the final value. Carry + progress on `content` instead. + === "Go" **workflow package**: a `FunctionNode` simply returns a typed Go value. @@ -131,6 +173,18 @@ Each step in a workflow produces output for its successor. one ***yield*** in a node, having two or more ***yield*** commands with an ***Event.output*** results in a runtime error. +=== "TypeScript" + + `output` is not limited to text. Any serializable value flows to the + next node, which receives it as a typed object — no JSON parsing and no + state reads. Attaching an `outputSchema` to the producer, or an + `inputSchema` to the consumer, makes the contract explicit and + validates it at runtime: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/structured_output.ts:structured-output" + ``` + === "Go" **workflow package**: a `FunctionNode` can return any JSON-serializable @@ -159,6 +213,16 @@ Each step in a workflow produces output for its successor. return Event(route="BUG") ``` +=== "TypeScript" + + `route` is independent of `output`, so one event can both select a + branch and forward a payload to it. `DEFAULT_ROUTE` catches everything + no other branch matched: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/routing_output.ts:routing-output" + ``` + === "Go" **workflow package**: an emitting `FunctionNode` constructs a @@ -184,6 +248,17 @@ Each step in a workflow produces output for its successor. yield Event(message="Beginning research process...") ``` +=== "TypeScript" + + A message for the human is the event's `content`. The runtime renders + it and the graph does **not** hand it to the next node — `content` is + for the user, `output` is for the next node. A node can emit both, as + two events of which only one carries `output`: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/user_message.ts:user-message" + ``` + === "Go" **workflow package**: to emit a user-visible message without advancing @@ -239,6 +314,25 @@ inside tools and callbacks regardless of which agent style you use. such as database Tools, to persist large data resources during the life cycle of a Workflow. +=== "TypeScript" + + State is written through `ctx.state`, not returned. A write is visible + to every later node in the same run and is committed with the writing + node's events: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/session_state.ts:session-state" + ``` + + !!! warning "Caution: `state` data limitations" + + Session state is a lightweight key-value store. Do not use it to + move large payloads between nodes — use artifacts or a database + tool for those. Passing a value along an edge as node `output` is + also the better choice when only the next node needs it; reach for + state when a value has to outlive the run, or be read by a tool, a + callback, or `{key}` instruction templating. + === "Go" State is written with `ctx.Session().State().Set(key, value)` and read @@ -306,6 +400,23 @@ accepted and produced by any agent node. ) ``` +=== "TypeScript" + + Schemas are Zod objects, or a genai `Schema`. Where the schema goes + matters: + + - `LlmAgent.outputSchema` forces the model to answer in that shape. + - `LlmAgent.inputSchema` is only consulted when the agent is exposed + as a **tool**. Inside a graph, the schema that validates a node's + input belongs on the node: `node(agent, {inputSchema})`. + + Agents in a graph must run in `single_turn` (the default) or `task` + mode. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/schemas.ts:schemas" + ``` + === "Go" **workflow package**: use `workflow.NewAgentNodeTyped[Input, Output]` to @@ -368,6 +479,23 @@ accepted and produced by any agent node. ) ``` +=== "TypeScript" + + Two data-selection forms are available inside an agent instruction: + + - `{Class.field}` reads a field off **this** node's input. + - `` reads a field off a named + predecessor's output. It is more restrictive, and unambiguous when + several upstream nodes share a field name. + + Both are distinct from `{state_key}`, which reads session state. The + `Class.` prefix is documentation only — resolution uses the field name + after the dot. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/data-handling/structured_access.ts:structured-access" + ``` + === "Go" In ADK Go v2.0.0, a `FunctionNode` returns a typed struct and the diff --git a/docs/graphs/dynamic.md b/docs/graphs/dynamic.md index d176affd09..b17388d44e 100644 --- a/docs/graphs/dynamic.md +++ b/docs/graphs/dynamic.md @@ -1,7 +1,7 @@ # Dynamic agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0 + Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
The ADK framework provides a programmatic way to define workflows as a more @@ -66,6 +66,24 @@ workflow containing a single node with a function: keep the written code as simple as possible. This annotation generates wrappers that allow the code to be run in the context of an ADK dynamic workflow. +=== "TypeScript" + + TypeScript has no `@node` decorator. `node(fn, options)` is the factory + form, and `ctx.runNode()` is the equivalent of `ctx.run_node()`: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/get_started.ts:get-started" + ``` + + Two things to know going in: + + - `ctx.runNode()` resolves to a node **result**, not the output + directly — read `.output`. + - An orchestrator that calls `ctx.runNode()` must set + `rerunOnResume: true`, so its body re-runs on resume and + already-finished children are replayed from their checkpoints + rather than executed again. + === "Go" In Go, `workflow.NewFunctionNode` replaces the `@node` decorator and @@ -125,6 +143,29 @@ run within a workflow. same function with different configurations, or if you are managing node references in a registry for advanced orchestration. +=== "TypeScript" + + There are two ways to build a node: the `node(fn, options)` factory, + and the explicit `new FunctionNode(name, fn, config)` constructor. + Reach for the constructor when you are wrapping a function from another + library, need several differently-configured nodes from one function, + or keep node references in a registry for advanced orchestration. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:node-forms" + ``` + + The most important option is `rerunOnResume`, which controls what + happens when a workflow resumes after a human-in-the-loop pause: + + - **`true` (re-entry):** the node body is re-run from the top. Use + this for any orchestrator that calls `ctx.runNode()` — the body + re-executes and already-completed child activations are skipped + automatically. + - **`false` (handoff, the leaf default):** the resume payload is + routed to the node's successor as input, bypassing the interrupted + node entirely. + === "Go" In Go, `workflow.NewFunctionNode[IN, OUT]` wraps a plain function as a @@ -196,6 +237,16 @@ execution logic (order and paths) for those nodes. ) ``` +=== "TypeScript" + + The orchestrator is an ordinary async function that awaits + `ctx.runNode()` for each child step, wrapped as a node with + `rerunOnResume: true` and used as the graph's only edge: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:workflows" + ``` + === "Go" `workflow.NewDynamicNode` creates an orchestrator whose body calls @@ -264,6 +315,20 @@ manually read and write session state keys for data transfer. return report_text ``` +=== "TypeScript" + + `ctx.runNode()` hands you the child's result directly, so there are no + session-state keys to read and write just to move a value one step + downstream. It accepts anything node-like, including an `LlmAgent`, + without wrapping it in `node()` first: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/data_handling.ts:data-handling" + ``` + + Schemas work the same as in a graph — attach them to the nodes you + run, as the [sequence route](#sequence-route) below does. + === "Go" In Go, `workflow.NewAgentNode` wraps an `agent.Agent` so it can be @@ -305,6 +370,15 @@ as you can with graph-based workflows. return report_text ``` +=== "TypeScript" + + A sequential route is just awaiting `ctx.runNode()` calls one after + another — each finishes before the next starts: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/sequence_route.ts:sequence-route" + ``` + === "Go" Call `workflow.RunNode` sequentially inside a `NewDynamicNode` body — @@ -366,6 +440,18 @@ workflows offer much more flexibility to define the routing logic you need. return code ``` +=== "TypeScript" + + This is where dynamic workflows earn their keep: the iteration is an + ordinary loop, not a back-edge you have to reason about. Values live in + local variables, and state is written only where an agent's instruction + template needs to read it back. Unlike a graph cycle, the loop is + trivially bounded, so a stubborn model cannot spin forever: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/loop_route.ts:loop-route" + ``` + === "Go" In Go, the loop is a plain `for` loop inside the dynamic node body. The @@ -412,6 +498,26 @@ Dynamic workflows in ADK can support parallel execution. only failed or interrupted worker nodes are re-executed, including parallel worker nodes. +=== "TypeScript" + + `ctx.runNode()` returns a promise, so starting every child before + awaiting any of them runs them concurrently, and `Promise.all` gathers + the results. Run ids are assigned in call order, so kick the children + off in a synchronous loop to keep them deterministic across a resume: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/parallel_route.ts:parallel-route" + ``` + + !!! tip "Tip: prefer the built-in parallel worker" + + When the shape is simply "map one node over a list", use + `node(worker, {parallelWorker: true, maxParallelWorkers: 4})`. It + does the fan-out for you and bounds concurrency (default 8). + Hand-rolling it, as above, is for when you need custom scheduling + or partial-failure handling. On resume, only failed or interrupted + workers re-execute either way. + === "Go" In Go, `workflow.NewParallelWorker` wraps a child node and runs it @@ -469,6 +575,23 @@ Dynamic workflows in ADK can also include human input or human in the loop Parent nodes in dynamic workflows that call `ctx.run_node` must set `rerun_on_resume=True` to handle interruptions properly. +=== "TypeScript" + + The leaf node returns a `RequestInput` to pause the workflow, and keeps + the default `rerunOnResume: false` so the reply becomes its output. The + orchestrator that calls it must set `rerunOnResume: true`: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/human_input.ts:human-input" + ``` + + !!! important "Important: check `interruptIds` before deciding" + + `ctx.runNode()` does **not** throw when a child interrupts. It + resolves with a result whose `interruptIds` are populated and whose + `output` is still `undefined`, so an orchestrator that does not + check will decide on an answer the human never gave. + === "Go" In Go, use `workflow.NewEmittingFunctionNode` with @@ -546,6 +669,16 @@ and logically remain the same for the input. least one non-numeric character to avoid collisions with these auto-generated IDs. +=== "TypeScript" + + Pass `{runId}` as a trailing option to `ctx.runNode()`. The id must + contain at least one non-numeric character so it cannot collide with + the auto-generated sequential ids: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts:custom-execution-ids" + ``` + === "Go" In Go, pass `workflow.WithRunID("order-x")` as a trailing option to diff --git a/docs/graphs/human-input.md b/docs/graphs/human-input.md index ca3d0a42d3..01fc6bfabf 100644 --- a/docs/graphs/human-input.md +++ b/docs/graphs/human-input.md @@ -1,7 +1,7 @@ # Human input for agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0 + Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Being able to request human input for data input, decision verification, or @@ -39,6 +39,23 @@ the input process more predictable and reliable. system receives an input from a user. Once the system receives input from the user, that input is passed to the next node. +=== "TypeScript" + + In ADK TypeScript v2.0.0, a human input node yields a `RequestInput`. + `step1` pauses the workflow until the user replies, and the reply is + handed to the next node as its input. A HITL node needs no model, which + makes the pause fully deterministic. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/human-input/get_started.ts:get-started" + ``` + + This is the default `rerunOnResume: false` handoff: the interrupted + node does **not** re-run — it completes with the user's reply as its + output. A node that calls `ctx.runNode()` needs `rerunOnResume: true` + instead; see + [human input in dynamic workflows](/graphs/dynamic/#human-input). + === "Go" In ADK Go v2.0.0, a HITL graph node is built with @@ -77,6 +94,38 @@ the input process more predictable and reliable. experience, consider providing a user interface to collect structured data or use an Agent node to conform unstructured data to the format required. +=== "TypeScript" + + `RequestInput` takes the following configuration options: + + - **`message`:** Text shown to the user explaining what is being + asked. + - **`payload`:** Structured data sent alongside the prompt, so a + client can render richer context. + - **`responseSchema`:** The shape the reply is expected to take. It + travels on the interrupt as + `functionCall.args.response_schema`, which is what a client reads + to render a form for the reply. + + `rerunOnResume` on the node controls what happens when the reply + arrives: + + - **`false`** (the leaf default): the reply is routed to the node's + successor as input, bypassing the interrupted node. + - **`true`**: the node body is re-run from the top. Required for any + node that calls `ctx.runNode()`, so it can deliver cached child + results on resume. + + !!! note "Note: Response schema input limitations" + + `RequestInput` does not reformat a human reply to fit + `responseSchema` — the reply must already be in that shape. A reply + carrying an *object* is checked against the schema and a mismatch + fails loudly, leaving the interrupt open so the next reply answers + it; a plain-text reply is never schema-checked. For a better user + experience, collect structured data in your UI, or put an agent + node after the pause to normalize whatever the human typed. + === "Go" `session.RequestInput` carries the following fields, which map directly to @@ -149,6 +198,16 @@ The following code examples demonstrate more detailed human input requests. ) ``` +=== "TypeScript" + + The following three-node graph builds a structured itinerary, sends it + as `payload` alongside the prompt so a client can render it, and acts + on the user's feedback: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/human-input/payload_and_schema.ts:payload-and-schema" + ``` + === "Go" The following code sample shows a three-node graph: a builder node generates @@ -190,6 +249,19 @@ specific tool call. yield RequestInput(message=input_message, response_schema=str) ``` +=== "TypeScript" + + Set `requireConfirmation: true` on a `FunctionTool` and the agent pauses + for approval before that tool runs. + + A graph HITL node is the other half of this: rather than confirming a + tool call, it can open the workflow by asking the user what they want. + `responseSchema: z.string()` asks for a plain text reply: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/human-input/initial_prompt.ts:initial-prompt" + ``` + === "Go" Set `RequireConfirmation: true` in `functiontool.Config` for a static diff --git a/docs/graphs/index.md b/docs/graphs/index.md index 3211edc40d..e5f66d14da 100644 --- a/docs/graphs/index.md +++ b/docs/graphs/index.md @@ -1,7 +1,7 @@ # Graph-based agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0 + Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Graph-based agent workflows in ADK let you build agents with more precise control, @@ -101,6 +101,20 @@ function, and the final agent reports the information. ) ``` +=== "TypeScript" + + In ADK TypeScript v2.0.0, a `Workflow` takes an `edges` array whose + rows list the nodes to run in order. `node()` wraps a function, an + agent, a tool, or another `Workflow` as a graph node, and is where you + attach the node's name and its `inputSchema` / `outputSchema` + contracts — schemas are Zod objects, or a genai `Schema`. Each node's + return value is handed to the next node as its input, so no session + state writes are needed. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/index/get_started.ts:get-started" + ``` + === "Go" In ADK Go v2.0.0, sequential workflows use the graph engine: @@ -194,6 +208,19 @@ translated into a graph-based agent: ) ``` +=== "TypeScript" + + In ADK TypeScript v2.0.0, a router node returns an event carrying a + `route` value, built with `createEvent({route})`. A second edge row + maps each route value to the node that handles it. Setting `route` to + an *array* dispatches to every matching branch, which is what lets the + classifier below reply with more than one category, and `DEFAULT_ROUTE` + catches anything no branch matched. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/index/process_pipeline.ts:process-pipeline" + ``` + === "Go" In ADK Go v2.0.0, conditional routing uses `workflow.NewEmittingFunctionNode` diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md index d05bcdf86b..a59aef8f57 100644 --- a/docs/graphs/routes.md +++ b/docs/graphs/routes.md @@ -1,7 +1,7 @@ # Build graph routes for agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0 + Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Graph-based workflows in ADK define agent logic as a graph of execution nodes @@ -35,6 +35,25 @@ agents. ) ``` +=== "TypeScript" + + ```typescript + export const rootAgent = new Workflow({ + name: 'routing_workflow', + edges: [ + ['START', processMessage, router], + [ + router, + { + 'output-1': response1, + 'output-2': response2, + 'output-3': response3, + }, + ], + ], + }); + ``` + === "Go" ADK Go v2.0.0 provides the following approach to graph-based @@ -91,6 +110,19 @@ objects. return Event(output=input_text_modified) ``` +=== "TypeScript" + + In ADK TypeScript v2.0.0, the primary node type is a `FunctionNode`, + built by passing a plain function to `node()`. A handler always takes + `(ctx, input)`; nothing is injected by parameter name. Returning a bare + value boxes it into an event's `output` for you, and returning + `createEvent({output})` is the explicit form — useful when you also need + to set `route` or `content`: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/function_node.ts:function-node" + ``` + === "Go" In ADK Go v2.0.0, the primary node type is `workflow.NewFunctionNode`. @@ -136,6 +168,23 @@ A sequential route runs each node once, in the listed order. task_C_node)] # 3 nodes run in order ``` +=== "TypeScript" + + An `edges` row starting with `'START'` runs each listed node once, in + order, forwarding every node's return value to the next one: + + ```typescript + edges: [['START', taskANode]] // a single node + edges: [['START', taskANode, taskBNode, taskCNode]] // three, in order + ``` + + Listing `'START'` in more than one row fans out into parallel paths + instead — see [fan out and join](#parallel-tasks-fan-out-and-join-paths). + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/sequence.ts:sequence" + ``` + === "Go" `workflow.Chain(workflow.Start, nodeA, nodeB, nodeC)` wires nodes into a @@ -185,6 +234,18 @@ A sequential route runs each node once, in the listed order. ) ``` +=== "TypeScript" + + Branching is a node that emits a `route`, plus an edge row mapping each + route value to the node that handles it. Route values may be strings, + numbers or booleans, and `DEFAULT_ROUTE` matches when no other route on + the same source node did. A branch target is anything node-like: + `taskBNode` below is an `LlmAgent`, `taskCNode` a plain function. + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/branches.ts:branches" + ``` + === "Go" In ADK Go v2.0.0, conditional dispatch uses the `workflow` graph engine. @@ -289,6 +350,24 @@ before passing results to the next step. Make sure to include failsafe output from any node that outputs to a ***JoinNode***. +=== "TypeScript" + + A `JoinNode` is the fan-in barrier. It waits for every predecessor and + then hands its successor a record keyed by predecessor node name: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/fan_out_join.ts:fan-out-join" + ``` + + !!! warning "Caution: Stuck JoinNode from incomplete nodes" + + The barrier waits for every predecessor to **complete**, not to + produce an output. A predecessor that finishes without one still + releases the join and arrives in the record as its name mapped to + `undefined`, so reading a field off it throws somewhere downstream, + far from the node that skipped it. Give anything feeding a join an + output of its own, and a `retryConfig` if it can fail. + === "Go" ADK Go v2.0.0 provides `workflow.NewJoinNode` for true fan-in in the @@ -370,6 +449,20 @@ accomplish this goal. its process, the parent node extracts data from the final leaf nodes and emits it as the output of the nested workflow. +=== "TypeScript" + + A `Workflow` is itself a node, so it can be dropped straight into + another workflow's edges to encapsulate a reusable sub-process: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/nested_workflow.ts:nested-workflow" + ``` + + **Nested workflow data output.** While the inner workflow runs, each of + its node events bubbles up to the parent for traceability. When it + finishes, the output of its terminal node becomes the output of the + nested-workflow node. + === "Go" ADK Go v2.0.0 supports nested workflows in two complementary ways: @@ -434,6 +527,24 @@ lifecycle on each iteration. ) ``` +=== "TypeScript" + + A loop is a **back-edge**: a downstream node routes back to an earlier + node, and the engine re-activates that node with a fresh lifecycle on + each iteration. The loop exits when the router picks the terminal + branch instead: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/loop_escalation.ts:loop-escalation" + ``` + + !!! warning "Caution: Unbounded graph cycles" + + A graph cycle is not capped by the framework. Make sure the exit + condition always becomes true, or bound the loop yourself with a + [dynamic workflow](/graphs/dynamic/#loop-route), where the + iteration is an ordinary loop you control. + === "Go" The following example uses the graph engine with `workflow.EdgeBuilder`. diff --git a/examples/typescript/snippets/graphs/data-handling/node_output.ts b/examples/typescript/snippets/graphs/data-handling/node_output.ts new file mode 100644 index 0000000000..93975add5e --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/node_output.ts @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A node hands data to its successor through the event's `output` field. + +// --8<-- [start:node-output] +import { createEvent, node, NodeContext, Workflow } from "@google/adk"; + +// 1. A bare return value — boxed into an event's `output` for you. +const returnRawValue = node( + (_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(), + { name: "return_raw_value" }, +); + +// 2. An explicit Event — for when you also need `route`, `content` or `actions`. +const returnEventOutput = node( + (_ctx: NodeContext, nodeInput: string) => + createEvent({ output: `${nodeInput}!` }), + { name: "return_event_output" }, +); + +// 3. A generator — stream progress, then emit the output event last. +const yieldProgressThenOutput = node( + async function* (_ctx: NodeContext, nodeInput: string) { + // Progress goes on `content`: displayed, and not passed to the successor. + yield createEvent({ + content: { role: "model", parts: [{ text: "Working on it..." }] }, + }); + // Exactly one event sets `output`, so there is nothing to overwrite it. + yield createEvent({ output: `<<${nodeInput}>>` }); + }, + { name: "yield_progress_then_output" }, +); + +export const rootAgent = new Workflow({ + name: "node_output_workflow", + edges: [ + ["START", returnRawValue, returnEventOutput, yieldProgressThenOutput], + ], +}); +// --8<-- [end:node-output] diff --git a/examples/typescript/snippets/graphs/data-handling/routing_output.ts b/examples/typescript/snippets/graphs/data-handling/routing_output.ts new file mode 100644 index 0000000000..e6eab132ee --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/routing_output.ts @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `route` drives conditional edge dispatch. It is independent of `output`, so a +// router can select a branch AND forward a payload in the same event. + +// --8<-- [start:routing-output] +import { + createEvent, + DEFAULT_ROUTE, + node, + NodeContext, + Workflow, +} from "@google/adk"; + +const router = node( + (_ctx: NodeContext, nodeInput: string) => + createEvent({ + route: /bug|crash|error/i.test(nodeInput) ? "BUG" : "OTHER", + // Forwarded to whichever branch fires. + output: nodeInput, + }), + { name: "router" }, +); + +const handleBug = node( + (_ctx: NodeContext, nodeInput: string) => `Filed a bug for: ${nodeInput}`, + { name: "handle_bug" }, +); + +const handleAnythingElse = node( + (_ctx: NodeContext, nodeInput: string) => `No bug detected in: ${nodeInput}`, + { name: "handle_anything_else" }, +); + +export const rootAgent = new Workflow({ + name: "routing_output_workflow", + edges: [ + ["START", router], + [ + router, + { + BUG: handleBug, + // Fires when no other route on this node matched. + [DEFAULT_ROUTE]: handleAnythingElse, + }, + ], + ], +}); +// --8<-- [end:routing-output] diff --git a/examples/typescript/snippets/graphs/data-handling/schemas.ts b/examples/typescript/snippets/graphs/data-handling/schemas.ts new file mode 100644 index 0000000000..1d25798397 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/schemas.ts @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Schemas constrain what a node accepts and produces. Use a Zod object, or a +// genai `Schema`. +// +// Where the schema goes: +// - `LlmAgent.outputSchema` forces the model to answer in that shape. +// - `LlmAgent.inputSchema` is only consulted when the agent is exposed as a +// tool. Inside a graph, the schema that VALIDATES a node's input belongs on +// the node: `node(agent, {inputSchema})`. + +// --8<-- [start:schemas] +import { + FunctionTool, + LlmAgent, + node, + NodeContext, + Workflow, +} from "@google/adk"; +import { z } from "zod"; + +const flightSearchInputSchema = z.object({ + origin: z.string().describe('Origin airport code, e.g. "SFO".'), + destination: z.string().describe('Destination airport code, e.g. "CDG".'), + departureDate: z.string().describe('Departure date, e.g. "2026-03-15".'), + passengers: z.number().describe("Number of passengers."), +}); +type FlightSearchInput = z.infer; + +const flightSchema = z.object({ + carrier: z.string(), + flightNumber: z.string(), + price: z.number(), +}); + +const flightSearchOutputSchema = z.object({ + flights: z.array(flightSchema), + cheapestPrice: z.number(), +}); +type FlightSearchOutput = z.infer; + +/** Stands in for a real flight-search API. */ +const searchFlightsApi = new FunctionTool({ + name: "search_flights_api", + description: "Searches available flights for a route and date.", + parameters: flightSearchInputSchema, + execute: ({ origin, destination }) => [ + { + carrier: "AF", + flightNumber: `AF${origin.length}${destination.length}0`, + price: 812.4, + }, + { + carrier: "UA", + flightNumber: `UA${origin.length}${destination.length}1`, + price: 947.0, + }, + ], +}); + +// Turns the free-text request into the structured node input the searcher +// expects. In a real app this would itself be an extraction agent. +const parseRequest = node( + (_ctx: NodeContext, nodeInput: string): FlightSearchInput => { + const codes = nodeInput.toUpperCase().match(/\b[A-Z]{3}\b/g) ?? []; + const date = nodeInput.match(/\d{4}-\d{2}-\d{2}/)?.[0]; + const passengers = Number( + nodeInput.match(/(\d+)\s*(people|pax|passengers?)/i)?.[1], + ); + return { + origin: codes[0] ?? "SFO", + destination: codes[1] ?? "CDG", + departureDate: date ?? "2026-03-15", + passengers: Number.isFinite(passengers) ? passengers : 1, + }; + }, + { name: "parse_request", outputSchema: flightSearchInputSchema }, +); + +// Agents in a graph must run in `single_turn` (the default) or `task` mode. +const flightSearcher = new LlmAgent({ + name: "flight_searcher", + model: "gemini-flash-latest", + mode: "single_turn", + instruction: + "Search for available flights with the search_flights_api tool and report " + + "every flight it returns plus the cheapest price.", + inputSchema: flightSearchInputSchema, + outputSchema: flightSearchOutputSchema, + tools: [searchFlightsApi], +}); + +const renderResults = node( + (_ctx: NodeContext, results: FlightSearchOutput) => + `Cheapest: $${results.cheapestPrice}\n` + + results.flights + .map((f) => ` ${f.carrier} ${f.flightNumber} — $${f.price}`) + .join("\n"), + { name: "render_results", inputSchema: flightSearchOutputSchema }, +); + +export const rootAgent = new Workflow({ + name: "flight_workflow", + edges: [ + [ + "START", + parseRequest, + // The graph-level input contract for the agent node. + node(flightSearcher, { inputSchema: flightSearchInputSchema }), + renderResults, + ], + ], +}); +// --8<-- [end:schemas] diff --git a/examples/typescript/snippets/graphs/data-handling/session_state.ts b/examples/typescript/snippets/graphs/data-handling/session_state.ts new file mode 100644 index 0000000000..186c2b9649 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/session_state.ts @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A node takes an explicit `(ctx, input)` pair and reads and writes session +// state through `ctx.state`. A write is visible to every later node in the same +// run, and is committed with the writing node's events. + +// --8<-- [start:session-state] +import { node, NodeContext, Workflow } from "@google/adk"; + +// State-key prefixes control lifetime and scope: +// "app:" shared across all users and sessions of the app +// "user:" tied to the user, shared across their sessions +// "temp:" discarded when the current invocation ends +// "" persists for the lifetime of the session +const initStateNode = node( + (ctx: NodeContext, nodeInput: string) => { + ctx.state.set("topic", nodeInput.trim()); + // Scoped key: dropped when this invocation ends, never persisted. + ctx.state.set("temp:started_at", new Date().toISOString()); + ctx.state.set("attempts", 0); + }, + { name: "init_state_node" }, +); + +const taskAttemptNode = node( + (ctx: NodeContext) => { + // Reads the value init_state_node wrote earlier in this same run. + const attempts = ctx.state.get("attempts") ?? 0; + ctx.state.set("attempts", attempts + 1); + }, + { name: "task_attempt_node" }, +); + +const readStateNode = node( + (ctx: NodeContext) => + `attempts state: ${ctx.state.get("attempts")} ` + + `(topic: ${ctx.state.get("topic")}, ` + + `started: ${ctx.state.get("temp:started_at")})`, + { name: "read_state_node" }, +); + +export const rootAgent = new Workflow({ + name: "session_state_workflow", + edges: [["START", initStateNode, taskAttemptNode, readStateNode]], +}); +// --8<-- [end:session-state] diff --git a/examples/typescript/snippets/graphs/data-handling/structured_access.ts b/examples/typescript/snippets/graphs/data-handling/structured_access.ts new file mode 100644 index 0000000000..04cab8b151 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/structured_access.ts @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Two data-selection forms are available inside an agent instruction: +// +// {Class.field} reads a field off THIS node's input +// reads a field off a named predecessor's +// output — more restrictive, and unambiguous +// when several upstream nodes share a field +// +// Both are distinct from `{state_key}`, which reads session state. + +// --8<-- [start:structured-access] +import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; +import { z } from "zod"; + +const cityTimeSchema = z.object({ + timeInfo: z.string().describe("Time information."), + city: z.string().describe("City name."), +}); +type CityTime = z.infer; + +const cityGeneratorAgent = new LlmAgent({ + name: "city_generator_agent", + model: "gemini-flash-latest", + instruction: "Return the name of a random city. Return only the name.", +}); + +/** Simulates returning the current time in the specified city. */ +const lookupTimeFunction = node( + (_ctx: NodeContext, city: string): CityTime => ({ + timeInfo: "10:10 AM", + city: city.trim(), + }), + { name: "lookup_time_function", outputSchema: cityTimeSchema }, +); + +const cityReportAgent = new LlmAgent({ + name: "city_report_agent", + model: "gemini-flash-latest", + + // Data selection based on class and parameter — reads this node's own input: + // instruction: `Return a sentence in the following format: + // It is {CityTime.timeInfo} in {CityTime.city} right now.`, + + // More restrictive data selection, qualified by source node name. Keep the + // template on ONE line: a model reproduces a line break inside the format + // string, which splits the answer mid-sentence. + instruction: + "Return a sentence in the following format: It is " + + " in " + + " right now.", +}); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [ + [ + "START", + cityGeneratorAgent, + lookupTimeFunction, + node(cityReportAgent, { inputSchema: cityTimeSchema }), + ], + ], +}); +// --8<-- [end:structured-access] diff --git a/examples/typescript/snippets/graphs/data-handling/structured_output.ts b/examples/typescript/snippets/graphs/data-handling/structured_output.ts new file mode 100644 index 0000000000..cee04b5c68 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/structured_output.ts @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `output` is not limited to text — any serializable value flows to the next +// node, which receives it as a typed object. + +// --8<-- [start:structured-output] +import { createEvent, node, NodeContext, Workflow } from "@google/adk"; +import { z } from "zod"; + +const cityInfoSchema = z.object({ + cityName: z.string(), + cityTime: z.string(), +}); +type CityInfo = z.infer; + +const emitStructuredOutput = node( + async function* () { + yield createEvent({ + output: { cityName: "Paris", cityTime: "10:10 AM" } satisfies CityInfo, + }); + }, + { name: "emit_structured_output", outputSchema: cityInfoSchema }, +); + +// The successor receives the object itself — no JSON parsing, no state reads. +const consumeStructuredOutput = node( + (_ctx: NodeContext, cityInfo: CityInfo) => + `It is ${cityInfo.cityTime} in ${cityInfo.cityName} right now.`, + { name: "consume_structured_output", inputSchema: cityInfoSchema }, +); + +export const rootAgent = new Workflow({ + name: "structured_output_workflow", + edges: [["START", emitStructuredOutput, consumeStructuredOutput]], +}); +// --8<-- [end:structured-output] diff --git a/examples/typescript/snippets/graphs/data-handling/user_message.ts b/examples/typescript/snippets/graphs/data-handling/user_message.ts new file mode 100644 index 0000000000..adb3ea7ffb --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/user_message.ts @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A message for the human is the event's `content`: the runtime renders it, and +// the graph does NOT forward it as node input. `content` is for the user, +// `output` is for the next node. + +// --8<-- [start:user-message] +import { createEvent, node, NodeContext, Workflow } from "@google/adk"; + +/** Emits a user-facing message: `content`, with no `output`. */ +const message = (text: string) => + createEvent({ content: { role: "model", parts: [{ text }] } }); + +// Tell the user the research process is starting. No `output`, so nothing is +// handed to the next node. +const userMessage = node( + async function* (_ctx: NodeContext, nodeInput: string) { + yield message(`Beginning research process for "${nodeInput}"...`); + }, + { name: "user_message" }, +); + +// A message AND an output in one node: two events, only one carrying `output`. +const research = node( + async function* () { + yield message("Gathering sources..."); + yield createEvent({ output: ["source-a", "source-b", "source-c"] }); + }, + { name: "research" }, +); + +const report = node( + (_ctx: NodeContext, sources: string[]) => + `Research complete. ${sources.length} sources: ${sources.join(", ")}.`, + { name: "report" }, +); + +export const rootAgent = new Workflow({ + name: "user_message_workflow", + edges: [["START", userMessage, research, report]], +}); +// --8<-- [end:user-message] diff --git a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts new file mode 100644 index 0000000000..051242516b --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ADK gives every child execution a deterministic id derived from the parent id +// and a per-node-name counter ("1", "2", "3", ...). Those ids are how a resumed +// or retried workflow recognises work that already completed and skips it. +// +// Avoid custom run ids. The one legitimate case is a REORDERABLE collection, +// where position is not stable but identity is — key the run id off the item's +// own id, as below. + +// --8<-- [start:custom-execution-ids] +import { node, NodeContext, Workflow } from "@google/adk"; + +interface Order { + orderId: string; + cartItems: string[]; +} + +/** Stands in for loading orders from a database. */ +async function getOrders(): Promise { + return [ + { orderId: "a91", cartItems: ["keyboard", "mouse"] }, + { orderId: "b02", cartItems: ["monitor"] }, + { orderId: "c73", cartItems: ["dock", "cable", "hub"] }, + ]; +} + +const processOrder = node( + (_ctx: NodeContext, order: Order) => + `order ${order.orderId}: ${order.cartItems.length} item(s) shipped`, + { name: "process_order" }, +); + +const processAllOrders = node( + async (ctx: NodeContext) => { + const orders = await getOrders(); + + const processTasks = orders.map((order) => + // Use runId to provide a custom identifier. It must contain at least one + // non-numeric character to avoid colliding with the auto-generated + // sequential numeric ids. + ctx.runNode(processOrder, order, { runId: `order-${order.orderId}` }), + ); + + const results = await Promise.all(processTasks); + return results.map((result) => result.output).join("\n"); + }, + { name: "process_all_orders", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", processAllOrders]], +}); +// --8<-- [end:custom-execution-ids] diff --git a/examples/typescript/snippets/graphs/dynamic/data_handling.ts b/examples/typescript/snippets/graphs/dynamic/data_handling.ts new file mode 100644 index 0000000000..3f61778301 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Passing data in a dynamic workflow is simpler than in a graph: `ctx.runNode()` +// hands you the child's result directly, so there are no session-state keys to +// read and write just to move a value one step downstream. + +// --8<-- [start:data-handling] +import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; + +const draftAgent = new LlmAgent({ + name: "draft_agent", + model: "gemini-flash-latest", + instruction: "Write a short draft for the user request.", +}); + +const formatFunctionNode = node( + (_ctx: NodeContext, rawDraft: string) => + rawDraft + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => `| ${line}`) + .join("\n"), + { name: "format_function_node" }, +); + +const editorialWorkflow = node( + async (ctx: NodeContext, userRequest: string) => { + // Agent node generates output. + const rawDraft = await ctx.runNode(draftAgent, userRequest); + + // Function node formats text. + const formattedText = await ctx.runNode( + formatFunctionNode, + rawDraft.output, + ); + + return formattedText.output; + }, + { name: "editorial_workflow", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", editorialWorkflow]], +}); +// --8<-- [end:data-handling] diff --git a/examples/typescript/snippets/graphs/dynamic/get_started.ts b/examples/typescript/snippets/graphs/dynamic/get_started.ts new file mode 100644 index 0000000000..6f2f691bee --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/get_started.ts @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A dynamic workflow drops the static edge graph and orchestrates in plain +// code: an outer node calls `ctx.runNode(child, input)` to execute children in +// whatever order your loops and conditionals dictate. + +// --8<-- [start:get-started] +import { node, NodeContext, Workflow } from "@google/adk"; + +const myNode = node(() => "Hello World", { name: "hello_node" }); + +// An orchestrator that calls `ctx.runNode` must set `rerunOnResume: true`, so +// its body re-runs on resume and already-finished children are replayed from +// their checkpoints rather than executed again. +const myWorkflow = node( + async (ctx: NodeContext, _nodeInput: string) => { + // runNode executes a node and resolves to its RESULT, so read `.output`. + const result = await ctx.runNode(myNode, "hello"); + return result.output; + }, + { name: "my_workflow", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", myWorkflow]], +}); +// --8<-- [end:get-started] diff --git a/examples/typescript/snippets/graphs/dynamic/human_input.ts b/examples/typescript/snippets/graphs/dynamic/human_input.ts new file mode 100644 index 0000000000..d6050031ed --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/human_input.ts @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A parent node that calls `ctx.runNode` must set `rerunOnResume: true`, or it +// cannot handle an interrupt raised by a child. The leaf keeps +// `rerunOnResume: false`: on resume it does not re-run its body, it completes +// with the human's reply as its output. + +// --8<-- [start:human-input] +import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; + +/** + * Pauses the workflow and waits for user input. + * + * `rerunOnResume: false` (the default, spelled out here because it is the + * point) is what makes this a one-liner: the reply is handed to the node as + * its output instead of the body running a second time to collect it. + */ +const getUserApproval = node( + () => new RequestInput({ message: "Please approve this request (Yes/No)" }), + { name: "get_user_approval", rerunOnResume: false }, +); + +/** The orchestrator calling the interactive step. */ +const handleProcess = node( + async (ctx: NodeContext, nodeInput: unknown) => { + const approval = await ctx.runNode(getUserApproval, nodeInput); + + // `ctx.runNode()` does NOT throw when a child interrupts: it resolves with + // a result whose `interruptIds` are populated and whose `output` is still + // undefined. Return without deciding — the workflow pauses and this body + // re-runs once the reply arrives. + if (approval.interruptIds.length > 0) { + return undefined; + } + + const userResponse = String(approval.output ?? "") + .trim() + .toLowerCase(); + + if (userResponse === "yes") { + return "Approved"; + } + return "Denied"; + }, + { name: "handle_process", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", handleProcess]], +}); +// --8<-- [end:human-input] diff --git a/examples/typescript/snippets/graphs/dynamic/loop_route.ts b/examples/typescript/snippets/graphs/dynamic/loop_route.ts new file mode 100644 index 0000000000..1fd61d6f59 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts @@ -0,0 +1,86 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is where dynamic workflows earn their keep: the iteration is an ordinary +// loop, not a back-edge you have to reason about. Values live in local +// variables; state is written only where an agent's instruction template needs +// to read it back (`{code}`, `{findings}`). + +// --8<-- [start:loop-route] +import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; + +/** Safety bound on the refine loop. */ +const MAX_FIX_ROUNDS = 3; + +const coderAgent = new LlmAgent({ + name: "generator_agent", + model: "gemini-flash-latest", + instruction: "Write TypeScript code for the user request. Output code only.", +}); + +/** Simulates a compile / lint pass. Empty findings means "clean". */ +const compileLintCheck = node( + (_ctx: NodeContext, code: string) => { + const findings: string[] = []; + if (!/\/\*\*/.test(code)) { + findings.push("every function needs a JSDoc comment"); + } + if (!/\)\s*:\s*\w/.test(code)) { + findings.push("add return type annotations"); + } + return { findings: findings.join("; ") }; + }, + { name: "lint_reviewer" }, +); + +const fixerAgent = new LlmAgent({ + name: "fixer_agent", + model: "gemini-flash-latest", + instruction: `Refactor current code {code}. + Based on compile & lint review: {findings} + Output code only.`, +}); + +const codeWorkflow = node( + async (ctx: NodeContext, userRequest: string) => { + let code = (await ctx.runNode(coderAgent, userRequest)).output as string; + let checkResp = (await ctx.runNode(compileLintCheck, code)).output as { + findings: string; + }; + + // Unlike a graph cycle, the loop is trivially bounded, so a stubborn model + // cannot spin forever burning live model calls. + for (let round = 0; checkResp.findings && round < MAX_FIX_ROUNDS; round++) { + // The fixer agent reads `{code}` / `{findings}` from session state. + ctx.state.set("code", code); + ctx.state.set("findings", checkResp.findings); + + code = ( + await ctx.runNode(fixerAgent, { code, findings: checkResp.findings }) + ).output as string; + checkResp = (await ctx.runNode(compileLintCheck, code)).output as { + findings: string; + }; + } + + return code; + }, + { name: "code_workflow", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", codeWorkflow]], +}); +// --8<-- [end:loop-route] diff --git a/examples/typescript/snippets/graphs/dynamic/nodes.ts b/examples/typescript/snippets/graphs/dynamic/nodes.ts new file mode 100644 index 0000000000..98bacc35fe --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/nodes.ts @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The two ways to build a node, and the orchestrator that composes them. + +// --8<-- [start:node-forms] +import { FunctionNode, node, NodeContext, Workflow } from "@google/adk"; + +/** The plain function both node forms wrap. */ +function myFunctionNode(_ctx: NodeContext, nodeInput: unknown): string { + return `Hello ${nodeInput ?? "World"}`; +} + +// Form 1 — the `node()` factory. TypeScript has no `@node` decorator form. +const helloNode = node(myFunctionNode, { name: "hello_node" }); + +// Form 2 — the explicit constructor, same function, different configuration. +// Reach for it when you are wrapping a function from another library, need +// several differently-configured nodes from one function, or keep node +// references in a registry for advanced orchestration. +const successNode = new FunctionNode("hello", myFunctionNode, { + rerunOnResume: true, +}); +// --8<-- [end:node-forms] + +// --8<-- [start:workflows] +const myFormattingNode = node( + (_ctx: NodeContext, nodeInput: string) => `>> ${nodeInput.trim()} <<`, + { name: "my_formatting_node" }, +); + +// The orchestrator: run children in order and return the last result. +const myWorkflow = node( + async (ctx: NodeContext, nodeInput: unknown) => { + const greeted = await ctx.runNode(helloNode, nodeInput); + const again = await ctx.runNode(successNode, greeted.output); + const formatted = await ctx.runNode(myFormattingNode, again.output); + return formatted.output; + }, + { name: "my_workflow", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", myWorkflow]], +}); +// --8<-- [end:workflows] diff --git a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts new file mode 100644 index 0000000000..605dd970fc --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `ctx.runNode()` returns a promise, so starting every child before awaiting any +// of them runs them concurrently, and `Promise.all` gathers the results. +// +// Prefer the built-in when the shape is "map one node over a list": +// node(worker, {parallelWorker: true, maxParallelWorkers: 4}) +// It does the fan-out for you and bounds concurrency (default 8). Hand-rolling +// it, as below, is for when you need custom scheduling or partial-failure +// handling. + +// --8<-- [start:parallel-route] +import { node, NodeContext, Workflow } from "@google/adk"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** The worker run once per list item. */ +const realNode = node( + async (_ctx: NodeContext, item: string) => { + await sleep(200); // stand-in for real work + return { item, length: item.length }; + }, + { name: "analyze_item" }, +); + +const parallelSupervisor = node( + async (ctx: NodeContext, nodeInput: string) => { + const items = nodeInput + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + + // Run ids are assigned in CALL order, so kick the children off in a + // synchronous loop to keep them deterministic across a resume. + const tasks = items.map((item) => ctx.runNode(realNode, item)); + const results = await Promise.all(tasks); + + return results.map((result) => result.output); + }, + { name: "parallel_supervisor", rerunOnResume: true }, +); + +const summarize = node( + (_ctx: NodeContext, results: Array<{ item: string; length: number }>) => + results.map((r) => `${r.item}: ${r.length} chars`).join("\n"), + { name: "summarize" }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", parallelSupervisor, summarize]], +}); +// --8<-- [end:parallel-route] diff --git a/examples/typescript/snippets/graphs/dynamic/sequence_route.ts b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts new file mode 100644 index 0000000000..249f5109c8 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A sequential route in a dynamic workflow is just awaiting `ctx.runNode()` +// calls one after another — each finishes before the next starts. Schemas work +// the same as in a graph: attach them to the nodes you run. + +// --8<-- [start:sequence-route] +import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; +import { z } from "zod"; + +const cityTimeSchema = z.object({ + timeInfo: z.string().describe("Time information."), + city: z.string().describe("City name."), +}); +type CityTime = z.infer; + +const cityGeneratorAgent = new LlmAgent({ + name: "city_generator_agent", + model: "gemini-flash-latest", + instruction: "Return the name of a random city. Return only the name.", +}); + +/** Simulates returning the current time in a specified city. */ +const cityTimeFunction = node( + (_ctx: NodeContext, city: string): CityTime => ({ + timeInfo: "10:10 AM", + city: city.trim(), + }), + { name: "city_time_function", outputSchema: cityTimeSchema }, +); + +const cityReportAgent = node( + new LlmAgent({ + name: "city_report_agent", + model: "gemini-flash-latest", + instruction: "Output the data provided by the previous node as a sentence.", + }), + { inputSchema: cityTimeSchema }, +); + +const cityWorkflow = node( + async (ctx: NodeContext) => { + const city = await ctx.runNode(cityGeneratorAgent); + const cityTime = await ctx.runNode(cityTimeFunction, city.output); + const reportText = await ctx.runNode(cityReportAgent, cityTime.output); + + return reportText.output; + }, + { name: "city_workflow", rerunOnResume: true }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", cityWorkflow]], +}); +// --8<-- [end:sequence-route] diff --git a/examples/typescript/snippets/graphs/human-input/get_started.ts b/examples/typescript/snippets/graphs/human-input/get_started.ts new file mode 100644 index 0000000000..9b3e210d30 --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/get_started.ts @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `step1` pauses the workflow until the user replies; the reply is then handed +// to the next node as its input. This is the default `rerunOnResume: false` +// handoff: the interrupted node does NOT re-run — it completes with the user's +// reply as its output. A HITL node needs no model, which makes the pause fully +// deterministic. + +// --8<-- [start:get-started] +import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; + +const step1 = node( + async function* () { + yield new RequestInput({ message: "Enter a number:" }); + }, + { name: "step1" }, +); + +const step2 = node( + (_ctx: NodeContext, nodeInput: string | number) => { + // An interactive reply arrives as text, so coerce before doing maths. + const value = Number(nodeInput); + return Number.isFinite(value) + ? value * 2 + : `"${nodeInput}" is not a number.`; + }, + { name: "step2" }, +); + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [["START", step1, step2]], +}); +// --8<-- [end:get-started] diff --git a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts new file mode 100644 index 0000000000..375fa20c79 --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A human-input node as the FIRST step of a workflow: instead of guessing what +// the user wants, the graph opens by asking, pauses, and then routes the reply +// into the rest of the process. +// +// `responseSchema: z.string()` asks for a plain text reply. Nothing coerces the +// human's answer into that shape; the schema tells a client what to collect. + +// --8<-- [start:initial-prompt] +import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; +import { z } from "zod"; + +/** Asks the user for itinerary information. */ +const initialPrompt = node( + async function* () { + const inputMessage = ` + This is an interactive concierge workflow tasked with making you a great + itinerary for you in your city of choice. If you give some details about + yourself or what you are generally looking for I can better personalize + your itinerary. + For example, input your: + City (Required), + Age, + Hobby, + Example of attraction you liked + `; + yield new RequestInput({ + message: inputMessage, + responseSchema: z.string(), + }); + }, + { name: "initial_prompt" }, +); + +// Receives the user's reply as its input and kicks off the real work. +const buildItinerary = node( + (_ctx: NodeContext, nodeInput: string) => { + const [city = "your city"] = nodeInput.split(","); + return ( + `Personalized itinerary for ${city.trim()}:\n` + + " 1. Morning walk through the old town\n" + + " 2. Lunch at a neighbourhood favourite\n" + + " 3. An afternoon activity matched to your hobby\n\n" + + `(based on: ${nodeInput.trim()})` + ); + }, + { name: "build_itinerary" }, +); + +export const rootAgent = new Workflow({ + name: "concierge_workflow", + edges: [["START", initialPrompt, buildItinerary]], +}); +// --8<-- [end:initial-prompt] diff --git a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts new file mode 100644 index 0000000000..6f3f344cc4 --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts @@ -0,0 +1,99 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `RequestInput` takes three configuration options: +// message text shown to the user explaining what is being asked +// payload structured data sent alongside the prompt, so a client can +// render richer context (here, the full itinerary) +// responseSchema the shape the reply is expected to take +// +// `RequestInput` does NOT reformat a human reply to fit `responseSchema` — the +// reply must already be in that shape. + +// --8<-- [start:payload-and-schema] +import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; +import { z } from "zod"; + +/** + * Itinerary is a list of activities. Each activity has a name and a + * description. + */ +const activitiesListSchema = z.object({ + itinerary: z.array(z.object({ name: z.string(), description: z.string() })), +}); +type ActivitiesList = z.infer; + +/** Expected response structure from the user. */ +const userFeedbackSchema = z.object({ + userResponse: z.string(), +}); + +// Stands in for the agent node that composes the base itinerary. +const buildItinerary = node( + (_ctx: NodeContext, city: string): ActivitiesList => { + const place = city.trim() || "your city"; + return { + itinerary: [ + { name: "Morning walk", description: `A stroll through old ${place}.` }, + { name: "Local lunch", description: `Regional food in ${place}.` }, + { name: "Museum visit", description: `The main museum of ${place}.` }, + ], + }; + }, + { name: "build_itinerary", outputSchema: activitiesListSchema }, +); + +/** + * Retrieves the user's thoughts on the agent's initial itinerary in order to + * either expand on it, change the list, or exit the loop. + */ +const getUserFeedback = node( + async function* (_ctx: NodeContext, nodeInput: ActivitiesList) { + const rendered = nodeInput.itinerary + .map((a, i) => ` ${i + 1}. ${a.name} — ${a.description}`) + .join("\n"); + + yield new RequestInput({ + message: + `Here is your recommended base itinerary:\n${rendered}\n\n` + + "Which of these items appeal to you (if any)?", + payload: nodeInput, + responseSchema: userFeedbackSchema, + }); + }, + { name: "get_user_feedback" }, +); + +// Receives the human's reply as its input (default handoff on resume). +const applyFeedback = node( + (_ctx: NodeContext, nodeInput: unknown) => { + // The reply is either the structured `UserFeedback` shape or, from an + // interactive client, plain text. + const feedback = + typeof nodeInput === "string" + ? nodeInput + : String( + (nodeInput as { userResponse?: unknown } | null)?.userResponse ?? + JSON.stringify(nodeInput), + ); + return `Noted. Building the final itinerary around: ${feedback}`; + }, + { name: "apply_feedback" }, +); + +export const rootAgent = new Workflow({ + name: "concierge_workflow", + edges: [["START", buildItinerary, getUserFeedback, applyFeedback]], +}); +// --8<-- [end:payload-and-schema] diff --git a/examples/typescript/snippets/graphs/index/get_started.ts b/examples/typescript/snippets/graphs/index/get_started.ts new file mode 100644 index 0000000000..ff26785cc8 --- /dev/null +++ b/examples/typescript/snippets/graphs/index/get_started.ts @@ -0,0 +1,85 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A sequential graph workflow that alternates between model reasoning and plain +// code: an agent names a random city, a code function looks up the time there, +// a second agent reports it, and a final function appends a completion message. + +// --8<-- [start:get-started] +import { + createEvent, + LlmAgent, + node, + NodeContext, + Workflow, +} from "@google/adk"; +import { z } from "zod"; + +const cityGeneratorAgent = new LlmAgent({ + name: "city_generator_agent", + model: "gemini-flash-latest", + instruction: `Return the name of a random city. + Return only the name, nothing else.`, +}); + +/** The structured payload handed from the lookup node to the report agent. */ +const cityTimeSchema = z.object({ + timeInfo: z.string().describe("Time information."), + city: z.string().describe("City name."), +}); +type CityTime = z.infer; + +/** Simulates returning the current time in the specified city. */ +function lookupTimeFunction(_ctx: NodeContext, nodeInput: string): CityTime { + return { timeInfo: "10:10 AM", city: nodeInput.trim() }; +} + +const cityReportAgent = new LlmAgent({ + name: "city_report_agent", + model: "gemini-flash-latest", + // `{CityTime.}` selects a field off THIS node's input. The `CityTime.` + // prefix is documentation; only the field name after the dot is resolved. + instruction: `Output the following line: + It is {CityTime.timeInfo} in {CityTime.city} right now.`, +}); + +function completedMessageFunction(_ctx: NodeContext, nodeInput: string) { + // A user-facing message is the event's `content` which, unlike `output`, is + // not handed to the next node. + return createEvent({ + content: { + role: "model", + parts: [{ text: `${nodeInput}\n WORKFLOW COMPLETED.` }], + }, + }); +} + +export const rootAgent = new Workflow({ + name: "root_agent", + edges: [ + [ + "START", + cityGeneratorAgent, + node(lookupTimeFunction, { + name: "lookup_time_function", + outputSchema: cityTimeSchema, + }), + // The validating schema belongs to the node wrapping the agent — an + // `LlmAgent.inputSchema` is only used when the agent is exposed as a tool. + node(cityReportAgent, { inputSchema: cityTimeSchema }), + node(completedMessageFunction, { name: "completed_message_function" }), + ], + ], +}); +// --8<-- [end:get-started] diff --git a/examples/typescript/snippets/graphs/index/process_pipeline.ts b/examples/typescript/snippets/graphs/index/process_pipeline.ts new file mode 100644 index 0000000000..f2ba098419 --- /dev/null +++ b/examples/typescript/snippets/graphs/index/process_pipeline.ts @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A prompt-based agent turned into a graph: one agent classifies the message, +// a router node emits the categories as routes, and the graph dispatches to the +// matching handler(s). Because the classifier may return more than one +// category, the router emits an ARRAY of routes — every matching branch fires. + +// --8<-- [start:process-pipeline] +import { + createEvent, + DEFAULT_ROUTE, + LlmAgent, + node, + NodeContext, + Workflow, +} from "@google/adk"; + +/** The routes this graph has edges for. */ +const ROUTES = ["BUG", "CUSTOMER_SUPPORT", "LOGISTICS"] as const; + +const processMessage = new LlmAgent({ + name: "process_message", + model: "gemini-flash-latest", + instruction: `Classify user message into either "BUG", "CUSTOMER_SUPPORT", + or "LOGISTICS". If you think a message applies to more than one category, + reply with a comma separated list of categories. + Reply with the categories only, nothing else.`, +}); + +// A route ARRAY fires every branch whose route key matches one of the listed +// values (multi-route dispatch), rather than just the first match. +const router = node( + (_ctx: NodeContext, nodeInput: string) => { + const text = String(nodeInput).toUpperCase(); + const matched = ROUTES.filter((route) => + new RegExp(`\\b${route}\\b`).test(text), + ); + return createEvent({ route: matched.length > 0 ? matched : DEFAULT_ROUTE }); + }, + { name: "router" }, +); + +/** Emits a user-facing message: `content`, with no `output`. */ +const message = (text: string) => + createEvent({ content: { role: "model", parts: [{ text }] } }); + +const response1Bug = node(() => message("Handling bug..."), { + name: "response_1_bug", +}); +const response2Support = node(() => message("Handling customer support..."), { + name: "response_2_support", +}); +const response3Logistics = node(() => message("Handling logistics..."), { + name: "response_3_logistics", +}); +const responseUnknown = node( + (_ctx: NodeContext, nodeInput: string) => + message(`Could not classify that (classifier said: ${nodeInput}).`), + { name: "response_unknown" }, +); + +export const rootAgent = new Workflow({ + name: "routing_workflow", + edges: [ + ["START", processMessage, router], + [ + router, + { + BUG: response1Bug, + CUSTOMER_SUPPORT: response2Support, + LOGISTICS: response3Logistics, + [DEFAULT_ROUTE]: responseUnknown, + }, + ], + ], +}); +// --8<-- [end:process-pipeline] diff --git a/examples/typescript/snippets/graphs/package.json b/examples/typescript/snippets/graphs/package.json new file mode 100644 index 0000000000..dcfa9be933 --- /dev/null +++ b/examples/typescript/snippets/graphs/package.json @@ -0,0 +1,30 @@ +{ + "name": "adk-docs-examples-graphs", + "version": "1.0.0", + "description": "TS graph workflow examples for the ADK Documentation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "clean": "rm -rf dist" + }, + "keywords": [ + "adk", + "google", + "agent", + "typescript", + "gemini", + "workflow" + ], + "author": "", + "license": "Apache-2.0", + "type": "module", + "devDependencies": { + "@types/node": "^20.14.2", + "typescript": "^5.9.2" + }, + "dependencies": { + "@google/adk": "^2.0.0", + "zod": "^4.2.1" + } +} diff --git a/examples/typescript/snippets/graphs/routes/branches.ts b/examples/typescript/snippets/graphs/routes/branches.ts new file mode 100644 index 0000000000..ad066d482c --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/branches.ts @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Branching is a node that emits a `route`, plus an edge row mapping each route +// value to the node that handles it. A branch target can be anything node-like: +// `taskBNode` here is an `LlmAgent`, `taskCNode` a plain function. + +// --8<-- [start:branches] +import { + createEvent, + LlmAgent, + node, + NodeContext, + Workflow, +} from "@google/adk"; + +const taskANode = node( + (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(), + { name: "task_A_node" }, +); + +/** Stands in for an application-specific branch condition. */ +const condition = (nodeInput: string) => /\d/.test(nodeInput); + +/** Routes to task B or C based on nodeInput. */ +const router = node( + (_ctx: NodeContext, nodeInput: string) => + condition(nodeInput) + ? createEvent({ route: "RUN_TASK_C", output: nodeInput }) + : createEvent({ route: "RUN_TASK_B", output: nodeInput }), + { name: "router" }, +); + +// An agent to execute node B. +const taskBNode = new LlmAgent({ + name: "task_B_agent", + model: "gemini-flash-latest", + instruction: "Answer the user in a single short sentence.", +}); + +// A FunctionNode to execute node C. +const taskCNode = node(() => "Task C completed", { name: "task_C_node" }); + +export const rootAgent = new Workflow({ + name: "routing_workflow", + edges: [ + ["START", taskANode, router], + [ + router, + { + // "route value": node_to_run + RUN_TASK_B: taskBNode, + RUN_TASK_C: taskCNode, + }, + ], + ], +}); +// --8<-- [end:branches] diff --git a/examples/typescript/snippets/graphs/routes/fan_out_join.ts b/examples/typescript/snippets/graphs/routes/fan_out_join.ts new file mode 100644 index 0000000000..61fcdcc1d3 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/fan_out_join.ts @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A `JoinNode` is a fan-in barrier: it waits for EVERY predecessor to finish +// and then hands the next node an object keyed by predecessor node name. + +// --8<-- [start:fan-out-join] +import { JoinNode, node, NodeContext, Workflow } from "@google/adk"; + +const parallelTaskA = node( + (_ctx: NodeContext, text: string) => text.toUpperCase(), + { name: "parallel_task_A" }, +); + +const parallelTaskB = node((_ctx: NodeContext, text: string) => text.length, { + name: "parallel_task_B", +}); + +const parallelTaskC = node( + (_ctx: NodeContext, text: string) => text.split("").reverse().join(""), + { name: "parallel_task_C" }, +); + +const myJoinNode = new JoinNode({ name: "my_join_node" }); + +// The join hands its successor a record keyed by predecessor node name. +const finalTaskD = node( + (_ctx: NodeContext, results: Record) => + [ + `Uppercase: ${results["parallel_task_A"]}`, + `Length: ${results["parallel_task_B"]}`, + `Reversed: ${results["parallel_task_C"]}`, + ].join("\n"), + { name: "final_task_D" }, +); + +export const rootAgent = new Workflow({ + name: "fan_out_workflow", + // One edge row per parallel path. The equivalent shorthand nests the + // parallel nodes in an array: + // [['START', [parallelTaskA, parallelTaskB, parallelTaskC], myJoinNode, + // finalTaskD]] + edges: [ + ["START", parallelTaskA, myJoinNode], + ["START", parallelTaskB, myJoinNode], + ["START", parallelTaskC, myJoinNode], + [myJoinNode, finalTaskD], + ], +}); +// --8<-- [end:fan-out-join] diff --git a/examples/typescript/snippets/graphs/routes/function_node.ts b/examples/typescript/snippets/graphs/routes/function_node.ts new file mode 100644 index 0000000000..2cf9d38131 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/function_node.ts @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The simplest node type: a plain function wrapped as a FunctionNode. It takes +// text in, returns text out, and the framework hands that value to the next +// node as its input — no session-state writes needed. + +// --8<-- [start:function-node] +import { + createEvent, + node, + NodeContext, + Workflow, + type FunctionNodeHandler, +} from "@google/adk"; + +/** A bare return value: boxed into an event's `output` for you. */ +const myFunctionNode: FunctionNodeHandler = ( + _ctx: NodeContext, + nodeInput: string, +) => { + const inputTextModified = nodeInput.toUpperCase(); + return inputTextModified; +}; + +/** The explicit form — identical behaviour, useful when you also set `route`. */ +const myExplicitEventNode = (_ctx: NodeContext, nodeInput: string) => + createEvent({ output: `${nodeInput} IS AWESOME!` }); + +export const rootAgent = new Workflow({ + name: "function_node_pipeline", + edges: [ + [ + "START", + node(myFunctionNode, { name: "my_function_node" }), + node(myExplicitEventNode, { name: "add_suffix" }), + ], + ], +}); +// --8<-- [end:function-node] diff --git a/examples/typescript/snippets/graphs/routes/loop_escalation.ts b/examples/typescript/snippets/graphs/routes/loop_escalation.ts new file mode 100644 index 0000000000..d3fbc4d6ef --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/loop_escalation.ts @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A loop is a BACK-EDGE in the graph: a downstream node routes back to an +// earlier node, and the engine re-activates that node with a fresh lifecycle on +// each iteration. The loop exits when the router picks the terminal branch. +// +// START -> seed_draft -> critic -> router --REVISE--> refine --+ +// ^ | +// +----------------------------------+ +// router --DONE--> finalize + +// --8<-- [start:loop-escalation] +import { createEvent, node, NodeContext, Workflow } from "@google/adk"; + +interface Draft { + topic: string; + bullets: string[]; +} + +/** The critic is satisfied once the draft has at least this many bullets. */ +const REQUIRED_BULLETS = 3; + +const seedDraft = node( + (_ctx: NodeContext, topic: string): Draft => ({ + topic: topic.trim(), + bullets: [`${topic.trim()} — point 1`], + }), + { name: "seed_draft" }, +); + +// Runs once per incoming trigger: first from seed_draft, then from every +// refine pass around the back-edge. +const critic = node( + (_ctx: NodeContext, draft: Draft) => + createEvent({ + route: draft.bullets.length >= REQUIRED_BULLETS ? "DONE" : "REVISE", + output: draft, + }), + { name: "critic" }, +); + +const refine = node( + (_ctx: NodeContext, draft: Draft): Draft => ({ + ...draft, + bullets: [ + ...draft.bullets, + `${draft.topic} — point ${draft.bullets.length + 1}`, + ], + }), + { name: "refine" }, +); + +const finalize = node( + (_ctx: NodeContext, draft: Draft) => + `Approved after ${draft.bullets.length} bullets:\n` + + draft.bullets.map((b) => ` • ${b}`).join("\n"), + { name: "finalize" }, +); + +export const rootAgent = new Workflow({ + name: "loop_workflow", + edges: [ + ["START", seedDraft, critic], + [critic, { REVISE: refine, DONE: finalize }], + // The back-edge that closes the loop. + [refine, critic], + ], +}); +// --8<-- [end:loop-escalation] diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts new file mode 100644 index 0000000000..93cb4f4082 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -0,0 +1,87 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A `Workflow` is itself a node, so it can be dropped straight into another +// workflow's edges to encapsulate a reusable sub-process. + +// --8<-- [start:nested-workflow] +import { createEvent, node, NodeContext, Workflow } from "@google/adk"; + +const taskA1 = node( + (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(), + { + name: "task_A1", + }, +); + +const router = node( + (_ctx: NodeContext, text: string) => + createEvent({ + route: text === text.toUpperCase() ? "RUN_WORKFLOW_C" : "RUN_WORKFLOW_B", + output: text, + }), + { name: "router" }, +); + +// --- Sub-workflow B: title-case each word, then frame it. --- +const workflowB = new Workflow({ + name: "workflow_B", + edges: [ + [ + "START", + node( + (_ctx: NodeContext, text: string) => + text.replace( + /(^|\P{L})(\p{L})/gu, + (_m, sep: string, ch: string) => sep + ch.toUpperCase(), + ), + { name: "b_title_case" }, + ), + node((_ctx: NodeContext, text: string) => `[B] ${text}`, { + name: "b_frame", + }), + ], + ], +}); + +// --- Sub-workflow C: lower-case, then frame it. --- +const workflowC = new Workflow({ + name: "workflow_C", + edges: [ + [ + "START", + node((_ctx: NodeContext, text: string) => text.toLowerCase(), { + name: "c_lower_case", + }), + node((_ctx: NodeContext, text: string) => `[C] ${text}`, { + name: "c_frame", + }), + ], + ], +}); + +export const rootAgent = new Workflow({ + name: "parent_workflow", + edges: [ + ["START", taskA1, router], + [ + router, + { + RUN_WORKFLOW_B: workflowB, + RUN_WORKFLOW_C: workflowC, + }, + ], + ], +}); +// --8<-- [end:nested-workflow] diff --git a/examples/typescript/snippets/graphs/routes/sequence.ts b/examples/typescript/snippets/graphs/routes/sequence.ts new file mode 100644 index 0000000000..61629d8696 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/sequence.ts @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A sequential route runs each node once, in the listed order. Each node's +// return value is delivered to the next node as its input. + +// --8<-- [start:sequence] +import { node, NodeContext, Workflow } from "@google/adk"; + +const taskANode = node( + (_ctx: NodeContext, nodeInput: string) => `Summary: ${nodeInput.trim()}`, + { name: "task_A_node" }, +); + +const taskBNode = node( + (_ctx: NodeContext, summary: string) => summary.toUpperCase(), + { name: "task_B_node" }, +); + +const taskCNode = node( + (_ctx: NodeContext, shouted: string) => `${shouted} (done)`, + { name: "task_C_node" }, +); + +// A single-node graph would simply be: +// edges: [['START', taskANode]] +export const rootAgent = new Workflow({ + name: "sequential_workflow", + edges: [["START", taskANode, taskBNode, taskCNode]], // 3 nodes run in order +}); +// --8<-- [end:sequence] diff --git a/examples/typescript/snippets/graphs/tsconfig.json b/examples/typescript/snippets/graphs/tsconfig.json new file mode 100644 index 0000000000..55dd7277b2 --- /dev/null +++ b/examples/typescript/snippets/graphs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + /* Build Options */ + "target": "es2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + + /* Strictness */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + + /* Module Interop */ + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "dist"] +} From 83af7c95d5fd519d648e83ad746af9c75b4bfc27 Mon Sep 17 00:00:00 2001 From: kalenkevich Date: Fri, 21 Aug 2026 13:50:29 -0700 Subject: [PATCH 2/6] Drop inline comments from the graph workflow snippets The `//` annotations inside the snippet regions duplicated the prose that already introduces each tab, and they were the first thing a reader saw in a rendered sample rather than the API itself. Removes the 83 `//` comments inside the `--8<--` regions across all 26 files. JSDoc blocks stay, since they document what a function or schema is rather than annotating a line; the Apache headers and the per-file orientation comments above each region are untouched, and neither renders on the docs site anyway. Verified comment-only: compiling every file before and after with `tsc --removeComments` produces byte-identical `.js` and `.d.ts` output across all 52 emitted files. --- .../snippets/graphs/data-handling/node_output.ts | 5 ----- .../snippets/graphs/data-handling/routing_output.ts | 2 -- .../typescript/snippets/graphs/data-handling/schemas.ts | 4 ---- .../snippets/graphs/data-handling/session_state.ts | 7 ------- .../snippets/graphs/data-handling/structured_access.ts | 8 -------- .../snippets/graphs/data-handling/structured_output.ts | 1 - .../snippets/graphs/data-handling/user_message.ts | 3 --- .../typescript/snippets/graphs/dynamic/custom_run_ids.ts | 3 --- .../typescript/snippets/graphs/dynamic/data_handling.ts | 2 -- .../typescript/snippets/graphs/dynamic/get_started.ts | 4 ---- .../typescript/snippets/graphs/dynamic/human_input.ts | 4 ---- examples/typescript/snippets/graphs/dynamic/loop_route.ts | 3 --- examples/typescript/snippets/graphs/dynamic/nodes.ts | 6 ------ .../typescript/snippets/graphs/dynamic/parallel_route.ts | 4 +--- .../typescript/snippets/graphs/human-input/get_started.ts | 1 - .../snippets/graphs/human-input/initial_prompt.ts | 1 - .../snippets/graphs/human-input/payload_and_schema.ts | 4 ---- examples/typescript/snippets/graphs/index/get_started.ts | 6 ------ .../typescript/snippets/graphs/index/process_pipeline.ts | 2 -- examples/typescript/snippets/graphs/routes/branches.ts | 3 --- .../typescript/snippets/graphs/routes/fan_out_join.ts | 5 ----- .../typescript/snippets/graphs/routes/loop_escalation.ts | 3 --- .../typescript/snippets/graphs/routes/nested_workflow.ts | 2 -- examples/typescript/snippets/graphs/routes/sequence.ts | 4 +--- 24 files changed, 2 insertions(+), 85 deletions(-) diff --git a/examples/typescript/snippets/graphs/data-handling/node_output.ts b/examples/typescript/snippets/graphs/data-handling/node_output.ts index 93975add5e..958796ccb1 100644 --- a/examples/typescript/snippets/graphs/data-handling/node_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/node_output.ts @@ -17,27 +17,22 @@ // --8<-- [start:node-output] import { createEvent, node, NodeContext, Workflow } from "@google/adk"; -// 1. A bare return value — boxed into an event's `output` for you. const returnRawValue = node( (_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(), { name: "return_raw_value" }, ); -// 2. An explicit Event — for when you also need `route`, `content` or `actions`. const returnEventOutput = node( (_ctx: NodeContext, nodeInput: string) => createEvent({ output: `${nodeInput}!` }), { name: "return_event_output" }, ); -// 3. A generator — stream progress, then emit the output event last. const yieldProgressThenOutput = node( async function* (_ctx: NodeContext, nodeInput: string) { - // Progress goes on `content`: displayed, and not passed to the successor. yield createEvent({ content: { role: "model", parts: [{ text: "Working on it..." }] }, }); - // Exactly one event sets `output`, so there is nothing to overwrite it. yield createEvent({ output: `<<${nodeInput}>>` }); }, { name: "yield_progress_then_output" }, diff --git a/examples/typescript/snippets/graphs/data-handling/routing_output.ts b/examples/typescript/snippets/graphs/data-handling/routing_output.ts index e6eab132ee..238815133c 100644 --- a/examples/typescript/snippets/graphs/data-handling/routing_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/routing_output.ts @@ -28,7 +28,6 @@ const router = node( (_ctx: NodeContext, nodeInput: string) => createEvent({ route: /bug|crash|error/i.test(nodeInput) ? "BUG" : "OTHER", - // Forwarded to whichever branch fires. output: nodeInput, }), { name: "router" }, @@ -52,7 +51,6 @@ export const rootAgent = new Workflow({ router, { BUG: handleBug, - // Fires when no other route on this node matched. [DEFAULT_ROUTE]: handleAnythingElse, }, ], diff --git a/examples/typescript/snippets/graphs/data-handling/schemas.ts b/examples/typescript/snippets/graphs/data-handling/schemas.ts index 1d25798397..4f77a7967e 100644 --- a/examples/typescript/snippets/graphs/data-handling/schemas.ts +++ b/examples/typescript/snippets/graphs/data-handling/schemas.ts @@ -70,8 +70,6 @@ const searchFlightsApi = new FunctionTool({ ], }); -// Turns the free-text request into the structured node input the searcher -// expects. In a real app this would itself be an extraction agent. const parseRequest = node( (_ctx: NodeContext, nodeInput: string): FlightSearchInput => { const codes = nodeInput.toUpperCase().match(/\b[A-Z]{3}\b/g) ?? []; @@ -89,7 +87,6 @@ const parseRequest = node( { name: "parse_request", outputSchema: flightSearchInputSchema }, ); -// Agents in a graph must run in `single_turn` (the default) or `task` mode. const flightSearcher = new LlmAgent({ name: "flight_searcher", model: "gemini-flash-latest", @@ -117,7 +114,6 @@ export const rootAgent = new Workflow({ [ "START", parseRequest, - // The graph-level input contract for the agent node. node(flightSearcher, { inputSchema: flightSearchInputSchema }), renderResults, ], diff --git a/examples/typescript/snippets/graphs/data-handling/session_state.ts b/examples/typescript/snippets/graphs/data-handling/session_state.ts index 186c2b9649..d285c1ba00 100644 --- a/examples/typescript/snippets/graphs/data-handling/session_state.ts +++ b/examples/typescript/snippets/graphs/data-handling/session_state.ts @@ -19,15 +19,9 @@ // --8<-- [start:session-state] import { node, NodeContext, Workflow } from "@google/adk"; -// State-key prefixes control lifetime and scope: -// "app:" shared across all users and sessions of the app -// "user:" tied to the user, shared across their sessions -// "temp:" discarded when the current invocation ends -// "" persists for the lifetime of the session const initStateNode = node( (ctx: NodeContext, nodeInput: string) => { ctx.state.set("topic", nodeInput.trim()); - // Scoped key: dropped when this invocation ends, never persisted. ctx.state.set("temp:started_at", new Date().toISOString()); ctx.state.set("attempts", 0); }, @@ -36,7 +30,6 @@ const initStateNode = node( const taskAttemptNode = node( (ctx: NodeContext) => { - // Reads the value init_state_node wrote earlier in this same run. const attempts = ctx.state.get("attempts") ?? 0; ctx.state.set("attempts", attempts + 1); }, diff --git a/examples/typescript/snippets/graphs/data-handling/structured_access.ts b/examples/typescript/snippets/graphs/data-handling/structured_access.ts index 04cab8b151..f18702a833 100644 --- a/examples/typescript/snippets/graphs/data-handling/structured_access.ts +++ b/examples/typescript/snippets/graphs/data-handling/structured_access.ts @@ -49,14 +49,6 @@ const lookupTimeFunction = node( const cityReportAgent = new LlmAgent({ name: "city_report_agent", model: "gemini-flash-latest", - - // Data selection based on class and parameter — reads this node's own input: - // instruction: `Return a sentence in the following format: - // It is {CityTime.timeInfo} in {CityTime.city} right now.`, - - // More restrictive data selection, qualified by source node name. Keep the - // template on ONE line: a model reproduces a line break inside the format - // string, which splits the answer mid-sentence. instruction: "Return a sentence in the following format: It is " + " in " + diff --git a/examples/typescript/snippets/graphs/data-handling/structured_output.ts b/examples/typescript/snippets/graphs/data-handling/structured_output.ts index cee04b5c68..64fffab5cd 100644 --- a/examples/typescript/snippets/graphs/data-handling/structured_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/structured_output.ts @@ -34,7 +34,6 @@ const emitStructuredOutput = node( { name: "emit_structured_output", outputSchema: cityInfoSchema }, ); -// The successor receives the object itself — no JSON parsing, no state reads. const consumeStructuredOutput = node( (_ctx: NodeContext, cityInfo: CityInfo) => `It is ${cityInfo.cityTime} in ${cityInfo.cityName} right now.`, diff --git a/examples/typescript/snippets/graphs/data-handling/user_message.ts b/examples/typescript/snippets/graphs/data-handling/user_message.ts index adb3ea7ffb..dd5bd6c1b9 100644 --- a/examples/typescript/snippets/graphs/data-handling/user_message.ts +++ b/examples/typescript/snippets/graphs/data-handling/user_message.ts @@ -23,8 +23,6 @@ import { createEvent, node, NodeContext, Workflow } from "@google/adk"; const message = (text: string) => createEvent({ content: { role: "model", parts: [{ text }] } }); -// Tell the user the research process is starting. No `output`, so nothing is -// handed to the next node. const userMessage = node( async function* (_ctx: NodeContext, nodeInput: string) { yield message(`Beginning research process for "${nodeInput}"...`); @@ -32,7 +30,6 @@ const userMessage = node( { name: "user_message" }, ); -// A message AND an output in one node: two events, only one carrying `output`. const research = node( async function* () { yield message("Gathering sources..."); diff --git a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts index 051242516b..0ff0e9aed5 100644 --- a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts +++ b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts @@ -48,9 +48,6 @@ const processAllOrders = node( const orders = await getOrders(); const processTasks = orders.map((order) => - // Use runId to provide a custom identifier. It must contain at least one - // non-numeric character to avoid colliding with the auto-generated - // sequential numeric ids. ctx.runNode(processOrder, order, { runId: `order-${order.orderId}` }), ); diff --git a/examples/typescript/snippets/graphs/dynamic/data_handling.ts b/examples/typescript/snippets/graphs/dynamic/data_handling.ts index 3f61778301..d99077ba52 100644 --- a/examples/typescript/snippets/graphs/dynamic/data_handling.ts +++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts @@ -38,10 +38,8 @@ const formatFunctionNode = node( const editorialWorkflow = node( async (ctx: NodeContext, userRequest: string) => { - // Agent node generates output. const rawDraft = await ctx.runNode(draftAgent, userRequest); - // Function node formats text. const formattedText = await ctx.runNode( formatFunctionNode, rawDraft.output, diff --git a/examples/typescript/snippets/graphs/dynamic/get_started.ts b/examples/typescript/snippets/graphs/dynamic/get_started.ts index 6f2f691bee..89b0bd142b 100644 --- a/examples/typescript/snippets/graphs/dynamic/get_started.ts +++ b/examples/typescript/snippets/graphs/dynamic/get_started.ts @@ -21,12 +21,8 @@ import { node, NodeContext, Workflow } from "@google/adk"; const myNode = node(() => "Hello World", { name: "hello_node" }); -// An orchestrator that calls `ctx.runNode` must set `rerunOnResume: true`, so -// its body re-runs on resume and already-finished children are replayed from -// their checkpoints rather than executed again. const myWorkflow = node( async (ctx: NodeContext, _nodeInput: string) => { - // runNode executes a node and resolves to its RESULT, so read `.output`. const result = await ctx.runNode(myNode, "hello"); return result.output; }, diff --git a/examples/typescript/snippets/graphs/dynamic/human_input.ts b/examples/typescript/snippets/graphs/dynamic/human_input.ts index d6050031ed..84f3e7eead 100644 --- a/examples/typescript/snippets/graphs/dynamic/human_input.ts +++ b/examples/typescript/snippets/graphs/dynamic/human_input.ts @@ -37,10 +37,6 @@ const handleProcess = node( async (ctx: NodeContext, nodeInput: unknown) => { const approval = await ctx.runNode(getUserApproval, nodeInput); - // `ctx.runNode()` does NOT throw when a child interrupts: it resolves with - // a result whose `interruptIds` are populated and whose `output` is still - // undefined. Return without deciding — the workflow pauses and this body - // re-runs once the reply arrives. if (approval.interruptIds.length > 0) { return undefined; } diff --git a/examples/typescript/snippets/graphs/dynamic/loop_route.ts b/examples/typescript/snippets/graphs/dynamic/loop_route.ts index 1fd61d6f59..160e914a2c 100644 --- a/examples/typescript/snippets/graphs/dynamic/loop_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts @@ -59,10 +59,7 @@ const codeWorkflow = node( findings: string; }; - // Unlike a graph cycle, the loop is trivially bounded, so a stubborn model - // cannot spin forever burning live model calls. for (let round = 0; checkResp.findings && round < MAX_FIX_ROUNDS; round++) { - // The fixer agent reads `{code}` / `{findings}` from session state. ctx.state.set("code", code); ctx.state.set("findings", checkResp.findings); diff --git a/examples/typescript/snippets/graphs/dynamic/nodes.ts b/examples/typescript/snippets/graphs/dynamic/nodes.ts index 98bacc35fe..5f8303b8f9 100644 --- a/examples/typescript/snippets/graphs/dynamic/nodes.ts +++ b/examples/typescript/snippets/graphs/dynamic/nodes.ts @@ -22,13 +22,8 @@ function myFunctionNode(_ctx: NodeContext, nodeInput: unknown): string { return `Hello ${nodeInput ?? "World"}`; } -// Form 1 — the `node()` factory. TypeScript has no `@node` decorator form. const helloNode = node(myFunctionNode, { name: "hello_node" }); -// Form 2 — the explicit constructor, same function, different configuration. -// Reach for it when you are wrapping a function from another library, need -// several differently-configured nodes from one function, or keep node -// references in a registry for advanced orchestration. const successNode = new FunctionNode("hello", myFunctionNode, { rerunOnResume: true, }); @@ -40,7 +35,6 @@ const myFormattingNode = node( { name: "my_formatting_node" }, ); -// The orchestrator: run children in order and return the last result. const myWorkflow = node( async (ctx: NodeContext, nodeInput: unknown) => { const greeted = await ctx.runNode(helloNode, nodeInput); diff --git a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts index 605dd970fc..fbd0f639e2 100644 --- a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts @@ -29,7 +29,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); /** The worker run once per list item. */ const realNode = node( async (_ctx: NodeContext, item: string) => { - await sleep(200); // stand-in for real work + await sleep(200); return { item, length: item.length }; }, { name: "analyze_item" }, @@ -42,8 +42,6 @@ const parallelSupervisor = node( .map((item) => item.trim()) .filter(Boolean); - // Run ids are assigned in CALL order, so kick the children off in a - // synchronous loop to keep them deterministic across a resume. const tasks = items.map((item) => ctx.runNode(realNode, item)); const results = await Promise.all(tasks); diff --git a/examples/typescript/snippets/graphs/human-input/get_started.ts b/examples/typescript/snippets/graphs/human-input/get_started.ts index 9b3e210d30..13e712d7d8 100644 --- a/examples/typescript/snippets/graphs/human-input/get_started.ts +++ b/examples/typescript/snippets/graphs/human-input/get_started.ts @@ -30,7 +30,6 @@ const step1 = node( const step2 = node( (_ctx: NodeContext, nodeInput: string | number) => { - // An interactive reply arrives as text, so coerce before doing maths. const value = Number(nodeInput); return Number.isFinite(value) ? value * 2 diff --git a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts index 375fa20c79..5a0404cbfd 100644 --- a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts +++ b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts @@ -45,7 +45,6 @@ const initialPrompt = node( { name: "initial_prompt" }, ); -// Receives the user's reply as its input and kicks off the real work. const buildItinerary = node( (_ctx: NodeContext, nodeInput: string) => { const [city = "your city"] = nodeInput.split(","); diff --git a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts index 6f3f344cc4..79a13f48b4 100644 --- a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts +++ b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts @@ -39,7 +39,6 @@ const userFeedbackSchema = z.object({ userResponse: z.string(), }); -// Stands in for the agent node that composes the base itinerary. const buildItinerary = node( (_ctx: NodeContext, city: string): ActivitiesList => { const place = city.trim() || "your city"; @@ -75,11 +74,8 @@ const getUserFeedback = node( { name: "get_user_feedback" }, ); -// Receives the human's reply as its input (default handoff on resume). const applyFeedback = node( (_ctx: NodeContext, nodeInput: unknown) => { - // The reply is either the structured `UserFeedback` shape or, from an - // interactive client, plain text. const feedback = typeof nodeInput === "string" ? nodeInput diff --git a/examples/typescript/snippets/graphs/index/get_started.ts b/examples/typescript/snippets/graphs/index/get_started.ts index ff26785cc8..ad7e532c35 100644 --- a/examples/typescript/snippets/graphs/index/get_started.ts +++ b/examples/typescript/snippets/graphs/index/get_started.ts @@ -48,15 +48,11 @@ function lookupTimeFunction(_ctx: NodeContext, nodeInput: string): CityTime { const cityReportAgent = new LlmAgent({ name: "city_report_agent", model: "gemini-flash-latest", - // `{CityTime.}` selects a field off THIS node's input. The `CityTime.` - // prefix is documentation; only the field name after the dot is resolved. instruction: `Output the following line: It is {CityTime.timeInfo} in {CityTime.city} right now.`, }); function completedMessageFunction(_ctx: NodeContext, nodeInput: string) { - // A user-facing message is the event's `content` which, unlike `output`, is - // not handed to the next node. return createEvent({ content: { role: "model", @@ -75,8 +71,6 @@ export const rootAgent = new Workflow({ name: "lookup_time_function", outputSchema: cityTimeSchema, }), - // The validating schema belongs to the node wrapping the agent — an - // `LlmAgent.inputSchema` is only used when the agent is exposed as a tool. node(cityReportAgent, { inputSchema: cityTimeSchema }), node(completedMessageFunction, { name: "completed_message_function" }), ], diff --git a/examples/typescript/snippets/graphs/index/process_pipeline.ts b/examples/typescript/snippets/graphs/index/process_pipeline.ts index f2ba098419..3da315d0d8 100644 --- a/examples/typescript/snippets/graphs/index/process_pipeline.ts +++ b/examples/typescript/snippets/graphs/index/process_pipeline.ts @@ -39,8 +39,6 @@ const processMessage = new LlmAgent({ Reply with the categories only, nothing else.`, }); -// A route ARRAY fires every branch whose route key matches one of the listed -// values (multi-route dispatch), rather than just the first match. const router = node( (_ctx: NodeContext, nodeInput: string) => { const text = String(nodeInput).toUpperCase(); diff --git a/examples/typescript/snippets/graphs/routes/branches.ts b/examples/typescript/snippets/graphs/routes/branches.ts index ad066d482c..525073cbbf 100644 --- a/examples/typescript/snippets/graphs/routes/branches.ts +++ b/examples/typescript/snippets/graphs/routes/branches.ts @@ -42,14 +42,12 @@ const router = node( { name: "router" }, ); -// An agent to execute node B. const taskBNode = new LlmAgent({ name: "task_B_agent", model: "gemini-flash-latest", instruction: "Answer the user in a single short sentence.", }); -// A FunctionNode to execute node C. const taskCNode = node(() => "Task C completed", { name: "task_C_node" }); export const rootAgent = new Workflow({ @@ -59,7 +57,6 @@ export const rootAgent = new Workflow({ [ router, { - // "route value": node_to_run RUN_TASK_B: taskBNode, RUN_TASK_C: taskCNode, }, diff --git a/examples/typescript/snippets/graphs/routes/fan_out_join.ts b/examples/typescript/snippets/graphs/routes/fan_out_join.ts index 61fcdcc1d3..6fe19b70ae 100644 --- a/examples/typescript/snippets/graphs/routes/fan_out_join.ts +++ b/examples/typescript/snippets/graphs/routes/fan_out_join.ts @@ -34,7 +34,6 @@ const parallelTaskC = node( const myJoinNode = new JoinNode({ name: "my_join_node" }); -// The join hands its successor a record keyed by predecessor node name. const finalTaskD = node( (_ctx: NodeContext, results: Record) => [ @@ -47,10 +46,6 @@ const finalTaskD = node( export const rootAgent = new Workflow({ name: "fan_out_workflow", - // One edge row per parallel path. The equivalent shorthand nests the - // parallel nodes in an array: - // [['START', [parallelTaskA, parallelTaskB, parallelTaskC], myJoinNode, - // finalTaskD]] edges: [ ["START", parallelTaskA, myJoinNode], ["START", parallelTaskB, myJoinNode], diff --git a/examples/typescript/snippets/graphs/routes/loop_escalation.ts b/examples/typescript/snippets/graphs/routes/loop_escalation.ts index d3fbc4d6ef..3e7c20e2fb 100644 --- a/examples/typescript/snippets/graphs/routes/loop_escalation.ts +++ b/examples/typescript/snippets/graphs/routes/loop_escalation.ts @@ -40,8 +40,6 @@ const seedDraft = node( { name: "seed_draft" }, ); -// Runs once per incoming trigger: first from seed_draft, then from every -// refine pass around the back-edge. const critic = node( (_ctx: NodeContext, draft: Draft) => createEvent({ @@ -74,7 +72,6 @@ export const rootAgent = new Workflow({ edges: [ ["START", seedDraft, critic], [critic, { REVISE: refine, DONE: finalize }], - // The back-edge that closes the loop. [refine, critic], ], }); diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts index 93cb4f4082..f06e6fe794 100644 --- a/examples/typescript/snippets/graphs/routes/nested_workflow.ts +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -34,7 +34,6 @@ const router = node( { name: "router" }, ); -// --- Sub-workflow B: title-case each word, then frame it. --- const workflowB = new Workflow({ name: "workflow_B", edges: [ @@ -55,7 +54,6 @@ const workflowB = new Workflow({ ], }); -// --- Sub-workflow C: lower-case, then frame it. --- const workflowC = new Workflow({ name: "workflow_C", edges: [ diff --git a/examples/typescript/snippets/graphs/routes/sequence.ts b/examples/typescript/snippets/graphs/routes/sequence.ts index 61629d8696..21e338b2c2 100644 --- a/examples/typescript/snippets/graphs/routes/sequence.ts +++ b/examples/typescript/snippets/graphs/routes/sequence.ts @@ -33,10 +33,8 @@ const taskCNode = node( { name: "task_C_node" }, ); -// A single-node graph would simply be: -// edges: [['START', taskANode]] export const rootAgent = new Workflow({ name: "sequential_workflow", - edges: [["START", taskANode, taskBNode, taskCNode]], // 3 nodes run in order + edges: [["START", taskANode, taskBNode, taskCNode]], }); // --8<-- [end:sequence] From 13ea5509d9933783182261fc1a69f85568edd918 Mon Sep 17 00:00:00 2001 From: kalenkevich Date: Mon, 24 Aug 2026 15:51:33 -0700 Subject: [PATCH 3/6] Use single-quoted strings in the graph workflow snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 26 files landed double-quoted, which reads as a deliberate choice next to the existing TypeScript snippets under examples/typescript/snippets/ — those are predominantly single-quoted (49 of 68 imports). The repo has no prettier config, so nothing enforces either style; this just stops the new directory looking different from its neighbours. Formatting only: `prettier --no-config --single-quote`, and every changed line differs from the original by a quote character alone. Compiling before and after with `tsc --removeComments` produces output whose only differences are the same quote swaps, since tsc preserves the source quote style. --- .../graphs/data-handling/node_output.ts | 14 +++---- .../graphs/data-handling/routing_output.ts | 14 +++---- .../snippets/graphs/data-handling/schemas.ts | 40 +++++++++---------- .../graphs/data-handling/session_state.ts | 28 ++++++------- .../graphs/data-handling/structured_access.ts | 32 +++++++-------- .../graphs/data-handling/structured_output.ts | 14 +++---- .../graphs/data-handling/user_message.ts | 20 +++++----- .../snippets/graphs/dynamic/custom_run_ids.ts | 18 ++++----- .../snippets/graphs/dynamic/data_handling.ts | 20 +++++----- .../snippets/graphs/dynamic/get_started.ts | 12 +++--- .../snippets/graphs/dynamic/human_input.ts | 20 +++++----- .../snippets/graphs/dynamic/loop_route.ts | 30 +++++++------- .../snippets/graphs/dynamic/nodes.ts | 16 ++++---- .../snippets/graphs/dynamic/parallel_route.ts | 16 ++++---- .../snippets/graphs/dynamic/sequence_route.ts | 30 +++++++------- .../graphs/human-input/get_started.ts | 12 +++--- .../graphs/human-input/initial_prompt.ts | 20 +++++----- .../graphs/human-input/payload_and_schema.ts | 28 ++++++------- .../snippets/graphs/index/get_started.ts | 28 ++++++------- .../snippets/graphs/index/process_pipeline.ts | 30 +++++++------- .../snippets/graphs/routes/branches.ts | 22 +++++----- .../snippets/graphs/routes/fan_out_join.ts | 30 +++++++------- .../snippets/graphs/routes/function_node.ts | 10 ++--- .../snippets/graphs/routes/loop_escalation.ts | 18 ++++----- .../snippets/graphs/routes/nested_workflow.ts | 28 ++++++------- .../snippets/graphs/routes/sequence.ts | 12 +++--- 26 files changed, 281 insertions(+), 281 deletions(-) diff --git a/examples/typescript/snippets/graphs/data-handling/node_output.ts b/examples/typescript/snippets/graphs/data-handling/node_output.ts index 958796ccb1..051fdcb499 100644 --- a/examples/typescript/snippets/graphs/data-handling/node_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/node_output.ts @@ -15,33 +15,33 @@ // A node hands data to its successor through the event's `output` field. // --8<-- [start:node-output] -import { createEvent, node, NodeContext, Workflow } from "@google/adk"; +import { createEvent, node, NodeContext, Workflow } from '@google/adk'; const returnRawValue = node( (_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(), - { name: "return_raw_value" }, + { name: 'return_raw_value' }, ); const returnEventOutput = node( (_ctx: NodeContext, nodeInput: string) => createEvent({ output: `${nodeInput}!` }), - { name: "return_event_output" }, + { name: 'return_event_output' }, ); const yieldProgressThenOutput = node( async function* (_ctx: NodeContext, nodeInput: string) { yield createEvent({ - content: { role: "model", parts: [{ text: "Working on it..." }] }, + content: { role: 'model', parts: [{ text: 'Working on it...' }] }, }); yield createEvent({ output: `<<${nodeInput}>>` }); }, - { name: "yield_progress_then_output" }, + { name: 'yield_progress_then_output' }, ); export const rootAgent = new Workflow({ - name: "node_output_workflow", + name: 'node_output_workflow', edges: [ - ["START", returnRawValue, returnEventOutput, yieldProgressThenOutput], + ['START', returnRawValue, returnEventOutput, yieldProgressThenOutput], ], }); // --8<-- [end:node-output] diff --git a/examples/typescript/snippets/graphs/data-handling/routing_output.ts b/examples/typescript/snippets/graphs/data-handling/routing_output.ts index 238815133c..666b6d5106 100644 --- a/examples/typescript/snippets/graphs/data-handling/routing_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/routing_output.ts @@ -22,31 +22,31 @@ import { node, NodeContext, Workflow, -} from "@google/adk"; +} from '@google/adk'; const router = node( (_ctx: NodeContext, nodeInput: string) => createEvent({ - route: /bug|crash|error/i.test(nodeInput) ? "BUG" : "OTHER", + route: /bug|crash|error/i.test(nodeInput) ? 'BUG' : 'OTHER', output: nodeInput, }), - { name: "router" }, + { name: 'router' }, ); const handleBug = node( (_ctx: NodeContext, nodeInput: string) => `Filed a bug for: ${nodeInput}`, - { name: "handle_bug" }, + { name: 'handle_bug' }, ); const handleAnythingElse = node( (_ctx: NodeContext, nodeInput: string) => `No bug detected in: ${nodeInput}`, - { name: "handle_anything_else" }, + { name: 'handle_anything_else' }, ); export const rootAgent = new Workflow({ - name: "routing_output_workflow", + name: 'routing_output_workflow', edges: [ - ["START", router], + ['START', router], [ router, { diff --git a/examples/typescript/snippets/graphs/data-handling/schemas.ts b/examples/typescript/snippets/graphs/data-handling/schemas.ts index 4f77a7967e..ecd8290636 100644 --- a/examples/typescript/snippets/graphs/data-handling/schemas.ts +++ b/examples/typescript/snippets/graphs/data-handling/schemas.ts @@ -28,14 +28,14 @@ import { node, NodeContext, Workflow, -} from "@google/adk"; -import { z } from "zod"; +} from '@google/adk'; +import { z } from 'zod'; const flightSearchInputSchema = z.object({ origin: z.string().describe('Origin airport code, e.g. "SFO".'), destination: z.string().describe('Destination airport code, e.g. "CDG".'), departureDate: z.string().describe('Departure date, e.g. "2026-03-15".'), - passengers: z.number().describe("Number of passengers."), + passengers: z.number().describe('Number of passengers.'), }); type FlightSearchInput = z.infer; @@ -53,17 +53,17 @@ type FlightSearchOutput = z.infer; /** Stands in for a real flight-search API. */ const searchFlightsApi = new FunctionTool({ - name: "search_flights_api", - description: "Searches available flights for a route and date.", + name: 'search_flights_api', + description: 'Searches available flights for a route and date.', parameters: flightSearchInputSchema, execute: ({ origin, destination }) => [ { - carrier: "AF", + carrier: 'AF', flightNumber: `AF${origin.length}${destination.length}0`, price: 812.4, }, { - carrier: "UA", + carrier: 'UA', flightNumber: `UA${origin.length}${destination.length}1`, price: 947.0, }, @@ -78,22 +78,22 @@ const parseRequest = node( nodeInput.match(/(\d+)\s*(people|pax|passengers?)/i)?.[1], ); return { - origin: codes[0] ?? "SFO", - destination: codes[1] ?? "CDG", - departureDate: date ?? "2026-03-15", + origin: codes[0] ?? 'SFO', + destination: codes[1] ?? 'CDG', + departureDate: date ?? '2026-03-15', passengers: Number.isFinite(passengers) ? passengers : 1, }; }, - { name: "parse_request", outputSchema: flightSearchInputSchema }, + { name: 'parse_request', outputSchema: flightSearchInputSchema }, ); const flightSearcher = new LlmAgent({ - name: "flight_searcher", - model: "gemini-flash-latest", - mode: "single_turn", + name: 'flight_searcher', + model: 'gemini-flash-latest', + mode: 'single_turn', instruction: - "Search for available flights with the search_flights_api tool and report " + - "every flight it returns plus the cheapest price.", + 'Search for available flights with the search_flights_api tool and report ' + + 'every flight it returns plus the cheapest price.', inputSchema: flightSearchInputSchema, outputSchema: flightSearchOutputSchema, tools: [searchFlightsApi], @@ -104,15 +104,15 @@ const renderResults = node( `Cheapest: $${results.cheapestPrice}\n` + results.flights .map((f) => ` ${f.carrier} ${f.flightNumber} — $${f.price}`) - .join("\n"), - { name: "render_results", inputSchema: flightSearchOutputSchema }, + .join('\n'), + { name: 'render_results', inputSchema: flightSearchOutputSchema }, ); export const rootAgent = new Workflow({ - name: "flight_workflow", + name: 'flight_workflow', edges: [ [ - "START", + 'START', parseRequest, node(flightSearcher, { inputSchema: flightSearchInputSchema }), renderResults, diff --git a/examples/typescript/snippets/graphs/data-handling/session_state.ts b/examples/typescript/snippets/graphs/data-handling/session_state.ts index d285c1ba00..2398173c7a 100644 --- a/examples/typescript/snippets/graphs/data-handling/session_state.ts +++ b/examples/typescript/snippets/graphs/data-handling/session_state.ts @@ -17,35 +17,35 @@ // run, and is committed with the writing node's events. // --8<-- [start:session-state] -import { node, NodeContext, Workflow } from "@google/adk"; +import { node, NodeContext, Workflow } from '@google/adk'; const initStateNode = node( (ctx: NodeContext, nodeInput: string) => { - ctx.state.set("topic", nodeInput.trim()); - ctx.state.set("temp:started_at", new Date().toISOString()); - ctx.state.set("attempts", 0); + ctx.state.set('topic', nodeInput.trim()); + ctx.state.set('temp:started_at', new Date().toISOString()); + ctx.state.set('attempts', 0); }, - { name: "init_state_node" }, + { name: 'init_state_node' }, ); const taskAttemptNode = node( (ctx: NodeContext) => { - const attempts = ctx.state.get("attempts") ?? 0; - ctx.state.set("attempts", attempts + 1); + const attempts = ctx.state.get('attempts') ?? 0; + ctx.state.set('attempts', attempts + 1); }, - { name: "task_attempt_node" }, + { name: 'task_attempt_node' }, ); const readStateNode = node( (ctx: NodeContext) => - `attempts state: ${ctx.state.get("attempts")} ` + - `(topic: ${ctx.state.get("topic")}, ` + - `started: ${ctx.state.get("temp:started_at")})`, - { name: "read_state_node" }, + `attempts state: ${ctx.state.get('attempts')} ` + + `(topic: ${ctx.state.get('topic')}, ` + + `started: ${ctx.state.get('temp:started_at')})`, + { name: 'read_state_node' }, ); export const rootAgent = new Workflow({ - name: "session_state_workflow", - edges: [["START", initStateNode, taskAttemptNode, readStateNode]], + name: 'session_state_workflow', + edges: [['START', initStateNode, taskAttemptNode, readStateNode]], }); // --8<-- [end:session-state] diff --git a/examples/typescript/snippets/graphs/data-handling/structured_access.ts b/examples/typescript/snippets/graphs/data-handling/structured_access.ts index f18702a833..6e9c22b1dc 100644 --- a/examples/typescript/snippets/graphs/data-handling/structured_access.ts +++ b/examples/typescript/snippets/graphs/data-handling/structured_access.ts @@ -22,44 +22,44 @@ // Both are distinct from `{state_key}`, which reads session state. // --8<-- [start:structured-access] -import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; -import { z } from "zod"; +import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; +import { z } from 'zod'; const cityTimeSchema = z.object({ - timeInfo: z.string().describe("Time information."), - city: z.string().describe("City name."), + timeInfo: z.string().describe('Time information.'), + city: z.string().describe('City name.'), }); type CityTime = z.infer; const cityGeneratorAgent = new LlmAgent({ - name: "city_generator_agent", - model: "gemini-flash-latest", - instruction: "Return the name of a random city. Return only the name.", + name: 'city_generator_agent', + model: 'gemini-flash-latest', + instruction: 'Return the name of a random city. Return only the name.', }); /** Simulates returning the current time in the specified city. */ const lookupTimeFunction = node( (_ctx: NodeContext, city: string): CityTime => ({ - timeInfo: "10:10 AM", + timeInfo: '10:10 AM', city: city.trim(), }), - { name: "lookup_time_function", outputSchema: cityTimeSchema }, + { name: 'lookup_time_function', outputSchema: cityTimeSchema }, ); const cityReportAgent = new LlmAgent({ - name: "city_report_agent", - model: "gemini-flash-latest", + name: 'city_report_agent', + model: 'gemini-flash-latest', instruction: - "Return a sentence in the following format: It is " + - " in " + - " right now.", + 'Return a sentence in the following format: It is ' + + ' in ' + + ' right now.', }); export const rootAgent = new Workflow({ - name: "root_agent", + name: 'root_agent', edges: [ [ - "START", + 'START', cityGeneratorAgent, lookupTimeFunction, node(cityReportAgent, { inputSchema: cityTimeSchema }), diff --git a/examples/typescript/snippets/graphs/data-handling/structured_output.ts b/examples/typescript/snippets/graphs/data-handling/structured_output.ts index 64fffab5cd..8df086d131 100644 --- a/examples/typescript/snippets/graphs/data-handling/structured_output.ts +++ b/examples/typescript/snippets/graphs/data-handling/structured_output.ts @@ -16,8 +16,8 @@ // node, which receives it as a typed object. // --8<-- [start:structured-output] -import { createEvent, node, NodeContext, Workflow } from "@google/adk"; -import { z } from "zod"; +import { createEvent, node, NodeContext, Workflow } from '@google/adk'; +import { z } from 'zod'; const cityInfoSchema = z.object({ cityName: z.string(), @@ -28,20 +28,20 @@ type CityInfo = z.infer; const emitStructuredOutput = node( async function* () { yield createEvent({ - output: { cityName: "Paris", cityTime: "10:10 AM" } satisfies CityInfo, + output: { cityName: 'Paris', cityTime: '10:10 AM' } satisfies CityInfo, }); }, - { name: "emit_structured_output", outputSchema: cityInfoSchema }, + { name: 'emit_structured_output', outputSchema: cityInfoSchema }, ); const consumeStructuredOutput = node( (_ctx: NodeContext, cityInfo: CityInfo) => `It is ${cityInfo.cityTime} in ${cityInfo.cityName} right now.`, - { name: "consume_structured_output", inputSchema: cityInfoSchema }, + { name: 'consume_structured_output', inputSchema: cityInfoSchema }, ); export const rootAgent = new Workflow({ - name: "structured_output_workflow", - edges: [["START", emitStructuredOutput, consumeStructuredOutput]], + name: 'structured_output_workflow', + edges: [['START', emitStructuredOutput, consumeStructuredOutput]], }); // --8<-- [end:structured-output] diff --git a/examples/typescript/snippets/graphs/data-handling/user_message.ts b/examples/typescript/snippets/graphs/data-handling/user_message.ts index dd5bd6c1b9..5497ab7d73 100644 --- a/examples/typescript/snippets/graphs/data-handling/user_message.ts +++ b/examples/typescript/snippets/graphs/data-handling/user_message.ts @@ -17,35 +17,35 @@ // `output` is for the next node. // --8<-- [start:user-message] -import { createEvent, node, NodeContext, Workflow } from "@google/adk"; +import { createEvent, node, NodeContext, Workflow } from '@google/adk'; /** Emits a user-facing message: `content`, with no `output`. */ const message = (text: string) => - createEvent({ content: { role: "model", parts: [{ text }] } }); + createEvent({ content: { role: 'model', parts: [{ text }] } }); const userMessage = node( async function* (_ctx: NodeContext, nodeInput: string) { yield message(`Beginning research process for "${nodeInput}"...`); }, - { name: "user_message" }, + { name: 'user_message' }, ); const research = node( async function* () { - yield message("Gathering sources..."); - yield createEvent({ output: ["source-a", "source-b", "source-c"] }); + yield message('Gathering sources...'); + yield createEvent({ output: ['source-a', 'source-b', 'source-c'] }); }, - { name: "research" }, + { name: 'research' }, ); const report = node( (_ctx: NodeContext, sources: string[]) => - `Research complete. ${sources.length} sources: ${sources.join(", ")}.`, - { name: "report" }, + `Research complete. ${sources.length} sources: ${sources.join(', ')}.`, + { name: 'report' }, ); export const rootAgent = new Workflow({ - name: "user_message_workflow", - edges: [["START", userMessage, research, report]], + name: 'user_message_workflow', + edges: [['START', userMessage, research, report]], }); // --8<-- [end:user-message] diff --git a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts index 0ff0e9aed5..c3422ec739 100644 --- a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts +++ b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts @@ -21,7 +21,7 @@ // own id, as below. // --8<-- [start:custom-execution-ids] -import { node, NodeContext, Workflow } from "@google/adk"; +import { node, NodeContext, Workflow } from '@google/adk'; interface Order { orderId: string; @@ -31,16 +31,16 @@ interface Order { /** Stands in for loading orders from a database. */ async function getOrders(): Promise { return [ - { orderId: "a91", cartItems: ["keyboard", "mouse"] }, - { orderId: "b02", cartItems: ["monitor"] }, - { orderId: "c73", cartItems: ["dock", "cable", "hub"] }, + { orderId: 'a91', cartItems: ['keyboard', 'mouse'] }, + { orderId: 'b02', cartItems: ['monitor'] }, + { orderId: 'c73', cartItems: ['dock', 'cable', 'hub'] }, ]; } const processOrder = node( (_ctx: NodeContext, order: Order) => `order ${order.orderId}: ${order.cartItems.length} item(s) shipped`, - { name: "process_order" }, + { name: 'process_order' }, ); const processAllOrders = node( @@ -52,13 +52,13 @@ const processAllOrders = node( ); const results = await Promise.all(processTasks); - return results.map((result) => result.output).join("\n"); + return results.map((result) => result.output).join('\n'); }, - { name: "process_all_orders", rerunOnResume: true }, + { name: 'process_all_orders', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", processAllOrders]], + name: 'root_agent', + edges: [['START', processAllOrders]], }); // --8<-- [end:custom-execution-ids] diff --git a/examples/typescript/snippets/graphs/dynamic/data_handling.ts b/examples/typescript/snippets/graphs/dynamic/data_handling.ts index d99077ba52..e2ecee2e12 100644 --- a/examples/typescript/snippets/graphs/dynamic/data_handling.ts +++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts @@ -17,23 +17,23 @@ // read and write just to move a value one step downstream. // --8<-- [start:data-handling] -import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; +import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; const draftAgent = new LlmAgent({ - name: "draft_agent", - model: "gemini-flash-latest", - instruction: "Write a short draft for the user request.", + name: 'draft_agent', + model: 'gemini-flash-latest', + instruction: 'Write a short draft for the user request.', }); const formatFunctionNode = node( (_ctx: NodeContext, rawDraft: string) => rawDraft - .split("\n") + .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => `| ${line}`) - .join("\n"), - { name: "format_function_node" }, + .join('\n'), + { name: 'format_function_node' }, ); const editorialWorkflow = node( @@ -47,11 +47,11 @@ const editorialWorkflow = node( return formattedText.output; }, - { name: "editorial_workflow", rerunOnResume: true }, + { name: 'editorial_workflow', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", editorialWorkflow]], + name: 'root_agent', + edges: [['START', editorialWorkflow]], }); // --8<-- [end:data-handling] diff --git a/examples/typescript/snippets/graphs/dynamic/get_started.ts b/examples/typescript/snippets/graphs/dynamic/get_started.ts index 89b0bd142b..ab87638ac3 100644 --- a/examples/typescript/snippets/graphs/dynamic/get_started.ts +++ b/examples/typescript/snippets/graphs/dynamic/get_started.ts @@ -17,20 +17,20 @@ // whatever order your loops and conditionals dictate. // --8<-- [start:get-started] -import { node, NodeContext, Workflow } from "@google/adk"; +import { node, NodeContext, Workflow } from '@google/adk'; -const myNode = node(() => "Hello World", { name: "hello_node" }); +const myNode = node(() => 'Hello World', { name: 'hello_node' }); const myWorkflow = node( async (ctx: NodeContext, _nodeInput: string) => { - const result = await ctx.runNode(myNode, "hello"); + const result = await ctx.runNode(myNode, 'hello'); return result.output; }, - { name: "my_workflow", rerunOnResume: true }, + { name: 'my_workflow', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", myWorkflow]], + name: 'root_agent', + edges: [['START', myWorkflow]], }); // --8<-- [end:get-started] diff --git a/examples/typescript/snippets/graphs/dynamic/human_input.ts b/examples/typescript/snippets/graphs/dynamic/human_input.ts index 84f3e7eead..fe589ce049 100644 --- a/examples/typescript/snippets/graphs/dynamic/human_input.ts +++ b/examples/typescript/snippets/graphs/dynamic/human_input.ts @@ -18,7 +18,7 @@ // with the human's reply as its output. // --8<-- [start:human-input] -import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; +import { node, NodeContext, RequestInput, Workflow } from '@google/adk'; /** * Pauses the workflow and waits for user input. @@ -28,8 +28,8 @@ import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; * its output instead of the body running a second time to collect it. */ const getUserApproval = node( - () => new RequestInput({ message: "Please approve this request (Yes/No)" }), - { name: "get_user_approval", rerunOnResume: false }, + () => new RequestInput({ message: 'Please approve this request (Yes/No)' }), + { name: 'get_user_approval', rerunOnResume: false }, ); /** The orchestrator calling the interactive step. */ @@ -41,20 +41,20 @@ const handleProcess = node( return undefined; } - const userResponse = String(approval.output ?? "") + const userResponse = String(approval.output ?? '') .trim() .toLowerCase(); - if (userResponse === "yes") { - return "Approved"; + if (userResponse === 'yes') { + return 'Approved'; } - return "Denied"; + return 'Denied'; }, - { name: "handle_process", rerunOnResume: true }, + { name: 'handle_process', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", handleProcess]], + name: 'root_agent', + edges: [['START', handleProcess]], }); // --8<-- [end:human-input] diff --git a/examples/typescript/snippets/graphs/dynamic/loop_route.ts b/examples/typescript/snippets/graphs/dynamic/loop_route.ts index 160e914a2c..a3ff6e6717 100644 --- a/examples/typescript/snippets/graphs/dynamic/loop_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts @@ -18,15 +18,15 @@ // to read it back (`{code}`, `{findings}`). // --8<-- [start:loop-route] -import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; +import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; /** Safety bound on the refine loop. */ const MAX_FIX_ROUNDS = 3; const coderAgent = new LlmAgent({ - name: "generator_agent", - model: "gemini-flash-latest", - instruction: "Write TypeScript code for the user request. Output code only.", + name: 'generator_agent', + model: 'gemini-flash-latest', + instruction: 'Write TypeScript code for the user request. Output code only.', }); /** Simulates a compile / lint pass. Empty findings means "clean". */ @@ -34,19 +34,19 @@ const compileLintCheck = node( (_ctx: NodeContext, code: string) => { const findings: string[] = []; if (!/\/\*\*/.test(code)) { - findings.push("every function needs a JSDoc comment"); + findings.push('every function needs a JSDoc comment'); } if (!/\)\s*:\s*\w/.test(code)) { - findings.push("add return type annotations"); + findings.push('add return type annotations'); } - return { findings: findings.join("; ") }; + return { findings: findings.join('; ') }; }, - { name: "lint_reviewer" }, + { name: 'lint_reviewer' }, ); const fixerAgent = new LlmAgent({ - name: "fixer_agent", - model: "gemini-flash-latest", + name: 'fixer_agent', + model: 'gemini-flash-latest', instruction: `Refactor current code {code}. Based on compile & lint review: {findings} Output code only.`, @@ -60,8 +60,8 @@ const codeWorkflow = node( }; for (let round = 0; checkResp.findings && round < MAX_FIX_ROUNDS; round++) { - ctx.state.set("code", code); - ctx.state.set("findings", checkResp.findings); + ctx.state.set('code', code); + ctx.state.set('findings', checkResp.findings); code = ( await ctx.runNode(fixerAgent, { code, findings: checkResp.findings }) @@ -73,11 +73,11 @@ const codeWorkflow = node( return code; }, - { name: "code_workflow", rerunOnResume: true }, + { name: 'code_workflow', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", codeWorkflow]], + name: 'root_agent', + edges: [['START', codeWorkflow]], }); // --8<-- [end:loop-route] diff --git a/examples/typescript/snippets/graphs/dynamic/nodes.ts b/examples/typescript/snippets/graphs/dynamic/nodes.ts index 5f8303b8f9..d6090e929c 100644 --- a/examples/typescript/snippets/graphs/dynamic/nodes.ts +++ b/examples/typescript/snippets/graphs/dynamic/nodes.ts @@ -15,16 +15,16 @@ // The two ways to build a node, and the orchestrator that composes them. // --8<-- [start:node-forms] -import { FunctionNode, node, NodeContext, Workflow } from "@google/adk"; +import { FunctionNode, node, NodeContext, Workflow } from '@google/adk'; /** The plain function both node forms wrap. */ function myFunctionNode(_ctx: NodeContext, nodeInput: unknown): string { - return `Hello ${nodeInput ?? "World"}`; + return `Hello ${nodeInput ?? 'World'}`; } -const helloNode = node(myFunctionNode, { name: "hello_node" }); +const helloNode = node(myFunctionNode, { name: 'hello_node' }); -const successNode = new FunctionNode("hello", myFunctionNode, { +const successNode = new FunctionNode('hello', myFunctionNode, { rerunOnResume: true, }); // --8<-- [end:node-forms] @@ -32,7 +32,7 @@ const successNode = new FunctionNode("hello", myFunctionNode, { // --8<-- [start:workflows] const myFormattingNode = node( (_ctx: NodeContext, nodeInput: string) => `>> ${nodeInput.trim()} <<`, - { name: "my_formatting_node" }, + { name: 'my_formatting_node' }, ); const myWorkflow = node( @@ -42,11 +42,11 @@ const myWorkflow = node( const formatted = await ctx.runNode(myFormattingNode, again.output); return formatted.output; }, - { name: "my_workflow", rerunOnResume: true }, + { name: 'my_workflow', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", myWorkflow]], + name: 'root_agent', + edges: [['START', myWorkflow]], }); // --8<-- [end:workflows] diff --git a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts index fbd0f639e2..86bdff371a 100644 --- a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts @@ -22,7 +22,7 @@ // handling. // --8<-- [start:parallel-route] -import { node, NodeContext, Workflow } from "@google/adk"; +import { node, NodeContext, Workflow } from '@google/adk'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -32,13 +32,13 @@ const realNode = node( await sleep(200); return { item, length: item.length }; }, - { name: "analyze_item" }, + { name: 'analyze_item' }, ); const parallelSupervisor = node( async (ctx: NodeContext, nodeInput: string) => { const items = nodeInput - .split(",") + .split(',') .map((item) => item.trim()) .filter(Boolean); @@ -47,17 +47,17 @@ const parallelSupervisor = node( return results.map((result) => result.output); }, - { name: "parallel_supervisor", rerunOnResume: true }, + { name: 'parallel_supervisor', rerunOnResume: true }, ); const summarize = node( (_ctx: NodeContext, results: Array<{ item: string; length: number }>) => - results.map((r) => `${r.item}: ${r.length} chars`).join("\n"), - { name: "summarize" }, + results.map((r) => `${r.item}: ${r.length} chars`).join('\n'), + { name: 'summarize' }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", parallelSupervisor, summarize]], + name: 'root_agent', + edges: [['START', parallelSupervisor, summarize]], }); // --8<-- [end:parallel-route] diff --git a/examples/typescript/snippets/graphs/dynamic/sequence_route.ts b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts index 249f5109c8..64fe505ed8 100644 --- a/examples/typescript/snippets/graphs/dynamic/sequence_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts @@ -17,35 +17,35 @@ // the same as in a graph: attach them to the nodes you run. // --8<-- [start:sequence-route] -import { LlmAgent, node, NodeContext, Workflow } from "@google/adk"; -import { z } from "zod"; +import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; +import { z } from 'zod'; const cityTimeSchema = z.object({ - timeInfo: z.string().describe("Time information."), - city: z.string().describe("City name."), + timeInfo: z.string().describe('Time information.'), + city: z.string().describe('City name.'), }); type CityTime = z.infer; const cityGeneratorAgent = new LlmAgent({ - name: "city_generator_agent", - model: "gemini-flash-latest", - instruction: "Return the name of a random city. Return only the name.", + name: 'city_generator_agent', + model: 'gemini-flash-latest', + instruction: 'Return the name of a random city. Return only the name.', }); /** Simulates returning the current time in a specified city. */ const cityTimeFunction = node( (_ctx: NodeContext, city: string): CityTime => ({ - timeInfo: "10:10 AM", + timeInfo: '10:10 AM', city: city.trim(), }), - { name: "city_time_function", outputSchema: cityTimeSchema }, + { name: 'city_time_function', outputSchema: cityTimeSchema }, ); const cityReportAgent = node( new LlmAgent({ - name: "city_report_agent", - model: "gemini-flash-latest", - instruction: "Output the data provided by the previous node as a sentence.", + name: 'city_report_agent', + model: 'gemini-flash-latest', + instruction: 'Output the data provided by the previous node as a sentence.', }), { inputSchema: cityTimeSchema }, ); @@ -58,11 +58,11 @@ const cityWorkflow = node( return reportText.output; }, - { name: "city_workflow", rerunOnResume: true }, + { name: 'city_workflow', rerunOnResume: true }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", cityWorkflow]], + name: 'root_agent', + edges: [['START', cityWorkflow]], }); // --8<-- [end:sequence-route] diff --git a/examples/typescript/snippets/graphs/human-input/get_started.ts b/examples/typescript/snippets/graphs/human-input/get_started.ts index 13e712d7d8..f6d9ef54a2 100644 --- a/examples/typescript/snippets/graphs/human-input/get_started.ts +++ b/examples/typescript/snippets/graphs/human-input/get_started.ts @@ -19,13 +19,13 @@ // deterministic. // --8<-- [start:get-started] -import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; +import { node, NodeContext, RequestInput, Workflow } from '@google/adk'; const step1 = node( async function* () { - yield new RequestInput({ message: "Enter a number:" }); + yield new RequestInput({ message: 'Enter a number:' }); }, - { name: "step1" }, + { name: 'step1' }, ); const step2 = node( @@ -35,11 +35,11 @@ const step2 = node( ? value * 2 : `"${nodeInput}" is not a number.`; }, - { name: "step2" }, + { name: 'step2' }, ); export const rootAgent = new Workflow({ - name: "root_agent", - edges: [["START", step1, step2]], + name: 'root_agent', + edges: [['START', step1, step2]], }); // --8<-- [end:get-started] diff --git a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts index 5a0404cbfd..c0d4ac7c5f 100644 --- a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts +++ b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts @@ -20,8 +20,8 @@ // human's answer into that shape; the schema tells a client what to collect. // --8<-- [start:initial-prompt] -import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; -import { z } from "zod"; +import { node, NodeContext, RequestInput, Workflow } from '@google/adk'; +import { z } from 'zod'; /** Asks the user for itinerary information. */ const initialPrompt = node( @@ -42,25 +42,25 @@ const initialPrompt = node( responseSchema: z.string(), }); }, - { name: "initial_prompt" }, + { name: 'initial_prompt' }, ); const buildItinerary = node( (_ctx: NodeContext, nodeInput: string) => { - const [city = "your city"] = nodeInput.split(","); + const [city = 'your city'] = nodeInput.split(','); return ( `Personalized itinerary for ${city.trim()}:\n` + - " 1. Morning walk through the old town\n" + - " 2. Lunch at a neighbourhood favourite\n" + - " 3. An afternoon activity matched to your hobby\n\n" + + ' 1. Morning walk through the old town\n' + + ' 2. Lunch at a neighbourhood favourite\n' + + ' 3. An afternoon activity matched to your hobby\n\n' + `(based on: ${nodeInput.trim()})` ); }, - { name: "build_itinerary" }, + { name: 'build_itinerary' }, ); export const rootAgent = new Workflow({ - name: "concierge_workflow", - edges: [["START", initialPrompt, buildItinerary]], + name: 'concierge_workflow', + edges: [['START', initialPrompt, buildItinerary]], }); // --8<-- [end:initial-prompt] diff --git a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts index 79a13f48b4..42003d7105 100644 --- a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts +++ b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts @@ -22,8 +22,8 @@ // reply must already be in that shape. // --8<-- [start:payload-and-schema] -import { node, NodeContext, RequestInput, Workflow } from "@google/adk"; -import { z } from "zod"; +import { node, NodeContext, RequestInput, Workflow } from '@google/adk'; +import { z } from 'zod'; /** * Itinerary is a list of activities. Each activity has a name and a @@ -41,16 +41,16 @@ const userFeedbackSchema = z.object({ const buildItinerary = node( (_ctx: NodeContext, city: string): ActivitiesList => { - const place = city.trim() || "your city"; + const place = city.trim() || 'your city'; return { itinerary: [ - { name: "Morning walk", description: `A stroll through old ${place}.` }, - { name: "Local lunch", description: `Regional food in ${place}.` }, - { name: "Museum visit", description: `The main museum of ${place}.` }, + { name: 'Morning walk', description: `A stroll through old ${place}.` }, + { name: 'Local lunch', description: `Regional food in ${place}.` }, + { name: 'Museum visit', description: `The main museum of ${place}.` }, ], }; }, - { name: "build_itinerary", outputSchema: activitiesListSchema }, + { name: 'build_itinerary', outputSchema: activitiesListSchema }, ); /** @@ -61,23 +61,23 @@ const getUserFeedback = node( async function* (_ctx: NodeContext, nodeInput: ActivitiesList) { const rendered = nodeInput.itinerary .map((a, i) => ` ${i + 1}. ${a.name} — ${a.description}`) - .join("\n"); + .join('\n'); yield new RequestInput({ message: `Here is your recommended base itinerary:\n${rendered}\n\n` + - "Which of these items appeal to you (if any)?", + 'Which of these items appeal to you (if any)?', payload: nodeInput, responseSchema: userFeedbackSchema, }); }, - { name: "get_user_feedback" }, + { name: 'get_user_feedback' }, ); const applyFeedback = node( (_ctx: NodeContext, nodeInput: unknown) => { const feedback = - typeof nodeInput === "string" + typeof nodeInput === 'string' ? nodeInput : String( (nodeInput as { userResponse?: unknown } | null)?.userResponse ?? @@ -85,11 +85,11 @@ const applyFeedback = node( ); return `Noted. Building the final itinerary around: ${feedback}`; }, - { name: "apply_feedback" }, + { name: 'apply_feedback' }, ); export const rootAgent = new Workflow({ - name: "concierge_workflow", - edges: [["START", buildItinerary, getUserFeedback, applyFeedback]], + name: 'concierge_workflow', + edges: [['START', buildItinerary, getUserFeedback, applyFeedback]], }); // --8<-- [end:payload-and-schema] diff --git a/examples/typescript/snippets/graphs/index/get_started.ts b/examples/typescript/snippets/graphs/index/get_started.ts index ad7e532c35..8ef8672fe7 100644 --- a/examples/typescript/snippets/graphs/index/get_started.ts +++ b/examples/typescript/snippets/graphs/index/get_started.ts @@ -23,31 +23,31 @@ import { node, NodeContext, Workflow, -} from "@google/adk"; -import { z } from "zod"; +} from '@google/adk'; +import { z } from 'zod'; const cityGeneratorAgent = new LlmAgent({ - name: "city_generator_agent", - model: "gemini-flash-latest", + name: 'city_generator_agent', + model: 'gemini-flash-latest', instruction: `Return the name of a random city. Return only the name, nothing else.`, }); /** The structured payload handed from the lookup node to the report agent. */ const cityTimeSchema = z.object({ - timeInfo: z.string().describe("Time information."), - city: z.string().describe("City name."), + timeInfo: z.string().describe('Time information.'), + city: z.string().describe('City name.'), }); type CityTime = z.infer; /** Simulates returning the current time in the specified city. */ function lookupTimeFunction(_ctx: NodeContext, nodeInput: string): CityTime { - return { timeInfo: "10:10 AM", city: nodeInput.trim() }; + return { timeInfo: '10:10 AM', city: nodeInput.trim() }; } const cityReportAgent = new LlmAgent({ - name: "city_report_agent", - model: "gemini-flash-latest", + name: 'city_report_agent', + model: 'gemini-flash-latest', instruction: `Output the following line: It is {CityTime.timeInfo} in {CityTime.city} right now.`, }); @@ -55,24 +55,24 @@ const cityReportAgent = new LlmAgent({ function completedMessageFunction(_ctx: NodeContext, nodeInput: string) { return createEvent({ content: { - role: "model", + role: 'model', parts: [{ text: `${nodeInput}\n WORKFLOW COMPLETED.` }], }, }); } export const rootAgent = new Workflow({ - name: "root_agent", + name: 'root_agent', edges: [ [ - "START", + 'START', cityGeneratorAgent, node(lookupTimeFunction, { - name: "lookup_time_function", + name: 'lookup_time_function', outputSchema: cityTimeSchema, }), node(cityReportAgent, { inputSchema: cityTimeSchema }), - node(completedMessageFunction, { name: "completed_message_function" }), + node(completedMessageFunction, { name: 'completed_message_function' }), ], ], }); diff --git a/examples/typescript/snippets/graphs/index/process_pipeline.ts b/examples/typescript/snippets/graphs/index/process_pipeline.ts index 3da315d0d8..433ddd67b7 100644 --- a/examples/typescript/snippets/graphs/index/process_pipeline.ts +++ b/examples/typescript/snippets/graphs/index/process_pipeline.ts @@ -25,14 +25,14 @@ import { node, NodeContext, Workflow, -} from "@google/adk"; +} from '@google/adk'; /** The routes this graph has edges for. */ -const ROUTES = ["BUG", "CUSTOMER_SUPPORT", "LOGISTICS"] as const; +const ROUTES = ['BUG', 'CUSTOMER_SUPPORT', 'LOGISTICS'] as const; const processMessage = new LlmAgent({ - name: "process_message", - model: "gemini-flash-latest", + name: 'process_message', + model: 'gemini-flash-latest', instruction: `Classify user message into either "BUG", "CUSTOMER_SUPPORT", or "LOGISTICS". If you think a message applies to more than one category, reply with a comma separated list of categories. @@ -47,32 +47,32 @@ const router = node( ); return createEvent({ route: matched.length > 0 ? matched : DEFAULT_ROUTE }); }, - { name: "router" }, + { name: 'router' }, ); /** Emits a user-facing message: `content`, with no `output`. */ const message = (text: string) => - createEvent({ content: { role: "model", parts: [{ text }] } }); + createEvent({ content: { role: 'model', parts: [{ text }] } }); -const response1Bug = node(() => message("Handling bug..."), { - name: "response_1_bug", +const response1Bug = node(() => message('Handling bug...'), { + name: 'response_1_bug', }); -const response2Support = node(() => message("Handling customer support..."), { - name: "response_2_support", +const response2Support = node(() => message('Handling customer support...'), { + name: 'response_2_support', }); -const response3Logistics = node(() => message("Handling logistics..."), { - name: "response_3_logistics", +const response3Logistics = node(() => message('Handling logistics...'), { + name: 'response_3_logistics', }); const responseUnknown = node( (_ctx: NodeContext, nodeInput: string) => message(`Could not classify that (classifier said: ${nodeInput}).`), - { name: "response_unknown" }, + { name: 'response_unknown' }, ); export const rootAgent = new Workflow({ - name: "routing_workflow", + name: 'routing_workflow', edges: [ - ["START", processMessage, router], + ['START', processMessage, router], [ router, { diff --git a/examples/typescript/snippets/graphs/routes/branches.ts b/examples/typescript/snippets/graphs/routes/branches.ts index 525073cbbf..5233c695c2 100644 --- a/examples/typescript/snippets/graphs/routes/branches.ts +++ b/examples/typescript/snippets/graphs/routes/branches.ts @@ -23,11 +23,11 @@ import { node, NodeContext, Workflow, -} from "@google/adk"; +} from '@google/adk'; const taskANode = node( (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(), - { name: "task_A_node" }, + { name: 'task_A_node' }, ); /** Stands in for an application-specific branch condition. */ @@ -37,23 +37,23 @@ const condition = (nodeInput: string) => /\d/.test(nodeInput); const router = node( (_ctx: NodeContext, nodeInput: string) => condition(nodeInput) - ? createEvent({ route: "RUN_TASK_C", output: nodeInput }) - : createEvent({ route: "RUN_TASK_B", output: nodeInput }), - { name: "router" }, + ? createEvent({ route: 'RUN_TASK_C', output: nodeInput }) + : createEvent({ route: 'RUN_TASK_B', output: nodeInput }), + { name: 'router' }, ); const taskBNode = new LlmAgent({ - name: "task_B_agent", - model: "gemini-flash-latest", - instruction: "Answer the user in a single short sentence.", + name: 'task_B_agent', + model: 'gemini-flash-latest', + instruction: 'Answer the user in a single short sentence.', }); -const taskCNode = node(() => "Task C completed", { name: "task_C_node" }); +const taskCNode = node(() => 'Task C completed', { name: 'task_C_node' }); export const rootAgent = new Workflow({ - name: "routing_workflow", + name: 'routing_workflow', edges: [ - ["START", taskANode, router], + ['START', taskANode, router], [ router, { diff --git a/examples/typescript/snippets/graphs/routes/fan_out_join.ts b/examples/typescript/snippets/graphs/routes/fan_out_join.ts index 6fe19b70ae..d528fe60d0 100644 --- a/examples/typescript/snippets/graphs/routes/fan_out_join.ts +++ b/examples/typescript/snippets/graphs/routes/fan_out_join.ts @@ -16,40 +16,40 @@ // and then hands the next node an object keyed by predecessor node name. // --8<-- [start:fan-out-join] -import { JoinNode, node, NodeContext, Workflow } from "@google/adk"; +import { JoinNode, node, NodeContext, Workflow } from '@google/adk'; const parallelTaskA = node( (_ctx: NodeContext, text: string) => text.toUpperCase(), - { name: "parallel_task_A" }, + { name: 'parallel_task_A' }, ); const parallelTaskB = node((_ctx: NodeContext, text: string) => text.length, { - name: "parallel_task_B", + name: 'parallel_task_B', }); const parallelTaskC = node( - (_ctx: NodeContext, text: string) => text.split("").reverse().join(""), - { name: "parallel_task_C" }, + (_ctx: NodeContext, text: string) => text.split('').reverse().join(''), + { name: 'parallel_task_C' }, ); -const myJoinNode = new JoinNode({ name: "my_join_node" }); +const myJoinNode = new JoinNode({ name: 'my_join_node' }); const finalTaskD = node( (_ctx: NodeContext, results: Record) => [ - `Uppercase: ${results["parallel_task_A"]}`, - `Length: ${results["parallel_task_B"]}`, - `Reversed: ${results["parallel_task_C"]}`, - ].join("\n"), - { name: "final_task_D" }, + `Uppercase: ${results['parallel_task_A']}`, + `Length: ${results['parallel_task_B']}`, + `Reversed: ${results['parallel_task_C']}`, + ].join('\n'), + { name: 'final_task_D' }, ); export const rootAgent = new Workflow({ - name: "fan_out_workflow", + name: 'fan_out_workflow', edges: [ - ["START", parallelTaskA, myJoinNode], - ["START", parallelTaskB, myJoinNode], - ["START", parallelTaskC, myJoinNode], + ['START', parallelTaskA, myJoinNode], + ['START', parallelTaskB, myJoinNode], + ['START', parallelTaskC, myJoinNode], [myJoinNode, finalTaskD], ], }); diff --git a/examples/typescript/snippets/graphs/routes/function_node.ts b/examples/typescript/snippets/graphs/routes/function_node.ts index 2cf9d38131..489be548f2 100644 --- a/examples/typescript/snippets/graphs/routes/function_node.ts +++ b/examples/typescript/snippets/graphs/routes/function_node.ts @@ -23,7 +23,7 @@ import { NodeContext, Workflow, type FunctionNodeHandler, -} from "@google/adk"; +} from '@google/adk'; /** A bare return value: boxed into an event's `output` for you. */ const myFunctionNode: FunctionNodeHandler = ( @@ -39,12 +39,12 @@ const myExplicitEventNode = (_ctx: NodeContext, nodeInput: string) => createEvent({ output: `${nodeInput} IS AWESOME!` }); export const rootAgent = new Workflow({ - name: "function_node_pipeline", + name: 'function_node_pipeline', edges: [ [ - "START", - node(myFunctionNode, { name: "my_function_node" }), - node(myExplicitEventNode, { name: "add_suffix" }), + 'START', + node(myFunctionNode, { name: 'my_function_node' }), + node(myExplicitEventNode, { name: 'add_suffix' }), ], ], }); diff --git a/examples/typescript/snippets/graphs/routes/loop_escalation.ts b/examples/typescript/snippets/graphs/routes/loop_escalation.ts index 3e7c20e2fb..d8605d54bf 100644 --- a/examples/typescript/snippets/graphs/routes/loop_escalation.ts +++ b/examples/typescript/snippets/graphs/routes/loop_escalation.ts @@ -22,7 +22,7 @@ // router --DONE--> finalize // --8<-- [start:loop-escalation] -import { createEvent, node, NodeContext, Workflow } from "@google/adk"; +import { createEvent, node, NodeContext, Workflow } from '@google/adk'; interface Draft { topic: string; @@ -37,16 +37,16 @@ const seedDraft = node( topic: topic.trim(), bullets: [`${topic.trim()} — point 1`], }), - { name: "seed_draft" }, + { name: 'seed_draft' }, ); const critic = node( (_ctx: NodeContext, draft: Draft) => createEvent({ - route: draft.bullets.length >= REQUIRED_BULLETS ? "DONE" : "REVISE", + route: draft.bullets.length >= REQUIRED_BULLETS ? 'DONE' : 'REVISE', output: draft, }), - { name: "critic" }, + { name: 'critic' }, ); const refine = node( @@ -57,20 +57,20 @@ const refine = node( `${draft.topic} — point ${draft.bullets.length + 1}`, ], }), - { name: "refine" }, + { name: 'refine' }, ); const finalize = node( (_ctx: NodeContext, draft: Draft) => `Approved after ${draft.bullets.length} bullets:\n` + - draft.bullets.map((b) => ` • ${b}`).join("\n"), - { name: "finalize" }, + draft.bullets.map((b) => ` • ${b}`).join('\n'), + { name: 'finalize' }, ); export const rootAgent = new Workflow({ - name: "loop_workflow", + name: 'loop_workflow', edges: [ - ["START", seedDraft, critic], + ['START', seedDraft, critic], [critic, { REVISE: refine, DONE: finalize }], [refine, critic], ], diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts index f06e6fe794..b7eeafaf37 100644 --- a/examples/typescript/snippets/graphs/routes/nested_workflow.ts +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -16,63 +16,63 @@ // workflow's edges to encapsulate a reusable sub-process. // --8<-- [start:nested-workflow] -import { createEvent, node, NodeContext, Workflow } from "@google/adk"; +import { createEvent, node, NodeContext, Workflow } from '@google/adk'; const taskA1 = node( (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(), { - name: "task_A1", + name: 'task_A1', }, ); const router = node( (_ctx: NodeContext, text: string) => createEvent({ - route: text === text.toUpperCase() ? "RUN_WORKFLOW_C" : "RUN_WORKFLOW_B", + route: text === text.toUpperCase() ? 'RUN_WORKFLOW_C' : 'RUN_WORKFLOW_B', output: text, }), - { name: "router" }, + { name: 'router' }, ); const workflowB = new Workflow({ - name: "workflow_B", + name: 'workflow_B', edges: [ [ - "START", + 'START', node( (_ctx: NodeContext, text: string) => text.replace( /(^|\P{L})(\p{L})/gu, (_m, sep: string, ch: string) => sep + ch.toUpperCase(), ), - { name: "b_title_case" }, + { name: 'b_title_case' }, ), node((_ctx: NodeContext, text: string) => `[B] ${text}`, { - name: "b_frame", + name: 'b_frame', }), ], ], }); const workflowC = new Workflow({ - name: "workflow_C", + name: 'workflow_C', edges: [ [ - "START", + 'START', node((_ctx: NodeContext, text: string) => text.toLowerCase(), { - name: "c_lower_case", + name: 'c_lower_case', }), node((_ctx: NodeContext, text: string) => `[C] ${text}`, { - name: "c_frame", + name: 'c_frame', }), ], ], }); export const rootAgent = new Workflow({ - name: "parent_workflow", + name: 'parent_workflow', edges: [ - ["START", taskA1, router], + ['START', taskA1, router], [ router, { diff --git a/examples/typescript/snippets/graphs/routes/sequence.ts b/examples/typescript/snippets/graphs/routes/sequence.ts index 21e338b2c2..4a785d15e8 100644 --- a/examples/typescript/snippets/graphs/routes/sequence.ts +++ b/examples/typescript/snippets/graphs/routes/sequence.ts @@ -16,25 +16,25 @@ // return value is delivered to the next node as its input. // --8<-- [start:sequence] -import { node, NodeContext, Workflow } from "@google/adk"; +import { node, NodeContext, Workflow } from '@google/adk'; const taskANode = node( (_ctx: NodeContext, nodeInput: string) => `Summary: ${nodeInput.trim()}`, - { name: "task_A_node" }, + { name: 'task_A_node' }, ); const taskBNode = node( (_ctx: NodeContext, summary: string) => summary.toUpperCase(), - { name: "task_B_node" }, + { name: 'task_B_node' }, ); const taskCNode = node( (_ctx: NodeContext, shouted: string) => `${shouted} (done)`, - { name: "task_C_node" }, + { name: 'task_C_node' }, ); export const rootAgent = new Workflow({ - name: "sequential_workflow", - edges: [["START", taskANode, taskBNode, taskCNode]], + name: 'sequential_workflow', + edges: [['START', taskANode, taskBNode, taskCNode]], }); // --8<-- [end:sequence] From 5e44b8d56f1464c31a4ac3d1807553f8d3bc96e7 Mon Sep 17 00:00:00 2001 From: kalenkevich Date: Wed, 26 Aug 2026 15:22:32 -0700 Subject: [PATCH 4/6] Restore the upstream titleCase guard in the nested workflow snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Porting samples/workflows/routes/nested_workflow inlined the `titleCase` helper and dropped the check that a character's uppercase form is a single code point, along with the comment explaining why it is there. That changed behaviour for word-initial characters whose uppercase expands: "first draft" became "FIrst Draft" and "ßeta test" became "SSeta Test", where upstream leaves both alone. Restores the helper, the guard and the rationale. Because the helper sits inside the --8<-- region, the explanation now renders on the page as well, so the next person to touch it can see what the guard is for. Also restores an unused `_ctx` parameter in the user_message snippet, the only other place the port had drifted from upstream. Verified by compiling each of the 26 snippets and its upstream counterpart at adk-v2.0.0 with `tsc --removeComments` and comparing the emitted JavaScript: all 26 are now semantically identical to samples/workflows/. --- .../graphs/data-handling/user_message.ts | 2 +- .../snippets/graphs/routes/nested_workflow.ts | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/typescript/snippets/graphs/data-handling/user_message.ts b/examples/typescript/snippets/graphs/data-handling/user_message.ts index 5497ab7d73..a31b4128cc 100644 --- a/examples/typescript/snippets/graphs/data-handling/user_message.ts +++ b/examples/typescript/snippets/graphs/data-handling/user_message.ts @@ -31,7 +31,7 @@ const userMessage = node( ); const research = node( - async function* () { + async function* (_ctx: NodeContext) { yield message('Gathering sources...'); yield createEvent({ output: ['source-a', 'source-b', 'source-c'] }); }, diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts index b7eeafaf37..1371c3a765 100644 --- a/examples/typescript/snippets/graphs/routes/nested_workflow.ts +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -34,19 +34,28 @@ const router = node( { name: 'router' }, ); +/** + * Upper-cases the first letter of each word. + * + * Unicode-aware on purpose: `\b\w` is ASCII-only, so `ü` never matches — and + * the word boundary it creates before the *next* ASCII letter upper-cases that + * one instead ("strässe" -> "SträSse"). A letter whose uppercase form is more + * than one code point (German `ß` -> "SS") is left alone rather than mangled. + */ +const titleCase = (text: string) => + text.replace(/(^|\P{L})(\p{L})/gu, (_match, sep: string, ch: string) => { + const upper = ch.toUpperCase(); + return sep + ([...upper].length === 1 ? upper : ch); + }); + const workflowB = new Workflow({ name: 'workflow_B', edges: [ [ 'START', - node( - (_ctx: NodeContext, text: string) => - text.replace( - /(^|\P{L})(\p{L})/gu, - (_m, sep: string, ch: string) => sep + ch.toUpperCase(), - ), - { name: 'b_title_case' }, - ), + node((_ctx: NodeContext, text: string) => titleCase(text), { + name: 'b_title_case', + }), node((_ctx: NodeContext, text: string) => `[B] ${text}`, { name: 'b_frame', }), From a9759bcf8117d8359595521de23097238b347c79 Mon Sep 17 00:00:00 2001 From: kalenkevich Date: Thu, 27 Aug 2026 07:54:48 -0700 Subject: [PATCH 5/6] Address review: plainer wording, and lift shared cautions out of the tabs Wording, across all TypeScript tabs: - No sentence starts with code syntax. "`route` is independent of..." becomes "The `route` value is independent of...", and the same for the other cases. - Removed informal and editorial phrasing: "earn their keep", "dropped straight into", "reach for", "hands you", "two things to know going in", "kick the children off", "fails loudly". - Spelled out "/" as "and" in the `inputSchema` and `outputSchema` sentence. - Described the `ctx.runNode()` interrupt behaviour in full rather than only as "does not throw": it returns normally with `interruptIds` populated and `output` undefined, and an orchestrator that skips the check continues with a value the user never supplied. - Explained what a JoinNode waits for instead of referring to "the barrier". - Tied the `rerunOnResume` option back to the code sample it follows, and introduced the two orchestrator details by saying when they matter. Structure: - The "Response schema input limitations" note appeared in both the Python and TypeScript tabs. Replaced both with one language-neutral note after the code examples. - The "Stuck JoinNode" caution appeared in all three tabs. Replaced them with one caution after the code examples, stating the rule that every node feeding a join must produce an output. - Moved the unbounded-cycle caution out of the TypeScript tab to the end of the section, since it is not language specific. Snippet header comments got the same wording pass. Verified afterwards: the 26 snippets still type-check, all 53 snippet includes resolve, and every snippet is still semantically identical to samples/workflows/ at adk-v2.0.0. --- docs/graphs/data-handling.md | 102 ++++++++-------- docs/graphs/dynamic.md | 111 ++++++++++-------- docs/graphs/human-input.md | 78 ++++++------ docs/graphs/index.md | 24 ++-- docs/graphs/routes.md | 90 ++++++-------- .../snippets/graphs/dynamic/data_handling.ts | 4 +- .../snippets/graphs/dynamic/loop_route.ts | 8 +- .../snippets/graphs/routes/nested_workflow.ts | 4 +- 8 files changed, 201 insertions(+), 220 deletions(-) diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md index 559801a795..5ddadc744f 100644 --- a/docs/graphs/data-handling.md +++ b/docs/graphs/data-handling.md @@ -33,17 +33,17 @@ receives it as its typed input. In ADK TypeScript v2.0.0, nodes exchange data through events. The key fields for node data handling are: - - **`output`**: the value handed to the next node. Return it bare and - it is boxed into an event for you, or set it explicitly with - `createEvent({output})`. - - **`content`**: a user-facing message. The runtime renders it, and - the graph does *not* forward it to the next node. - - **`route`**: the routing key(s) that select which conditional edge - to follow. - - Session state is separate from the event: a node reads and writes it + - **`output`**: the value passed to the next node. Return a value + directly and ADK wraps it in an event, or set the field explicitly + with `createEvent({output})`. + - **`content`**: a message for the user. The runtime renders this + field, but the graph does not pass it to the next node. + - **`route`**: the routing keys that select which conditional edge to + follow. + + Session state is separate from the event. A node reads and writes state through `ctx.state`, and the accumulated delta is attached to that - node's events. State keys may carry a prefix that controls their + node's events. State keys can carry a prefix that controls their lifetime and scope: | Prefix | Scope | @@ -117,8 +117,8 @@ Each step in a workflow produces output for its successor. === "TypeScript" There are three equivalent ways to produce a node's output: return a - bare value, return `createEvent({output})`, or yield events from an - async generator when you want to stream progress alongside the result. + value directly, return `createEvent({output})`, or yield events from an + async generator to stream progress alongside the result. ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/node_output.ts:node-output" @@ -126,10 +126,10 @@ Each step in a workflow produces output for its successor. !!! warning "Caution: emit `output` from one event per execution" - Nothing enforces this, so getting it wrong is silent. A node may - yield any number of events carrying `output`, each overwrites the - last, and the successor receives only the final value. Carry - progress on `content` instead. + A node can yield any number of events carrying `output`, and ADK + does not raise an error in this case. Each event overwrites the + previous one, and the successor node receives only the final value. + Use `content` for progress messages instead. === "Go" @@ -175,11 +175,11 @@ Each step in a workflow produces output for its successor. === "TypeScript" - `output` is not limited to text. Any serializable value flows to the - next node, which receives it as a typed object — no JSON parsing and no - state reads. Attaching an `outputSchema` to the producer, or an - `inputSchema` to the consumer, makes the contract explicit and - validates it at runtime: + The `output` field is not limited to text. Any serializable value is + passed to the next node, which receives it as a typed object, with no + JSON parsing or state reads required. Attaching an `outputSchema` to the + producing node, or an `inputSchema` to the consuming node, makes the + contract explicit and validates it at runtime: ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/structured_output.ts:structured-output" @@ -215,9 +215,9 @@ Each step in a workflow produces output for its successor. === "TypeScript" - `route` is independent of `output`, so one event can both select a - branch and forward a payload to it. `DEFAULT_ROUTE` catches everything - no other branch matched: + The `route` value is independent of `output`, so one event can both + select a branch and forward a payload to it. The `DEFAULT_ROUTE` setting + catches any value that no other branch matched: ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/routing_output.ts:routing-output" @@ -250,10 +250,10 @@ Each step in a workflow produces output for its successor. === "TypeScript" - A message for the human is the event's `content`. The runtime renders - it and the graph does **not** hand it to the next node — `content` is - for the user, `output` is for the next node. A node can emit both, as - two events of which only one carries `output`: + A message for the user is the event's `content` field. The runtime + renders `content`, but the graph does not pass it to the next node. Use + `content` for the user and `output` for the next node. A node can emit + both by sending two events, where only one carries `output`: ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/user_message.ts:user-message" @@ -316,9 +316,9 @@ inside tools and callbacks regardless of which agent style you use. === "TypeScript" - State is written through `ctx.state`, not returned. A write is visible - to every later node in the same run and is committed with the writing - node's events: + Write state through `ctx.state` rather than returning it. A write is + visible to every later node in the same run, and is committed with the + writing node's events: ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/session_state.ts:session-state" @@ -327,11 +327,10 @@ inside tools and callbacks regardless of which agent style you use. !!! warning "Caution: `state` data limitations" Session state is a lightweight key-value store. Do not use it to - move large payloads between nodes — use artifacts or a database - tool for those. Passing a value along an edge as node `output` is - also the better choice when only the next node needs it; reach for - state when a value has to outlive the run, or be read by a tool, a - callback, or `{key}` instruction templating. + move large payloads between nodes; use artifacts or a database tool + instead. When only the next node needs a value, pass it along the + edge as node `output`. Use state when a value must outlive the run, + or be read by a tool, a callback, or `{key}` instruction templating. === "Go" @@ -402,16 +401,17 @@ accepted and produced by any agent node. === "TypeScript" - Schemas are Zod objects, or a genai `Schema`. Where the schema goes - matters: + Schemas are Zod objects or a genai `Schema`. The location of the schema + determines its effect: - - `LlmAgent.outputSchema` forces the model to answer in that shape. - - `LlmAgent.inputSchema` is only consulted when the agent is exposed - as a **tool**. Inside a graph, the schema that validates a node's - input belongs on the node: `node(agent, {inputSchema})`. + - The `LlmAgent.outputSchema` option requires the model to answer in + that shape. + - The `LlmAgent.inputSchema` option applies only when the agent is + exposed as a tool. Inside a graph, set the schema that validates a + node's input on the node itself, using `node(agent, {inputSchema})`. - Agents in a graph must run in `single_turn` (the default) or `task` - mode. + Agents in a graph must run in `single_turn` mode, which is the default, + or `task` mode. ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/schemas.ts:schemas" @@ -483,14 +483,14 @@ accepted and produced by any agent node. Two data-selection forms are available inside an agent instruction: - - `{Class.field}` reads a field off **this** node's input. - - `` reads a field off a named - predecessor's output. It is more restrictive, and unambiguous when - several upstream nodes share a field name. + - The `{Class.field}` form reads a field from this node's input. + - The `` form reads a field from a named + predecessor's output. Use this form when several upstream nodes + share a field name. - Both are distinct from `{state_key}`, which reads session state. The - `Class.` prefix is documentation only — resolution uses the field name - after the dot. + Both forms are distinct from `{state_key}`, which reads session state. + The `Class.` prefix is documentation only; resolution uses the field + name after the dot. ```typescript --8<-- "examples/typescript/snippets/graphs/data-handling/structured_access.ts:structured-access" diff --git a/docs/graphs/dynamic.md b/docs/graphs/dynamic.md index b17388d44e..8568d6db84 100644 --- a/docs/graphs/dynamic.md +++ b/docs/graphs/dynamic.md @@ -68,21 +68,23 @@ workflow containing a single node with a function: === "TypeScript" - TypeScript has no `@node` decorator. `node(fn, options)` is the factory - form, and `ctx.runNode()` is the equivalent of `ctx.run_node()`: + TypeScript has no `@node` decorator. Use the `node(fn, options)` factory + function instead. The `ctx.runNode()` method is the equivalent of + `ctx.run_node()`: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/get_started.ts:get-started" ``` - Two things to know going in: + When you write an orchestrator node, two details affect how you read + results and how the workflow behaves after a pause: - - `ctx.runNode()` resolves to a node **result**, not the output - directly — read `.output`. + - The `ctx.runNode()` method resolves to a node result, not to the + output value. Read the `.output` property to get the value. - An orchestrator that calls `ctx.runNode()` must set - `rerunOnResume: true`, so its body re-runs on resume and - already-finished children are replayed from their checkpoints - rather than executed again. + `rerunOnResume: true`. This setting causes the node body to re-run + on resume, so already-finished children are replayed from their + checkpoints instead of being executed again. === "Go" @@ -145,26 +147,28 @@ run within a workflow. === "TypeScript" - There are two ways to build a node: the `node(fn, options)` factory, - and the explicit `new FunctionNode(name, fn, config)` constructor. - Reach for the constructor when you are wrapping a function from another - library, need several differently-configured nodes from one function, - or keep node references in a registry for advanced orchestration. + There are two ways to build a node: the `node(fn, options)` factory + function, and the explicit `new FunctionNode(name, fn, config)` + constructor. Use the constructor when you are wrapping a function from + another library, need several differently configured nodes from one + function, or keep node references in a registry for advanced + orchestration. ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:node-forms" ``` - The most important option is `rerunOnResume`, which controls what - happens when a workflow resumes after a human-in-the-loop pause: + In this code sample, the most important option is `rerunOnResume`, which + controls what happens when a workflow resumes after a human-in-the-loop + pause: - - **`true` (re-entry):** the node body is re-run from the top. Use - this for any orchestrator that calls `ctx.runNode()` — the body - re-executes and already-completed child activations are skipped + - **`true` (re-entry):** the node body re-runs from the top. Use this + setting for any orchestrator that calls `ctx.runNode()`. The body + re-executes, and already-completed child activations are skipped automatically. - **`false` (handoff, the leaf default):** the resume payload is routed to the node's successor as input, bypassing the interrupted - node entirely. + node. === "Go" @@ -239,9 +243,9 @@ execution logic (order and paths) for those nodes. === "TypeScript" - The orchestrator is an ordinary async function that awaits - `ctx.runNode()` for each child step, wrapped as a node with - `rerunOnResume: true` and used as the graph's only edge: + The orchestrator is an async function that awaits `ctx.runNode()` for + each child step. Wrap it as a node with `rerunOnResume: true` and use it + as the graph's only edge: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:workflows" @@ -317,17 +321,17 @@ manually read and write session state keys for data transfer. === "TypeScript" - `ctx.runNode()` hands you the child's result directly, so there are no - session-state keys to read and write just to move a value one step - downstream. It accepts anything node-like, including an `LlmAgent`, - without wrapping it in `node()` first: + The `ctx.runNode()` function returns the child's result directly, so + there are no session-state keys to read and write to move a value one + step downstream. This function accepts any node-like value, including an + `LlmAgent`, without wrapping it in `node()` first: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/data_handling.ts:data-handling" ``` - Schemas work the same as in a graph — attach them to the nodes you - run, as the [sequence route](#sequence-route) below does. + Schemas work the same way as in a graph. Attach them to the nodes you + run, as shown in the [sequence route](#sequence-route) section. === "Go" @@ -372,8 +376,8 @@ as you can with graph-based workflows. === "TypeScript" - A sequential route is just awaiting `ctx.runNode()` calls one after - another — each finishes before the next starts: + A sequential route awaits `ctx.runNode()` calls one after another. Each + call finishes before the next one starts: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/sequence_route.ts:sequence-route" @@ -442,11 +446,11 @@ workflows offer much more flexibility to define the routing logic you need. === "TypeScript" - This is where dynamic workflows earn their keep: the iteration is an - ordinary loop, not a back-edge you have to reason about. Values live in - local variables, and state is written only where an agent's instruction - template needs to read it back. Unlike a graph cycle, the loop is - trivially bounded, so a stubborn model cannot spin forever: + Dynamic workflows can help keep workflow logic simple by defining an + iteration as an ordinary loop rather than a back-edge in a graph. Values + are held in local variables, and state is written only where an agent's + instruction template needs to read it back. Unlike a graph cycle, the + loop is bounded by its loop condition: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/loop_route.ts:loop-route" @@ -500,10 +504,11 @@ Dynamic workflows in ADK can support parallel execution. === "TypeScript" - `ctx.runNode()` returns a promise, so starting every child before - awaiting any of them runs them concurrently, and `Promise.all` gathers - the results. Run ids are assigned in call order, so kick the children - off in a synchronous loop to keep them deterministic across a resume: + The `ctx.runNode()` method returns a promise, so starting every child + before awaiting any of them runs the children concurrently, and + `Promise.all` collects the results. Run IDs are assigned in call order, + so start the children in a synchronous loop to keep the IDs + deterministic across a resume: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/parallel_route.ts:parallel-route" @@ -511,12 +516,12 @@ Dynamic workflows in ADK can support parallel execution. !!! tip "Tip: prefer the built-in parallel worker" - When the shape is simply "map one node over a list", use - `node(worker, {parallelWorker: true, maxParallelWorkers: 4})`. It - does the fan-out for you and bounds concurrency (default 8). - Hand-rolling it, as above, is for when you need custom scheduling - or partial-failure handling. On resume, only failed or interrupted - workers re-execute either way. + To run one node over each item in a list, use + `node(worker, {parallelWorker: true, maxParallelWorkers: 4})`. This + option performs the fan-out and bounds concurrency, which defaults + to 8. Use the manual approach shown above when you need custom + scheduling or partial-failure handling. On resume, only failed or + interrupted workers re-execute in both cases. === "Go" @@ -587,10 +592,12 @@ Dynamic workflows in ADK can also include human input or human in the loop !!! important "Important: check `interruptIds` before deciding" - `ctx.runNode()` does **not** throw when a child interrupts. It - resolves with a result whose `interruptIds` are populated and whose - `output` is still `undefined`, so an orchestrator that does not - check will decide on an answer the human never gave. + The `ctx.runNode()` method does **not** throw an error when a child + node interrupts. It returns normally, with the `interruptIds` + property of the result populated and the `output` property still + `undefined`. Check `interruptIds` before you use the result. An + orchestrator that skips this check treats the missing output as an + answer and continues with a value the user never supplied. === "Go" @@ -671,9 +678,9 @@ and logically remain the same for the input. === "TypeScript" - Pass `{runId}` as a trailing option to `ctx.runNode()`. The id must - contain at least one non-numeric character so it cannot collide with - the auto-generated sequential ids: + Pass a `runId` as a trailing option to `ctx.runNode()`. The ID must + contain at least one non-numeric character so it does not collide with + the auto-generated sequential IDs: ```typescript --8<-- "examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts:custom-execution-ids" diff --git a/docs/graphs/human-input.md b/docs/graphs/human-input.md index 01fc6bfabf..ce63964ec3 100644 --- a/docs/graphs/human-input.md +++ b/docs/graphs/human-input.md @@ -42,18 +42,18 @@ the input process more predictable and reliable. === "TypeScript" In ADK TypeScript v2.0.0, a human input node yields a `RequestInput`. - `step1` pauses the workflow until the user replies, and the reply is - handed to the next node as its input. A HITL node needs no model, which - makes the pause fully deterministic. + The `step1` node pauses the workflow until the user replies, and the + reply is passed to the next node as its input. A human-in-the-loop node + does not require a model, which makes the pause deterministic. ```typescript --8<-- "examples/typescript/snippets/graphs/human-input/get_started.ts:get-started" ``` - This is the default `rerunOnResume: false` handoff: the interrupted - node does **not** re-run — it completes with the user's reply as its - output. A node that calls `ctx.runNode()` needs `rerunOnResume: true` - instead; see + This implementation shows the default `rerunOnResume: false` handoff: + the interrupted node does not re-run. It completes with the user's reply + as its output. A node that calls `ctx.runNode()` needs + `rerunOnResume: true` instead. For more information, see [human input in dynamic workflows](/graphs/dynamic/#human-input). === "Go" @@ -86,45 +86,27 @@ the input process more predictable and reliable. request. - **`response_schema`:** A data structure the human response must conform to. - !!! note "Note: Response schema input limitations" - - For the **response_schema** setting, the ***RequestInput*** class does not - automatically reformat human responses to fit a specified data structure. The - human response must be provided in the specified format. For a better user - experience, consider providing a user interface to collect structured data - or use an Agent node to conform unstructured data to the format required. - === "TypeScript" - `RequestInput` takes the following configuration options: + The `RequestInput` class takes the following configuration options: - **`message`:** Text shown to the user explaining what is being asked. - - **`payload`:** Structured data sent alongside the prompt, so a - client can render richer context. - - **`responseSchema`:** The shape the reply is expected to take. It - travels on the interrupt as - `functionCall.args.response_schema`, which is what a client reads - to render a form for the reply. + - **`payload`:** Structured data sent with the prompt, so a client can + render additional context. + - **`responseSchema`:** The shape the reply is expected to take. The + schema travels on the interrupt as + `functionCall.args.response_schema`, which a client reads to render + a form for the reply. - `rerunOnResume` on the node controls what happens when the reply - arrives: + The `rerunOnResume` option on the node controls what happens when the + reply arrives: - **`false`** (the leaf default): the reply is routed to the node's successor as input, bypassing the interrupted node. - - **`true`**: the node body is re-run from the top. Required for any - node that calls `ctx.runNode()`, so it can deliver cached child - results on resume. - - !!! note "Note: Response schema input limitations" - - `RequestInput` does not reformat a human reply to fit - `responseSchema` — the reply must already be in that shape. A reply - carrying an *object* is checked against the schema and a mismatch - fails loudly, leaving the interrupt open so the next reply answers - it; a plain-text reply is never schema-checked. For a better user - experience, collect structured data in your UI, or put an agent - node after the pause to normalize whatever the human typed. + - **`true`**: the node body re-runs from the top. This setting is + required for any node that calls `ctx.runNode()`, so it can deliver + cached child results on resume. === "Go" @@ -155,6 +137,13 @@ the input process more predictable and reliable. include a UI or a downstream agent node to validate the response before acting on it. +!!! note "Note: Response schema input limitations" + + A response schema does not reformat a human reply to fit the specified + structure. The reply must already be in that format. For a better user + experience, collect structured data in your client interface, or place an + agent node after the pause to convert the reply into the required format. + ## Human input examples The following code examples demonstrate more detailed human input requests. @@ -201,8 +190,8 @@ The following code examples demonstrate more detailed human input requests. === "TypeScript" The following three-node graph builds a structured itinerary, sends it - as `payload` alongside the prompt so a client can render it, and acts - on the user's feedback: + as `payload` with the prompt so a client can render it, and then acts on + the user's feedback: ```typescript --8<-- "examples/typescript/snippets/graphs/human-input/payload_and_schema.ts:payload-and-schema" @@ -251,12 +240,11 @@ specific tool call. === "TypeScript" - Set `requireConfirmation: true` on a `FunctionTool` and the agent pauses - for approval before that tool runs. - - A graph HITL node is the other half of this: rather than confirming a - tool call, it can open the workflow by asking the user what they want. - `responseSchema: z.string()` asks for a plain text reply: + Set `requireConfirmation: true` on a `FunctionTool` to make the agent + pause for approval before that tool runs. A graph human-in-the-loop node + serves a different purpose: instead of confirming a tool call, it can + start the workflow by asking the user for input. The + `responseSchema: z.string()` option requests a plain text reply: ```typescript --8<-- "examples/typescript/snippets/graphs/human-input/initial_prompt.ts:initial-prompt" diff --git a/docs/graphs/index.md b/docs/graphs/index.md index e5f66d14da..c71848bcf6 100644 --- a/docs/graphs/index.md +++ b/docs/graphs/index.md @@ -103,13 +103,13 @@ function, and the final agent reports the information. === "TypeScript" - In ADK TypeScript v2.0.0, a `Workflow` takes an `edges` array whose - rows list the nodes to run in order. `node()` wraps a function, an - agent, a tool, or another `Workflow` as a graph node, and is where you - attach the node's name and its `inputSchema` / `outputSchema` - contracts — schemas are Zod objects, or a genai `Schema`. Each node's - return value is handed to the next node as its input, so no session - state writes are needed. + In ADK TypeScript v2.0.0, a `Workflow` takes an `edges` array. Each row + lists the nodes to run in order. The `node()` function wraps a function, + an agent, a tool, or another `Workflow` as a graph node, and sets the + node's name and its `inputSchema` and `outputSchema` contracts. Schemas + are Zod objects or a genai `Schema`. Each node's return value is passed + to the next node as its input, so you do not need to write to session + state. ```typescript --8<-- "examples/typescript/snippets/graphs/index/get_started.ts:get-started" @@ -211,11 +211,11 @@ translated into a graph-based agent: === "TypeScript" In ADK TypeScript v2.0.0, a router node returns an event carrying a - `route` value, built with `createEvent({route})`. A second edge row - maps each route value to the node that handles it. Setting `route` to - an *array* dispatches to every matching branch, which is what lets the - classifier below reply with more than one category, and `DEFAULT_ROUTE` - catches anything no branch matched. + `route` value, created with `createEvent({route})`. A second edge row + maps each route value to the node that handles it. Setting `route` to an + array dispatches to every matching branch, which lets the classifier in + this example return more than one category. The `DEFAULT_ROUTE` setting + catches any value that no branch matched. ```typescript --8<-- "examples/typescript/snippets/graphs/index/process_pipeline.ts:process-pipeline" diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md index a59aef8f57..4a19b7ab85 100644 --- a/docs/graphs/routes.md +++ b/docs/graphs/routes.md @@ -113,11 +113,11 @@ objects. === "TypeScript" In ADK TypeScript v2.0.0, the primary node type is a `FunctionNode`, - built by passing a plain function to `node()`. A handler always takes - `(ctx, input)`; nothing is injected by parameter name. Returning a bare - value boxes it into an event's `output` for you, and returning - `createEvent({output})` is the explicit form — useful when you also need - to set `route` or `content`: + created by passing a function to `node()`. A handler always takes + `(ctx, input)` parameters; ADK does not inject values by parameter name. + Returning a value directly wraps it in the event's `output` field. + Returning `createEvent({output})` is the explicit form, which you need + when you also set `route` or `content`: ```typescript --8<-- "examples/typescript/snippets/graphs/routes/function_node.ts:function-node" @@ -170,16 +170,17 @@ A sequential route runs each node once, in the listed order. === "TypeScript" - An `edges` row starting with `'START'` runs each listed node once, in - order, forwarding every node's return value to the next one: + An `edges` row that starts with `'START'` runs each listed node once, in + order, and passes every node's return value to the next node: ```typescript edges: [['START', taskANode]] // a single node edges: [['START', taskANode, taskBNode, taskCNode]] // three, in order ``` - Listing `'START'` in more than one row fans out into parallel paths - instead — see [fan out and join](#parallel-tasks-fan-out-and-join-paths). + Listing `'START'` in more than one row creates parallel paths instead. + For more information, see + [fan out and join](#parallel-tasks-fan-out-and-join-paths). ```typescript --8<-- "examples/typescript/snippets/graphs/routes/sequence.ts:sequence" @@ -236,11 +237,12 @@ A sequential route runs each node once, in the listed order. === "TypeScript" - Branching is a node that emits a `route`, plus an edge row mapping each - route value to the node that handles it. Route values may be strings, - numbers or booleans, and `DEFAULT_ROUTE` matches when no other route on - the same source node did. A branch target is anything node-like: - `taskBNode` below is an `LlmAgent`, `taskCNode` a plain function. + Branching requires a node that emits a `route` value, and an edge row + that maps each route value to the node that handles it. Route values can + be strings, numbers, or booleans. The `DEFAULT_ROUTE` setting matches + when no other route on the same source node matches. A branch target can + be any node-like value: in this example, `taskBNode` is an `LlmAgent` + and `taskCNode` is a function. ```typescript --8<-- "examples/typescript/snippets/graphs/routes/branches.ts:branches" @@ -342,32 +344,16 @@ before passing results to the next step. ] ``` - !!! warning "Caution: Stuck JoinNode from incomplete nodes" - - The ***JoinNode*** object proceeds only after all its upstream nodes - have provided an Event output. If one of the upstream nodes fails to - provide output, the JoinNode is stuck and workflow execution stops. - Make sure to include failsafe output from any node that outputs to a - ***JoinNode***. - === "TypeScript" - A `JoinNode` is the fan-in barrier. It waits for every predecessor and - then hands its successor a record keyed by predecessor node name: + A `JoinNode` is the fan-in barrier. This logic mechanism waits for every + predecessor task to complete, and then passes its successor a record + keyed by predecessor node name: ```typescript --8<-- "examples/typescript/snippets/graphs/routes/fan_out_join.ts:fan-out-join" ``` - !!! warning "Caution: Stuck JoinNode from incomplete nodes" - - The barrier waits for every predecessor to **complete**, not to - produce an output. A predecessor that finishes without one still - releases the join and arrives in the record as its name mapped to - `undefined`, so reading a field off it throws somewhere downstream, - far from the node that skipped it. Give anything feeding a join an - output of its own, and a `retryConfig` if it can fail. - === "Go" ADK Go v2.0.0 provides `workflow.NewJoinNode` for true fan-in in the @@ -402,13 +388,14 @@ before passing results to the next step. --8<-- "examples/go/snippets/graphs/routes/main.go:parallel-fan-out" ``` - !!! warning "Caution: Stuck JoinNode from incomplete nodes" +!!! warning "Caution: nodes that feed a JoinNode must produce output" - `workflow.NewJoinNode` proceeds only after every predecessor node has - emitted an `event.Output`. If a predecessor fails without emitting - output, the JoinNode is stuck and workflow execution stops. Attach a - `RetryConfig` to flaky predecessor nodes to guard against transient - failures. + A `JoinNode` releases only after all of its predecessor nodes finish. + Give every node that feeds a join an output of its own, and attach a + retry configuration to any node that can fail. A predecessor that + finishes without an output leaves the join with no value for that + branch, and the resulting failure appears downstream, away from the node + that caused it. ## Nested workflows @@ -451,8 +438,8 @@ accomplish this goal. === "TypeScript" - A `Workflow` is itself a node, so it can be dropped straight into - another workflow's edges to encapsulate a reusable sub-process: + A `Workflow` is itself a node, so you can use one inside another + workflow's edges to encapsulate a reusable sub-process: ```typescript --8<-- "examples/typescript/snippets/graphs/routes/nested_workflow.ts:nested-workflow" @@ -529,22 +516,14 @@ lifecycle on each iteration. === "TypeScript" - A loop is a **back-edge**: a downstream node routes back to an earlier - node, and the engine re-activates that node with a fresh lifecycle on - each iteration. The loop exits when the router picks the terminal - branch instead: + A loop is a back-edge: a downstream node routes back to an earlier node, + and the engine re-activates that node with a fresh lifecycle on each + iteration. The loop exits when the router selects the terminal branch: ```typescript --8<-- "examples/typescript/snippets/graphs/routes/loop_escalation.ts:loop-escalation" ``` - !!! warning "Caution: Unbounded graph cycles" - - A graph cycle is not capped by the framework. Make sure the exit - condition always becomes true, or bound the loop yourself with a - [dynamic workflow](/graphs/dynamic/#loop-route), where the - iteration is an ordinary loop you control. - === "Go" The following example uses the graph engine with `workflow.EdgeBuilder`. @@ -555,3 +534,10 @@ lifecycle on each iteration. ```go --8<-- "examples/go/snippets/graphs/routes/main.go:loop-escalate" ``` + +!!! warning "Caution: unbounded graph cycles" + + A graph cycle is not bounded automatically. Make sure the exit condition + eventually becomes true, or express the iteration as a + [dynamic workflow](/graphs/dynamic/#loop-route), where the loop runs in + your own code and you control its bound. diff --git a/examples/typescript/snippets/graphs/dynamic/data_handling.ts b/examples/typescript/snippets/graphs/dynamic/data_handling.ts index e2ecee2e12..bb320fac53 100644 --- a/examples/typescript/snippets/graphs/dynamic/data_handling.ts +++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts @@ -13,8 +13,8 @@ // limitations under the License. // Passing data in a dynamic workflow is simpler than in a graph: `ctx.runNode()` -// hands you the child's result directly, so there are no session-state keys to -// read and write just to move a value one step downstream. +// returns the child's result directly, so there are no session-state keys to +// read and write to move a value one step downstream. // --8<-- [start:data-handling] import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; diff --git a/examples/typescript/snippets/graphs/dynamic/loop_route.ts b/examples/typescript/snippets/graphs/dynamic/loop_route.ts index a3ff6e6717..588aef14d7 100644 --- a/examples/typescript/snippets/graphs/dynamic/loop_route.ts +++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This is where dynamic workflows earn their keep: the iteration is an ordinary -// loop, not a back-edge you have to reason about. Values live in local -// variables; state is written only where an agent's instruction template needs -// to read it back (`{code}`, `{findings}`). +// A dynamic workflow defines the iteration as an ordinary loop rather than a +// back-edge in a graph. Values are held in local variables, and state is +// written only where an agent's instruction template needs to read it back +// (`{code}`, `{findings}`). // --8<-- [start:loop-route] import { LlmAgent, node, NodeContext, Workflow } from '@google/adk'; diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts index 1371c3a765..2dcb144b09 100644 --- a/examples/typescript/snippets/graphs/routes/nested_workflow.ts +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// A `Workflow` is itself a node, so it can be dropped straight into another -// workflow's edges to encapsulate a reusable sub-process. +// A `Workflow` is itself a node, so you can use one inside another workflow's +// edges to encapsulate a reusable sub-process. // --8<-- [start:nested-workflow] import { createEvent, node, NodeContext, Workflow } from '@google/adk'; From 954a193e2f00d855394a08ca6eef0d8dcb5c262e Mon Sep 17 00:00:00 2001 From: Joe Fernandez <931947+joefernandez@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:34:31 -0700 Subject: [PATCH 6/6] Apply suggestion from @joefernandez --- docs/graphs/routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md index 4a19b7ab85..b4186e455e 100644 --- a/docs/graphs/routes.md +++ b/docs/graphs/routes.md @@ -391,7 +391,7 @@ before passing results to the next step. !!! warning "Caution: nodes that feed a JoinNode must produce output" A `JoinNode` releases only after all of its predecessor nodes finish. - Give every node that feeds a join an output of its own, and attach a + Make sure that every node that feeds a join has an output of its own, and attach a retry configuration to any node that can fail. A predecessor that finishes without an output leaves the join with no value for that branch, and the resulting failure appears downstream, away from the node