fix(core): settle foreign typed tool failures instead of dropping them - #43576
Open
kitlangton wants to merge 1 commit into
Open
fix(core): settle foreign typed tool failures instead of dropping them#43576kitlangton wants to merge 1 commit into
kitlangton wants to merge 1 commit into
Conversation
A tool implementation declares `Effect<Result, Tool.Error>` but a plugin can
fail with any typed error at runtime. Such a failure slipped past every
`catchTag("Tool.Error")" downstream and past classifyToolExits (which drops
Fail reasons on the assumption they are all declines), so the call was never
settled: the part stayed "running" forever, the step continued anyway, and
the execution reported success.
Enforce the declared contract at the untrusted boundary (tool runtime maps
foreign typed failures to Tool.Error) and harden classifyToolExits to surface
any surviving non-decline typed failure as a defect instead of silently
dropping it.
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
A plugin tool that fails with a foreign typed error (anything that isn't
Tool.Error) leaves its tool call permanently unsettled: the part staysrunningwith a live spinner forever, while the step continues anyway and the execution reports success. Found by a drive gauntlet probe whose tool transport died mid-execution.Tool.Info.executedeclaresEffect<Result, Tool.Error>, but a plugin can fail with anything at runtime. Nothing enforced the declaration, and every downstream handler assumes it:catchTag("Tool.Error", failTool)misses it (llm.ts)classifyToolExitsdrops Fail reasons wholesale, assuming any typed failure surviving the fiber'scatchTagis a decline (llm.ts:91)So the failure threads every needle: no
tool.failedevent is ever published,needsContinuationstill fires offcall.called, and the part is only healed if some later drain'ssettleStaleToolCallssweep happens to run.Before
running, spinner animates.session.tool.*terminal event is published.session.execution.succeeded.running. If the user never sends another message, it never settles.Event stream from the repro (note: no tool terminal event between
tool.calledandexecution.succeeded):After
Tool.Errorat the untrusted boundary; the fiber's existingfailToolpath publishes the durable failure immediately.errorwith the real message, the model reads the failure, the continuation proceeds, the UI is quiescent right after the turn.How
packages/core/src/tool/runtime.ts— enforce the declared contract where untrusted tool implementations enter typed code: any typed failure that isn'tTool.Errorbecomes one, preserving its message. Decline tunneling (defects) and interrupts are untouched.packages/core/src/session/runner/llm.ts— defense in depth inclassifyToolExits: a non-decline Fail reason is surfaced as a defect (failing the unsettled calls) instead of being silently dropped.packages/core/test/tool-execute.test.ts— regression test: a tool failing with a foreign tagged error settles asTool.Error.Scope
Testing
bun run typecheck(packages/core) clean.bun run test -- test/tool-execute.test.ts test/session-runner.test.ts test/session-runner-tool-events.test.ts test/session-runner-tool-registry.test.ts— 198 pass, plus the new regression test.v2, passes on this branch.Demo
Before — turn is over (
T0X_CONTINUED, 9.0s footer), but the shell row is still spinning; the server projection reports the part asrunningindefinitely:After — identical scenario: the part settles immediately as an error with the real transport message, then the continuation completes and the UI is quiescent:
Flow
sequenceDiagram participant P as Plugin tool participant R as Tool runtime (runtime.ts) participant F as Runner tool fiber (llm.ts) participant S as Settlement P->>R: fail with foreign tagged error rect rgb(60,30,30) note over R,S: before: error escapes catchTag("Tool.Error"),<br/>classifyToolExits drops the Fail reason,<br/>part never settles, step continues end rect rgb(30,60,30) note over R: after: mapError enforces the declared contract R->>F: Tool.Error F->>S: failTool → durable tool.failed, part settles end