Skip to content

feat(workflow): RunNode WithUseAsOutput for child -> parent output delegation#920

Merged
wolo-lab merged 5 commits into
v2from
wolo/run_node_use_as_output
Jun 8, 2026
Merged

feat(workflow): RunNode WithUseAsOutput for child -> parent output delegation#920
wolo-lab merged 5 commits into
v2from
wolo/run_node_use_as_output

Conversation

@wolo-lab
Copy link
Copy Markdown

@wolo-lab wolo-lab commented May 29, 2026

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 LlmAgentis 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.

@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch 3 times, most recently from 5ddf6e4 to 982aa73 Compare May 29, 2026 18:30
@wolo-lab wolo-lab changed the base branch from v2 to wolo/run_node_idempotent_cache May 29, 2026 18:40
@wolo-lab wolo-lab changed the title feat(workflow): WithUseAsOutput + idempotent RunNode dispatch feat(workflow): RunNode WithUseAsOutput for child→parent output delegation May 29, 2026
@wolo-lab wolo-lab changed the title feat(workflow): RunNode WithUseAsOutput for child→parent output delegation feat(workflow): RunNode WithUseAsOutput for child -> parent output delegation May 29, 2026
@wolo-lab wolo-lab force-pushed the wolo/run_node_idempotent_cache branch from 0089191 to b39a686 Compare May 29, 2026 19:09
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch 3 times, most recently from f864f49 to 94fd0ec Compare May 29, 2026 20:03
Base automatically changed from wolo/run_node_idempotent_cache to v2 June 1, 2026 07:21
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch 8 times, most recently from bddb1a1 to 1156e83 Compare June 1, 2026 09:30
@wolo-lab wolo-lab requested a review from hanorik June 1, 2026 09:32
@wolo-lab wolo-lab marked this pull request as ready for review June 1, 2026 09:32
hanorik
hanorik previously approved these changes Jun 1, 2026
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch from 1156e83 to f6ed1c3 Compare June 1, 2026 16:04
@kdroste-google kdroste-google changed the base branch from v2 to main June 3, 2026 08:18
@kdroste-google kdroste-google dismissed hanorik’s stale review June 3, 2026 08:18

The base branch was changed.

@kdroste-google kdroste-google changed the base branch from main to v2 June 3, 2026 08:19
…tion

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.
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch 2 times, most recently from da03c57 to 0d7d9b1 Compare June 5, 2026 11:35
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch 4 times, most recently from 467581b to 3f8bec4 Compare June 5, 2026 12:36
@wolo-lab wolo-lab self-assigned this Jun 5, 2026
@wolo-lab wolo-lab requested a review from hanorik June 5, 2026 12:39
…, 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.
wolo-lab added a commit that referenced this pull request Jun 5, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch from 3f8bec4 to 843dfca Compare June 5, 2026 20:04
wolo-lab added a commit that referenced this pull request Jun 5, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
@wolo-lab wolo-lab changed the base branch from v2 to wolo/cl-workflow June 5, 2026 20:06
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch from 843dfca to 94e6366 Compare June 8, 2026 08:04
wolo-lab added a commit that referenced this pull request Jun 8, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
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.
@wolo-lab wolo-lab force-pushed the wolo/cl-workflow branch from 17aa0ce to 6861671 Compare June 8, 2026 08:13
RunNode gains a per-call WithUseAsOutput() option that promotes a
dynamic child's output to the parent dynamic node's terminal
Event.Output, suppressing the orchestrator body's own return value.
At most one delegating child per parent activation is allowed; a
second attempt returns ErrOutputAlreadyDelegated without invoking
the child.

Builds on the idempotent cache from the previous CL: a WithRunID
replay re-honours the delegation but does not re-run the child.

BUG=515645490
@wolo-lab wolo-lab force-pushed the wolo/run_node_use_as_output branch from 94e6366 to 558781b Compare June 8, 2026 08:16
wolo-lab added a commit that referenced this pull request Jun 8, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
wolo-lab added a commit that referenced this pull request Jun 8, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
wolo-lab added a commit that referenced this pull request Jun 8, 2026
…ution

Add NodeInfo.OutputFor: the node paths an event's Output counts for —
the emitter plus any WithUseAsOutput delegating ancestors. A delegating
child's single event is stamped OutputFor=[child, parent, ...] and flows
up, and the parent no longer re-emits a duplicate terminal output event
(full suppression, matching adk-python's _output_delegated + output_for).
Resume attributes a descendant's output to its delegating ancestors via
OutputFor. Every output event records OutputFor (own path minimum),
mirroring adk-python _enrich_event.

Built on the temp integration branch (#960 + #920 + #966); rebase onto v2
once those merge.
Base automatically changed from wolo/cl-workflow to v2 June 8, 2026 10:27
@wolo-lab wolo-lab merged commit f8bf35e into v2 Jun 8, 2026
3 checks passed
@wolo-lab wolo-lab deleted the wolo/run_node_use_as_output branch June 8, 2026 10:50
wolo-lab added a commit that referenced this pull request Jun 8, 2026
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.
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.

2 participants