Files
893e4a403a V2 release (#1109)
* 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 commit 1525386178.

* Revert "feat(runner): HITL via long-running interrupts with history rehydra… (#960)"

This reverts commit 18be97632d.

* Revert "feat: remove unused TriggeredBy() interface method (#841)"

This reverts commit 16a3ff1dae.

* 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>
2026-06-30 15:26:42 +02:00

99 lines
2.5 KiB
Go

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"errors"
"testing"
"google.golang.org/genai"
"google.golang.org/adk/v2/agent"
)
func TestNew(t *testing.T) {
mockOnUserMsg := func(agent.InvocationContext, *genai.Content) (*genai.Content, error) { return nil, nil }
mockCloseErr := errors.New("close error")
tests := []struct {
name string
cfg Config
validate func(*testing.T, *Plugin)
expectCloseErr error
}{
{
name: "Successfully maps all fields",
cfg: Config{
OnUserMessageCallback: mockOnUserMsg,
},
validate: func(t *testing.T, p *Plugin) {
if p.OnUserMessageCallback() == nil {
t.Error("OnUserMessageCallback was not mapped correctly")
}
// Verify Close() is safe even if we didn't provide a CloseFunc
if err := p.Close(); err != nil {
t.Errorf("Expected nil error from default close, got %v", err)
}
},
},
{
name: "Safety: Handles nil CloseFunc gracefully",
cfg: Config{
// No CloseFunc provided
},
validate: func(t *testing.T, p *Plugin) {
// This should not panic
defer func() {
if r := recover(); r != nil {
t.Errorf("Plugin.Close() panicked with nil CloseFunc")
}
}()
if err := p.Close(); err != nil {
t.Errorf("Expected nil error, got %v", err)
}
},
},
{
name: "Functionality: Executes provided CloseFunc",
cfg: Config{
CloseFunc: func() error {
return mockCloseErr
},
},
validate: func(t *testing.T, p *Plugin) {
err := p.Close()
if err != mockCloseErr {
t.Errorf("Expected error '%v', got '%v'", mockCloseErr, err)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.cfg)
if err != nil {
t.Fatalf("New() returned unexpected error: %v", err)
}
if p == nil {
t.Fatal("New() returned nil Plugin")
}
if tt.validate != nil {
tt.validate(t, p)
}
})
}
}