mirror of
https://github.com/google/adk-go.git
synced 2026-10-28 16:44:38 +08:00
* feat(workflow): initial naive agent implementation Besides naive implementation, basic struct and interfaces were added. Includes the core workflow implementation, unit tests, and an example to run it in the web UI. * Adjust api of basic componentes after review (#769) * feat: implement workflow routing support with tagged events and conditional graph traversal (#768) * feat: implement workflow routing support with tagged events and conditional graph traversal * refactor: rename event Route field to Routes and update workflow traversal logic * feat: add MultiRoute generic type to support matching multiple route values (#781) feat: add tool node to workflow agents (#776) * feat: add tool node to worflow agents * update tool node so it's not converting the output to the types, but only validates the schema * check the tool is runnable inside constructor * nit: fix node names in test * feat: implement explicit Default route support in workflow graph traversal (#787) * feat: add EdgeBuilder to simplify workflow graph construction (#795) feat: add NodeConfig to the workflow nodes (#796) * feat: implement node name validation and update workflow construction to return errors (#797) * feat: add start node validation (#807) * feat: introduce scheduler-based engine with goroutine-per-node execution (#803) * workflow: introduce scheduler-based engine with goroutine-per-node execution Squashes the workflow engine WIP work onto the latest wolo/workflows base. The branch had 8 incremental commits; this single commit captures the cumulative state on top of upstream PR #795 (EdgeBuilder), PR #796 (NodeConfig), and PR #797 (node name validation). Architecture: * Goroutine-per-node execution model: each scheduled node runs in its own goroutine, pushing events into a buffered channel. * Single consumer goroutine (runState.run) drains the channel, applies state-side effects, yields events to the caller, and schedules successors when nodes complete. * Replaces the legacy in-process BFS in Workflow.Run with the new scheduler, removing findNextNodes and the inline event loop. New types and constructors: * BaseNode (and NewBaseNode) for shared Name/Description/Config bookkeeping; FunctionNode and toolNode now embed it. * Graph helpers in graph.go: indexed adjacency for O(1) successor lookup. * RunState (persistable) and runState (in-process scheduler bag) in state.go; node lifecycle map (NodeStatus + per-node accumulators). * NodeContext wraps InvocationContext with a per-node TriggeredBy accessor; agent.InvocationContext gains TriggeredBy() returning "" for non-workflow contexts (mocks updated accordingly). * Scheduler (scheduler.go): runNode goroutine wrapper, eventQueue, cancelAll, and the run consumer loop with sibling cancellation and per-node timeout. Routing and validation: * findSuccessors honours unconditional edges, concrete Routes, and the Default fallback. Silent dead-ends remain intentional per adk-python parity. * Workflow.New now returns (*Workflow, error) — picks up the name validation introduced upstream by PR #797. NodeConfig timeout shape: * Timeout is time.Duration (not *time.Duration), with zero meaning "inherit parent context". Matches net.Dialer.Timeout and the http.Server.*Timeout convention; keeps call sites free of pointer boilerplate. * Adds RerunOnResumeOr / WaitForOutputOr accessor helpers for pointer-typed pointer-typed tri-state fields. * Adds TestDefaultRetryConfig from upstream PR #796. Examples and tests: * examples/workflow/basic uses NodeConfig{RetryConfig: DefaultRetryConfig()} to demo the helper. * 7 New(edges) callers updated for the new (*Workflow, error) signature. Tests verified: go build ./..., go vet ./..., go test -race ./workflow/... ./agent/workflowagent/... all pass. Init function node in agent workflows (#810) * feat: implement default route validation to prevent multiple default routes per node (#814) * refactor: implement unconditional cycle detection in workflow validation. (#811) * feat: implement workflow validation to reject duplicate edges and remove redundant edge filtering logic (#808) * feat: implement workflow connectivity validation to ensure all nodes are reachable from start (#809) * workflow: add HITL pause primitives (RequestInput, waiting node) (#829) Adds the engine-side scaffolding for human-in-the-loop pauses: a workflow node can now emit a session.RequestInput, and the scheduler will park the node in NodeWaiting and stop scheduling its successors instead of finalising the run. This is the pause half of HITL only; the resume half (Workflow.Resume, the agent.Agent wrapper, schema validation, handoff vs. re-entry modes) is left for follow-up PRs. Type names and field names are aligned with adk-python's RequestInput in src/google/adk/events/request_input.py: interrupt_id / message / response_schema / payload. API additions: * session.RequestInput carries the prompt: InterruptID (stable correlation key), Message (UI text), ResponseSchema (reserved for the future validator), Payload (opaque UI context). * session.Event.RequestedInput field, parallel to Routes; populated by the node, consumed by the scheduler and forwarded to the UI surface unchanged. * workflow.NewRequestInputEvent(ctx, req) constructor, including UUID auto-generation when InterruptID is empty. * workflow.NodeState.PendingRequest, persisted on the per-node state when the waiting branch fires. * workflow.ErrMultipleInputRequests sentinel for the single-request-per-activation invariant. Scheduler changes: * nodeRun gains an inputRequest field with a setInputRequest method that mirrors the existing setRoutingEvent / setOutput pattern. * handleEvent dispatches on ev.RequestedInput exactly parallel to the existing dispatch on ev.Routes. * handleCompletion gains a waiting branch checked AFTER the error/cancel branches: a clean activation that recorded a request transitions to NodeWaiting, persists the request on NodeState, and skips successor scheduling. Failures take precedence so a node that recorded a request and then errored out lands in NodeFailed, not NodeWaiting. * The scheduler.run loop is unchanged: it terminates when the runsByName map empties, which now happens when every live node has either completed or moved into NodeWaiting. Tests: * TestScheduler_HitlNode_PausesAndForwardsRequest pins the happy-path single-waiting-node behaviour. * TestScheduler_HitlNode_AutoGeneratesInterruptID and TestScheduler_HitlNode_PreservesExplicitInterruptID lock in the InterruptID contract. * TestScheduler_HitlNode_MultipleRequestsFails surfaces ErrMultipleInputRequests at completion. * TestScheduler_HitlNode_ErrorAfterRequestFails pins the fail-over-park precedence in handleCompletion. * TestScheduler_HitlNode_ConcurrentBranches_PausesOnlyWhenAllNonRunning exercises a parallel-branch graph: the non-HITL branch finishes normally while the HITL branch parks; the workflow ends only when both reach a terminal state. All existing workflow tests still pass; the suite is race-free under go test -race. * workflow: add HITL resume API with handoff mode and session-state persistence (#847) Builds on the pause-side primitives in the previous PR by adding the resume half of the human-in-the-loop cycle: Workflow.Resume routes a user-supplied response back into a paused workflow, and the existing workflowagent.New wrapper learns to detect and dispatch resume turns automatically. Run state survives across processes by riding in session.State. Engine additions in workflow/: * Workflow.Resume(ctx, state, responses) iter.Seq2 — for each NodeWaiting whose InterruptID matches an entry in responses, validates the payload against PendingRequest.ResponseSchema (when non-nil), consumes PendingRequest before re-scheduling for idempotency, then routes the response to the asker's successors via findSuccessors so handoff reuses the normal routing / fan-out / fan-in path. ErrInvalidResumeResponse surfaces validation failures and leaves the node parked so the caller can retry. * newSchedulerFromState lets Resume seed a scheduler with a loaded RunState rather than a fresh one. * Workflow.SetName / Workflow.Name expose the persistence-namespacing identifier set by workflowagent.New. * Workflow.Run now persists the post-run state at the end of every invocation so a follow-up Resume can pick it up. * WorkflowInputFunctionCallName constant ('adk_request_workflow_input') and the synthesised FunctionCall part on RequestInputEvent — the same shape tool confirmation uses, lets the runner's generic ID-based dispatch route the user's follow-up FunctionResponse back to this agent without runner-side changes. * RunState / NodeState / RequestInput gain JSON tags. Persistence uses session.State.Get/Set with JSON marshalling; binary payloads must be stashed via agent.Artifacts and referenced by URI in Payload (mirrors how the Python Live API surfaces audio). Persistence helpers (workflow/persistence.go): * LoadRunState / SaveRunState exported so workflowagent (and any custom wrapper) can manage RunState lifecycle without taking a dependency on internal package details. * Both helpers handle nil session and nil session.State as no-ops for tests using minimal mocks. Wrapper changes in agent/workflowagent/: * workflowAgent.run dispatches between Workflow.Run and Workflow.Resume by inspecting ctx.UserContent for a FunctionResponse with the magic name. Stateless: every run state lives in session.State. * detectResume + decodeWorkflowInputResponse handle the dual wire format used by the existing tool-confirmation processor (ADK web wraps payloads as {response: <json string>}; other clients inline {payload: ...}). * MockSession in the existing workflow_test gains a State() method returning nil, supported by the persistence helpers' nil guards. Tests (agent/workflowagent/hitl_test.go, ~430 lines): * TestWorkflowAgent_RunThenResume_Handoff — canonical round-trip; pause on RequestInput, resume with payload, handler receives it as input. * TestWorkflowAgent_Resume_RestoresStateFromSession — fresh agent instance built from the same edges resumes successfully off the same session, verifying cross-instance persistence. * TestWorkflowAgent_Resume_Idempotent — two Resume calls with the same payload run the handler exactly once. * TestWorkflowAgent_Resume_NoMatchingResponse — Resume with an InterruptID that no longer matches a waiting node falls through cleanly without blocking on an empty scheduler. * TestWorkflowAgent_Resume_SchemaValidation_{Pass,Fail} — engine validates responses against ResponseSchema; invalid payloads surface ErrInvalidResumeResponse and leave the node parked, so a retry with a corrected payload still succeeds. * TestWorkflowAgent_Resume_FanOut — handoff into a multi-successor asker delivers the response to every successor. * TestWorkflowAgent_FreshTurn_NotMistakenForResume — a fresh user message that happens to share a session with leftover state from a prior workflow does not get misinterpreted as a resume. All existing workflow and workflowagent tests still pass; the suite is race-free under go test -race. * feat: add HITL re-entry resume mode for workflow agents (#832) Builds on #831 (HITL handoff resume) by adding re-entry mode: when a paused node is configured with `NodeConfig.RerunOnResume = true`, `Workflow.Resume` re-activates the asker itself instead of forwarding the response to its successors. The asker observes the response via `ctx.ResumedInput(interruptID)` and decides what to emit. Direct analogue of adk-python's `@node(rerun_on_resume=True)`. Re-entry preserves the asker's original input, so the node can recompute its decision with the same context it had on the first turn — only the user's response arrives separately. Successors fire only when the re-entry activation produces an output, not on the bare resume call. Support of multiple asks while executing one node will be added in a follow-up PR. - [x] `go build ./...` clean - [x] `go test ./workflow/... ./agent/workflowagent/...` — all pass - [x] `go test -race` clean - [x] 4 new tests covering the canonical re-entry round-trip, original-input preservation across re-entry, the no-successor-before-output invariant, and a regression guard pinning that handoff is still the default mode - [x] 1 new test (`TestNodeContext_ResumedInput`) pinning the per-node context-level contract * feat: remove unused TriggeredBy() interface method (#841) agent.InvocationContext.TriggeredBy() was added in the workflow scheduler PR (#803) as 'engine-supplied metadata available to nodes' but no production caller ever materialised: the method is called only by its own round-trip test and by zero workflow nodes, samples, agents, tools, callbacks, or telemetry sites in the repo. The godoc on the interface method is aspirational; the implementation behind it is a dead branch. This change drops the public API surface (one interface method plus six implementations: agent.invocationContext, internal/context.InvocationContext, and the four MockInvocationContext types in workflowagent, workflow, replayplugin, and llminternal). The unrelated NodeState.TriggeredBy field remains: scheduler.go populates it for resume bookkeeping (workflow.Resume reads it back when re-scheduling a paused node), and it is part of the JSON-serialised RunState so dropping it would break forward compatibility for anyone already running the engine. The TestNewNodeContext_TriggeredByRoundTrip test is removed (it exercised the now-deleted method); TestNodeContext_ResumedInput keeps its coverage of the surviving wrapper functionality. * fix: persist resume inputs across re-entry cycles (#844) * workflow: persist resume inputs across re-entry cycles A re-entry-mode node (NodeConfig.RerunOnResume = true) that yields RequestInput more than once across resume cycles previously lost prior responses on each subsequent resume: ctx.ResumedInput exposed only the response to the most recent InterruptID, and asking the node about an earlier ID returned (nil, false) even though the user had already answered it. The fix accumulates response payloads on NodeState across resume cycles. Each Resume call merges the new {InterruptID: response} entry into ns.ResumedInputs; the scheduler hands the full map to the per-node context on every re-entry activation. The node sees every prior response, not only the most recent one. feat: add retry config implementation (#853) * feat: add generic Output field to session events and persist via storage layer (#863) * feat: workflow - add JoinNode fan-in barrier (#856) Adds a fan-in primitive built on the orchestrator-aggregator model: the scheduler waits until every predecessor of a JoinNode has completed, assembles a map[string]any keyed by predecessor name, and triggers the JoinNode once with that aggregated input. The node itself is a pass-through that emits the input as its output. * feat: implement AgentNode (#840) * fix: align WorkflowInputFunctionCallName with adk-python (#875) The Go workflow runtime synthesises a FunctionCall part on every RequestInput event so the generic FunctionResponse-by-ID dispatch can route the user's follow-up reply back to the agent that issued the request. The synthesised call's Name was 'adk_request_workflow_input'. adk-python uses 'adk_request_input' for the equivalent constant (REQUEST_INPUT_FUNCTION_CALL_NAME in google/adk/workflow/utils/_workflow_hitl_utils.py). The divergence broke cross-runtime HITL workflows: a session recorded by Python (with function_call.name='adk_request_input' in the interrupt event) could not be replayed in Go, because Go-side conformance compare would see 'adk_request_workflow_input' and flag the mismatch. The reverse direction is broken too — a function_response addressed by name to 'adk_request_input' from a Python-authored spec.yaml could not be routed to a Go workflow agent's pending interrupt. This change renames the constant value to 'adk_request_input'. The exported symbol WorkflowInputFunctionCallName is unchanged, so no Go consumer is affected. The only behaviour difference is the literal value placed on the wire, which now matches Python. Discovered while preparing the first cross-language conformance test for graph-based Workflow + HITL. * refactor: replace StateDelta output tracking with direct Event.Output field usage (#872) * feat: add HITL console launcher support for workflow input prompts + sample (#845) * console: add HITL support for workflow input prompts Adds engine-agnostic HITL handling to the console launcher: detect interrupts emitted on the previous turn, render a prompt for the operator, and forward the typed reply as a FunctionResponse on the next turn. Detection is uniform across interrupt kinds: * collectPendingInterrupts walks the events yielded during a turn and returns one pendingInterrupt per FunctionCall part whose ID appears in Event.LongRunningToolIDs. The call's Name is only used for rendering and response shaping, never for detection. Workflow RequestInput and any future long-running call kind flow through the same path. Per-name dispatch (renderInterruptPrompt / buildInterruptResponse): * workflow.WorkflowInputFunctionCallName: prints '[HITL input]' with the message; pretty-prints the payload (the proposal / context attached by the asker node) and the JSON schema if either is present. Operator's reply is JSON-parsed first so structured replies (objects, arrays, scalars) round-trip as typed values; falls back to raw text. Wrapped under 'payload', the conventional key for workflow input responses. * Anything else: prints a generic banner and wraps the raw input as {result: <text>}. Lets e.g. a future adk_request_credential path be answered through this launcher without a code change here, even before its dedicated renderer exists. Tool confirmation (toolconfirmation.FunctionCallName) currently hits this generic fallback and works at the transport layer (the reply still routes back to the tool by FunctionCall.ID), but the {result: <text>} envelope does not match what ctx.ToolConfirmation() expects to read. A follow-up adds a dedicated renderer and yes/no parser. Main loop integration (console.go): * After every r.Run iteration drains, scan collectedEvents for pending interrupts. If any, render the head's prompt and skip the normal '\nUser -> ' banner. * The next stdin line is interpreted as the answer to that head; successive lines drain the rest. Once every interrupt has an answer, the assembled FunctionResponse parts are sent in one *genai.Content as the next 'turn' through the same r.Run path. The reply routes back to the agent by FunctionCall.ID. * Multiple parallel pauses (rare but legal — e.g. two parallel workflow branches both yielding RequestInput on the same turn) are collected together and answered one prompt at a time, then submitted as one Content with multiple FunctionResponse parts. Renamed the local 'session' variable to 'sess' inside Run to avoid shadowing the session package import once the new code path needs []*session.Event. Race-free under go test -race. * examples/workflow/hitl_simple: minimal HITL sample for console launcher verification A no-LLM, no-API-key sample that exercises the console launcher's pause/resume support end-to-end. Two workflow nodes: Start → ask_name → greet ask_name yields a RequestInput so the launcher renders the prompt. greet receives the user's reply as plain text and emits a greeting that the launcher prints. Useful for verifying that wolo/workflow_hitl_console produces the expected console output without any LLM streaming in the mix. Run with: go run ./examples/workflow/hitl_simple/ console User -> hello Agent -> What's your name? User -> Alice Agent -> Hello, Alice! * feat: add llmagent mode api (#900) * feat: add llmagent mode api feat: add new parallel worker node type to the adk workflows so it could process multiple inputs simultaneously (#862) * feat(telemetry): add functional test infrastructure and agent telemetry tests (#894) Adds shared infrastructure for telemetry functional tests: - internal/telemetry/functionaltest: end-to-end functional test package for asserting emitted span+log trees. - internal/telemetry/telemetrytest: helpers for in-memory log exporter, span/log digesting, and scenario runners. - internal/telemetry/telemetrytestcase: reusable expected-output fixtures keyed per scenario. Adds an end-to-end functional test for the canonical "llmagent with one FunctionTool" scenario, parametrized over OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT to cover both the elided (default) and full-content variants. Adds test hooks (OverrideTracerForTesting, OverrideLoggerForTesting) to internal/telemetry so functional tests can install in-memory exporters hermetically. Removes the WithGenAICaptureMessageContent option which aliased the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var toggle. The option introduced unnecessary global state which made hermetic testing difficult; getGenAICaptureMessageContent now reads the env var directly so tests can flip behaviour via t.Setenv. * feat: add FinishTaskTool (#901) * feat(workflow): publish NodeContext + add error sentinels and sub-scheduler skeleton (#889) Adds the foundation the dynamic-workflows track builds on: a public NodeContext interface with composite-path support, typed errors, and a per-activation sub-scheduler that runs one child and classifies the outcome. workflow.NodeContext is now an exported interface extending agent.InvocationContext, mirroring the interface-not-struct shape of agent.CallbackContext and the forthcoming agent.ToolContext so future context-unification can swap the backing type without breaking users. dynamicSubScheduler runs one child per call, forwards its events upstream via an emitUp callback, derives a composite child path, and classifies the outcome (HITL → ErrNodeInterrupted, runtime failure → ErrNodeFailed). Child run ids come from a per-(parent activation, child name) auto-counter protected by a mutex for concurrent errgroup-style use; user-supplied custom ids are rejected when empty, purely numeric, or containing the composite-path separators / and @. Precedence matches adk-python: a child that fails after emitting RequestedInput surfaces as ErrNodeFailed, not ErrNodeInterrupted. Resume/replay-skip, parent NodeWaiting persistence, parallel HITL detection, and the public NewDynamicNode / RunNode API stay deferred to follow-up PRs. * feat(workflow): add dynamic-node orchestration (#896) Wires the user-facing API for dynamic workflows on top of the sub-scheduler skeleton from the previous PR. A dynamic node's execution order is expressed as Go code (loops, branches, goroutines) that calls other nodes inline via RunNode, branches on their typed output, and pauses for HITL input. The public surface: workflow.NewDynamicNode[IN, OUT](name, fn, cfg) — orchestrator constructor; cfg.RerunOnResume defaults to &true (an explicit &false is respected). workflow.RunNode[OUT](ctx, child, input, opts...) — generic helper for scheduling a child. Returns its typed output, or errors.Is-matchable ErrNodeInterrupted / ErrNodeFailed. workflow.WithRunID(id) — option overriding the auto-counter with a stable id (rejected if empty, purely numeric, or containing / or @). session.NodeInfo — substruct on Event carrying the emitting node's composite path; shape mirrors adk-python's event.nodeInfo. The scheduler's handleEvent scopes per-activation Output/Routes invariants by NodeInfo.Path, so a dynamic node forwarding a child's terminal output plus its own no longer trips ErrMultipleOutputs. Descendant RequestedInput events are promoted onto the parent's accumulator so Workflow.Resume matches the InterruptID against the parent's NodeState.PendingRequest — enabling HITL inside a dynamic orchestrator. * examples: minimal dynamic-workflow sample (#897) * examples/workflow/dynamic/basic: minimal dynamic-workflow sample Runnable mirror of the "Get started" snippet from https://adk.dev/graphs/dynamic/: a parent dynamic node orchestrates a single FunctionNode child via workflow.RunNode and emits its output upstream. ~50 lines of actual logic, no API keys required. Sits alongside examples/workflow/basic/ and uses the same launcher integration so users can `go run` it and exercise the workflow through the standard CLI/UI. Stacked on the NewDynamicNode + RunNode public-API PR; intentionally minimal so reviewers can validate the API shape against the canonical Python example. Richer samples (loop, parallel, HITL) follow once the corresponding Go features (resume, parallel HITL detection) land. * examples/workflow/dynamic/llm: add LlmAgent-backed dynamic-workflow sample Smallest sensible composition that puts an LLM into a dynamic workflow: one llmagent.New (gemini-3.1-flash-lite, one-line greeting instruction) wrapped via workflow.NewAgentNode and invoked from a NewDynamicNode body through workflow.RunNode. Mirrors the existing examples/workflow/dynamic sample but replaces the trivial FunctionNode child with a real LlmAgent, demonstrating the agent->node->dynamic composition path. Verified end-to-end with 'echo hi | go run ./examples/workflow/dynamic/llm console': greeter responds with a one-sentence greeting via the deployed Gemini endpoint. * examples/workflow/dynamic/hitl: add HITL dynamic-workflow sample Smallest sensible composition that pauses for human input from inside a dynamic-node orchestrator: an inline askName node yields workflow.NewRequestInputEvent; the orchestrator drives it via workflow.RunNode, swallows ErrNodeInterrupted on the pause activation, and on resume re-entry reads the reply via NodeContext.ResumedInput (RerunOnResume defaults to &true for dynamic nodes). A mid-body emit publishes the greeting as Content so the console launcher renders it; the terminal Output carries the same string for downstream nodes. * feat: extend Node interface with JSON schema support and input validation methods (#895) * workflow: experimental NodeContext bridge for runnable tools (#907) Adds an opaque go-context value stash, populated by the scheduler at each per-node activation, that lets tools running inside an LlmAgent (which is itself running as a workflow node) recover the surrounding NodeContext via context.Value lookup. This is a temporary bridge to unblock NewSingleTurnTool (and any future runnable tool that needs to schedule sub-nodes) without modifying the public tool.Context interface. The longer-term solution is the CallbackContext / ToolContext unification tracked in workflow/node_context.go. Mechanism: tool.Context embeds context.Context (transitively via agent.CallbackContext -> agent.ReadonlyContext), so the value survives every downstream NewInvocationContext / WithContext call on the path scheduler -> AgentNode.Run -> LlmAgent.run -> Flow.handleFunctionCalls -> NewToolContext. No interface changes, no agent_node modifications. * feat(workflow): branch isolation across static, parallel, and dynamic schedulers (#906) Adds branch derivation across the three workflow schedulers so the LLM flow's branch-prefix history filter (already present in contents_processor.go) actually scopes events per parallel branch instead of seeing every node run on the empty root branch. Before this PR, Event.Branch and InvocationContext.Branch() existed and the filter respected them, but no scheduler ever derived a non-empty branch — so an LlmAgent wrapped by ParallelWorker (or by a hand-written errgroup fan-out) saw every sibling worker's events in its prompt history. The static scheduler now derives <successor>@1 sub-branches at fan-out, computes the longest common dot-prefix of predecessor branches for JoinNode, stamps Event.Branch when the node leaves it empty, and persists each activation's branch on NodeState for resume. The ParallelWorker replaces its single shared workerCtx with per-iteration sub-contexts derived as <parent>.<wrapped>@<i+1>. The dynamic sub-scheduler gains two opt-in RunNode options — WithUseSubBranch() and WithOverrideBranch(base) — for dynamic-node bodies that want to isolate child activations. * fix(examples): remove unused telemetry import in multipletools (#909) PR #894 (feat(telemetry): add functional test infrastructure) replaced the TelemetryOptions config in examples/tools/multipletools with an os.Setenv call but left the telemetry import behind. This broke 'go build ./...' on v2 with: examples/tools/multipletools/main.go:31:2: "google.golang.org/adk/telemetry" imported and not used Removes the import to unblock the v2 CI. * feat: add SingleTurnTool (#914) * feat: add SingleTurnTool * upd * feat: add TaskAgentTool (#915) feat: add the workflow node to allow the nested workflows (#910) * feat(telemetry): Instrument nodes and workflows (#891) Enables emission of the following spans: - invoke_workflow <workflow_name> - invoke_node <node_name> Additionally: - Adds a functional test for graph-based telemetry emission (workflow chain scenario). * feat(workflow): add WithMaxConcurrency option to cap graph-scheduled node parallelism (#917) Add a workflow-level concurrency cap that limits how many nodes may run concurrently within a single Workflow invocation. When the cap is reached, additional ready-to-run nodes enter NodePending and queue in FIFO order; the scheduler drains the queue as in-flight nodes complete. API: workflow.New(name, edges, workflow.WithMaxConcurrency(n)) - n > 0: cap is enforced - n == 0 (default): unlimited (no behavioural change for existing callers) - n < 0: clamped to 0 (unlimited) The cap applies to all top-level static scheduling paths: initial Start dispatch, fan-out from completed nodes, retry scheduling, and Resume. It explicitly does NOT apply to dynamic sub-nodes invoked via workflow.RunNode from inside a DynamicNode body — gating them would deadlock the parent that awaits the child inline. Same exclusion as adk-python (see _workflow.py:164 comment). * feat: add JSON schema validation support to BaseNode and update constructors accordingly (#911) Implements JSON schema support and default input validation for BaseNode and integrates them across all workflow node types. * feat(workflow): AgentNode synthesizes Event.Output from LLM text (#925) AgentNode now sets Event.Output from concatenated model text on final agent responses, so RunNode(agentNode, ...) returns the agent's reply instead of the zero value. Without this, dynamic workflows that wrap an LlmAgent via NewAgentNode and call RunNode[string] received "" and could not chain on the agent's output. Mirrors adk-python's process_llm_agent_output (workflow/_llm_agent_wrapper.py:251-279) minus the output_schema and output_key side-effects, which have no equivalent here yet. Partial streaming events are not promoted; thoughts are excluded from the concatenation; the synthesis is skipped when Output is already set so callers retain control. * feat(workflow): idempotent RunNode dispatch via WithRunID cache (#921) dynamicSubScheduler gains an in-memory cache keyed by the resolved childPath ("<parentPath>/<name>@<runID>"). A repeated RunNode call with the same WithRunID inside one parent activation returns the cached output without re-running the child, enabling idempotent dispatch for replayed or reorderable children. Failures and HITL interrupts are not cached. Cross-resume idempotency (rehydrating the cache from session events on parent re-run) is out of scope here; a follow-up CL will replicate adk-python's DynamicNodeScheduler._rehydrate_from_events via an OutputFor event annotation. * fix(typeutil): decode validation input into `any` to accept non-object JSON values (#933) ConvertToWithJSONSchema previously decoded the marshalled value into map[string]any before validating against the resolved schema. That worked for object-shaped inputs but failed for scalars, arrays, and booleans with: json: cannot unmarshal string into Go value of type map[string]interface {} * docs: add single_turn and task sub agent examples (#934) * fix(workflow): repair v2 build after rebase onto main (#942) * fix(workflow): repair v2 build after rebase onto main context unification Rebasing v2 onto main pulled in the context-unification change (#935) which removed internal/toolinternal.NewToolContext and added ResumedInput to the agent.InvocationContext interface. Three call sites in v2 workflow code broke as a result: - workflow/tool_node.go: switch toolinternal.NewToolContext to the public agent.NewToolContext (drops the internal import). - internal/workflowinternal/single_turn_tool_test.go: same swap. - recordplugin/record_plugin_test.go: add the now-required ResumedInput stub to MockInvocationContext (mirrors the existing replayplugin mock). go build ./..., go vet ./..., and the affected package tests pass. Note: plugin/TestCallTool fails on this branch but is a pre-existing v2 failure (reproduces on clean origin/v2, passes on main) unrelated to the rebase. * style(workflow): apply gofmt/goimports formatting Pure formatting cleanup surfaced by the linter after the rebase: regroup third-party imports (separate genai/runconfig from adk imports with a blank line) and remove gofmt-flagged field-alignment padding in AgentNode/FunctionNode struct literals. No functional changes. * fix: resolve golangci-lint findings Address the four stable linter findings surfaced by golangci-lint: - cmd/launcher/console/hitl_test.go: explicitly ignore w.Close() error in the stdout-capture test helper (errcheck). - examples/tools/multipletools/main.go: check os.Setenv error and log.Fatalf on failure (errcheck). - workflow/agent_node_test.go: drop the unused ErrorOutput type (unused). - workflow/parallel_worker_test.go: replace single-case select with a plain channel receive (staticcheck S1000). go build ./..., go vet ./..., golangci-lint run, and affected package tests all pass. * fix(typeutil): treat null input as empty object for object schemas #933 switched ConvertToWithJSONSchema to decode the validation input into `any` so scalar/array/bool inputs are accepted. That regressed a nil/absent input (e.g. a tool invoked with no arguments): a nil map marshals to JSON `null`, which an object schema rejects with `has type "null", want "object"`. The previous map[string]any decoding had validated a nil map as an empty object. Restore that behaviour narrowly: when the decoded value is nil and the resolved schema's root type is (or includes) "object", validate an empty object instead. Scalar/array/bool broadening from #933 is preserved; genuine type mismatches and null-against-non-object schemas still fail. Fixes plugin/TestCallTool (tool called with no args) which failed on v2 since #933. Adds typeutil unit tests covering the regression, the preserved scalar/array behaviour, and the negative cases. * refactor: create runner.getOrCreateSession method (#944) * Context unification: merge interfaces ToolContext and CallbackContext into Context. Introduce aliases (#945) * Created wrapper for logging nonsensical calls to ToolContext related methods for CallbackContexts * Renamed callbackContext to commonContext, Added aliases for CallbackContext and ToolContext * Fixes * Added tests for callback_wrapper * Linter fixes * Added README for v2 * Fixes for README-v2 * Added comments * Replaced deprecated tool.Context with agent.ToolContext (#951) Replaced deprecated tool.Context with agent.ToolContext * feat(workflow): add ValidateOutput method to Node interface (#929) Completes the symmetric input/output validation contract on the Node interface, alongside the existing ValidateInput. The scheduler is expected to invoke ValidateOutput on every yielded event whose output is non-nil before forwarding the event to the consumer (wired up in a follow-up). Interface and conformance - Add ValidateOutput(output any) (any, error) to the Node interface. - Add explicit stubs on the two implementations that do not embed BaseNode: startNode and the test-only dummyNode. - Extend the compile-time Node-conformance assertions in base_node_test.go to cover AgentNode, ToolNode, JoinNode, ParallelWorker, and WorkflowNode. Default implementation on BaseNode - BaseNode.ValidateOutput delegates to a shared defaultValidateOutput helper that validates the output against the node's outputSchema field (added in #911) when set, otherwise returns the output unchanged. - The default deliberately performs no type coercion or Content/JSON fallback handling; ToolNode will override ValidateOutput to add its FunctionTool {"result": X} unwrap fallback in a follow-up. * feat(console): add HITL support for tool confirmation prompts (#852) Extends the console launcher's HITL prompt dispatch to handle tool confirmation interrupts (toolconfirmation.FunctionCallName) alongside the workflow input path added in the previous commit. Detection path is unchanged — collectPendingInterrupts already walks events name-agnostically via Event.LongRunningToolIDs. This commit only adds a per-name case to the render and response switches: * renderToolConfirmationPrompt prints the confirmation hint after the standard "Agent -> " banner, or "Confirm <name>?" derived from the original function call as fallback. * toolConfirmationResponseFromUserInput maps yes/y/true/confirm (case-insensitive) to {"confirmed": true}, everything else (including blank lines) to {"confirmed": false}. Without this commit tool confirmation hits the generic fallback which wraps the reply as {"result": <text>} — the transport works (reply routes back by FunctionCall.ID) but the envelope does not match what ctx.ToolConfirmation() expects, so the confirmation is effectively unparseable. * feat(runner): HITL via long-running interrupts with history rehydra… (#960) * feat(workflow): HITL via long-running interrupts with history rehydration Workflow-engine support for human-in-the-loop, unified on a single mechanism — history rehydration — matching adk-python (no persisted run-state event, no PendingRequest field). - scheduler: per-event back-pressure handshake (a non-partial function-response is persisted before the node's flow rebuilds the next model request, fixing a non-deterministic re-issue race); pause a node on accumulated Event.LongRunningToolIDs (RequestInput rides on them); stamp NodeInfo.Path = node name on static node events so rehydration can attribute interrupts (dynamic children fold into their static ancestor). - persistence: ReconstructRunState ports adk-python's _reconstruct_node_states + _infer_node_state — per-node scan (interrupts, resolved user responses, schemas, output), status inference (WAITING / PENDING+ResumedInputs re-entry / COMPLETED+Output handoff), backward-edge predecessor input, and schema validation on the surviving (last-wins) response. - resume: single path over the rehydrated state, gated on the current turn's responses for idempotency; already-run handoff successors are skipped (RunState.completed). - state: NodeState.Interrupts + unexported interruptSchemas; RunState.completed; HasWaiting. No PendingRequest, no persisted run-state blob. - workflowagent: detectResume uses ReconstructRunState and surfaces reconstruction (schema-validation) errors. A node may raise multiple interrupts per activation. workflow and workflowagent suites pass with -race. * fix(session): persist workflow event fields (NodeInfo, RequestedInput, Routes) AppendEvent (in-memory) and the database storage layer dropped Event's workflow fields when persisting: the in-memory copy omitted NodeInfo, RequestedInput and Routes, and the database layer never serialized NodeInfo or RequestedInput. History-based resume attributes interrupts by NodeInfo.Path, so losing it broke HITL resume — a RequestInput workflow (e.g. examples/workflow/hitl_simple) would re-prompt instead of continuing after the reply. Persist all three fields in both backends and add round-trip regression tests for each. * feat(workflow): dynamic-node resume dedup + fix terminal-asker resume Two resume-correctness fixes for dynamic orchestrators and HITL. 1. Cross-resume dedup. A dynamic node body re-runs from the top on resume, so every RunNode before the pause point would re-execute its child. rehydrateCache rebuilds the sub-scheduler's resultByPath from session events (child terminal events carry NodeInfo.Path + Output), so completed children with a stable WithRunID are served from cache. Mirrors adk-python's _rehydrate_from_events / DynamicNodeScheduler. 2. Terminal handoff asker now resumes. Resume only bumped its scheduled counter per scheduled successor, so a single-asker workflow (no successors) wrongly returned ErrNothingToResume. A matched handoff asker now counts as an effective resume itself, gated on answeredThisTurn (from a per-interrupt resolvedCount during rehydration) so a duplicate resume stays an idempotent no-op. * feat(runner): drive LlmAgent through the ADK 2.0 node runtime with HITL (#961) * feat(runner): run LlmAgent through the node runtime with HITL support Detect an LlmAgent root and drive it through the workflow node runtime (the Go equivalent of adk-python's Runner._run_node_async, which is reached for an LlmAgent). Detection and wrapping are automatic and require no user configuration: after findAgentToRun, if the resolved agent is an LlmAgent it is wrapped in a node and run as a single-node workflow (START -> node) named "<AppName>/<agent name>". HITL bridge: adk-go pauses the workflow scheduler only on RequestedInput, whereas an LlmAgent's own HITL (long-running tools / tool confirmation) emits LongRunningToolIDs. The wrapping node bridges the two: when the agent emits a long-running call this turn did not answer, it yields a content-free pause event that sets only RequestedInput (keyed by the real long-running call ID), so the scheduler pauses and persists RunState without injecting a synthetic adk_request_input FunctionCall into the model conversation (which a real model rejects on resume). Also add WithYieldUserMessage() to optionally yield the user message event before node events (parity with adk-python yield_user_message); appendMessageToSession now returns the appended event to support this. * fix(console): dedup pending HITL interrupts across SSE partial events (#959) In SSE streaming mode the same long-running function-call event is emitted multiple times (partial chunks plus the final aggregated event), each carrying the same LongRunningToolIDs. collectPendingInterrupts queued one prompt per duplicate, so the user's reply was consumed against a phantom interrupt instead of resuming the run. Skip partial events and dedup by call ID so each interrupt surfaces exactly once, from the final aggregated event. * feat(workflow): RunNode WithUseAsOutput for child -> parent output delegation (#920) What RunNode gains WithUseAsOutput(), which promotes a dynamic child's output to the parent dynamic node's terminal output. The value returned by the orchestrator body is discarded in favour of the delegated child's. At most one delegating child per parent activation is permitted; a second attempt fails with ErrOutputAlreadyDelegated. When to use Use it for thin-dispatcher orchestrators: the parent picks one child (e.g. by routing logic) and that child's output — including its streamed tokens, when the child is an LlmAgent — is the orchestrator's output. Without delegation the parent would have to wait for the child to finish and re-emit a single string, collapsing streaming. * samples(workflow): routing samples (#979) Examples for routing in workflows * feat: introduce robust JSON-aware input validation in base node for string-encoded types (#946) * feat: introduce robust JSON-aware input validation in base node for string-encoded types * Merge remote-tracking branch 'upstream/v2' into validate_input * feat: setup LLMAgent mode in the runner before run (#983) Depending on the LLMAgent mode the agent which starts invocation should be different. Following python ref: python ref: https://github.com/google/adk-python/blob/62bcdd343c5ea12583f98fa479c265f3b108a50e/src/google/adk/runners.py#L990-L1017 * feat: implement input validation in scheduler and exclude ErrInputValidation from retries (#956) * feat: introduce robust JSON-aware input validation in base node for string-encoded types * feat: implement input validation in scheduler and exclude ErrInputValidation from retries * feat(llm): isolation_scope to filter agent prompt history by scope (#987) Add Event.IsolationScope plus an InvocationContext.IsolationScope() accessor and an exact-match filter in the content builder: an agent includes a session event in its LLM prompt history only when the event's scope equals the agent's scope (empty sees only empty). Mirrors adk-python's isolation_scope, including the include_contents="none" pivot gate so an out-of-scope event cannot start the agent's turn. This is the standalone filter mechanism; scope stamping on append and task-input reconstruction belong to the task-delegation layer that will consume it. * feat(workflow): derive node output from message via NodeInfo.MessageAsOutput (#966) Problem A workflow node's output normally lives in Event.Output. But an LlmAgent node in chat mode emits its answer as message content with no separate Output value — so anything consuming that node's output (a successor on a handoff, or a WithUseAsOutput delegation) would see no output. adk-python solves this with node_info.message_as_output; adk-go had no equivalent. Solution Add NodeInfo.MessageAsOutput. When set and Event.Output is nil, readers derive the node's output from the event's model text. This mirrors adk-python's _track_event_in_context (explicit Output wins; message text is the fallback). * feat(workflow): NodeInfo.OutputFor for delegation-chain output attribution (#969) Add NodeInfo.OutputFor: the node paths an event's Output counts for — the emitting node plus any WithUseAsOutput delegating ancestors. A delegating child emits a single event stamped OutputFor=[child, parent, ...] that flows up the whole chain, so the parent no longer re-emits a duplicate terminal output event (full suppression, matching adk-python's _output_delegated+ output_for). On resume, collectNodeOutputs attributes that one event's output to every static node named in OutputFor, and every output event records its own path as a minimum (mirroring adk-python _enrich_event). * feat(workflow): stamp isolation_scope on delegated node events (#988) Problem PR #987 added the isolation_scope filter (an agent's LLM history is restricted to events whose scope matches its own), but nothing set or stamped a scope — so the filter never engaged. In particular a task-mode LlmAgent run as a node had no way to isolate its multi-turn conversation from peer workflow nodes. Solution Add the producer side. RunNode gains WithIsolationScope (explicit value) and WithIsolationScopeFromNodePath (scope = the child's full node path, the task-mode case). The dynamic scheduler resolves the scope (explicit > node path > inherited), sets it on the child context, and stamps it onto every event the child emits; the agent wrapper stamps the direct (non-delegated) path. Using the full node path keeps scopes unique across nested workflows and reused node names. Mirrors adk-python _compute_isolation_scope_for_node and NodeRunner._enrich_event. Wiring the task-delegation dispatch to pass WithIsolationScopeFromNodePath is left as a TODO on TaskAgentTool. * feat: add ValidateInput override to JoinNode to validate individual predecessor outputs (#985) * refactor: Migrate existing nodes (Function/Tool/start + test nodes) to new Node interface (#984) * feat: add singleTurnNudge in contents processor (#989) Scoped agents (task/single_turn) are invoked by task-delegation FC. This commit handles proper content construction for such agents: making FC args visible to the agent + adding SingleTurnNudge text part for single_turn agent. * feat: reject task mode llmagents as static graph nodes (#995) * feat: add NewFunctionNodeFromState to support binding node inputs and state fields via struct parameters (#990) * feat: exclude task&single_turn from direct transfer (#998) task and single_turn agents are reached via FC delegation (the wrapper sniffs TaskAgentTool / SingleTurnTool function calls), not via transfer_to_agent * feat: add tool deferred response (#997) feat(workflow): Node-to-Node Data Flows (#943) feat(workflow): Support configurable workflows (#936) * feat(workflow): initial naive agent implementation Besides naive implementation, basic struct and interfaces were added. Includes the core workflow implementation, unit tests, and an example to run it in the web UI. * Adjust api of basic componentes after review (#769) * feat: implement workflow routing support with tagged events and conditional graph traversal (#768) * feat: implement workflow routing support with tagged events and conditional graph traversal * refactor: rename event Route field to Routes and update workflow traversal logic * feat: add MultiRoute generic type to support matching multiple route values (#781) * feat: add tool node to workflow agents (#776) * feat: add tool node to worflow agents * update tool node so it's not converting the output to the types, but only validates the schema * check the tool is runnable inside constructor * nit: fix node names in test * feat: implement explicit Default route support in workflow graph traversal (#787) * feat: add EdgeBuilder to simplify workflow graph construction (#795) * feat: add NodeConfig to the workflow nodes (#796) * feat: implement node name validation and update workflow construction to return errors (#797) * feat: add start node validation (#807) * feat: introduce scheduler-based engine with goroutine-per-node execution (#803) * workflow: introduce scheduler-based engine with goroutine-per-node execution Squashes the workflow engine WIP work onto the latest wolo/workflows base. The branch had 8 incremental commits; this single commit captures the cumulative state on top of upstream PR #795 (EdgeBuilder), PR #796 (NodeConfig), and PR #797 (node name validation). Architecture: * Goroutine-per-node execution model: each scheduled node runs in its own goroutine, pushing events into a buffered channel. * Single consumer goroutine (runState.run) drains the channel, applies state-side effects, yields events to the caller, and schedules successors when nodes complete. * Replaces the legacy in-process BFS in Workflow.Run with the new scheduler, removing findNextNodes and the inline event loop. New types and constructors: * BaseNode (and NewBaseNode) for shared Name/Description/Config bookkeeping; FunctionNode and toolNode now embed it. * Graph helpers in graph.go: indexed adjacency for O(1) successor lookup. * RunState (persistable) and runState (in-process scheduler bag) in state.go; node lifecycle map (NodeStatus + per-node accumulators). * NodeContext wraps InvocationContext with a per-node TriggeredBy accessor; agent.InvocationContext gains TriggeredBy() returning "" for non-workflow contexts (mocks updated accordingly). * Scheduler (scheduler.go): runNode goroutine wrapper, eventQueue, cancelAll, and the run consumer loop with sibling cancellation and per-node timeout. Routing and validation: * findSuccessors honours unconditional edges, concrete Routes, and the Default fallback. Silent dead-ends remain intentional per adk-python parity. * Workflow.New now returns (*Workflow, error) — picks up the name validation introduced upstream by PR #797. NodeConfig timeout shape: * Timeout is time.Duration (not *time.Duration), with zero meaning "inherit parent context". Matches net.Dialer.Timeout and the http.Server.*Timeout convention; keeps call sites free of pointer boilerplate. * Adds RerunOnResumeOr / WaitForOutputOr accessor helpers for pointer-typed pointer-typed tri-state fields. * Adds TestDefaultRetryConfig from upstream PR #796. Examples and tests: * examples/workflow/basic uses NodeConfig{RetryConfig: DefaultRetryConfig()} to demo the helper. * 7 New(edges) callers updated for the new (*Workflow, error) signature. Tests verified: go build ./..., go vet ./..., go test -race ./workflow/... ./agent/workflowagent/... all pass. * Init function node in agent workflows (#810) * feat: implement default route validation to prevent multiple default routes per node (#814) * refactor: implement unconditional cycle detection in workflow validation. (#811) * feat: implement workflow validation to reject duplicate edges and remove redundant edge filtering logic (#808) * feat: implement workflow connectivity validation to ensure all nodes are reachable from start (#809) * workflow: add HITL pause primitives (RequestInput, waiting node) (#829) Adds the engine-side scaffolding for human-in-the-loop pauses: a workflow node can now emit a session.RequestInput, and the scheduler will park the node in NodeWaiting and stop scheduling its successors instead of finalising the run. This is the pause half of HITL only; the resume half (Workflow.Resume, the agent.Agent wrapper, schema validation, handoff vs. re-entry modes) is left for follow-up PRs. Type names and field names are aligned with adk-python's RequestInput in src/google/adk/events/request_input.py: interrupt_id / message / response_schema / payload. API additions: * session.RequestInput carries the prompt: InterruptID (stable correlation key), Message (UI text), ResponseSchema (reserved for the future validator), Payload (opaque UI context). * session.Event.RequestedInput field, parallel to Routes; populated by the node, consumed by the scheduler and forwarded to the UI surface unchanged. * workflow.NewRequestInputEvent(ctx, req) constructor, including UUID auto-generation when InterruptID is empty. * workflow.NodeState.PendingRequest, persisted on the per-node state when the waiting branch fires. * workflow.ErrMultipleInputRequests sentinel for the single-request-per-activation invariant. Scheduler changes: * nodeRun gains an inputRequest field with a setInputRequest method that mirrors the existing setRoutingEvent / setOutput pattern. * handleEvent dispatches on ev.RequestedInput exactly parallel to the existing dispatch on ev.Routes. * handleCompletion gains a waiting branch checked AFTER the error/cancel branches: a clean activation that recorded a request transitions to NodeWaiting, persists the request on NodeState, and skips successor scheduling. Failures take precedence so a node that recorded a request and then errored out lands in NodeFailed, not NodeWaiting. * The scheduler.run loop is unchanged: it terminates when the runsByName map empties, which now happens when every live node has either completed or moved into NodeWaiting. Tests: * TestScheduler_HitlNode_PausesAndForwardsRequest pins the happy-path single-waiting-node behaviour. * TestScheduler_HitlNode_AutoGeneratesInterruptID and TestScheduler_HitlNode_PreservesExplicitInterruptID lock in the InterruptID contract. * TestScheduler_HitlNode_MultipleRequestsFails surfaces ErrMultipleInputRequests at completion. * TestScheduler_HitlNode_ErrorAfterRequestFails pins the fail-over-park precedence in handleCompletion. * TestScheduler_HitlNode_ConcurrentBranches_PausesOnlyWhenAllNonRunning exercises a parallel-branch graph: the non-HITL branch finishes normally while the HITL branch parks; the workflow ends only when both reach a terminal state. All existing workflow tests still pass; the suite is race-free under go test -race. * workflow: add HITL resume API with handoff mode and session-state persistence (#847) Builds on the pause-side primitives in the previous PR by adding the resume half of the human-in-the-loop cycle: Workflow.Resume routes a user-supplied response back into a paused workflow, and the existing workflowagent.New wrapper learns to detect and dispatch resume turns automatically. Run state survives across processes by riding in session.State. Engine additions in workflow/: * Workflow.Resume(ctx, state, responses) iter.Seq2 — for each NodeWaiting whose InterruptID matches an entry in responses, validates the payload against PendingRequest.ResponseSchema (when non-nil), consumes PendingRequest before re-scheduling for idempotency, then routes the response to the asker's successors via findSuccessors so handoff reuses the normal routing / fan-out / fan-in path. ErrInvalidResumeResponse surfaces validation failures and leaves the node parked so the caller can retry. * newSchedulerFromState lets Resume seed a scheduler with a loaded RunState rather than a fresh one. * Workflow.SetName / Workflow.Name expose the persistence-namespacing identifier set by workflowagent.New. * Workflow.Run now persists the post-run state at the end of every invocation so a follow-up Resume can pick it up. * WorkflowInputFunctionCallName constant ('adk_request_workflow_input') and the synthesised FunctionCall part on RequestInputEvent — the same shape tool confirmation uses, lets the runner's generic ID-based dispatch route the user's follow-up FunctionResponse back to this agent without runner-side changes. * RunState / NodeState / RequestInput gain JSON tags. Persistence uses session.State.Get/Set with JSON marshalling; binary payloads must be stashed via agent.Artifacts and referenced by URI in Payload (mirrors how the Python Live API surfaces audio). Persistence helpers (workflow/persistence.go): * LoadRunState / SaveRunState exported so workflowagent (and any custom wrapper) can manage RunState lifecycle without taking a dependency on internal package details. * Both helpers handle nil session and nil session.State as no-ops for tests using minimal mocks. Wrapper changes in agent/workflowagent/: * workflowAgent.run dispatches between Workflow.Run and Workflow.Resume by inspecting ctx.UserContent for a FunctionResponse with the magic name. Stateless: every run state lives in session.State. * detectResume + decodeWorkflowInputResponse handle the dual wire format used by the existing tool-confirmation processor (ADK web wraps payloads as {response: <json string>}; other clients inline {payload: ...}). * MockSession in the existing workflow_test gains a State() method returning nil, supported by the persistence helpers' nil guards. Tests (agent/workflowagent/hitl_test.go, ~430 lines): * TestWorkflowAgent_RunThenResume_Handoff — canonical round-trip; pause on RequestInput, resume with payload, handler receives it as input. * TestWorkflowAgent_Resume_RestoresStateFromSession — fresh agent instance built from the same edges resumes successfully off the same session, verifying cross-instance persistence. * TestWorkflowAgent_Resume_Idempotent — two Resume calls with the same payload run the handler exactly once. * TestWorkflowAgent_Resume_NoMatchingResponse — Resume with an InterruptID that no longer matches a waiting node falls through cleanly without blocking on an empty scheduler. * TestWorkflowAgent_Resume_SchemaValidation_{Pass,Fail} — engine validates responses against ResponseSchema; invalid payloads surface ErrInvalidResumeResponse and leave the node parked, so a retry with a corrected payload still succeeds. * TestWorkflowAgent_Resume_FanOut — handoff into a multi-successor asker delivers the response to every successor. * TestWorkflowAgent_FreshTurn_NotMistakenForResume — a fresh user message that happens to share a session with leftover state from a prior workflow does not get misinterpreted as a resume. All existing workflow and workflowagent tests still pass; the suite is race-free under go test -race. * feat: add HITL re-entry resume mode for workflow agents (#832) Builds on #831 (HITL handoff resume) by adding re-entry mode: when a paused node is configured with `NodeConfig.RerunOnResume = true`, `Workflow.Resume` re-activates the asker itself instead of forwarding the response to its successors. The asker observes the response via `ctx.ResumedInput(interruptID)` and decides what to emit. Direct analogue of adk-python's `@node(rerun_on_resume=True)`. Re-entry preserves the asker's original input, so the node can recompute its decision with the same context it had on the first turn — only the user's response arrives separately. Successors fire only when the re-entry activation produces an output, not on the bare resume call. Support of multiple asks while executing one node will be added in a follow-up PR. - [x] `go build ./...` clean - [x] `go test ./workflow/... ./agent/workflowagent/...` — all pass - [x] `go test -race` clean - [x] 4 new tests covering the canonical re-entry round-trip, original-input preservation across re-entry, the no-successor-before-output invariant, and a regression guard pinning that handoff is still the default mode - [x] 1 new test (`TestNodeContext_ResumedInput`) pinning the per-node context-level contract * feat: remove unused TriggeredBy() interface method (#841) agent.InvocationContext.TriggeredBy() was added in the workflow scheduler PR (#803) as 'engine-supplied metadata available to nodes' but no production caller ever materialised: the method is called only by its own round-trip test and by zero workflow nodes, samples, agents, tools, callbacks, or telemetry sites in the repo. The godoc on the interface method is aspirational; the implementation behind it is a dead branch. This change drops the public API surface (one interface method plus six implementations: agent.invocationContext, internal/context.InvocationContext, and the four MockInvocationContext types in workflowagent, workflow, replayplugin, and llminternal). The unrelated NodeState.TriggeredBy field remains: scheduler.go populates it for resume bookkeeping (workflow.Resume reads it back when re-scheduling a paused node), and it is part of the JSON-serialised RunState so dropping it would break forward compatibility for anyone already running the engine. The TestNewNodeContext_TriggeredByRoundTrip test is removed (it exercised the now-deleted method); TestNodeContext_ResumedInput keeps its coverage of the surviving wrapper functionality. * fix: persist resume inputs across re-entry cycles (#844) * workflow: persist resume inputs across re-entry cycles A re-entry-mode node (NodeConfig.RerunOnResume = true) that yields RequestInput more than once across resume cycles previously lost prior responses on each subsequent resume: ctx.ResumedInput exposed only the response to the most recent InterruptID, and asking the node about an earlier ID returned (nil, false) even though the user had already answered it. The fix accumulates response payloads on NodeState across resume cycles. Each Resume call merges the new {InterruptID: response} entry into ns.ResumedInputs; the scheduler hands the full map to the per-node context on every re-entry activation. The node sees every prior response, not only the most recent one. * feat: add retry config implementation (#853) * feat: add generic Output field to session events and persist via storage layer (#863) * feat: workflow - add JoinNode fan-in barrier (#856) Adds a fan-in primitive built on the orchestrator-aggregator model: the scheduler waits until every predecessor of a JoinNode has completed, assembles a map[string]any keyed by predecessor name, and triggers the JoinNode once with that aggregated input. The node itself is a pass-through that emits the input as its output. * feat: implement AgentNode (#840) * fix: align WorkflowInputFunctionCallName with adk-python (#875) The Go workflow runtime synthesises a FunctionCall part on every RequestInput event so the generic FunctionResponse-by-ID dispatch can route the user's follow-up reply back to the agent that issued the request. The synthesised call's Name was 'adk_request_workflow_input'. adk-python uses 'adk_request_input' for the equivalent constant (REQUEST_INPUT_FUNCTION_CALL_NAME in google/adk/workflow/utils/_workflow_hitl_utils.py). The divergence broke cross-runtime HITL workflows: a session recorded by Python (with function_call.name='adk_request_input' in the interrupt event) could not be replayed in Go, because Go-side conformance compare would see 'adk_request_workflow_input' and flag the mismatch. The reverse direction is broken too — a function_response addressed by name to 'adk_request_input' from a Python-authored spec.yaml could not be routed to a Go workflow agent's pending interrupt. This change renames the constant value to 'adk_request_input'. The exported symbol WorkflowInputFunctionCallName is unchanged, so no Go consumer is affected. The only behaviour difference is the literal value placed on the wire, which now matches Python. Discovered while preparing the first cross-language conformance test for graph-based Workflow + HITL. * refactor: replace StateDelta output tracking with direct Event.Output field usage (#872) * feat: add HITL console launcher support for workflow input prompts + sample (#845) * console: add HITL support for workflow input prompts Adds engine-agnostic HITL handling to the console launcher: detect interrupts emitted on the previous turn, render a prompt for the operator, and forward the typed reply as a FunctionResponse on the next turn. Detection is uniform across interrupt kinds: * collectPendingInterrupts walks the events yielded during a turn and returns one pendingInterrupt per FunctionCall part whose ID appears in Event.LongRunningToolIDs. The call's Name is only used for rendering and response shaping, never for detection. Workflow RequestInput and any future long-running call kind flow through the same path. Per-name dispatch (renderInterruptPrompt / buildInterruptResponse): * workflow.WorkflowInputFunctionCallName: prints '[HITL input]' with the message; pretty-prints the payload (the proposal / context attached by the asker node) and the JSON schema if either is present. Operator's reply is JSON-parsed first so structured replies (objects, arrays, scalars) round-trip as typed values; falls back to raw text. Wrapped under 'payload', the conventional key for workflow input responses. * Anything else: prints a generic banner and wraps the raw input as {result: <text>}. Lets e.g. a future adk_request_credential path be answered through this launcher without a code change here, even before its dedicated renderer exists. Tool confirmation (toolconfirmation.FunctionCallName) currently hits this generic fallback and works at the transport layer (the reply still routes back to the tool by FunctionCall.ID), but the {result: <text>} envelope does not match what ctx.ToolConfirmation() expects to read. A follow-up adds a dedicated renderer and yes/no parser. Main loop integration (console.go): * After every r.Run iteration drains, scan collectedEvents for pending interrupts. If any, render the head's prompt and skip the normal '\nUser -> ' banner. * The next stdin line is interpreted as the answer to that head; successive lines drain the rest. Once every interrupt has an answer, the assembled FunctionResponse parts are sent in one *genai.Content as the next 'turn' through the same r.Run path. The reply routes back to the agent by FunctionCall.ID. * Multiple parallel pauses (rare but legal — e.g. two parallel workflow branches both yielding RequestInput on the same turn) are collected together and answered one prompt at a time, then submitted as one Content with multiple FunctionResponse parts. Renamed the local 'session' variable to 'sess' inside Run to avoid shadowing the session package import once the new code path needs []*session.Event. Race-free under go test -race. * examples/workflow/hitl_simple: minimal HITL sample for console launcher verification A no-LLM, no-API-key sample that exercises the console launcher's pause/resume support end-to-end. Two workflow nodes: Start → ask_name → greet ask_name yields a RequestInput so the launcher renders the prompt. greet receives the user's reply as plain text and emits a greeting that the launcher prints. Useful for verifying that wolo/workflow_hitl_console produces the expected console output without any LLM streaming in the mix. Run with: go run ./examples/workflow/hitl_simple/ console User -> hello Agent -> What's your name? User -> Alice Agent -> Hello, Alice! * feat: add llmagent mode api (#900) * feat: add llmagent mode api * feat: add new parallel worker node type to the adk workflows so it could process multiple inputs simultaneously (#862) * feat(telemetry): add functional test infrastructure and agent telemetry tests (#894) Adds shared infrastructure for telemetry functional tests: - internal/telemetry/functionaltest: end-to-end functional test package for asserting emitted span+log trees. - internal/telemetry/telemetrytest: helpers for in-memory log exporter, span/log digesting, and scenario runners. - internal/telemetry/telemetrytestcase: reusable expected-output fixtures keyed per scenario. Adds an end-to-end functional test for the canonical "llmagent with one FunctionTool" scenario, parametrized over OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT to cover both the elided (default) and full-content variants. Adds test hooks (OverrideTracerForTesting, OverrideLoggerForTesting) to internal/telemetry so functional tests can install in-memory exporters hermetically. Removes the WithGenAICaptureMessageContent option which aliased the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var toggle. The option introduced unnecessary global state which made hermetic testing difficult; getGenAICaptureMessageContent now reads the env var directly so tests can flip behaviour via t.Setenv. * feat: add FinishTaskTool (#901) * feat(workflow): publish NodeContext + add error sentinels and sub-scheduler skeleton (#889) Adds the foundation the dynamic-workflows track builds on: a public NodeContext interface with composite-path support, typed errors, and a per-activation sub-scheduler that runs one child and classifies the outcome. workflow.NodeContext is now an exported interface extending agent.InvocationContext, mirroring the interface-not-struct shape of agent.CallbackContext and the forthcoming agent.ToolContext so future context-unification can swap the backing type without breaking users. dynamicSubScheduler runs one child per call, forwards its events upstream via an emitUp callback, derives a composite child path, and classifies the outcome (HITL → ErrNodeInterrupted, runtime failure → ErrNodeFailed). Child run ids come from a per-(parent activation, child name) auto-counter protected by a mutex for concurrent errgroup-style use; user-supplied custom ids are rejected when empty, purely numeric, or containing the composite-path separators / and @. Precedence matches adk-python: a child that fails after emitting RequestedInput surfaces as ErrNodeFailed, not ErrNodeInterrupted. Resume/replay-skip, parent NodeWaiting persistence, parallel HITL detection, and the public NewDynamicNode / RunNode API stay deferred to follow-up PRs. * feat(workflow): add dynamic-node orchestration (#896) Wires the user-facing API for dynamic workflows on top of the sub-scheduler skeleton from the previous PR. A dynamic node's execution order is expressed as Go code (loops, branches, goroutines) that calls other nodes inline via RunNode, branches on their typed output, and pauses for HITL input. The public surface: workflow.NewDynamicNode[IN, OUT](name, fn, cfg) — orchestrator constructor; cfg.RerunOnResume defaults to &true (an explicit &false is respected). workflow.RunNode[OUT](ctx, child, input, opts...) — generic helper for scheduling a child. Returns its typed output, or errors.Is-matchable ErrNodeInterrupted / ErrNodeFailed. workflow.WithRunID(id) — option overriding the auto-counter with a stable id (rejected if empty, purely numeric, or containing / or @). session.NodeInfo — substruct on Event carrying the emitting node's composite path; shape mirrors adk-python's event.nodeInfo. The scheduler's handleEvent scopes per-activation Output/Routes invariants by NodeInfo.Path, so a dynamic node forwarding a child's terminal output plus its own no longer trips ErrMultipleOutputs. Descendant RequestedInput events are promoted onto the parent's accumulator so Workflow.Resume matches the InterruptID against the parent's NodeState.PendingRequest — enabling HITL inside a dynamic orchestrator. * examples: minimal dynamic-workflow sample (#897) * examples/workflow/dynamic/basic: minimal dynamic-workflow sample Runnable mirror of the "Get started" snippet from https://adk.dev/graphs/dynamic/: a parent dynamic node orchestrates a single FunctionNode child via workflow.RunNode and emits its output upstream. ~50 lines of actual logic, no API keys required. Sits alongside examples/workflow/basic/ and uses the same launcher integration so users can `go run` it and exercise the workflow through the standard CLI/UI. Stacked on the NewDynamicNode + RunNode public-API PR; intentionally minimal so reviewers can validate the API shape against the canonical Python example. Richer samples (loop, parallel, HITL) follow once the corresponding Go features (resume, parallel HITL detection) land. * examples/workflow/dynamic/llm: add LlmAgent-backed dynamic-workflow sample Smallest sensible composition that puts an LLM into a dynamic workflow: one llmagent.New (gemini-3.1-flash-lite, one-line greeting instruction) wrapped via workflow.NewAgentNode and invoked from a NewDynamicNode body through workflow.RunNode. Mirrors the existing examples/workflow/dynamic sample but replaces the trivial FunctionNode child with a real LlmAgent, demonstrating the agent->node->dynamic composition path. Verified end-to-end with 'echo hi | go run ./examples/workflow/dynamic/llm console': greeter responds with a one-sentence greeting via the deployed Gemini endpoint. * examples/workflow/dynamic/hitl: add HITL dynamic-workflow sample Smallest sensible composition that pauses for human input from inside a dynamic-node orchestrator: an inline askName node yields workflow.NewRequestInputEvent; the orchestrator drives it via workflow.RunNode, swallows ErrNodeInterrupted on the pause activation, and on resume re-entry reads the reply via NodeContext.ResumedInput (RerunOnResume defaults to &true for dynamic nodes). A mid-body emit publishes the greeting as Content so the console launcher renders it; the terminal Output carries the same string for downstream nodes. * feat: extend Node interface with JSON schema support and input validation methods (#895) * workflow: experimental NodeContext bridge for runnable tools (#907) Adds an opaque go-context value stash, populated by the scheduler at each per-node activation, that lets tools running inside an LlmAgent (which is itself running as a workflow node) recover the surrounding NodeContext via context.Value lookup. This is a temporary bridge to unblock NewSingleTurnTool (and any future runnable tool that needs to schedule sub-nodes) without modifying the public tool.Context interface. The longer-term solution is the CallbackContext / ToolContext unification tracked in workflow/node_context.go. Mechanism: tool.Context embeds context.Context (transitively via agent.CallbackContext -> agent.ReadonlyContext), so the value survives every downstream NewInvocationContext / WithContext call on the path scheduler -> AgentNode.Run -> LlmAgent.run -> Flow.handleFunctionCalls -> NewToolContext. No interface changes, no agent_node modifications. * feat(workflow): branch isolation across static, parallel, and dynamic schedulers (#906) Adds branch derivation across the three workflow schedulers so the LLM flow's branch-prefix history filter (already present in contents_processor.go) actually scopes events per parallel branch instead of seeing every node run on the empty root branch. Before this PR, Event.Branch and InvocationContext.Branch() existed and the filter respected them, but no scheduler ever derived a non-empty branch — so an LlmAgent wrapped by ParallelWorker (or by a hand-written errgroup fan-out) saw every sibling worker's events in its prompt history. The static scheduler now derives <successor>@1 sub-branches at fan-out, computes the longest common dot-prefix of predecessor branches for JoinNode, stamps Event.Branch when the node leaves it empty, and persists each activation's branch on NodeState for resume. The ParallelWorker replaces its single shared workerCtx with per-iteration sub-contexts derived as <parent>.<wrapped>@<i+1>. The dynamic sub-scheduler gains two opt-in RunNode options — WithUseSubBranch() and WithOverrideBranch(base) — for dynamic-node bodies that want to isolate child activations. * fix(examples): remove unused telemetry import in multipletools (#909) PR #894 (feat(telemetry): add functional test infrastructure) replaced the TelemetryOptions config in examples/tools/multipletools with an os.Setenv call but left the telemetry import behind. This broke 'go build ./...' on v2 with: examples/tools/multipletools/main.go:31:2: "google.golang.org/adk/telemetry" imported and not used Removes the import to unblock the v2 CI. * feat: add SingleTurnTool (#914) * feat: add SingleTurnTool * upd * feat: add TaskAgentTool (#915) * feat: add the workflow node to allow the nested workflows (#910) * feat(telemetry): Instrument nodes and workflows (#891) Enables emission of the following spans: - invoke_workflow <workflow_name> - invoke_node <node_name> Additionally: - Adds a functional test for graph-based telemetry emission (workflow chain scenario). * feat(workflow): add WithMaxConcurrency option to cap graph-scheduled node parallelism (#917) Add a workflow-level concurrency cap that limits how many nodes may run concurrently within a single Workflow invocation. When the cap is reached, additional ready-to-run nodes enter NodePending and queue in FIFO order; the scheduler drains the queue as in-flight nodes complete. API: workflow.New(name, edges, workflow.WithMaxConcurrency(n)) - n > 0: cap is enforced - n == 0 (default): unlimited (no behavioural change for existing callers) - n < 0: clamped to 0 (unlimited) The cap applies to all top-level static scheduling paths: initial Start dispatch, fan-out from completed nodes, retry scheduling, and Resume. It explicitly does NOT apply to dynamic sub-nodes invoked via workflow.RunNode from inside a DynamicNode body — gating them would deadlock the parent that awaits the child inline. Same exclusion as adk-python (see _workflow.py:164 comment). * feat: add JSON schema validation support to BaseNode and update constructors accordingly (#911) Implements JSON schema support and default input validation for BaseNode and integrates them across all workflow node types. * feat(workflow): AgentNode synthesizes Event.Output from LLM text (#925) AgentNode now sets Event.Output from concatenated model text on final agent responses, so RunNode(agentNode, ...) returns the agent's reply instead of the zero value. Without this, dynamic workflows that wrap an LlmAgent via NewAgentNode and call RunNode[string] received "" and could not chain on the agent's output. Mirrors adk-python's process_llm_agent_output (workflow/_llm_agent_wrapper.py:251-279) minus the output_schema and output_key side-effects, which have no equivalent here yet. Partial streaming events are not promoted; thoughts are excluded from the concatenation; the synthesis is skipped when Output is already set so callers retain control. * feat(workflow): idempotent RunNode dispatch via WithRunID cache (#921) dynamicSubScheduler gains an in-memory cache keyed by the resolved childPath ("<parentPath>/<name>@<runID>"). A repeated RunNode call with the same WithRunID inside one parent activation returns the cached output without re-running the child, enabling idempotent dispatch for replayed or reorderable children. Failures and HITL interrupts are not cached. Cross-resume idempotency (rehydrating the cache from session events on parent re-run) is out of scope here; a follow-up CL will replicate adk-python's DynamicNodeScheduler._rehydrate_from_events via an OutputFor event annotation. * fix(typeutil): decode validation input into `any` to accept non-object JSON values (#933) ConvertToWithJSONSchema previously decoded the marshalled value into map[string]any before validating against the resolved schema. That worked for object-shaped inputs but failed for scalars, arrays, and booleans with: json: cannot unmarshal string into Go value of type map[string]interface {} * docs: add single_turn and task sub agent examples (#934) * feat(workflow): Support configurable workflows Introduce dynamic loading of complete declarative agent graph workflows from YAML configuration files, bringing the Go SDK to full architectural feature parity with the Python ADK 2.0 workflow runtime. - Implement new YAML parser to dynamically resolve and cache nested nodes (FunctionNode, JoinNode, ToolNode, AgentNode) and map complex edge chains with routing. - Support automatic output extraction in AgentNode to seamlessly propagate final non-thought text blocks as clean string outputs downstream. - Enable self-healing JSON deserialization in ToolNode to convert raw string inputs into structured map[string]any args expected by the tool's schema. - Allow direct event yielding in FunctionNode to support explicit, stateful event routing and conditional transition mechanics. - Register conformance node functions at CLI startup to enable direct replay testing. * lint fix * tool node fix * Remove node to node change to separate PR * lint fix * Add MaxConcurrency option * Add isolation scope --------- Co-authored-by: wolo <wolo@google.com> Co-authored-by: Mikalai Senkevich <hanorik@google.com> Co-authored-by: Dmitry Pasiukevich <20398573+dpasiukevich@users.noreply.github.com> Co-authored-by: RKest <68107862+RKest@users.noreply.github.com> * feat: introduce WorkflowConfig to support state schema validation during workflow construction (#999) * fix(webui): rebuild embedded bundle from adk-web HEAD to fix HITL rendering (#1003) Regenerates the embedded web UI bundle (cmd/launcher/web/webui/distr) from a production build of adk-web at HEAD. The previously embedded bundle predated adk-web's "Major Refactor" (d4152d6) and carried a chat-rendering bug: HITL resume responses were delivered correctly over SSE but never appeared in the chat. The old code kept a single mutable streamingTextMessage cursor and processed the long-running-response reply through a separate runSse subscription that never reset it; the resumed event then took the stale-cursor branch and was merged in place (or dropped) instead of being added as a new chat bubble. The answer only showed after an unrelated click triggered change detection. The refactored adk-web removes that cursor and reconciles events per id in appendEventRow, so the resumed response renders immediately. Build: `ng build` (production) in adk-web; copied output/browser/* over distr/. The higher file count is adk-web's new lazy-chunk code splitting, not a debug build (main chunk size is comparable). * feat(workflow): validate node output with passthrough and model-text fallback (#970) * feat(workflow): output validation with passthrough and text fallback Wire ValidateOutput into the static and dynamic schedulers, and extend defaultValidateOutput beyond plain schema checking: - Framework control values (*session.Event, *session.RequestInput) bypass validation: they ride through Event.Output but are not user output payloads. - Nil output passes through unchanged, mirroring adk-python _validate_schema (_base_node.py:135). - Text fallback: when direct validation fails on model text (a string or a *genai.Content of model parts), project it onto the schema — return it for a string schema, else JSON-parse and re-validate. Thought parts are skipped in modelText so validation sees the same text the agent surfaces as output (parity with synthesizeAgentOutput/messageText and adk-python extract_text_from_content). Mirrors ADK Python _validate_output_data and makes structured LlmAgent output validate end-to-end. - scheduler.handleEvent and dynamicSubScheduler.runNode now run ValidateOutput on yielded output before committing/forwarding it; an unknown node fails instead of forwarding unvalidated output. The static scheduler wraps validation errors with ErrNodeFailed so callers can use errors.Is uniformly across both schedulers. Tests cover passthrough types, nil passthrough, string/Content fallback (including thought-part skipping and whitespace handling for string schemas), AgentNode projection, and static/dynamic scheduler validation including ErrNodeFailed wrapping. * fix(workflow): recognize string in multi-type schema roots rootSchemaIsString only inspected Schema.Type, so a root declared via the mutually-exclusive Types list (e.g. {"type": ["string", "null"]}) was not treated as string-capable. The model-text fallback then JSON-parsed the projected text and failed on plain text, surfacing as NodeFailed for outputs that the schema actually permits. Check both Type and Types, and add multi-type-root tests driven through the *genai.Content path that exercises the fallback. * feat: add utility to generate canonical, key-sorted JSON for schemas and nested maps (#992) * feat: add static schema validation to workflow graph construction (#993) * feat: add utility to generate canonical, key-sorted JSON for schemas and nested maps * feat: add static schema validation to workflow graph construction * fix(workflow): make RunNode work from tools inside a static dynamic node (#1006) A dynamic node's body builds an orchestrator NodeContext carrying a live subScheduler, but only the scheduler's top-level NodeContext (subScheduler == nil) was stashed on the context bridge. A tool running inside an LlmAgent that is itself the node's body recovers its NodeContext via NodeContextFromGoContext, so it got the subScheduler-less one and RunNode failed with ErrInvalidRunNodeContext (e.g. SingleTurnTool delegating to a single_turn sub-agent under a root coordinator). Re-stash the orchestrator NodeContext onto the bridge in dynamicNode.Run, mirroring the existing child path in dynamicSubScheduler.runNode, so the recovered context is RunNode-capable. * refactor(workflow) Context unification NodeContext with InvocationContext added to commonContext (#1008) Context unification * refactor(agent): migrate all consumers to Context (#1016) The context unification introduced a single unified Context, with agent.CallbackContext, agent.ToolContext and tool.Context kept as aliases to ease migration. Migrate every consumer (~90 files) to use agent.Context directly. The three aliases remain defined (now marked Deprecated) so this change is safe to import into downstream repositories that still reference them; a follow-up removes the aliases for a clean open-source surface. * feat: llm agent wrapper (#1004) * feat: llm agent wrapper * make sure llm_wrapper is called at agentnode.Run * update to use unified context * fix(llm): use set_model_response workaround for output schema + tools on Gemini API (#1026) Problem: an agent with both OutputSchema and tools on the Gemini API (any version) was sent a request with native JSON output (ResponseMIMEType + ResponseSchema) AND tool declarations at once, a combination Gemini does not support. The model never produced a final response, causing an infinite loop (e.g. a single_turn sub-agent with a tool). The gate keyed on 'Gemini 2.5 or lower' instead of the backend variant, so Gemini 3.x on the Gemini API wrongly skipped the workaround. Solution: gate on whether the model can use output schema with tools natively (Vertex AI, Gemini 2.0+), mirroring adk-python's can_use_output_schema_with_tools. Every other case (incl. all Gemini API models) uses the set_model_response tool workaround. Mirrors can_use_output_schema_with_tools from adk python. * fix(workflow): restore single_turn delegation after context unification (#1025) Problem: after the context unification (#1008/#1004), SingleTurnTool panicked (nil sub-scheduler) when an LlmAgent delegated to a single_turn sub-agent, because the sub-scheduler is dropped when the context is re-wrapped before running the agent (in the runner's agent-node body and in the llmagent wrapper). Solution: stash the sub-scheduler in the embedded context value chain so it survives re-wrapping, and recover it in Context.SubScheduler() when the struct field is absent. The llmagent wrapper and the runner's generic agent body re-stash it before running the agent. RunNode now returns ErrInvalidRunNodeContext instead of panicking on a nil sub-scheduler. * refactor: consolidate agent node input conversion into a helper and add validation test coverage (#991) * fix(webui): server-side fixes for HITL in the web UI (#1002) Two independent server-side bugs broke human-in-the-loop (HITL) workflows under the web UI. Both are needed for a HITL resume to reach the browser; the web UI bundle update is handled separately. Problem functionCallEventId rejected (400). The web UI sends functionCallEventId on every HITL response — fields that exist on adk-python's /run / /run_sse request body. adk-go's RunAgentRequest did not declare them, and decodeRequestBody uses DisallowUnknownFields, so the request failed with json: unknown field "functionCallEventId". The web UI buffers the SSE response and renders on completion, so the 4xx surfaced as a silent no-op. Missing Content.Role. Workflow nodes that build Event.Content directly (FunctionNode and BaseNode-derived nodes) set Parts but not Role. adk's convention is that model-authored Content carries Role="model", and the web UI relies on it to render an event. A resume reply was persisted and returned over SSE with an empty role, so the UI ignored it. Solution fix(adkrest) — declare functionCallEventId on RunAgentRequest as optional fields (adk-python parity, accepted-and-ignored for now). Strict decoding stays on for typo safety. Adds positive + regression tests. fix(workflow) — stamp Content.Role="model" centrally in scheduler.handleEvent, the one chokepoint every node event passes through (it already stamps Branch and NodeInfo.Path). Runs before the dynamic-descendant short-circuit and leaves an explicitly-set role untouched. Mirrors the convention used by NewRequestInputEvent and the LLM flow. Adds tests. Verified end to end with examples/workflow/hitl_simple * Added reading the Environment variables while setting-up the logger. (#1027) * Added reading the Environment variables while setting-up the logger. * Simplified evalsToTrue * fix(llm): use set_model_response workaround for output schema + tools on Gemini API (#1026) Problem: an agent with both OutputSchema and tools on the Gemini API (any version) was sent a request with native JSON output (ResponseMIMEType + ResponseSchema) AND tool declarations at once, a combination Gemini does not support. The model never produced a final response, causing an infinite loop (e.g. a single_turn sub-agent with a tool). The gate keyed on 'Gemini 2.5 or lower' instead of the backend variant, so Gemini 3.x on the Gemini API wrongly skipped the workaround. Solution: gate on whether the model can use output schema with tools natively (Vertex AI, Gemini 2.0+), mirroring adk-python's can_use_output_schema_with_tools. Every other case (incl. all Gemini API models) uses the set_model_response tool workaround. Mirrors can_use_output_schema_with_tools from adk python. * fix(workflow): restore single_turn delegation after context unification (#1025) Problem: after the context unification (#1008/#1004), SingleTurnTool panicked (nil sub-scheduler) when an LlmAgent delegated to a single_turn sub-agent, because the sub-scheduler is dropped when the context is re-wrapped before running the agent (in the runner's agent-node body and in the llmagent wrapper). Solution: stash the sub-scheduler in the embedded context value chain so it survives re-wrapping, and recover it in Context.SubScheduler() when the struct field is absent. The llmagent wrapper and the runner's generic agent body re-stash it before running the agent. RunNode now returns ErrInvalidRunNodeContext instead of panicking on a nil sub-scheduler. * refactor: consolidate agent node input conversion into a helper and add validation test coverage (#991) * fix(webui): server-side fixes for HITL in the web UI (#1002) Two independent server-side bugs broke human-in-the-loop (HITL) workflows under the web UI. Both are needed for a HITL resume to reach the browser; the web UI bundle update is handled separately. Problem functionCallEventId rejected (400). The web UI sends functionCallEventId on every HITL response — fields that exist on adk-python's /run / /run_sse request body. adk-go's RunAgentRequest did not declare them, and decodeRequestBody uses DisallowUnknownFields, so the request failed with json: unknown field "functionCallEventId". The web UI buffers the SSE response and renders on completion, so the 4xx surfaced as a silent no-op. Missing Content.Role. Workflow nodes that build Event.Content directly (FunctionNode and BaseNode-derived nodes) set Parts but not Role. adk's convention is that model-authored Content carries Role="model", and the web UI relies on it to render an event. A resume reply was persisted and returned over SSE with an empty role, so the UI ignored it. Solution fix(adkrest) — declare functionCallEventId on RunAgentRequest as optional fields (adk-python parity, accepted-and-ignored for now). Strict decoding stays on for typo safety. Adds positive + regression tests. fix(workflow) — stamp Content.Role="model" centrally in scheduler.handleEvent, the one chokepoint every node event passes through (it already stamps Branch and NodeInfo.Path). Runs before the dynamic-descendant short-circuit and leaves an explicitly-set role untouched. Mirrors the convention used by NewRequestInputEvent and the LLM flow. Adds tests. Verified end to end with examples/workflow/hitl_simple --------- Co-authored-by: wolo <wolo@google.com> Co-authored-by: Mikalai Senkevich <hanorik@google.com> * test: add end-to-end input validation flow test for workflow nodes (#1028) input validation test * fix: pass non-partial events in task delegation (#1024) * fix(workflow): scope dynamic node cache rehydration to current invocation (#1010) rehydrateCache scanned the entire session history and cached child outputs keyed by childPath, without filtering by invocation_id. Because auto-counter run-ids reset to 1 on every fresh activation, a later user turn reuses the same childPath (e.g. "wf/child@1") as a prior turn, so the stale output was served from cache and the child never re-ran. This caused the dynamic workflow example to return empty output on the second and subsequent turns. Filter rehydration to events from the current invocation, mirroring adk-python (_reconstruct_node_states / _scan_parent_child_sequence gate on event.invocation_id). Within-invocation resume/dedup is preserved. * fix(workflow): select nested workflow output by terminal node, not arrival order (#1013) WorkflowNode surfaced a nested workflow's result as "the last yielded event with non-nil Output", which is iteration-order dependent. This diverged from adk-python and was wrong for two shapes: a fan-out with multiple terminals picked a nondeterministic winner, and a silent terminal returned an earlier node's output as if it were the result. Select the output from the workflow's terminal node(s) (nodes with no outgoing edges) by graph topology, mirroring adk-python's _set_ctx_output_or_interrupts: exactly one terminal output is forwarded, more than one distinct terminal with output is ErrMultipleOutputs, and zero means no output. Outputs are keyed by terminal node name (last write wins, like adk-python's node_outputs dict) so re-activating one terminal is a single output. Terminal output is read via childEventOutput, so a MessageAsOutput terminal is handled too. * fix(v2): four XS pre-merge bugs (H1/H4/M2/L2) with regression tests (#1038) ## Problem Four small but high-impact bugs found during the v2 pre-merge review. Each is a one- or few-line defect on a real code path, and none was covered by a test. Severities/IDs match the internal review notes. - **nil-deref panic when a plugin modifies the user message.** In `appendMessageToSession`, when a plugin's `OnUserMessage` returns a modified message, v2 rebuilt the context and returned `agent.NewCallbackContext(ic, nil)` as the **run** context. That callback-only wrapper's run-critical accessors (`Agent()`, `Session()`, `Memory()`, `WithAgentContext()`, ...) all return `nil`/`""`. The runner then calls `r.rootAgent.Run(ctx)` with it, and the first downstream `ctx.Agent()` / `ctx.Session()` access nil-derefs. Regression vs `main` (which returned a full context here). Triggered by any redaction/normalization/PII plugin on a normal turn. - **in-memory `AppendEvent` drops `IsolationScope`.** The manual event deep-copy copies the other new v2 fields but omits `IsolationScope`, so the stored copy resets it to `""`. After the per-turn session reload, the contents processor filters history by exact match, so a scoped agent stops seeing its own past events and unscoped agents start seeing them. The SQL backend persists the field correctly, so in-memory (the default service) diverged. - **typed-nil `*genai.Content` panics `AgentNode`.** A typed-nil `(*genai.Content)(nil)` boxed in `any` does not match `case nil`, falls into the `*genai.Content` case, and dereferences `v.Parts`. Triggered by an upstream node whose `Output` is a nil `*genai.Content`. - **retry backoff collapses to 0s.** With an unset (zero) `BackoffFactor` in a struct-literal `RetryConfig`, `delay *= cfg.BackoffFactor` makes attempts >= 2 wait 0 -> tight retry hot-loop. ## Solution - return a full run context. This PR also adds `agent.NewContext(ic)` — a plain run context with no callback/tool/node specializations — and uses it here, instead of borrowing the graph-oriented `NewNodeContext` for a non-graph call site. `NewNodeContext` now builds on `NewContext` (behavior unchanged; it just adds `resumeInputs`). - add `IsolationScope: event.IsolationScope` to the `eventCopy` literal. - add `if v == nil { return nil, nil }` at the top of the `*genai.Content` case, mirroring the untyped-`nil` case. - treat `BackoffFactor <= 0` as `1.0` (constant delay). * feat: add WithRaiseOnWait option (#1037) * feat: add WithRaiseOnWait option This will mark the node as ErrNodeInterrupted if there's unresolved function call and no matching function responses on the node. * remove verbose comments * tests * fix(workflow): reject non-JoinNode fan-in at validation time (#1041) A plain (non-Join) node with two or more unconditional incoming edges is activated once per completed predecessor. The scheduler keys bookkeeping (runsByName / runCancels) by node name with no already-running guard, so the second activation overwrites the first while it is still running: mixed outputs (ErrMultipleOutputs / wrong output) and a lost cancel func. adk-python serializes per-node executions (skip-if-RUNNING) and allows plain fan-in, but adk-go has no such serialization yet. Until it does, reject the graph at construction with a clear "not yet supported" error instead of corrupting the run. Only unconditional (Route == nil) incoming edges are counted, so conditional fan-in and loop-back back-edges — whose predecessors don't all fire together — are not rejected. JoinNode (barrier-gated) fan-in is exempt. Add validation tests (diamond rejected; JoinNode diamond, loop-back, and conditional fan-in allowed) plus an e2e check that New() surfaces it. * fix(workflow): serialize dynamic-node emit across concurrent children (#1040) * fix(workflow): serialize dynamic-node emit across concurrent children A DynamicFn may run children concurrently (the documented WithUseSubBranch pattern), and every child forwards its events up through one shared emit callback (makeEmit, wrapping the parent's single yield). With no synchronization, concurrent children call the same yield at once, which panics the range-over-func iterator and races the parent runNode's completion accumulator. Guard emit with a per-activation mutex so all yields — from the DynamicFn's own emit and from RunNode via the sub-scheduler — are serialized. Add a -race regression test that fans children out across goroutines; it panics/races on the unpatched code. * fix(workflow): move emit mutex into makeEmit Address review nit: the per-activation mutex is owned solely by the emit closure, so create it inside makeEmit instead of passing it in. Simplifies the makeEmit prototype and removes the caller-side mutex plumbing. No behavior change. * fix(workflow): stop WorkflowNode re-yielding after consumer break (#1039) When the parent scheduler stops consuming a nested WorkflowNode mid-stream (a sibling fails, a deadline elapses, or the caller breaks), the node's yield returns false. The drain path then did `cancel(); continue` and re-invoked yield on the next drained inner event (and at the trailing output yield), violating Go 1.23's range-over-func contract — a "continued iteration after ... returned false" panic, recovered as a spurious NodeFailed that masks the cancellation and aborts the inner drain, leaking sub-scheduler goroutines. Track a consumerGone flag (mirroring scheduler.run): set it when yield returns false, then drain-only and skip all further yields, including the trailing error/output yields. Add a regression test that breaks the range loop mid-stream and panics on the unpatched code. * feat(workflow): add NewEmittingFunctionNode for streaming and HITL function node (#1000) * feat(workflow): add NewEmittingFunctionNode for streaming/HITL function nodes FunctionNode previously emitted exactly one event with the function's return value, leaving HITL prompts and progress streaming impossible from a plain function body — users had to drop down to a custom Node or wrap logic in an AgentNode. This mirrors the gap with adk-python, where a generator-style function node can yield RequestInput, Content, or Event objects mid-execution. Add NewEmittingFunctionNode and NewEmittingFunctionNodeWithSchema that accept an EmittingFunctionFn[IN, OUT] whose body receives an emit callback. The shape and HITL contract mirror DynamicFn from dynamic_node.go: the body may emit any number of intermediate events (typically via NewRequestInputEvent for HITL), and returns ErrNodeInterrupted as the sentinel meaning "pause event already forwarded; do not emit a terminal event". Any other returned error fails the node — matching the precedence already tested for bare nodes in TestScheduler_HitlNode_ErrorAfterRequestFails. A nil return without error suppresses the auto-emitted terminal event for nodes whose entire payload was sent inline. No scheduler, resume, or persistence changes are needed: the emit helper and ErrNodeInterrupted sentinel already power dynamicNode, so the existing pause/handoff/re-entry machinery applies unchanged. Default NodeConfig.RerunOnResume keeps handoff semantics consistent with adk-python's rerun_on_resume=False default in _function_node.py. The classic NewFunctionNode constructor and signature are unchanged. Tests cover the same scenarios as hitl_test.go (pause+forward, auto-generated InterruptID, multiple parked requests, error precedence) plus a non-HITL streaming case (intermediate content event alongside the terminal output). Simplify the workflow examples that previously hand-rolled a workflow.BaseNode subclass solely to emit a single custom event: hitl_simple and dynamic/hitl (RequestInput for HITL), and all three routing examples int/string/llm (Event.Routes). All now use NewEmittingFunctionNode. * fix(workflow): return nil output on error in emitting function node * chore(examples): remove accidentally committed go build binary examples/quickstart/main was a 35MB ELF build artifact tracked in the repo (alongside its main.go source). Remove it; the source is unaffected. * refactor(workflow): use agent.Context directly in emitting function node API The emitting FunctionNode API (EmittingFunctionFn, the emittingFn field, and the internal wrapper) used the NodeContext alias; switch it to agent.Context directly. Updates the NewEmittingFunctionNode callers in tests and examples to match. DynamicFn keeps NodeContext (out of scope). * refactor(examples): rename nc param to ctx for agent.Context The emitting function node callbacks now take agent.Context, so the nc (node context) parameter name no longer fits; rename to ctx. DynamicFn callbacks keep nc (still NodeContext). * feat(workflow): add ResumeOrRequestInput helper and validate node input against schema Add ResumeOrRequestInput(ctx, emit, req): collapse the two-branch "read ResumedInput, else ask and pause" pattern into one call so emitting/dynamic node bodies read top-to-bottom. Simplify the hitl_rerun example to use it. Enforce inputSchema constraints (e.g. maxLength) on the type-assertion hit path: previously schema validation only ran when the input needed ConvertToWithJSONSchema, so an already-typed IN value bypassed all constraint checks. Now both FunctionNode constructors (plain and emitting) validate a successfully-asserted input via ValidateWithJSONSchema, mirroring the existing output validation. Addresses PR #1000 review comments. * fix(session): persist Event.NodeInfo through Vertex AI backend (#1017) * fix(session): persist Event.NodeInfo through Vertex AI backend The Vertex AI session backend only round-tripped fields with a dedicated SessionEvent column (Content, Actions, EventMetadata), silently dropping Output, NodeInfo, IsolationScope, RequestedInput and Routes. With the workflow engine now reconstructing resume state from session history, NodeInfo is load-bearing, so a Vertex-persisted session resumed incorrectly. Persist the full event in SessionEvent.raw_event (the same field adk-python uses) and prefer it on read, falling back to the legacy field-based reconstruction for older events. raw_event is only attached when the event carries state without a dedicated column, so plain events keep their existing wire format and the recorded replay fixtures stay valid. The raw_event payload uses Go's own JSON encoding for now; byte-level camelCase parity with adk-python (cross-runtime interop) is tracked separately. Integers in the any-typed Output and StateDelta fields come back as float64 after the round-trip; this matches the SQL backend's existing contract (both JSON-encode those fields the same way) and is now documented and pinned by a test. * Upcase for event json * fix(session): simplify eventToRawEvent with structpb.UnmarshalJSON --------- Co-authored-by: Karol Droste <kdroste@google.com> * fix: force llm_agent_wrapper in base_flow delegation (#1059) * feat(workflow): enforce at-most-one-terminal-output at run finalize (#1049) Add a runtime finalize check to the scheduler that mirrors adk-python's Workflow._finalize: after a clean workflow completion, if more than one terminal node (a node with no outgoing edges, excluding START) actually produced an output, the run fails with ErrMultipleTerminalOutputs naming the offending terminals. A workflow's output is otherwise ambiguous. This is deliberately a runtime check on the nodes that produced output in a given activation, not a topological constraint. Fan-out and conditional-routing graphs frequently have several terminal branches; they remain valid as long as at most one terminal branch yields output per run. The check is skipped while the run is draining (consumer stop or node failure) and when any node is parked on a human-input interrupt (the workflow has not finished, so its terminal output is undetermined), matching Python's early return on interrupt_ids. The full Workflow._finalize port (collecting the single terminal output, validating it against a workflow-level output schema, and writing it to a parent-readable channel) is intentionally out of scope; it depends on nested-workflow support and is tracked separately. Tests: - New TestScheduler_Finalize_SingleTerminalOutput covers the acceptance cases: one terminal output succeeds; several terminals with at most one producing output succeeds; zero output-producing terminals succeeds; multiple output-producing terminals errors and names them. - Existing fan-out tests (max-concurrency, scheduler branch derivation, fan-out concurrency) now fan in to a single JoinNode terminal so the graph is single-terminal-output while still exercising concurrent execution; their assertions (peak concurrency, FIFO dispatch, retry gating, branch strings) are unchanged. - TestWorkflowRouting's leaf nodes A/B/D no longer emit Event.Output (the test asserts on which nodes ran, not their output), so routing graphs with several terminal branches do not trip the check. * refactor(workflow): move output validation off inline node paths (#1047) * refactor(workflow): move output validation off inline node paths Make the scheduler the single authoritative point for output validation. FunctionNode.wrappedFn and ToolNode.runTool no longer validate the output against their schema inline; the schema still flows through BaseNode so the scheduler enforces it on every yielded event with non-nil output. This removes duplicate validation and the risk of drift as the scheduler logic evolves (symmetric to the input validation cleanup). The FunctionTool {"result": X} unwrap fallback is not lost: it moves to a ToolNode.ValidateOutput override, the correct home for that tool-specific convention. The override tries standard validation first, then unwraps a "result" key and validates the unwrapped value, and finally re-runs standard validation so callers see the original schema-mismatch error rather than a fallback artifact. nil schema is a passthrough. Tests that previously asserted on inline behavior are updated: - FunctionNode: the inline ValidationError table case is replaced by TestFunctionNode_ValidateOutput, which asserts Run passes the output through unchanged and ValidateOutput surfaces the schema mismatch. - ToolNode: TestToolNode_Run now asserts the raw FunctionTool map output from Run (no inline unwrap); the new TestToolNode_ValidateOutput covers the four override cases (direct-valid passthrough, {"result": X} unwrap, both-fail original error, nil-schema passthrough). BUG=516382303 BUG=516382092 * test(workflow): add success case to TestFunctionNode_ValidateOutput Cover the positive path: a schema-conforming output passes ValidateOutput and is returned unchanged. Brings the test to parity with TestToolNode_ValidateOutput, which already asserts a direct-valid case. * test(workflow): split FunctionNode validation tests, make ValidateOutput table-driven Separate the two concerns the test conflated: Run-passthrough now lives in TestFunctionNode_RunDoesNotValidate, while TestFunctionNode_ValidateOutput becomes table-driven (direct-valid, schema-mismatch, nil-schema). Mirrors the structure of TestToolNode_Run + TestToolNode_ValidateOutput. * test: add llmagent delegation tests (#1060) These tests cover different delegation scenarios for all 3 modes: chat, task, single_turn. * fix(llminternal): handle nil entries in parallel function-response merge (#1042) When a parallel tool call is long-running or defers its response, the producer returns early and leaves a nil slot in the pre-sized fnResponseEvents slice. mergeParallelFunctionResponseEvents then did `ev := events[0]; ev.LLMResponse = ...` (panics if index 0 is nil) and `ev.Actions = *actions` (panics if all entries are nil, leaving actions nil). v2 added the early-return, so merge started seeing nils. Base the merged event on the first non-nil entry and return (nil, nil) when every entry is nil. Add a regression test (first-nil, all-nil, mixed) that panics on the unpatched code. * feat(workflow): park parent when a WaitForOutput child yields no output (#986) RunNode now honors NodeConfig.WaitForOutput: a child that opts in and finishes without producing output parks the parent with ErrNodeInterrupted instead of returning the zero value, so it re-runs and re-invokes RunNode once the child can produce output. Mirrors adk-python's ctx.run_node(raise_on_wait=True). Tracked via a sawOutput flag (nil is a valid output) and a waitsForOutput helper reading the tri-state config. * fix: repair v2 build after rebase onto main The rebase onto main pulled in StrictContextMock (agent/context_mock.go), which only implemented the pre-unification Context interface. v2's context unification grew that interface (Agent, Memory, Session, the NodeContext section, etc.), so the auto-merged mock no longer satisfied Context. Add the missing methods following the existing panic("not implemented") pattern, matching the up-to-date ContextMock. * fix(workflow): Align hierarchical node pathing and OutputFor metadata (#1063) Standalone agent wrappers previously stamped extraneous namespace prefixes onto events and omitted ancestor output tracking. Added synthetic root workflow tagging and dynamic context ancestor resolution to ensure deterministic parity with conformance test recordings. ### Summary Aligns event `NodeInfo.Path` and `NodeInfo.OutputFor` metadata generation in the Go graph engine with the Python reference implementation. ### Root Cause & Solution 1. **Root Path Distortion**: Standalone agents run via `Runner.runNode` ride inside a synthetic wrapper workflow. Added `WithRootWrapper()` so the scheduler suppresses extraneous root path prefixes on emitted events. 2. **Missing Ancestor Metadata**: Terminal nodes designated as `use_as_output` in hierarchical sub-workflows were not reporting ancestor workflow paths in `OutputFor`. Added dynamic context propagation to pass ancestor lineage down through sub-schedulers. 3. **Terminal Output Capture**: Updated `WorkflowNode.Run` to correctly extract terminal node names from compound paths (e.g., splitting `parent/child@1` to match `child`). * fix(workflow): Align Go workflows with Python ADK (#1046) * fix(workflow): Align Go workflows with Python ADK - Default AgentNode instances to ModeSingleTurn to prevent unexpected identity instruction injection in multi-step graphs. - Re-introduce wrappedSession in LLMAgent wrappers to thread transient node inputs without permanently appending them to session history. - Update FunctionNode and Runner logic to prioritize structured genai.Content and suppress redundant Output fields when MessageAsOutput is enabled. - Enhance YAML replay normalization to handle scalar 'system_instruction' strings and optional flight preference fields. * test(llmagent): re-record TestDelegation_03 after wrappedSession change The wrappedSession change in this PR makes single_turn sub-agent input ephemeral (no longer persisted to the session), so the coordinator's turn-2 history no longer carries the echoed sub-agent argument. This shortened the generateContent request, breaking the recorded httprr key match and failing the test deterministically with "cached HTTP response not found". Re-records the trace (and events.yaml) to reflect the intended behavior. --------- Co-authored-by: wolo <wolo@google.com> * feat(agent): add NewStrictContextMock constructor (#1064) Add a NewStrictContextMock(ctx) constructor for StrictContextMock so test fakes that embed the mock as their only field can be built from a concise literal instead of StrictContextMock{Ctx: ctx}. * refactor(workflow): declare Default sentinel via Route interface (#1071) Align the Default route sentinel with the Start node sentinel by declaring it with its exported interface type (var Default Route) instead of inferring the unexported concrete *defaultRoute type. This keeps the unexported defaultRoute type out of the public godoc, asserts Route conformance at the declaration site, and matches the existing var Start Node = ... pattern. Behavior is unchanged: the sentinel is still compared by identity (edge.Route == Default). Also fix the doc comment to start with the symbol name (Default). * fix(workflow): reject chat-mode agent wired after a non-Start node (#1070) * fix(workflow): reject chat-mode agent wired after a non-Start node A chat-mode LlmAgent builds its prompt from conversation history and ignores the node input passed down an edge, so wiring it after a predecessor silently drops that predecessor's output. Reject such graphs at construction time with a clear error instead of failing silently at run time. A chat node fed by both a Start edge and a non-Start edge is rejected too. Mirrors adk-python's _validate_chat_agent_wiring. Factors the AgentNode-mode lookup into agentNodeMode, shared with the existing task-mode validator. * tests(workflow): add integration tests for hitl (#980) * runner: add HITL workflow integration tests with real Runner Three end-to-end tests exercising the full pause/resume round-trip through a real runner.Runner — not the lightweight mocks the agent/workflowagent unit tests use. They verify that the contract the engine relies on (FunctionCall.ID round-trips into a follow-up FunctionResponse, runner.findAgentToRun routes by that ID, RunState survives the turn boundary via session.State delta) actually holds when the runner, session.InMemoryService, and the workflow agent are wired together as a production user would wire them. * TestRunner_WorkflowHITL_Roundtrip_Handoff exercises the default handoff resume path: turn 1 yields an event with LongRunningToolIDs and a synthesised adk_request_workflow_input FunctionCall part; turn 2 sends a matching FunctionResponse, the runner routes it back to the same agent, and the asker's successor receives the response payload as its input. * TestRunner_WorkflowHITL_Roundtrip_ReEntry covers the re-entry path (NodeConfig.RerunOnResume = true) with the same runner setup: the asker is re-activated, observes the response via ctx.ResumedInput, and emits it as an output that flows to the successor. * TestRunner_WorkflowHITL_FunctionResponseRoutedByID pins the runner-level routing contract: it asserts the interrupt event's Author equals the workflow agent name (used by findAgentToRun) and that the second turn does not produce a fresh interrupt (it would if findAgentToRun fell back to the root agent and treated the FunctionResponse as a new user message). Tests are in runner_test (external test package) so they exercise only the public Runner API, no internals. * runner: fix HITL re-entry test to use Event.Output The re-entry asker emitted the resumed response via the obsolete Event.Actions.StateDelta["output"] channel, which the v2 scheduler no longer reads (node output now flows through Event.Output). On v2 the handler received an empty input and the test failed. Switch to ev.Output, matching the canonical HITL tests in workflowagent. * runner: add dynamic-orchestrator dedup+resume HITL integration test Adds TestRunner_WorkflowHITL_DynamicOrchestrator_DedupAndResume covering the end-to-end acceptance scenario for b/515644762: a dynamic orchestrator runs two children via RunNode, the second suspends on a HITL interrupt, and on resume the first child must be served from cache (not re-executed) while the second observes the user's response. * Context uni cleanup (#1058) * refactor(workflow) Context unification NodeContext with InvocationContext added to commonContext (#1008) Context unification * Cleaning up agent * Cleaning up examples * Cleaning up internal * Cleaning up tool * Cleaning up workflow * Removed workflow.newDynamicNodeContext, workflow.newNodeContext * Cleaning up workflow * Various fixes * Comments * Removed TODO * Cleaned commented code, added TODO * Fix for context with subschedules * Post merge fixes * Revert "feat: add tool deferred response (#997)" This reverts commit1525386178. * Revert "feat(runner): HITL via long-running interrupts with history rehydra… (#960)" This reverts commit18be97632d. * Revert "feat: remove unused TriggeredBy() interface method (#841)" This reverts commit16a3ff1dae. * Allow to use Path and OutputForAncestors for tool context wrapper * Fixed missing SubScheduler for context created for agent. * Linter fixes * Removed binary & comments --------- Co-authored-by: wolo <wolo@google.com> Co-authored-by: Dmitry Pasiukevich <20398573+dpasiukevich@users.noreply.github.com> * test(workflow): regression tests for PR #1046 alignment fixes (#1069) * test(workflow): add regression tests for PR #1046 alignment fixes Adds the regression coverage that #1046 (fix(workflow): Align Go workflows with Python ADK) deferred: - wrappedSession: the single_turn seed event is visible in the prompt view but is NOT persisted to the underlying session history. - FunctionNode: a *genai.Content / genai.Content return populates event.Content (renderable), not event.Output; non-Content returns still go to event.Output. - Runner: a MessageAsOutput event is yielded with Output cleared (no duplicated model text) while Content survives. * docs(agent): document StrictContextMock as alternative for v2 context migration (#1072) ## Problem The v2 migration guide (`README-v2.md`) documents only the manual way to fix mock contexts after `ToolContext` and `CallbackContext` are unified into a single `Context`: hand-add each missing method to every mock. This is reactive — every time the context surface grows, mocks break again and must be patched, and the example asserts against `agent.CallbackContext`, a transitional alias users shouldn't rely on. ## Solution Add a section documenting `agent.StrictContextMock` (shipping with the v2 release) as the recommended alternative for OSS migrators: - Embed `agent.StrictContextMock` and override only the methods a test uses; embedders keep compiling as the unified interface grows. - Un-overridden methods panic with "not implemented" (loud failures instead of silent zero values); `Deadline`/`Done`/`Err`/`Value` delegate to the supplied `Ctx`. - Assert against the unified `agent.Context` directly, with an explicit note that `CallbackContext`/`ToolContext` are transitional aliases — migrate straight to `agent.Context`. The existing manual approach is kept for reference. * docs(v2): document session.NewEvent context.Context requirement (#1080) Problem: The v2 breaking-changes guide (README-v2.md) does not document that session.NewEvent now takes a context.Context as its first argument. Callers migrating to v2 have no guidance on how to update their call sites. Solution: Add a "Breaking changes" section describing the new NewEvent(ctx, invocationID) signature, why the change was made (event ID and timestamp flow through the platform package for deterministic, replay-safe events), removal of the parameterless form and the temporary NewEventWithContext helper, plus a before/after migration snippet. * fix(conformance): Align cross-language event attribution and replay m… (#1074) * fix(conformance): Align cross-language event attribution and replay matching * test fix * fix: keep position of the seed event (#1082) This is only needed for single_turn agents. Before this commit, the seed event in the wrappedSession was always appended at the end, which hid any subsequent events if for example single_turn agent produces multiple function calls. Now the seed event is injected at the wrappedSession creation time and has a fixed position. I've also added a regression test for this case. * feat(otel): Instrument dynamic workflows. (#947) Problem: Children executed via workflow.RunNode inside a DynamicNode were not instrumented, so RunNode-driven delegation was invisible in the trace tree. Solution: dynamicSubScheduler.runNode now opens an invoke_node span per delegated child, nested under the dynamic node's span, and records genuine runtime failures while skipping expected control flow (context.Canceled, ErrNodeInterrupted) to match the top scheduler. The telemetry schema test case was replaced (WorkflowChainCase → WorkflowDynamicCase) to assert the nested span shape. * fix(llmflow): don't end invocation on a thought-only model turn (#1086) A model turn whose content carries only thought / thought-signature parts has no function call/response and is not partial, so IsFinalResponse() reported it as final and the run loop stopped after that single model call — the real answer never surfaced. adk-python parity. Treat a thought-only turn as non-final in the LLM run loop and call the model again so the answer is produced. Adds a unit test for the isThoughtOnlyTurn helper and a run-loop regression test. * fix(adkrest): round-trip v2 workflow fields through REST event model (#1087) The REST event wire model dropped session.Event fields added in v2 (IsolationScope, Routes, RequestedInput, and Actions.RequestedToolConfirmations), so events mapped to the REST model and back silently lost them. Add the missing fields to the REST Event/EventActions structs and map them in both FromSessionEvent and ToSessionEvent. Adds a full round-trip test covering all four fields. * fix(llmagent): stop maybeSaveOutputToState from stamping event.Output (#1081) maybeSaveOutputToState wrote both StateDelta and event.Output, so an OutputKey agent run as a node produced a second output-carrying event that competed with the node's own output, tripping the scheduler's one-output-per-node rule (ErrMultipleOutputs). Write only the state_delta, matching adk-python __maybe_save_output_to_state; the node wrapper remains the single source of event.Output. Tests: unit guard (Output stays nil), a workflow integration test (function call then text), and a runner integration test (single_turn agent with OutputKey + OutputSchema). * refactor(session): remove usage of deprecated NewEvent (#1065) * refactor(session): migrate call sites to NewEventWithContext Switch all call sites (production, examples, and tests) from the deprecated session.NewEvent to session.NewEventWithContext so that platform-installed time and UUID providers are honored. For call sites without a context in scope (synthesizeTaskFREvent and a few test helpers) the surrounding context is threaded through or t.Context() / context.Background() is used. Because NewEventWithContext now reads providers from the context, workflow's MockInvocationContext literals must set an embedded context; the ones created without it (function_node_test.go) previously left it nil, which panicked once those nodes started calling NewEventWithContext. They now seed Context: t.Context(). NewEvent itself (and its dedicated test) is intentionally left in place; its removal will follow in a separate PR. * fix(console): surface terminal node output and trim input newline (#1091) The console launcher only rendered model content (Event.LLMResponse.Content), so a workflow whose terminal node returns a value via Event.Output (e.g. a FunctionNode) printed an empty "Agent ->". Console input also kept the trailing newline ReadString returns, leaking it into string outputs (e.g. "HELLO\n IS AWESOME!") — unlike the web UI, which submits without one. Trim the trailing line terminator from input (matching the web UI), and when a turn streams no model content, surface the terminal Event.Output (strings as-is, other types as compact JSON). Content-bearing turns are unchanged, so agent replies are never duplicated. * fix(workflow): synthesize node output only from the node agent's own events (#1089) * fix(workflow): synthesize node output only from the node agent's own events A classic composite agent (Sequential/Parallel/Loop) wrapped as a single graph node streams one final response per sub-agent. AgentNode synthesized Event.Output (and NodeInfo.MessageAsOutput) on every final-response event, so each sub-agent reply became a node output and tripped the scheduler's one-output-per-node rule: workflow: node produced multiple events with output values; only one event per execution can carry output: node "<name>" Gate synthesis to the node agent's own events (empty author, or author == the node agent's name); sub-agent events carry the sub-agent's name and now pass through untouched. Mirrors adk-python, where a node's output is derived only from the node agent itself (run_llm_agent_as_node), not its sub-agents. The task-mode exclusion and single-agent output extraction are preserved. Verified against the adk-python conformance goldens: graph_015 (SequentialAgent as node) and graph_017 (LoopAgent as node) now pass, and graph_016 (ParallelAgent as node) no longer hits the multiple-output error. * chore(workflow): remove dead commented-out debug code (#1099) Drop leftover commented-out debugging statements (logContext/log.Printf calls referencing a helper that no longer exists) and the disabled NodeContext "stash" experiment in both the dynamic and static schedulers, along with the now-orphaned EXPERIMENTAL comments that only described that removed code. * fix(workflow): don't mark cancelled delegated-child spans as errors (#1084) The invoke_node span emitted for a RunNode-delegated child marked context.Canceled with an Error status. The returned NodeRunError wraps the child error via "%w: %v", which flattens context.Canceled out of the error chain, so the span guard's errors.Is(err, context.Canceled) never matched. Parent cancellation is expected control flow; the top scheduler already skips it, so the dynamic path diverged. Classify the span status on the raw, unwrapped child/emit error while still recording the wrapped error (with its full message) on the span. The propagated error keeps its "%w: %v" shape, so the scheduler's own error classification stays unchanged. Also covers the emitUp error path. Adds a regression test asserting a cancelled delegated child's span is not marked Error while a genuine failure still is. * refactor(session)!: NewEvent takes a context.Context (#1073) Make session.NewEvent context-aware and drop the temporary NewEventWithContext helper. NewEvent now reads the event ID and timestamp through the platform package, so time/UUID providers installed on the context (platform.WithTimeProvider / platform.WithUUIDProvider) are honored, enabling deterministic, replay-safe events. All call sites (production, examples, tests) now pass the in-scope context. Helpers that lacked one (synthesizeTaskFREvent and a couple of test helpers) thread it through from their callers or use t.Context(); workflow MockInvocationContext literals now seed an embedded context. BREAKING CHANGE: session.NewEvent(invocationID) becomes session.NewEvent(ctx, invocationID) and session.NewEventWithContext is removed. See README-v2.md for migration guidance. * chore(examples): use gemini-flash-latest alias for standard text models (#1102) Replace pinned model versions (gemini-3.1-flash-lite, gemini-3.5-flash, gemini-2.5-flash) in the examples with the gemini-flash-latest alias so the samples track the latest standard Flash model without manual version bumps. Modality-specific examples are intentionally left on their specialized models, since gemini-flash-latest does not serve those API surfaces: - bidi/ (Live API): gemini-3.1-flash-live-preview - bidi/sequential (native audio): gemini-2.5-flash-native-audio-preview-12-2025 - vertexai/imagegenerator (image output): gemini-3.1-flash-image-preview * fix(workflow): scope HITL resume rehydration to the run's invocation (#1095) * fix(workflow): scope HITL resume rehydration to the run's invocation A workflow HITL agent could not be re-run in the same session: after a full pause/resume cycle, a second run that reused a stable InterruptID collided with the prior run's resolved interrupt and failed with "no waiting node matched the supplied responses". Scope rehydration to one logical run (ReconstructRunState and its scans skip events from other invocations) and reuse the paused run's invocation ID on resume, matching adk-python. Update the HITL examples to use a unique InterruptID per request and document the contract. Add console and REST integration tests plus a runner regression test covering two full cycles in one session. * test(workflow): drive HITL rerun scope test through ReconstructRunState The prior TestScanHistory_InvocationScope pinned the unscoped leak (0 unresolved interrupts) as an expected outcome, locking in buggy behavior on a non-production code path. Rewrite it as TestReconstructRunState_InvocationScope: exercise the public resume API with two invocations in one session, asserting the fresh run stays waiting while the earlier run resolves. * test(hitl): add console and REST integration tests for session reruns (#1098) Cover two full pause/resume cycles in one session end-to-end: the console flow through the real launcher helpers, and the REST API the Dev UI uses. The REST test also asserts each pause carries a distinct interrupt ID, the invariant the Dev UI relies on to re-prompt. Both fail without invocation-scoped resume rehydration. * docs(examples): add complex workflow research pipeline sample (#1101) Add examples/workflow/complex, a blog-ready sample for the graph workflow engine. Three researcher agents fan out from Start and run concurrently, a JoinNode barrier gathers their findings, a FunctionNode formats the gathered map into one prompt, and a single-turn synthesis agent merges everything into one report. Uses the default in-memory session, per-node retries, and the built-in Google Search tool. The README documents the fan-out console behavior (all three summaries print before the synthesis). * refactor(agent): remove context type aliases in favor of agent.Context (#1100) * refactor(agent): remove context type aliases in favor of agent.Context Drop the four context type aliases so a single context type is used across callbacks, tools, and workflow nodes: - CallbackContext, ToolContext (agent/context.go) - tool.Context (tool/tool.go) - workflow.NodeContext (workflow/node_context.go deleted) Convert all usages to the unified agent.Context. Also reword stale alias names left in doc comments, wrapper log.Print messages, and test failure strings to role wording ("callback context", "tool context", "node context") or agent.Context. No behavior change. Constructors (NewCallbackContext, NewToolContext, NewNodeContext, NewDynamicNodeContext) and the internal callbackContextWrapper / toolContextWrapper types are intentionally kept -- they are construction/implementation machinery, not aliases. * refactor(tool): drop the deprecated tool.NewToolContext wrapper tool.NewToolContext is a thin, deprecated wrapper that only delegates to agent.NewToolContext and has no callers in the repo (all sites already use agent.NewToolContext directly). With the context type aliases gone, remove this wrapper as well. This also drops the last stale "ToolContext" type-name reference left in its doc comment. No behavior change. * test(workflow): fold orphaned node-context propagation test into run_node_test.go This change deletes workflow/node_context.go, which left node_context_propagation_test.go named after a source file that no longer exists. The test really exercises RunNode propagation of context to a dynamic-node child, so move it (renamed to TestRunNode_DynamicChildRecoversOwnContext) together with its fnNode helper into run_node_test.go and drop the orphaned file. No behavior change. * test(workflow): rename node_context_test.go to reflect what it tests With workflow/node_context.go gone, node_context_test.go was named after a source file that no longer exists. Its tests cover construction of node contexts via agent.NewNodeContext / agent.NewDynamicNodeContext, so rename it to node_context_construction_test.go. Content is unchanged. * test(agent): move node-context construction tests into the agent package NewNodeContext / NewDynamicNodeContext live in the agent package, so their construction tests belong there too. They previously sat in workflow only because they reused workflow's unexported newMockCtx and newDynamicSubScheduler helpers; an internal agent test cannot import workflow (workflow imports agent, which would be an import cycle). Rewrite them against the agent package's own invocationContext and a small stubSubScheduler implementing the agent-owned DynamicSubScheduler interface, add them as agent/node_context_test.go, and delete the workflow copy. No behavior change. * docs: update stale context-alias references to agent.Context Complete the alias removal by fixing references the code change missed: update the tool.Context snippet in examples/bidi/README.md, the agent.CallbackContext assertion and prose in README-v2.md, and two doc comments in internal/context/callback_context.go to use agent.Context / "callback context" wording. * docs: drop stale CallbackContext/ToolContext alias notes in README-v2 The transitional aliases have been removed, so remove the claims that they "became aliases to Context" and "have been removed" — the latter has no reference point for a v2 migrator. Just point readers at the unified agent.Context. * Update main.go * Update main.go * docs: replace internal go link with public context package docs The go/how-to-use-a-context shortlink does not resolve outside Google. Cite the public pkg.go.dev/context documentation instead. --------- Co-authored-by: Karol Droste <kdroste@google.com> * docs: add agent collaboration example (#1083) * Clean context (#1094) * TODOS * Introduced deltas and Apply * clean NewContext * Updated agent.go * Minimized NewNodeContext * Wider Apply * Fixed mock * Removed NewDynamicContextNode * Order for context in dynamic_scheduler * Removed NodeContext * Updated callback_context_wrapper * Removed unused code * Removed clean_context * Removed branch override * Renamed Apply to WithDelta * Deleted NewContextForAgent * Some renames * Removed NewNodeContext * Removed WithBranch * Minor cleanups * Minor fixes * Comments & removed TODOs * Comments about TODOs * Simplifications * Merge - removed extra file * docs(runner): fix stale newAgentContext sub-scheduler comment The comment described the pre-refactor design where a fresh InvocationContext was built and the sub-scheduler re-stashed in the value chain. With the copy-and-modify WithDelta approach the context is no longer fresh: it copies the parent and overrides only Agent/UserContent (resetting Path/RunID/ OutputForAncestors), so the sub-scheduler is carried over unchanged by the copy rather than dropped and re-stashed. Update the comment to match. Also drop a leftover commented-out line in NewCleanToolContext (agent/common_context.go) that referenced the removed newCleanToolContext method. * Fixes from the review --------- Co-authored-by: wolo <wolo@google.com> * fix(examples): keep complex workflow from hanging on flaky Google Search (#1105) * fix(examples): keep complex workflow from hanging on flaky Google Search The complex research pipeline fanned three Google Search-grounded researchers into a JoinNode barrier. Gemini's Search grounding intermittently stalls with neither a response nor an error, and the example had no per-call deadline, so a single stalled researcher hung the whole run forever. The per-node RetryConfig did not help: it only retries calls that *return* an error, and a hang returns nothing. Add a resilientModel decorator (model.LLM) that wraps each agent's model and: - bounds every call with a per-attempt context timeout, turning a stall into a context.DeadlineExceeded instead of an infinite wait; - retries timed-out/errored attempts with exponential backoff; - fails open after exhausting attempts, returning a short fallback message instead of an error so the JoinNode still fires and the synthesis agent still produces a report (graceful degradation). Wire it into every researcher (30s/3 attempts, topic-specific fallback) and synthesis (60s/2 attempts, minimal-report fallback), drop the misleading node RetryConfig, and refresh the docs/comments. * docs(examples): tighten verbose comments in complex workflow * Fix artifact version create time format (#1088) * fix: ArtifactVersion.CreateTime as int64 instead of float64 * Update CreateTime type to time.Time in ArtifactVersion Changed CreateTime from int64 to time.Time for better datetime handling. * Change CreateTime to use timestamp directly * empty commit to rerun checks --------- Co-authored-by: Karol Droste <kdroste@google.com> * chore(release)!: migrate module path to google.golang.org/adk/v2 (#1106) * chore(release)!: migrate module path to google.golang.org/adk/v2 Go's semantic import versioning requires a /v2 path suffix for modules at major version v2+. Without it, `go get google.golang.org/adk/v2@v2.0.0` fails after the v2.0.0 tag is pushed. This is a pure, mechanical rename (no net change): - go.mod: module google.golang.org/adk -> .../adk/v2 - all .go import paths and doc comments -> .../adk/v2 - .golangci.yml: goimports local-prefixes + gofumpt module-path - README.md (pkg.go.dev badge, `go get`) and AGENTS.md references The pre-existing a2a compat subpackages named `v2` (agent/remoteagent/v2, server/adka2a/v2) correctly become .../adk/v2/agent/remoteagent/v2 and .../adk/v2/server/adka2a/v2. Rebased onto v2 @ #1105 so the new examples/workflow/complex files (main.go, resilient_model.go) are migrated too. DoD: go build, go vet, `go test -race -shuffle=on ./...` (0 failures), golangci-lint (0 issues), and `go mod tidy -diff` (empty) all pass. * ci: re-trigger lint with fresh golangci-lint cache The previous run restored a poisoned golangci-lint analysis cache that produced non-deterministic SA5011 false positives. The stale cache entry has been deleted; this empty commit triggers a fresh analysis. * fix lint errors * bump version --------- Co-authored-by: Mikalai Senkevich <hanorik@google.com> Co-authored-by: Dmitry Pasiukevich <20398573+dpasiukevich@users.noreply.github.com> Co-authored-by: RKest <68107862+RKest@users.noreply.github.com> Co-authored-by: Karol Droste <kdroste@google.com> Co-authored-by: João Westerberg <westerberg@google.com> Co-authored-by: Nilesh Das <nilesh101das@gmail.com>