feat(runtime-core): add optional signal to AgentBlockContext for cancelling agent runs - #445
Conversation
…elling agent runs A host with a Stop control had no way to halt an agent run in progress. Throwing from onAgentEvent leaves the model request billed, throwing from a tool callback is fed back to the model as context, and returning a sentinel still burns turns up to maxTurns. In the VS Code extension that means Stop still submits generated code to the kernel and still inserts cells. Passing abortSignal to agent.stream() alone is not enough: the SDK hands over every remaining tool call of a step after the signal aborts, so each callback needs its own guard or a cancelled run keeps mutating the notebook. A late abort also leaves streamResult.text resolving with the previous step's text, which would be returned as a successful result. Cancellation always rejects with signal.reason rather than resolving, so the execution engine records a failed block and breaks the run instead of counting the block executed and continuing. Additive and backwards compatible: the field is optional, no in-repo caller passes it, and no exported type is narrowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwFZ8m75wzt2MgYTJope9j
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #445 +/- ##
==========================================
+ Coverage 87.78% 88.11% +0.33%
==========================================
Files 187 187
Lines 9927 9936 +9
Branches 2768 2847 +79
==========================================
+ Hits 8714 8755 +41
+ Misses 1212 1180 -32
Partials 1 1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Added the MCPClient type to the import statement for better type safety. Refactored the execution logic to initialize mcpClients as an empty array and wrapped the Promise.all call in a try-catch block for improved error handling. This change ensures that the agent block can handle tool execution more robustly, maintaining the existing functionality while enhancing code clarity and maintainability.
The abort tests drove the run from outside and then spun the event loop waiting for it to settle, which needed a gated model stream, a deferred helper, a promise-state observer and a fixed-round drain loop. Aborting from inside a callback the SDK already invokes — a host tool callback, or doStream itself — orders the abort deterministically against the code under test, so plain `await expect(...).rejects` works and all four helpers go away. Mutation-checked: removing any one of the four abort guards (entry, agent.stream abortSignal, per-tool, post-stream) still fails exactly one test, same as before. Drops the MCP limitation test: no mutation killed it because it asserted ai core stays broken, and it would go red on an ai upgrade that fixed it. That caveat now lives on the AgentBlockContext.signal docstring, where a caller will see it. Also replaces the manual OPENAI_BASE_URL save/restore with vi.stubEnv, matching the convention already used elsewhere in the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwFZ8m75wzt2MgYTJope9j
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwFZ8m75wzt2MgYTJope9j
|
@coderabbitai full review |
✅ Action performedFull review finished. |
📝 WalkthroughWalkthrough
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant executeAgentBlock
participant MCPClient
participant OpenAI
participant NotebookTool
Caller->>executeAgentBlock: Provide AgentBlockContext.signal
executeAgentBlock->>executeAgentBlock: Check abort state
executeAgentBlock->>MCPClient: Create and track clients
executeAgentBlock->>OpenAI: Start agent stream with signal
OpenAI-->>executeAgentBlock: Request notebook tool
executeAgentBlock->>NotebookTool: Invoke when not aborted
NotebookTool-->>executeAgentBlock: Return tool result
Caller-->>executeAgentBlock: Abort signal
executeAgentBlock->>executeAgentBlock: Reject aborted execution
executeAgentBlock->>MCPClient: Clean up tracked clients
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/runtime-core/src/agent-handler.ts`:
- Around line 175-211: In the MCP initialization flow, add
context.signal?.throwIfAborted() immediately after the awaited createMCPClient
Promise.all and again after the awaited mcpClients.map(client => client.tools())
discovery, before continuing. Add a regression test that keeps createMCPClient
pending, aborts the signal, resolves creation, and verifies client.tools() is
never invoked.
- Around line 175-186: Update the MCP client initialization around
createMCPClient to collect individual outcomes instead of allowing Promise.all
to discard fulfilled clients when another rejects. Assign all fulfilled clients
to mcpClients, then rethrow the startup error so existing cleanup closes partial
clients; add a test covering one successful and one rejected client and
verifying the successful client is retained and cleaned up.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f44a2559-ff67-473a-ae2d-e63bb2a31042
📒 Files selected for processing (2)
packages/runtime-core/src/agent-handler.tspackages/runtime-core/src/execute-agent-block.test.ts
Adds an optional
signal?: AbortSignaltoAgentBlockContextso a host with a Stop control — the VS Code extension's cell Stop button — can halt an agent run in progress. Additive and backwards compatible: the field is optional, no in-repo caller passes it, and no exported type is narrowed.Why a host can't do this today
onAgentEventthrowsfullStreamis atee()branch, so cancelling the consumer never reaches the fetch. The request completes and is billed.tool-errorstream part fed back to the model, which keeps looping.stepCountIs(maxTurns).In the extension that means: after Stop, agent-generated code can still be submitted to the kernel, new cells can still be inserted, and spend continues for up to ~10 turns.
What the change does
Five edits in
packages/runtime-core/src/agent-handler.ts(+18 −3):signal?: AbortSignalonAgentBlockContextexecutewrappersabortSignal: context.signalforwarded toagent.stream()await streamResult.textItem 3 is load-bearing and not obvious. Measured A/B with three tool calls in one step, aborting from inside tool 1:
The SDK dispatches every remaining tool call of the step after the signal aborts. Forwarding
abortSignalalone does not stop it — without the in-executeguard a cancelled run keeps adding and executing cells, one per queued call.Item 5 matters because a late abort leaves
.textresolving with the previous step's text, which would otherwise be returned as{ finalOutput: 'stale text' }— a success.Cancellation always rejects with
signal.reasonrather than resolving. Resolving would be fail-open: the engine marks the blocksuccess: trueand continues to the next block, which is exactly what Stop must not do.🤖 Generated with Claude Code
https://claude.ai/code/session_01RwFZ8m75wzt2MgYTJope9j
Summary by CodeRabbit
New Features
Bug Fixes
Tests