feat: phase 3 — HITL permissions (sub-project 4.5)#171
Merged
Conversation
Designs the human-in-the-loop permission system that builds on sub- project 4's workspace capability. Path-jail escapes and every first- occurrence bash command trigger an interrupt prompt with three approval scopes (Once / Always-for-pattern / Deny). Persisted "always" decisions live in .dawn/permissions.json (project-local, gitignored) using a tool-keyed flat-string format that's forward-compatible with future tool categories. Three operating modes: interactive (dev default), non-interactive (production / CI), bypass (explicit "operator knows what they're doing"). dawn.config.ts gains a permissions field with mode, allow, deny — same shape as the runtime file. DAWN_PERMISSIONS_MODE env var overrides config for the session. SSE envelope shape is Agent-Protocol-compatible so sub-project 7 can implement the spec on top of this without refactoring. New @dawn-ai/permissions package ships types + pattern-matching engine + store. Workspace capability gains a permission check between the path-jail / bash invocation and the actual backend call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bite-sized, TDD-structured plan for sub-project 4.5. 13 tasks across five phases: @dawn-ai/permissions package (T1-T5), config + capability changes (T6-T7), runtime interrupt + resume (T8-T9), chat demo updates (T10-T11), smoke + PR (T12-T13). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the package skeleton for the upcoming HITL permissions system. No exports yet — types, pattern matching, and store land in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nore handling Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Type-only edge to @dawn-ai/permissions. Workspace capability will read context.permissions in a subsequent commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each tool's run() consults the optional PermissionsStore in the capability context: - Path tools (readFile/writeFile/listDir): silent for paths inside the workspace; consult the store for paths outside. - runBash: gate every command regardless of path. Three modes: - interactive: unknown ops emit LangGraph interrupt() and pause the run - non-interactive: unknown ops hard-refuse (fail-closed) - bypass: all ops proceed (path-jail disabled), warn on capability load The old pathJail() helper is removed — the gate now handles out-of-workspace cases via the permission flow. Also packs @dawn-ai/permissions in the CLI typegen install test so the external install can resolve core's new runtime dep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LangGraph 1.x's `interrupt()` does not emit a dedicated streamEvents v2
event; the parked state surfaces as `__interrupt__: [{value, ...}]` in
the graph's final `on_chain_end` output. Detect this and yield a
{type: "interrupt", data: payload} chunk so the SSE consumer (and the
soon-to-arrive resume endpoint) sees the workspace capability's
PermissionRequest envelope verbatim.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds POST /threads/:thread_id/resume to the dev runtime server, backed
by a module-level pending-interrupts map keyed by thread_id. The
endpoint validates interrupt_id (409 on stale, 400 on missing/invalid
decision) and invokes the registered resolve() callback before
clearing the entry.
execute-route.ts now loads the permissions config from dawn.config.ts,
honors the DAWN_PERMISSIONS_MODE env override, constructs a
PermissionsStore via createPermissionsStore, and threads it into
applyCapabilities so the workspace capability's gates have a store to
consult. streamResolvedRoute also bridges {type: "interrupt"} chunks
from the agent-adapter into the pending map when called with a
threadId.
Approach: two-call via checkpointer was the only viable option (the
in-process Deferred pattern would require the parked tool call to
yield back into Node's microtask queue, which LangGraph's
GraphInterrupt unwinds). However, Dawn does not yet wire a
LangGraph MemorySaver or propagate thread_id into createReactAgent —
that plumbing arrives with the Agent Protocol work in sub-project 7.
Until then resolve() is a no-op stub: the endpoint accepts decisions
and clears the entry, but cannot actually replay the parked graph.
The smoke test (T12) will exercise the loop end-to-end once
checkpointer support lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…chanism
The original T8/T9 implementation propagated interrupts to the SSE stream
and registered them in pendingByThread, but the resume callback was a
no-op stub because LangGraph 1.x requires a MemorySaver checkpointer +
a stable thread_id to actually replay from the parked state.
This commit:
- Wires a process-level MemorySaver into createReactAgent.
- Propagates thread_id from the request body through streamResolvedRoute
to streamAgent to config.configurable.thread_id.
- When agent-adapter detects an interrupt, it registers a resolve
callback in pendingByThread and awaits the user's decision.
- On resume, the adapter re-invokes the graph with new Command({resume})
and yields the resulting events into the same SSE stream.
- Moves pending-interrupts.ts to @dawn-ai/langchain so the adapter
imports the same map the resume endpoint writes to.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Detects 'event: interrupt' frames in the SSE stream; renders an inline panel with Once / Always-for-pattern / Deny buttons; POSTs the decision through /api/permission-resume to the Dawn server's resume endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…runs/stream Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LangGraph 1.x surfaces a tool-invoked interrupt() via streamEvents v2 as an on_tool_error whose data.error is a stringified GraphInterrupt — the leading JSON array is the interrupts list. The top-level LangGraph on_chain_end does not carry __interrupt__ on this path. Parse the error string in on_tool_error to surface the interrupt SSE event; keep __interrupt__-on-chain-end and live GraphInterrupt object detection as defensive fallbacks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
blove
added a commit
that referenced
this pull request
May 21, 2026
…t 4) (#170) * docs: phase 3 workspace capability + pluggable backends design spec Design for sub-project 4 of the Dawn opinionated agent harness. The workspace tools (readFile, writeFile, listDir, runBash) become a built- in capability auto-wired by the convention of having a workspace/ directory under a route. Filesystem and exec implementations become pluggable via a new @dawn-ai/workspace package shipping the type interfaces, localFilesystem/localExec defaults, a compose() helper, and one demonstration middleware (withLogging). dawn.config.ts switches from the existing hand-rolled string-only parser to tsx-evaluated import so callable backend values can be expressed naturally. Default behavior is unchanged: apps that don't touch dawn.config.ts keep working. Path-jail enforcement lives in the capability; backends receive already-resolved absolute paths. Human-in-the-loop permission gating (interrupt to ask the user about jail escapes) is deferred to a separate sub-project (4.5) with its own brainstorm + spec + plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: implementation plan for phase 3 workspace + backends Bite-sized, TDD-structured plan covering: @dawn-ai/workspace package (types, localFilesystem, localExec, compose, withLogging), the createWorkspaceMarker capability, dawn.config.ts loader switch from hand-rolled parser to tsx import, tool-name uniqueness check inversion for overridable tools, runtime wiring, typegen, chat example migration, and the smoke + PR steps. 15 tasks; each commits independently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * scaffold(workspace): empty @dawn-ai/workspace package Adds the package skeleton (manifest, tsconfig, vitest config) for the upcoming pluggable workspace backends. No exports yet — types, defaults, and helpers land in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(workspace): type interfaces for filesystem + exec backends Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(workspace): localFilesystem default backend Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(workspace): localExec default backend Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(workspace): compose() middleware helper Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(workspace): withFilesystemLogging + withExecLogging middlewares Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(core): switch dawn.config.ts loader from hand-rolled parser to tsx import The hand-rolled parser supported only string-literal property values and const string bindings. The upcoming workspace capability needs to express callable backend values in dawn.config.ts, which strings can't express. Switch to a tsx-evaluated dynamic import (same loader Dawn already uses for route discovery and tool execution). Existing dawn.config.ts files (just { appDir }) remain valid TS modules and continue to load without modification. Side-effects of the loader swap: - Two CLI integration tests assumed the old parser's specific error message or its fresh-read-from-disk behavior. The verify test's expected error string is updated to match the runtime ReferenceError that the tsx import now surfaces, and the dev test that mutated dawn.config.ts mid-session is rewritten to start the dev process with the invalid config in place (Node's ESM cache prevents a re-import of the same module URL within one process — mid-session config edits will become a per-task concern as backends land). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(core): add backends field to DawnConfig + CapabilityMarkerContext Type-only edge: @dawn-ai/core now imports FilesystemBackend/ExecBackend types from @dawn-ai/workspace via 'import type'. No runtime weight yet (workspace stays in devDependencies until the marker lands). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(core): createWorkspaceMarker capability Auto-detects a route's workspace/ directory and contributes four tools (readFile/writeFile/listDir/runBash) routed through configurable backends. Defaults to localFilesystem + localExec when no backends are configured in dawn.config.ts. Path-jail enforced in the capability; backends receive resolved absolute paths. Tools carry an `overridable: true` flag so a future uniqueness-check inversion can let user-authored tools/<name>.ts files supersede them. Promotes @dawn-ai/workspace to a runtime dependency of @dawn-ai/core, and extends the cli typegen harness to pack @dawn-ai/workspace alongside cli/core/langchain/langgraph/sdk so externally installed dawn bin tests resolve the new transitive dep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cli): support overridable capability tools Tools marked overridable on a capability contribution can be shadowed by a user-authored tool with the same name. Used by the workspace capability so authors can override readFile/writeFile/listDir/runBash by dropping a file in tools/. Non-overridable capability tools (writeTodos, readSkill, task) retain the collision error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style: biome auto-fixes (import order) on workspace marker + tests Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cli): register workspace capability + thread backends from dawn.config Registers createWorkspaceMarker in the capability registry. Loads dawn.config.ts at the start of prepareRouteExecution and threads config.backends into the CapabilityMarkerContext so the workspace marker uses the configured backends (defaulting to localFilesystem + localExec when none are configured). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cli): typegen surfaces workspace tools for routes with workspace/ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(examples/chat): migrate to workspace capability Delete the hand-rolled readFile/writeFile/listDir/runBash tool files (and their workspace-path helpers) from both the /chat route and the research subagent. The workspace capability auto-contributes these tools when the route has a workspace/ directory, so add empty workspace/ dirs (with .gitkeep) under both routes to opt in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core,cli): workspace capability uses cwd-relative root, matching agents-md T13's migration of the chat example surfaced a mismatch: the workspace capability was resolving to <routeDir>/workspace/ while the agents-md capability (and the prior hand-rolled tools) used <process.cwd()>/workspace/. Result: post-migration, the chat agent's memory file and its workspace tools pointed at completely different directories. Align the workspace capability with the existing convention: process.cwd()/workspace/. Same trigger as agents-md; same root as the deleted hand-rolled tools. The chat example's pre-existing examples/chat/server/workspace/ directory (with AGENTS.md) now serves as the workspace for both /chat and the research subagent. Removes the empty per-route workspace/ stubs T13 created. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(examples/chat): update prompt + README for workspace capability - system-prompt: runBash signature is { command } now (no timeoutSeconds); returns { stdout, stderr, exitCode } instead of a formatted string - README: status reflects shipped subagents + workspace capabilities; layout shows current file structure (no tools/, no workspace-path.ts); deferred list updated to flag HITL permission gating (sub-project 4.5) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: phase 3 — HITL permissions (sub-project 4.5) (#171) * docs: phase 3 HITL permissions design spec (sub-project 4.5) Designs the human-in-the-loop permission system that builds on sub- project 4's workspace capability. Path-jail escapes and every first- occurrence bash command trigger an interrupt prompt with three approval scopes (Once / Always-for-pattern / Deny). Persisted "always" decisions live in .dawn/permissions.json (project-local, gitignored) using a tool-keyed flat-string format that's forward-compatible with future tool categories. Three operating modes: interactive (dev default), non-interactive (production / CI), bypass (explicit "operator knows what they're doing"). dawn.config.ts gains a permissions field with mode, allow, deny — same shape as the runtime file. DAWN_PERMISSIONS_MODE env var overrides config for the session. SSE envelope shape is Agent-Protocol-compatible so sub-project 7 can implement the spec on top of this without refactoring. New @dawn-ai/permissions package ships types + pattern-matching engine + store. Workspace capability gains a permission check between the path-jail / bash invocation and the actual backend call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: implementation plan for phase 3 HITL permissions Bite-sized, TDD-structured plan for sub-project 4.5. 13 tasks across five phases: @dawn-ai/permissions package (T1-T5), config + capability changes (T6-T7), runtime interrupt + resume (T8-T9), chat demo updates (T10-T11), smoke + PR (T12-T13). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * scaffold(permissions): empty @dawn-ai/permissions package Adds the package skeleton for the upcoming HITL permissions system. No exports yet — types, pattern matching, and store land in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(permissions): public types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(permissions): suggested-pattern helpers Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(permissions): pattern-matching engine Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(permissions): PermissionsStore with file I/O, write queue, gitignore handling Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(core): extend DawnConfig + CapabilityMarkerContext with permissions Type-only edge to @dawn-ai/permissions. Workspace capability will read context.permissions in a subsequent commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(core): workspace capability gates through PermissionsStore Each tool's run() consults the optional PermissionsStore in the capability context: - Path tools (readFile/writeFile/listDir): silent for paths inside the workspace; consult the store for paths outside. - runBash: gate every command regardless of path. Three modes: - interactive: unknown ops emit LangGraph interrupt() and pause the run - non-interactive: unknown ops hard-refuse (fail-closed) - bypass: all ops proceed (path-jail disabled), warn on capability load The old pathJail() helper is removed — the gate now handles out-of-workspace cases via the permission flow. Also packs @dawn-ai/permissions in the CLI typegen install test so the external install can resolve core's new runtime dep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(langchain): propagate LangGraph interrupt events to the SSE stream LangGraph 1.x's `interrupt()` does not emit a dedicated streamEvents v2 event; the parked state surfaces as `__interrupt__: [{value, ...}]` in the graph's final `on_chain_end` output. Detect this and yield a {type: "interrupt", data: payload} chunk so the SSE consumer (and the soon-to-arrive resume endpoint) sees the workspace capability's PermissionRequest envelope verbatim. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cli): resume endpoint + PermissionsStore wiring Adds POST /threads/:thread_id/resume to the dev runtime server, backed by a module-level pending-interrupts map keyed by thread_id. The endpoint validates interrupt_id (409 on stale, 400 on missing/invalid decision) and invokes the registered resolve() callback before clearing the entry. execute-route.ts now loads the permissions config from dawn.config.ts, honors the DAWN_PERMISSIONS_MODE env override, constructs a PermissionsStore via createPermissionsStore, and threads it into applyCapabilities so the workspace capability's gates have a store to consult. streamResolvedRoute also bridges {type: "interrupt"} chunks from the agent-adapter into the pending map when called with a threadId. Approach: two-call via checkpointer was the only viable option (the in-process Deferred pattern would require the parked tool call to yield back into Node's microtask queue, which LangGraph's GraphInterrupt unwinds). However, Dawn does not yet wire a LangGraph MemorySaver or propagate thread_id into createReactAgent — that plumbing arrives with the Agent Protocol work in sub-project 7. Until then resolve() is a no-op stub: the endpoint accepts decisions and clears the entry, but cannot actually replay the parked graph. The smoke test (T12) will exercise the loop end-to-end once checkpointer support lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(langchain,cli): wire checkpointer + thread_id; complete resume mechanism The original T8/T9 implementation propagated interrupts to the SSE stream and registered them in pendingByThread, but the resume callback was a no-op stub because LangGraph 1.x requires a MemorySaver checkpointer + a stable thread_id to actually replay from the parked state. This commit: - Wires a process-level MemorySaver into createReactAgent. - Propagates thread_id from the request body through streamResolvedRoute to streamAgent to config.configurable.thread_id. - When agent-adapter detects an interrupt, it registers a resolve callback in pendingByThread and awaits the user's decision. - On resume, the adapter re-invokes the graph with new Command({resume}) and yields the resulting events into the same SSE stream. - Moves pending-interrupts.ts to @dawn-ai/langchain so the adapter imports the same map the resume endpoint writes to. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(examples/chat): seed permissions allow/deny in dawn.config.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(examples/chat-web): inline permission panel + resume proxy Detects 'event: interrupt' frames in the SSE stream; renders an inline panel with Once / Always-for-pattern / Deny buttons; POSTs the decision through /api/permission-resume to the Dawn server's resume endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(langchain): bind streamEvents to its Pregel instance to restore /runs/stream Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(langchain): detect LangGraph interrupts at correct event/data path LangGraph 1.x surfaces a tool-invoked interrupt() via streamEvents v2 as an on_tool_error whose data.error is a stringified GraphInterrupt — the leading JSON array is the interrupts list. The top-level LangGraph on_chain_end does not carry __interrupt__ on this path. Parse the error string in on_tool_error to surface the interrupt SSE event; keep __interrupt__-on-chain-end and live GraphInterrupt object detection as defensive fallbacks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * style(langchain): rename escape→escaped + biome auto-format CI lint failed on two issues: - Shadow of global `escape` in the interrupt-error JSON parser - Line-length formatting in a test helper signature Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(harness): pack @dawn-ai/workspace + @dawn-ai/permissions in framework verify Adds the two new workspace packages (introduced in sub-projects 4 and 4.5) to the framework-verification harness's pack list, override maps, and fixture snapshots so the generated-app contract tests can install them locally instead of trying the npm registry (404). Also extends create-dawn-ai-app's internal-mode overrides to point @dawn-ai/permissions and @dawn-ai/workspace at the in-repo packages so the contributor-local lifecycle resolves transitive workspace:* deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(harness): pack workspace + permissions in runtime contract verify Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(harness): pack workspace + permissions in runtime smoke verify Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <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.
Summary
Sub-project 4.5 of the Dawn opinionated agent harness, building on PR #170 (workspace capability). Replaces the hard-refuse-on-path-jail-escape behavior with a human-in-the-loop interrupt prompt, and adds the same prompt-for-approval gating to
runBash. Three operating modes (interactive/non-interactive/bypass) configurable indawn.config.tsor viaDAWN_PERMISSIONS_MODEenv var. Persisted "always" decisions live in.dawn/permissions.json(project-local, auto-gitignored).Spec:
docs/superpowers/specs/2026-05-21-phase3-permissions-design.mdPlan:
docs/superpowers/plans/2026-05-21-phase3-permissions.mdThis PR is stacked on PR #170 (
claude/phase3-workspace). Once that merges, change base tomainand rebase.Changes
@dawn-ai/permissionspackage (30 unit tests): public types, prefix-matching engine, smart-default pattern inference (first 2 tokens for commands, parent dir for paths),PermissionsStorewith write queue + auto-gitignore.PermissionsStore. Path tools silent for paths inside the workspace; gate when outside.runBashgates every command. Three modes encode the deployment shapes;bypassdisables the path-jail and warns loudly on capability load.DawnConfig+CapabilityMarkerContextextend with optionalpermissionsfield of the same shape as.dawn/permissions.json(config + runtime merge for effective permissions).MemorySavercheckpointer +thread_idpropagation in agent-adapter enable LangGraph'sCommand({resume})to replay parked state.POST /threads/:thread_id/resumein the dev server. Validatesinterrupt_id, resolves the pending Promise, parked graph resumes via a freshCommand({resume})invocation that yields continuation events into the same SSE response.GraphInterruptthrown from inside a tool surfaces ason_tool_errorwith the interrupts JSON-stringified inevent.data.error. Defensive fallback also reads__interrupt__onon_chain_endif a future LangGraph version changes the path.allow.bash: ["ls","pwd","cat","echo","head","tail","wc"]anddeny.bash: ["rm -rf","sudo","chmod 777"]. Web client renders an inline permission panel with three buttons; POSTs the decision via/api/permission-resumeproxy.Test plan
@dawn-ai/permissions(30 across pattern-matching, suggested-pattern, store), workspace capability gating (5+ new tests covering all three modes), agent-adapter interrupt propagation (5 covering stringified error, live error, fallback, threadId variants), resume endpoint (4)git status→event: interruptfires with{interruptId, kind: "command", suggestedPattern: "git status"}→ POST resume withdecision: "once"returns{ok: true}→ stream continues with tool_call/tool_result/chunks → finalevent: doneSharp edge resolved
The plan flagged the interrupt-to-resume bridging as the highest-uncertainty piece. Empirically:
on_tool_errorevents (noton_interrupt)MemorySavercheckpointer (added) + stablethread_id(propagated through stream pipeline)graph.streamEvents(new Command({resume}), {configurable: {thread_id}})replays from the parked stateThe detection code defensively handles three error shapes (live
GraphInterruptobject, stringified message, bare string) so future LangGraph version drift fails open rather than crashing.Deferred / known limitations
setPendingfires (server returns "Stale interrupt_id"). Real human reaction time avoids this; a ~200ms client-side delay (or retry-on-stale) would make automated tests robust. Not a real-world UX concern.MemorySaveris in-memory. Multi-process / horizontal scaling needs SQLite/Postgres saver. Sub-project 7 (Agent Protocol) territory.🤖 Generated with Claude Code