PR2 v0.2.0 - supervisor watches the stream - #2
Merged
Conversation
Adds supervisor.ts: a small async-generator that wraps a runner stream
and emits a SupervisorVerdict per RunEvent. Orchestrator now drains the
supervised stream instead of the raw one.
Detection rules in this PR:
- loop: N consecutive identical assistant messages (default N=3)
- off-track: tool_use for a tool not on the pattern's allowedTools list
- cost-cap: accumulated cost > costCapUsd (verdict: kill)
PR2 limit (lifted in PR3 v0.3.0): supervisor verdicts are LOGGED as
'intervened' events with acted=false. PR3 wires opts.runner.intervene()
so the suggestedMessage actually flows back into the running session.
For kill verdicts the orchestrator already calls runner.kill() in PR2 -
no run will burn past the cost cap.
NOT in PR2 (PR3): stuck-timeout. Needs Promise.race against a timer in
the orchestrator's drain loop, not inside watch(). 5-line change.
Tests: 23/23 pass (+8 supervisor cases on top of PR1's 15).
- continue baseline when no rules trip
- off-track flag + payload check
- empty allowedTools = allow all
- loop detected at threshold, not before
- different messages don't loop
- cost-cap on 'cost' events
- cost-cap on 'complete' event costUsd
- no-op when opts={}
Bumped to 0.2.0. README + npm tarball updates land with the merge tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
What this PR teaches
In PR1 the orchestrator drained the runner's event stream blindly. A bug-fix Hermes session could loop, drift into a banned tool, or burn past its cost cap, and the orchestrator would happily report it as
completed.PR2 introduces
supervisor.watch()- a small async generator that sits between the runner stream and the orchestrator and emits aSupervisorVerdictper event. The orchestrator now reasons about each event in light of three live signals.Detection rules
intervenetool_usefor a tool not inpattern.allowedToolsintervenecostCapUsdkillA
killverdict in PR2 is real:orchestratorcallsrunner.kill(handle)and marks the outcomeaborted. No run can burn past the cost cap, even in PR2.An
interveneverdict in PR2 is logged but not yet acted on - the recordedintervenedevent hasacted: false. PR3 wiresrunner.intervene()so thesuggestedMessageflows back into the live session.What's in this PR
src/supervisor.ts- the watch generator, ~100 LOCsrc/orchestrator.ts- drain loop now consumes{ event, verdict }, logs interventions, callsrunner.killonkillverdictsrc/index.ts- exportswatch+SupervisorOptions+SupervisorVerdicttests/supervisor.test.ts- 8 unit testspackage.json- bump to0.2.0Limits left for later PRs
Promise.raceagainst a timer in the drain loop; ~5 LOC, deliberately deferred to keep this PR scoped to per-event logic--print(one-shot) to a session-based mode so we can append a message; PR3 changesHermesRunnerto useclaude --session-id ... --append-messageand setsacted: trueloopThresholdis per-pattern static today; later it learns from pastintervenedoutcomes vialearner.retrieveVerification
The PR1 orchestrator tests still pass unchanged because supervisor defaults to
continuefor any event when no rule trips.Build-in-public note
This is the first PR where I'd have caught a real Hermes failure mode in the field. The pattern: a fixer agent kept calling
WebFetchto investigate a bug instead of reading the file. The supervisor would have flagged that as off-track on the second tool call and (in PR3) nudged it to useRead. Worth tracking how many off-track interventions actually fire once PR3 lands and ZOE starts dispatching through this.Lands on merge
v0.2.0(will cut release post-merge)npm install git+...#v0.2.0pin🤖 Generated with Claude Code