Skip to content

Wolo/context unification#971

Draft
wolo-lab wants to merge 7 commits into
wolo/output-validation-finalizefrom
wolo/context-unification
Draft

Wolo/context unification#971
wolo-lab wants to merge 7 commits into
wolo/output-validation-finalizefrom
wolo/context-unification

Conversation

@wolo-lab
Copy link
Copy Markdown

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

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: #issue_number
  • Related: #issue_number

2. Or, if no issue exists, describe the change:

If applicable, please follow the issue templates to provide as much detail as
possible.

Problem:
A clear and concise description of what the problem is.

Solution:
A clear and concise description of what you want to happen and why you choose
this solution.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes.

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Please include a summary of passed go test results.

Manual End-to-End (E2E) Tests:

Please provide instructions on how to manually test your changes, including any
necessary setup or configuration. Please provide logs or screenshots to help
reviewers better understand the fix.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Add any other context or screenshots about the feature request here.

@wolo-lab wolo-lab force-pushed the wolo/output-validation-finalize branch 2 times, most recently from bb51d73 to 96a1101 Compare June 8, 2026 08:21
wolo-lab added 7 commits June 8, 2026 08:22
…urface

Foundation for context unification (folding the workflow NodeContext
into the unified agent.Context, per go/orcas-rfc-580 and the adk-python
model where Context wraps an InvocationContext and carries tool,
callback, and workflow-node state).

- agent.Context gains Path() and RunID() accessors. They are meaningful
  for workflow graph nodes and return "" for plain tool/callback
  contexts.
- commonContext (the single concrete Context impl) now forwards the
  remaining InvocationContext methods (Agent, Memory, Session,
  RunConfig, EndInvocation, Ended, ResumedInput, WithContext) to its
  wrapped InvocationContext, so a single unified Context value also
  satisfies InvocationContext. This lets one value flow wherever an
  InvocationContext is expected, mirroring adk-python's Context.
- Path()/RunID() forward to the wrapped InvocationContext when it
  carries node information, else return "".
- callbackContextWrapper forwards the two new accessors.

Purely additive: no signature changes yet. The Node.Run switch to
agent.Context comes in a follow-up commit.
Second additive step of context unification. Introduce the agent-level
abstractions that let the unified Context carry workflow-node state and
schedule dynamic children, without the agent package importing workflow.

- NodeScheduler: an opaque interface (implemented by the workflow layer)
  stored on Context, the Go analog of adk-python's ScheduleDynamicNode
  protocol carried on Context.
- NodeRunOptions: plain data describing a dynamic child run (run id,
  sub-branch, override branch, use-as-output), so agent can describe the
  call without importing workflow's functional option type.
- agent.Context gains NodeScheduler() alongside the previously added
  Path()/RunID().
- commonContext gains node-state fields (path, runID, resumeInputs,
  nodeScheduler); Path/RunID/ResumedInput/NodeScheduler now read them,
  and WithContext preserves them.
- NewNodeContext(ic, path, runID, resumeInputs, sched, actions) builds
  the unified Context for a workflow node activation (full Context +
  satisfies InvocationContext), with artifact-delta tracking.
- callbackContextWrapper forwards NodeScheduler().

Still additive: no existing callers changed, nothing constructs a node
context via the new path yet. The workflow wiring and the Node.Run
signature switch follow.
Complete the context unification (go/orcas-rfc-580) for workflow nodes:
Node.Run now receives the unified agent.Context instead of a separate
workflow.NodeContext, matching adk-python where BaseNode.run takes the
unified Context (which wraps an InvocationContext and carries tool,
callback, and workflow-node state).

agent package:
- agent.Context now embeds InvocationContext, so a single unified value
  exposes the full invocation surface (Agent/Memory/Session/RunConfig/
  ...) plus callback, tool, and node accessors. commonContext and
  callbackContextWrapper forward the InvocationContext methods.
- The node surface (Path, RunID, NodeScheduler) and NewNodeContext were
  added in the preceding commits; this commit makes the workflow layer
  build and use them.

workflow package:
- Node.Run signature: agent.InvocationContext -> agent.Context, across
  the Node interface and every node implementation (Function, Tool,
  Agent, Join, Dynamic, Workflow, ParallelWorker, Start).
- Removed workflow.NodeContext / nodeContext / newNodeContext /
  newDynamicNodeContext. The per-node context is now built via
  agent.NewNodeContext.
- DynamicFn takes agent.Context. RunNode takes agent.Context and reaches
  the scheduler via ctx.NodeScheduler() instead of a *nodeContext type
  assertion.
- dynamicSubScheduler implements agent.NodeScheduler (ScheduleNode),
  adapting agent.NodeRunOptions to the internal runNodeOptions; it keeps
  the workflow-only state (outputForAncestors, resume inputs) that must
  not live on the agent package.
- The Go-context bridge now carries a *nodeBridge (the node's
  agent.Context plus workflow-only outputForAncestors/resumeInputs).
  NodeContextFromGoContext returns agent.Context. The bridge is stashed
  on static activations, dynamic children, and the dynamic orchestrator
  body so tools can recover a node context whose NodeScheduler can
  RunNode. Each bridge is pointed at the final (rewrapped) context.

