Skip to content

feat(workflow): add NewEmittingFunctionNode for streaming and HITL function node#1000

Open
wolo-lab wants to merge 9 commits into
v2from
wolo/emitting-function-node
Open

feat(workflow): add NewEmittingFunctionNode for streaming and HITL function node#1000
wolo-lab wants to merge 9 commits into
v2from
wolo/emitting-function-node

Conversation

@wolo-lab

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

Copy link
Copy Markdown

Problem
A FunctionNode emits exactly one event built from its return value, so
a plain function body cannot emit a HITL RequestInput, a routing event
(Event.Routes), or progress updates. Authors who needed any of these had
to drop down to a custom workflow.Node (BaseNode subclass) just to call
yield. adk-python supports this directly via generator-style function
nodes; adk-go had no equivalent.

Solution
Add NewEmittingFunctionNode / NewEmittingFunctionNodeWithSchema taking
an EmittingFunctionFn[IN, OUT] whose body receives an emit callback. The
shape and HITL contract mirror DynamicFn: emit any number of intermediate
events, and return ErrNodeInterrupted after a RequestInput to pause. No
scheduler/resume/persistence changes are needed — the existing
pause/handoff machinery applies unchanged. NewFunctionNode is untouched.

Simplifies five workflow examples (hitl_simple, dynamic/hitl, routing
int/string/llm) that previously hand-rolled a BaseNode subclass solely to
emit a single custom event.

@wolo-lab wolo-lab force-pushed the wolo/emitting-function-node branch 4 times, most recently from 846ada6 to 5e6faa8 Compare June 10, 2026 16:21
@wolo-lab wolo-lab changed the title feat(workflow): add NewEmittingFunctionNode for streaming/HITL functi… feat(workflow): add NewEmittingFunctionNode for streaming and HITL function node Jun 10, 2026
@wolo-lab wolo-lab force-pushed the wolo/emitting-function-node branch 2 times, most recently from 0233ecc to 93951e7 Compare June 10, 2026 17:32
…on 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.
@wolo-lab wolo-lab force-pushed the wolo/emitting-function-node branch from 93951e7 to c3a77c3 Compare June 10, 2026 18:21
@wolo-lab wolo-lab requested a review from kdroste-google June 10, 2026 18:22
@wolo-lab wolo-lab marked this pull request as ready for review June 10, 2026 18:22
Comment thread workflow/function_node.go Outdated
Comment thread workflow/function_node.go Outdated
Comment thread workflow/function_node.go
yield(nil, err)
return
}
if output == nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a no-op function there is no trace of completion. To be considered: emit an event with invocation id and no content

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. It makes sense. I've updated it for dynamic_nodes as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will handle it in a follow-up PR as it is a bigger change.

@wolo-lab wolo-lab force-pushed the wolo/emitting-function-node branch from e2681cf to 1b4f4e9 Compare June 12, 2026 12:49
wolo-lab added 4 commits June 12, 2026 14:24
…node

# Conflicts:
#	examples/workflow/dynamic/hitl/main.go
#	examples/workflow/hitl_simple/main.go
#	examples/workflow/routing/int/main.go
#	examples/workflow/routing/llm/main.go
#	examples/workflow/routing/string/main.go
#	workflow/function_node.go
examples/quickstart/main was a 35MB ELF build artifact tracked in the
repo (alongside its main.go source). Remove it; the source is unaffected.
…ode 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).
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).
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