Skip to content

fix(sdk/graph,scriptnode): repair built-in JS scripts against v0.3 bindings#89

Merged
lIang70 merged 1 commit into
mainfrom
fix/scriptnode-builtin-script-bindings
May 11, 2026
Merged

fix(sdk/graph,scriptnode): repair built-in JS scripts against v0.3 bindings#89
lIang70 merged 1 commit into
mainfrom
fix/scriptnode-builtin-script-bindings

Conversation

@lIang70

@lIang70 lIang70 commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Several built-in JS node scripts under sdk/graph/node/scripts had silently drifted out of sync with the v0.3 binding surface:

  • answer.js referenced stream.emit("answer", ...) — the stream global was retired in v0.3 along with workflow.StreamCallback, so the node threw ReferenceError on every default invocation.
  • approval.js read config.__node_id for approval_request.node_id, but nothing in scriptnode ever injected that reserved key. The field was permanently undefined.
  • iteration.js passed a stale third argument to runtime.execScript, silently dropped child signals, and leaked the previous iteration's __iteration_result when the body skipped setting it.

Approach

Restored emission through the executor's canonical per-node channel and added the missing identity bridge, while keeping each layer aware of only its own concepts:

Concern Layer Where it lives now
Stream emit host.emit(type, payload) engine bindings.NewHostBridge (gains StreamEmitter parameter, backed by ctx.Publisher)
Per-run identity (run.*) agent bindings.NewRunInfoBridge(agent.RunInfo{...}) — unchanged on the script side
Per-step identity (node.id(), node.type()) graph New sdk/graph/node/scriptnode/bridge_node.go (kept out of bindings per its no-graph-deps invariant)
Control plane (host.publish, askUser, checkInterrupt, reportUsage) engine bindings.NewHostBridge

Key invariants preserved:

  • agent.RunInfo stays per-run immutable — no per-step fields added.
  • sdk/script/bindings remains zero-dependency on sdk/graph (see bindings/doc.go).
  • host.emit uses a local StreamEmitter duck-typed interface that graph.StreamPublisher satisfies, so the executor's per-node publisher wiring carries the run/node identity inside the subject without leaking it back through host.

Other fixes folded in

  • iteration.js: clear __iteration_result per iteration, check child signal before pushing, propagate signal.interrupt / error / done to the parent loop.
  • scriptnode/register.go: rename needsShellFSneedsShell and tighten the comment ("only shell is the extra bridge"; fs is unconditional).

Test coverage

New sdk/graph/node/scriptnode/builtins_test.go drives answer / approval / iteration and the new node bridge through ScriptNode with a recording graph.StreamPublisher and an enginetest.MockHost, so future binding renames trip the suite at compile/test time instead of breaking silently in production. bridge_host_test.go gains coverage for host.emit forwarding and the nil-emitter no-op contract.

Test plan

  • cd sdk && go build ./...
  • cd sdk && go test ./... — all packages green
  • Smoke a downstream graph using the answer / approval / iteration nodes once merged

Made with Cursor

…ndings

Several built-in node scripts under sdk/graph/node/scripts referenced
removed v0.2.x globals or otherwise drifted out of sync with the
v0.3.0 bindings:

- answer.js called stream.emit("answer", ...). The stream binding was
  retired in v0.3 along with workflow.StreamCallback, so the script
  threw ReferenceError on every default invocation.
- approval.js read config.__node_id for approval_request.node_id, but
  nothing in scriptnode ever injected that reserved key — the field was
  permanently undefined.
- iteration.js passed a stale third argument to runtime.execScript,
  silently dropped child signals, and leaked the previous iteration's
  __iteration_result when the body skipped setting it.

Fixed by routing emission through the canonical per-node publisher and
adding the missing identity bridge:

- bindings.NewHostBridge gains a StreamEmitter parameter exposing
  host.emit(type, payload) — backed by ctx.Publisher, which the
  executor pre-bakes with runID/nodeID so subjects land on the
  canonical engine.run.<runID>.stream.<nodeID>.delta channel. No
  separate stream bridge: host already speaks engine.Publisher.
- New graph-layer bridge scriptnode/bridge_node.go exposes node.id() /
  node.type(). NodeID is per-step and graph-specific, so the bridge
  lives in scriptnode rather than in bindings (which by design knows
  nothing about graph; see sdk/script/bindings/doc.go).
- agent.RunInfo stays per-run immutable. NewRunInfoBridge is unchanged
  on the script side.
- scriptnode wires NewRunInfoBridge(agent.RunInfo{RunID: ctx.RunID})
  alongside the new node bridge so scripts read run vs node identity
  from clearly-scoped globals.
- iteration.js clears __iteration_result before each body run, checks
  the child signal before pushing results, and propagates
  signal.interrupt / error / done to the parent loop.
- register.go renames needsShellFS -> needsShell and tightens the doc
  string ("only shell is the extra bridge", fs is unconditional).

Adds an integration-style test file (builtins_test.go) that drives
answer / approval / iteration / node-bridge through ScriptNode with a
recording publisher and a recording host so future binding renames
trip the suite instead of breaking silently at runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
@lIang70
lIang70 merged commit dca1ccb into main May 11, 2026
19 checks passed
@lIang70
lIang70 deleted the fix/scriptnode-builtin-script-bindings branch May 11, 2026 06:59
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