Tests/examples updated for the new signatures; mock contexts gain the
InvocationContext methods now required of agent.Context. The
context_test assertion that a CallbackContext is not an
InvocationContext is inverted to match the unified design.
Adjust the context unification to match adk-python more closely: the
unified agent.Context now WRAPS an InvocationContext and is deliberately
NOT itself an InvocationContext, instead of embedding it.

Rationale: embedding made agent.Context an InvocationContext (IS-A),
exposing the full invocation surface (EndInvocation, WithContext,
RunConfig, ...) on every callback/tool/node context — broader and more
surprising than adk-python, whose Context(ReadonlyContext) holds an
InvocationContext as a private field and re-exposes only selected
properties.

Changes:
- agent.Context no longer embeds InvocationContext. It adds
  InvocationContext() (the accessor, analogous to Python's
  get_invocation_context()) plus the selected surface node bodies need
  (Agent, Memory, Session, RunConfig, Ended, ResumedInput) and a
  WithContext that returns Context (not InvocationContext).
- commonContext / callbackContextWrapper expose InvocationContext() and
  no longer satisfy InvocationContext; WithContext returns Context.
- Workflow call sites that need the raw InvocationContext (withBranch,
  NewNodeContext, NewToolContext, NewRequestInputEvent, startNodeSpan,
  sub-workflow RunNode, FunctionNode user fn) now call
  ctx.InvocationContext().
- Tests/examples/mocks updated: reentry/hitl callbacks that read
  ResumedInput take agent.Context (so resume payloads on the wrapper are
  visible); mock contexts expose InvocationContext() and a
  Context-returning WithContext. The context_test assertion confirms a
  CallbackContext is NOT an InvocationContext (the two are now provably
  disjoint via differing WithContext return types).

Full build and tests pass (the A2A cleanup test is pre-existing flaky
under full-suite load; it passes in isolation).
Remove the redundant resumeInputs field from nodeBridge. Resume
payloads already ride on the unified agent.Context; expose them via a
new ResumeInputs() accessor (mirrors adk-python's Context.resume_inputs)
so the dynamic node reads ctx.ResumeInputs() instead of carrying a
second copy on the bridge.

nodeBridge now carries only:
- ctx: the node's agent.Context, recovered by tools running inside an
  LlmAgent node (NodeContextFromGoContext) to reach the NodeScheduler —
  the essential reason the bridge exists.
- outputForAncestors: workflow-only output-delegation state that cannot
  live on agent.Context (the agent package must not import workflow).

agent.Context gains ResumeInputs() map[string]any (commonContext +
callbackContextWrapper). Mock contexts in tests implement it.

HITL resume (hitl_simple, dynamic/hitl) verified end-to-end; full build
and tests pass.
Tighten visibility of the workflow-only scheduler so it is not part of
the public agent API, mirroring adk-python where _workflow_scheduler is
a private Context field touched only by run_node.

V1 — off the interface:
- Remove NodeScheduler() from the agent.Context interface. Implementers
  (including every test mock) no longer need it; the public Context
  surface for nodes is now just Path() and RunID() (both public in
  adk-python too).

V3a — types out of the public agent package:
- Move the NodeScheduler interface and NodeRunOptions struct from agent
  to package workflow (their natural home).
- commonContext now carries the scheduler as an opaque `any` token
  (agent never calls it; it only stores and returns it). NewNodeContext
  takes `sched any`. The concrete commonContext.NodeScheduler() any
  accessor remains exported so package workflow can recover the token,
  but it is not on the Context interface.
- workflow.RunNode recovers the token via a local capability interface
  (nodeScheduled{ NodeScheduler() any }) and asserts it to
  workflow.NodeScheduler.

Net: agent's public API has no scheduling concepts; the scheduler is an
opaque token agent cannot interpret — as close to Python's private
field as Go allows across a package boundary. The 4 test mocks drop
their NodeScheduler() method. Full build, vet, tests, and the dynamic /
HITL samples pass.
Seal the public API: NodeContextFromGoContext was the only exported
symbol of the node-context bridge, and it is an internal mechanism
(consumed only by single_turn_tool to reach the surrounding node's
scheduler). Making it unexported means a future full bridge removal /
event-architecture change touches no public symbol.

- NodeContextFromGoContext -> nodeContextFromGoContext (unexported).
- RunNode now recovers the scheduler internally via schedulerFor(ctx):
  it first checks whether ctx itself carries a scheduler (a dynamic node
  body), then falls back to the node context stashed on ctx's embedded
  Go context (the tool/callback-context-inside-a-node case). The bridge
  recovery is now an implementation detail of RunNode.
- single_turn_tool calls workflow.RunNode(toolCtx, ...) directly instead
  of recovering the node context itself.

Behavior is unchanged (single_turn agent-as-node still propagates child
events through the orchestrator). The public workflow API no longer
exposes any bridge recovery; only RunNode + the Mode:single_turn
configuration remain user-facing. Tests in package workflow use the
unexported name; full build and tests pass.
@wolo-lab wolo-lab force-pushed the wolo/context-unification branch from 5d2c70c to 1190931 Compare June 8, 2026 08:26
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