Workflow Supervisor PRs 4 and 5: the CLI surface and workflows as agents - #4635
Merged
Conversation
A saved workflow can now be run as an agent: `Agent({ graph })` hydrates the
graph (inline, or `{ workflowId }` read under the context's user), runs it on
the kernel through `ExecutionSession` with the agent itself as the run's
`SupervisorHandle`, forwards the run's messages, and returns the run outputs
from `getResults()`. There is no planning phase — the graph is the plan — and
the branch obeys the same `AgentPolicy` as the other three modes: its turn and
token bounds are stamped onto model-less Agent nodes by the same
`applyRunPolicy` the graph-planner branch uses. Supervision is opt-in
(`supervise: true`); without it the run is an ordinary kernel run that never
constructs an escalation.
Websocket: `supervise` on run requests, plus an optional `supervisor` block
(provider, model, bounds, cost cap). `createRunSupervisor` builds the handle for
both server entry points — the websocket runner and the headless/trigger runner
— and returns none unless the flag is explicit and a model resolves.
`supervisor_escalation` / `supervisor_decision` relay to clients unchanged.
Trigger rows carry a `supervise` column, default 0, never migrated on.
Deviations from the plan:
- A flag alone cannot start a supervisor, so the run request also carries
`SupervisorRunOptions` and the resolution falls back to the connection's
default model and then to `NODETOOL_SUPERVISOR_PROVIDER` /
`NODETOOL_SUPERVISOR_MODEL` (trigger runs have no connection defaults). A run
that asks for supervision it cannot get runs unsupervised rather than failing.
- The supervisor gets a dedicated provider instance and a listener-free context
copy: per-turn spend is reconciled from the provider's own running cost, and
the decision's provider traffic is not the run's message stream.
- `ExecutionSessionOptions.supervisor` is PR 4's deliverable and landed here
because PR 5 needs it; the two branches carry the same three-line change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K83nCW1jVkfQCTKV96iBgP
Supervision reaches the CLI through one integration point:
`ExecutionSessionOptions.supervisor` forwards to the runner, and every CLI
surface configures the facade rather than constructing `WorkflowRunner`.
- `packages/execution`: `supervisor` on the session options; `src/supervisor.ts`
rolls up `Intervention[]` and formats the `⛨` lines, re-exported from the
dependency-free `/debug` subpath; the shared debug reducer folds
`supervisor_decision` into `ExecutionSummary.interventions`, and
`collectInterventionWarnings` turns them into verdict warnings (never issues —
a rescued run still completed).
- CLI flags `--supervise`, `--max-decisions`, `--max-retries`,
`--supervisor-cost-cap`, `--supervisor-model` on `nodetool run`,
`nodetool workflows run`, and `nodetool debug`. Flag parsing and model-spec
resolution are pure; the agents package and a provider load only for a run
that asked for them. `debug` goes through the shared debug service, not
around it.
- Output: a `⛨` line per decision as it happens (stderr, so `--json` stdout
stays parseable), a supervised summary line, and an `interventions` block in
`--json`. The record is `Intervention` from `@nodetool-ai/protocol`,
unchanged, so PR 6 has one shape to consume.
- Supervisor spend lands in the prediction ledger `nodetool costs` reads: one
row per billable decision, attributed to the run and tagged `supervisor` in
`node_type` (job id, decider and verdict in `metadata`).
- Docs: supervised-run sections in root `CLAUDE.md` and `docs/cli.md`; the
plan's PR 4 entry marked shipped.
Deviations from the plan:
- `nodetool run <dsl-file>` executes through `@nodetool-ai/dsl`'s own
`WorkflowRunner` — a grandfathered direct-construction site this PR may not
grow supervision on. `--supervise` switches that command onto
`ExecutionSession` (`run-dsl-supervised.ts`); without the flag it keeps the
untouched path. The supervised path does not rethrow the first node error the
way the DSL path does: a node error the supervisor resolved is not a run
failure. Its `--json` output is `{results, interventions}`; the unsupervised
shape is unchanged.
- The summary line's "198/200 items" presumes a batch the kernel never names.
`formatSupervisedSummary` accepts an item total from a caller that knows one
and otherwise reports decisions.
Tests: supervised session run records interventions and surfaces them in the
`--json` summary; an unsupervised run emits no supervisor messages and no
interventions; flags map to bounds (and a bound without `--supervise` is an
error); supervisor cost writes one ledger row tagged `supervisor`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K83nCW1jVkfQCTKV96iBgP
# Conflicts: # packages/execution/src/session.ts
Contributor
There was a problem hiding this comment.
Pull request overview
Adds opt-in “workflow supervision” across NodeTool’s CLI and WebSocket run surfaces, and introduces an agent execution mode that runs an existing workflow graph directly (with optional supervision), while keeping the integration point centralized in ExecutionSessionOptions.supervisor.
Changes:
- Introduces
ExecutionSessionOptions.supervisorplus shared intervention reporting/rollups (⛨lines, summaries, warnings) in@nodetool-ai/execution. - Adds CLI flags (
--supervise+ bounds), supervised DSL runs, cost ledger attribution for supervisor decisions, and debug output wiring. - Extends run APIs (protocol + websocket + triggers) to support supervised runs and adds “workflows as agents” (
Agent({ graph })) execution path.
Reviewed changes
Copilot reviewed 42 out of 43 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/websocket/tests/supervisor-relay.test.ts | Verifies supervisor_* messages are relayed to WS clients with job/workflow IDs. |
| packages/websocket/tests/run-supervisor.test.ts | Tests createRunSupervisor opt-in behavior and fallback selection. |
| packages/websocket/src/unified-websocket-runner.ts | Wires per-request supervision into websocket runs via createRunSupervisor. |
| packages/websocket/src/triggers/dispatcher.ts | Forwards per-trigger supervise bit into headless trigger runs. |
| packages/websocket/src/run-supervisor.ts | Adds server-side supervisor factory (provider/model resolution + bounded handle). |
| packages/websocket/src/headless-job-runner.ts | Adds supervised option to headless job runner and forwards handle into ExecutionSession. |
| packages/protocol/src/api-types.ts | Extends RunJobRequest with supervise + supervisor options; defines SupervisorRunOptions. |
| packages/models/tests/migrations.test.ts | Updates expected migration count after adding trigger supervision column. |
| packages/models/src/trigger-registration.ts | Adds supervise field to trigger registration model + defaulting. |
| packages/models/src/schema/trigger-registrations.ts | Adds supervise column to sqlite trigger registrations schema. |
| packages/models/src/schema-pg/trigger-registrations.ts | Adds supervise column to postgres trigger registrations schema. |
| packages/models/src/migrations/versions.ts | Adds migration 20260801_000001 to add trigger_registrations.supervise. |
| packages/models/src/db.ts | Updates sqlite schema SQL to include trigger_registrations.supervise. |
| packages/execution/tests/supervisor.test.ts | Tests session supervision behavior + intervention formatting/rollups. |
| packages/execution/tests/session-supervisor.test.ts | Tests facade forwarding of supervisor handle (skip rescues, none escalates without handle). |
| packages/execution/src/types.ts | Adds ExecutionSessionOptions.supervisor and re-exports SupervisorHandle. |
| packages/execution/src/supervisor.ts | Implements shared intervention rollups and printable ⛨ lines. |
| packages/execution/src/session.ts | Conditionally forwards supervisor handle into WorkflowRunner options. |
| packages/execution/src/index.ts | Re-exports intervention reporting + adds collectInterventionWarnings. |
| packages/execution/src/debug/verdict.ts | Adds intervention-derived warnings (not issues) for supervised runs. |
| packages/execution/src/debug/types.ts | Adds interventions + intervention counts to ExecutionSummary. |
| packages/execution/src/debug/index.ts | Re-exports intervention reporting from debug entrypoint for hosts. |
| packages/execution/src/debug/collector.ts | Folds supervisor_decision into ExecutionSummary.interventions. |
| packages/cli/tests/supervisor.test.ts | Tests CLI supervisor flag parsing, model-spec parsing, and cost attribution. |
| packages/cli/src/supervisor.ts | Implements CLI supervision flags, dynamic handle creation, streaming ⛨ lines, and cost ledger writes. |
| packages/cli/src/run-dsl.ts | Refactors DSL workflow loading and adds DSL→graph mapping for supervised path. |
| packages/cli/src/run-dsl-supervised.ts | Adds nodetool run --supervise implementation via ExecutionSession. |
| packages/cli/src/nodetool.ts | Wires supervision flags into run and workflows run, including json shape and cost ledger. |
| packages/cli/src/debug/verdict.ts | Incorporates supervision warnings into debug verdict. |
| packages/cli/src/debug/types.ts | Adds supervised metadata to debug report options and server report types. |
| packages/cli/src/debug/server-runner.ts | Adds supervisor handle wiring + ⛨ streaming + supervised rollup in server debug report. |
| packages/cli/src/debug/markdown.ts | Renders interventions section into debug bundle markdown. |
| packages/cli/src/debug/harness.ts | Plumbs supervision options and intervention-line sink into debug harness. |
| packages/cli/src/commands/debug.ts | Adds supervisor flags to nodetool debug and prints interventions/warnings in summaries. |
| packages/agents/tests/agent-graph-mode.test.ts | Adds coverage for Agent({ graph }) execution and supervised outcomes. |
| packages/agents/src/workflow-agent.ts | Implements workflow-as-agent runner (execute graph via ExecutionSession, forward messages, cancellation). |
| packages/agents/src/index.ts | Exposes workflow-agent utilities/types from package entrypoint. |
| packages/agents/src/agent.ts | Adds graph execution branch to Agent, with optional supervision via SupervisorAgent. |
| packages/agents/package.json | Adds dependency on @nodetool-ai/execution for graph-run branch. |
| package-lock.json | Lockfile update for new agents→execution dependency. |
| docs/workflow-supervisor-implementation-plan.md | Marks PR4/PR5 as shipped and documents deviations/added mechanics. |
| docs/cli.md | Documents supervised run flags/output/cost behavior. |
| CLAUDE.md | Documents supervised runs and nodetool debug --supervise usage. |
…ecider Two fixes from Copilot's review of #4635, both real. The CLI built its `SupervisorAgent` on the run's own `ProcessingContext`, so the supervisor's provider chatter landed in the stream the CLI drains for `⛨` lines — buffer pressure plus the supervisor's prompt and response in the run's messages. The agent surface already took a listener-free copy with memory shared; `createSupervisorHandle` now does the same, which covers all three call sites (`run`, `workflows run`, `debug`). `readIntervention` defaulted a missing `decided_by` to `"agent"`, reporting a model decision where none happened and inflating the agent-decision count the cost rollup reads. `decided_by` is required by the message schema, so an absent or unrecognized value now drops the record the same way a missing verdict already does. The accepted set is a total record over the protocol enum, so a new value fails this file instead of slipping through. Both fixes are covered by tests that fail without them.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/websocket/tests/supervisor-relay.test.ts:97
- This test uses a fixed
setTimeoutsleep to wait for async relay output, which is prone to flakiness and is discouraged bydocs/DEVELOPMENT_STANDARDS.md(Testing §8: async assertions should usewaitFor-style polling, not arbitrary sleeps). Prefer polling until the expected message arrives.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships the two parallel PRs of Phase B from docs/workflow-supervisor-implementation-plan.md. Both depend on PR 3 (#4634) and on nothing else, so they land together.
PR 4 — the CLI surface
ExecutionSessionOptions.supervisorinpackages/executionis the single integration point (design §7). It forwards toWorkflowRunnerOptions.supervisor; no CLI code touchesWorkflowRunner, and no grandfathered direct-construction site gained supervision.--supervise,--max-decisions,--max-retries,--supervisor-cost-cap,--supervisor-modelonnodetool run,nodetool workflows run, andnodetool debug. Parsing and model-spec resolution are pure (packages/cli/src/supervisor.ts); the agents package and the provider load behind dynamic imports, so an unsupervised run pays nothing.debuggoes through the shared debug service — the collector foldssupervisor_decisionintoExecutionSummary.interventions, and intervention warnings feed the verdict'swarnings, never itsissues.⛨lines streamed from the session messages (on stderr, so--jsonstdout stays parseable), a supervised summary line (⛨ supervised: 2 skipped, 1 retried, 3 decisions, +$0.0200), and aninterventionsblock in--json. The record is protocol'sInterventionunchanged, so PR 6's UI consumes one shape.recordSupervisorCostwrites onePredictionrow per billable decision —node_type: "supervisor",workflow_id= the run,metadata.job_id/decided_by/verdict. Everynodetool costssubcommand sees it.Two deviations, both recorded in the plan:
nodetool runis a DSL command executing through@nodetool-ai/dsl's own runner, so--superviseswitches it ontoExecutionSessionwhile the unsupervised path stays untouched; and the plan's "198/200 items" is a batch the kernel never names, so the summary takes an optional item total and otherwise reports decisions.PR 5 — workflows as agents
AgentOptions.graph?: GraphData | { workflowId: string }plussupervise,supervisorBounds,maxSupervisorCostUsd.Agent._executeImpldispatches toexecuteSuppliedGraphbefore every planning mode; the mechanics live inpackages/agents/src/workflow-agent.ts— child context with shared memory, messages forwarded to the parent, signal wired tosession.cancel,getResults()returning the run outputs. Supervision buildsBoundedHandle(SupervisorAgent)from the agent's own provider and reasoning model. The branch adopts the existingAgentPolicy:maxStepIterations/maxTokensreach the graph's Agent nodes through the sameapplyRunPolicythe graph-planner branch uses, so there is no fifth ad-hoc policy.RunJobRequest.superviseandsupervisor?: SupervisorRunOptions.createRunSupervisoris the single factory shared by the main run path and the headless/trigger runner; it returnsnullunlesssupervise === trueand a provider and model resolve (request → connection defaults → env).supervisor_*messages pass through the relay untouched, now under test.supervisecolumn (sqlite + pg, migration20260801_000001), defaulting to0and forwarded by the dispatcher; registration sync mutates rows in place so a re-sync never resets it. Nothing is flipped on anywhere — the default flip belongs to PR 8's gate.Merge reconciliation
Both branches independently added
ExecutionSessionOptions.supervisor(PR 5 cannot run without it). Resolved to a single field with the stricter doc comment, and to the conditional-spread forwarding form insession.ts.Tests
New:
packages/execution/tests/supervisor.test.ts,packages/cli/tests/supervisor.test.ts,packages/agents/tests/agent-graph-mode.test.ts,packages/execution/tests/session-supervisor.test.ts,packages/websocket/tests/run-supervisor.test.ts,packages/websocket/tests/supervisor-relay.test.ts.The graph-mode suite covers the load-bearing claims: outputs identical to a bare
WorkflowRunneron a clean graph, a scriptedskipcompleting a run that would otherwise fail while surfacingsupervisor_escalation/supervisor_decision, a scriptedfailstill failing, a clean supervised run emitting nosupervisor_*at all, and an aborted signal cancelling the run.Verification
Run on the merged tree, not on the individual branches:
npm run build:packages— 60/60npx vitest run --root packages/{execution,agents,cli,websocket,kernel,models}— 23, 1877, 458, 2008, 1001, 783 passednpm run lint— exit 0 (pre-existing react-hooks warnings only, none in touched files);check:deps,check:circular,check:execution-boundary,check:coupled-deps,check:lockfileall passnpm run typecheck— web and electron clean. Mobile fails for an environmental reason:mobile/is not a root workspace and itsnode_moduleswas never installed in this container, so every error isTS2307: Cannot find module 'react-native' / 'expo-*'plusTS6053: File 'expo/tsconfig.base' not found. No error names a file in this diff, and no mobile file was touched.Out of scope
No UI (PR 6), no
Assertnode (PR 7), noReplayHandleor eval suite (PR 8), no default flipped on. There is no editor or API surface for setting a trigger'ssupervisebit yet — the column, model field, and dispatcher read exist; the write path is a PR 6/8 toggle. The browser debug surface stays unsupervised until PR 6.Generated by Claude Code