diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md index ff02684041..5ddadc744f 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 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 can 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 + 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" + ``` + + !!! warning "Caution: emit `output` from one event per execution" + + 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" **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" + + 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" + ``` + === "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" + + 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" + ``` + === "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 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" + ``` + === "Go" **workflow package**: to emit a user-visible message without advancing @@ -239,6 +314,24 @@ 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" + + 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" + ``` + + !!! 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 + 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" State is written with `ctx.Session().State().Set(key, value)` and read @@ -306,6 +399,24 @@ accepted and produced by any agent node. ) ``` +=== "TypeScript" + + Schemas are Zod objects or a genai `Schema`. The location of the schema + determines its effect: + + - 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` mode, which is 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: + + - 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 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" + ``` + === "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..8568d6db84 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,26 @@ 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. 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" + ``` + + When you write an orchestrator node, two details affect how you read + results and how the workflow behaves after a pause: + + - 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`. 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" In Go, `workflow.NewFunctionNode` replaces the `@node` decorator and @@ -125,6 +145,31 @@ 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 + 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" + ``` + + 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 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. + === "Go" In Go, `workflow.NewFunctionNode[IN, OUT]` wraps a plain function as a @@ -196,6 +241,16 @@ execution logic (order and paths) for those nodes. ) ``` +=== "TypeScript" + + 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" + ``` + === "Go" `workflow.NewDynamicNode` creates an orchestrator whose body calls @@ -264,6 +319,20 @@ manually read and write session state keys for data transfer. return report_text ``` +=== "TypeScript" + + 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 way as in a graph. Attach them to the nodes you + run, as shown in the [sequence route](#sequence-route) section. + === "Go" In Go, `workflow.NewAgentNode` wraps an `agent.Agent` so it can be @@ -305,6 +374,15 @@ as you can with graph-based workflows. return report_text ``` +=== "TypeScript" + + 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" + ``` + === "Go" Call `workflow.RunNode` sequentially inside a `NewDynamicNode` body — @@ -366,6 +444,18 @@ workflows offer much more flexibility to define the routing logic you need. return code ``` +=== "TypeScript" + + 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" + ``` + === "Go" In Go, the loop is a plain `for` loop inside the dynamic node body. The @@ -412,6 +502,27 @@ Dynamic workflows in ADK can support parallel execution. only failed or interrupted worker nodes are re-executed, including parallel worker nodes. +=== "TypeScript" + + 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" + ``` + + !!! tip "Tip: prefer the built-in parallel worker" + + 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" In Go, `workflow.NewParallelWorker` wraps a child node and runs it @@ -469,6 +580,25 @@ 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" + + 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" In Go, use `workflow.NewEmittingFunctionNode` with @@ -546,6 +676,16 @@ and logically remain the same for the input. least one non-numeric character to avoid collisions with these auto-generated IDs. +=== "TypeScript" + + 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" + ``` + === "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..ce63964ec3 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`. + 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 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" In ADK Go v2.0.0, a HITL graph node is built with @@ -69,13 +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" +=== "TypeScript" - 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. + The `RequestInput` class takes the following configuration options: + + - **`message`:** Text shown to the user explaining what is being + asked. + - **`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. + + 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 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" @@ -106,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. @@ -149,6 +187,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` 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" + ``` + === "Go" The following code sample shows a three-node graph: a builder node generates @@ -190,6 +238,18 @@ specific tool call. yield RequestInput(message=input_message, response_schema=str) ``` +=== "TypeScript" + + 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" + ``` + === "Go" Set `RequireConfirmation: true` in `functiontool.Config` for a static diff --git a/docs/graphs/index.md b/docs/graphs/index.md index 3211edc40d..c71848bcf6 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. 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" + ``` + === "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, 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" + ``` + === "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..b4186e455e 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`, + 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" + ``` + === "Go" In ADK Go v2.0.0, the primary node type is `workflow.NewFunctionNode`. @@ -136,6 +168,24 @@ A sequential route runs each node once, in the listed order. task_C_node)] # 3 nodes run in order ``` +=== "TypeScript" + + 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 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" + ``` + === "Go" `workflow.Chain(workflow.Start, nodeA, nodeB, nodeC)` wires nodes into a @@ -185,6 +235,19 @@ A sequential route runs each node once, in the listed order. ) ``` +=== "TypeScript" + + 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" + ``` + === "Go" In ADK Go v2.0.0, conditional dispatch uses the `workflow` graph engine. @@ -281,13 +344,15 @@ before passing results to the next step. ] ``` - !!! warning "Caution: Stuck JoinNode from incomplete nodes" +=== "TypeScript" - 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***. + 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" + ``` === "Go" @@ -323,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. + 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 + that caused it. ## Nested workflows @@ -370,6 +436,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 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" + ``` + + **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 +514,16 @@ 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 selects the terminal branch: + + ```typescript + --8<-- "examples/typescript/snippets/graphs/routes/loop_escalation.ts:loop-escalation" + ``` + === "Go" The following example uses the graph engine with `workflow.EdgeBuilder`. @@ -444,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/data-handling/node_output.ts b/examples/typescript/snippets/graphs/data-handling/node_output.ts new file mode 100644 index 0000000000..051fdcb499 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/node_output.ts @@ -0,0 +1,47 @@ +// 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'; + +const returnRawValue = node( + (_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(), + { name: 'return_raw_value' }, +); + +const returnEventOutput = node( + (_ctx: NodeContext, nodeInput: string) => + createEvent({ output: `${nodeInput}!` }), + { name: 'return_event_output' }, +); + +const yieldProgressThenOutput = node( + async function* (_ctx: NodeContext, nodeInput: string) { + yield createEvent({ + content: { role: 'model', parts: [{ text: 'Working on 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..666b6d5106 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/routing_output.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. + +// `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', + 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, + [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..ecd8290636 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/schemas.ts @@ -0,0 +1,122 @@ +// 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, + }, + ], +}); + +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 }, +); + +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, + 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..2398173c7a --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/session_state.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. + +// 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'; + +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); + }, + { name: 'init_state_node' }, +); + +const taskAttemptNode = node( + (ctx: NodeContext) => { + 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..6e9c22b1dc --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/structured_access.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. + +// 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', + 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..8df086d131 --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/structured_output.ts @@ -0,0 +1,47 @@ +// 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 }, +); + +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..a31b4128cc --- /dev/null +++ b/examples/typescript/snippets/graphs/data-handling/user_message.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. + +// 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 }] } }); + +const userMessage = node( + async function* (_ctx: NodeContext, nodeInput: string) { + yield message(`Beginning research process for "${nodeInput}"...`); + }, + { name: 'user_message' }, +); + +const research = node( + async function* (_ctx: NodeContext) { + 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..c3422ec739 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.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. + +// 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) => + 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..bb320fac53 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts @@ -0,0 +1,57 @@ +// 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()` +// 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'; + +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) => { + const rawDraft = await ctx.runNode(draftAgent, userRequest); + + 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..ab87638ac3 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/get_started.ts @@ -0,0 +1,36 @@ +// 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' }); + +const myWorkflow = node( + async (ctx: NodeContext, _nodeInput: string) => { + 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..fe589ce049 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/human_input.ts @@ -0,0 +1,60 @@ +// 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); + + 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..588aef14d7 --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts @@ -0,0 +1,83 @@ +// 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 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'; + +/** 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; + }; + + for (let round = 0; checkResp.findings && round < MAX_FIX_ROUNDS; round++) { + 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..d6090e929c --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/nodes.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. + +// 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'}`; +} + +const helloNode = node(myFunctionNode, { name: 'hello_node' }); + +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' }, +); + +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..86bdff371a --- /dev/null +++ b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts @@ -0,0 +1,63 @@ +// 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); + 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); + + 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..64fe505ed8 --- /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..f6d9ef54a2 --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/get_started.ts @@ -0,0 +1,45 @@ +// 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) => { + 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..c0d4ac7c5f --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts @@ -0,0 +1,66 @@ +// 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' }, +); + +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..42003d7105 --- /dev/null +++ b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts @@ -0,0 +1,95 @@ +// 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(), +}); + +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' }, +); + +const applyFeedback = node( + (_ctx: NodeContext, nodeInput: unknown) => { + 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..8ef8672fe7 --- /dev/null +++ b/examples/typescript/snippets/graphs/index/get_started.ts @@ -0,0 +1,79 @@ +// 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', + instruction: `Output the following line: + It is {CityTime.timeInfo} in {CityTime.city} right now.`, +}); + +function completedMessageFunction(_ctx: NodeContext, nodeInput: string) { + 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, + }), + 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..433ddd67b7 --- /dev/null +++ b/examples/typescript/snippets/graphs/index/process_pipeline.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 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.`, +}); + +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..5233c695c2 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/branches.ts @@ -0,0 +1,66 @@ +// 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' }, +); + +const taskBNode = new LlmAgent({ + 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' }); + +export const rootAgent = new Workflow({ + name: 'routing_workflow', + edges: [ + ['START', taskANode, router], + [ + router, + { + 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..d528fe60d0 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/fan_out_join.ts @@ -0,0 +1,56 @@ +// 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' }); + +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', + 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..489be548f2 --- /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..d8605d54bf --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/loop_escalation.ts @@ -0,0 +1,78 @@ +// 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' }, +); + +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 }], + [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..2dcb144b09 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts @@ -0,0 +1,94 @@ +// 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 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'; + +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' }, +); + +/** + * 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) => titleCase(text), { + name: 'b_title_case', + }), + node((_ctx: NodeContext, text: string) => `[B] ${text}`, { + name: 'b_frame', + }), + ], + ], +}); + +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..4a785d15e8 --- /dev/null +++ b/examples/typescript/snippets/graphs/routes/sequence.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 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' }, +); + +export const rootAgent = new Workflow({ + name: 'sequential_workflow', + edges: [['START', taskANode, taskBNode, taskCNode]], +}); +// --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"] +}