Skip to content

feat(workflow): park parent on WaitForOutput child with no output#986

Draft
wolo-lab wants to merge 6 commits into
v2from
wolo/wait-for-output
Draft

feat(workflow): park parent on WaitForOutput child with no output#986
wolo-lab wants to merge 6 commits into
v2from
wolo/wait-for-output

Conversation

@wolo-lab

@wolo-lab wolo-lab commented Jun 8, 2026

Copy link
Copy Markdown

Problem
NodeConfig.WaitForOutput existed but was never honored — no production
code read it. A child run via RunNode that opts into WaitForOutput but
finishes without producing output (e.g. an LlmAgent task/chat node that
delegated to a sub-agent and has not yet yielded its final answer) would
return the zero value, falsely completing the parent instead of waiting.
adk-python handles this with ctx.run_node(raise_on_wait=True); adk-go
had no equivalent.

Solution
RunNode now honors WaitForOutput. When a child opts in and emits no
output (and is not interrupting), the parent is parked with
ErrNodeInterrupted instead of completing, so it re-runs and re-invokes
RunNode once the child can produce output. Mirrors adk-python's
raise_on_wait. Output presence is tracked via a hasOutput flag (nil is
a valid output, distinct from "no output"), and a waitsForOutput helper
reads the tri-state NodeConfig.WaitForOutput.

This is the RunNode slice of WaitForOutput; the static-scheduler /
resume parity is deferred (Go fan-in already works via JoinNode's
predecessor-completion barrier, so no current consumer needs it).

wolo-lab added 5 commits June 8, 2026 11:30
…sOutput

Add NodeInfo.MessageAsOutput: when set and Event.Output is nil, readers
derive the node's output from the event's model text. The static and
dynamic schedulers both honor it (Output wins, message text is the
fallback), mirroring adk-python's _track_event_in_context. Empty text is
a valid output, matching python; AgentNode's own empty-text-skips
behavior is unchanged.

This lets a delegated child whose message IS its output (e.g. an LlmAgent
node) promote its text to the parent via WithUseAsOutput, and feeds it to
a successor on a normal handoff.
Complete the MessageAsOutput feature to match adk-python's single
mechanism, where a node sets Event.Output and NodeInfo.MessageAsOutput
together:

- Producer: synthesizeAgentOutput now stamps NodeInfo.MessageAsOutput
  when it derives output from model text (mirrors
  process_llm_agent_output), so the flag is present in history.
- Resume reader: collectNodeOutputs derives a node's output from the
  model message when an event is flagged MessageAsOutput and carries no
  explicit Output (mirrors _reconstruct_node_states' use_message_as_output),
  so a message-as-output node recovers its output on resume.

Previously the flag was only read (live, via childEventOutput) but never
produced and never consumed on resume.
Adopt adk-python's delegation model, replacing the v2 re-emit approach
(revises #920). When a WithUseAsOutput child delegates the parent's
output, the child's own event now carries the output up and the parent
emits no terminal event (full suppression, mirroring
_output_delegated). Previously the child's event was dropped and the
parent re-emitted the delegated value, which could not support
output_for attribution and lost the value when the orchestrator body
returned nil.

Also stamp NodeInfo.Path on a child event that set NodeInfo without a
Path (e.g. MessageAsOutput), since such events are now emitted up.

Tests updated to assert the child event carries the delegated output.
Add OutputFor to session.NodeInfo and stamp it on a delegated child's
output event with the whole delegation chain ([childPath, parentPath,
...ancestors]), mirroring adk-python's node_info.output_for /
_enrich_event. outputForAncestors is threaded through RunNode
(WithUseAsOutput) so a multi-level chain (grandchild -> parent -> top)
records every ancestor on the single output event.

On resume collectNodeOutputs attributes the output to the static owner
of each OutputFor path, so a delegating ancestor recovers its output
without re-emitting (output_for parity).

Also fixes nested dynamic-node path composition: a child context already
carries the full "<parent>/<name>@<runID>" path, so composePath uses it
as-is instead of appending the name again (surfaced by multi-level
delegation).
@wolo-lab wolo-lab self-assigned this Jun 8, 2026
@wolo-lab wolo-lab changed the title feat(workflow): park parent when a WaitForOutput child yields no output feat(workflow): park parent on WaitForOutput child with no output Jun 8, 2026
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.
@wolo-lab wolo-lab force-pushed the wolo/wait-for-output branch from 2a4fd24 to 70e6d52 Compare June 8, 2026 20:38
Base automatically changed from wolo/output-for to v2 June 9, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant