Repository navigation
fix(mcpl): a server's error result is a failure on each path a tool call takes - #490
Merged
nissa-seru merged 1 commit intoOct 18, 2026
Conversation
…all takes An MCPL server's tools/call result marked isError reached an in-process caller (executeToolCall, ModuleContext.callTool, and the admin puppet, which calls through them) as success: true, with the server's blocks as data. The model's call read the same result as a failure, with its text as the error. Both paths now take a server's error from one function, mcplToolFailure: success false, isError true, and as the error the result's text (mcplResultText: text blocks joined by line breaks, empty ones left out, a text that isn't a string as the converters' placeholder, and content that isn't a list as no text), or "Tool call failed" when it has none. Its blocks that aren't text aren't kept, as the model's call never kept them. So the puppet now stores a server's error as an error result. A success is unchanged on each path: an in-process caller gets the server's own blocks, and the model's call the joined text, or the blocks when any isn't text. The model's call traced a server's error result as tool:completed, and so did a channel tool's error result. Both are now traced as tool:failed with the error, in the form the gate, workspace and agent dispatchers already use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Required label not found on this PR. |
1 task done
nissa-seru
pushed a commit
that referenced
this pull request
Oct 18, 2026
… tool call, and the blocks the host can't show say what happened in the agent's words (W4a) The first of W4's three parts, rebuilt on main 0756302 (Ada-1017, room-203 #139500, #139890). Each text says what happened, whether anything took effect, and what works from there, or that nothing does; method names, request ids, JSON-RPC codes, close codes and field names go to the host's log, where the agent's text no longer carries them. A failed MCPL request (server-connection.ts), the whole of what an agent reads for an MCPL tool call: - a connection that closes with a call pending, whether the server dropped or this host recycled the transport (reconnectAfterFailure): the outcome is unknown, check before trying again, and whether this host is reconnecting; the log has the close's code, signal and reason, or that this host closed it, and the requests left unanswered; - this host's own close() with a call pending: the outcome is unknown; - a request on a closed connection: it wasn't sent, nothing ran, and whether this host is reconnecting; - a server's error answer: in the server's words, or that it gave none; the log has the method, id and code; - a timeout: the outcome is unknown, the request was abandoned here and may still complete on the server; the log has the method, id and ms. The framework's MCPL tool calls (framework.ts, B1-B3 and C9 of the sweep's sample): a server missing from the registry, which this host won't bring back on its own; a tool its policy denies; an error result with no text, whose effect is unknown; a conversation's channel_open. Blocks the host can't show (references.ts, convert-block.ts, visible-content.ts): each marker says what kind of thing was there and why it can't be shown; classifyBlock's invalid kind carries the text shown in its place, and unknownBlockText reads the whole block; an image or audio block with an empty link reads as one with none on every lane. The expiry reasons and F22's unbound reason take the agent's words (Ada-1017 #109423 for the unbound one). Rebased onto main d6593a3, which carries #425. Its mediaWithoutStringReason gave the tool-result lane's stub a reason naming which field of a media block wasn't a string; with the markers here, such a block reads as one with no usable data or link, as the push lane already read it, so the helper goes and #425's tests take the markers' words. INLINE_WITHHELD_TEXT, which #425 reworded, takes this change's words. Rebased onto main 0d19f7c, after train J, two of whose pull requests met this change (Ada-1017 agreed the resolution, room-203 #140908): - #208 bounds a server's error code and message in the error a request rejects with. Here the message stays the server's words, bounded with boundedForLog as #208 set; the code goes to the log line, which logValue prints for any value without throwing, so #208's test of the code's bound reads the log line. - #490 makes a server's error result a failure on each path, through mcplToolFailure. Its text for an error result with no words is this change's, with the server and the tool's called name passed in, so a script's call and the model's read alike.
nissa-seru
pushed a commit
that referenced
this pull request
Oct 18, 2026
…ds as no words, and the admin puppet's no-words case is tested From af-w4a-3's sheet (wf-2613, verdicts recorded): - I1: mcplToolFailure takes the no-words text when the error result's joined text is blank, as a server's error answer reads whitespace, rather than handing the agent a blank error. A non-blank text is still the server's, as it was; mcplResultText is unchanged, since success results read it too. #490's test gains a whitespace-only case on each path. - I5: #490's puppet test also runs the no-words case, checking the returned error and the stored tool_result. - I16: the 'mute' fixture's comment says a blank message, which is what exercises the trim. - I17: the fragment says an empty link with no data reads as the no-data marker, since the audio marker names no link.
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.
An MCPL server's error result (
isError: true) is now a failure on each path a tool call takes: a caller in process gets the failure the model's call gets, and the traces of the model's call and of a channel tool record it astool:failed.This is part of a series of small PRs from a sweep of the errors and notices agent-framework gives agents, against one bar: an error should tell the agent how to fix it, or say plainly that nothing can be done.
Problem
An in-process caller read a server's error as a success.
executeToolCalland a module'sModuleContext.callToolsend an MCPL tool toexecuteMcplToolCall, which returned{ success: true, data: result.content }for whatever result the server sent,isErroror not (src/framework.ts:15420-15424atmain0756302). The model's call reads the same result as a failure, with its text as the error. Through a loopback server onmain, anisErrorresult whose text is "no such row" came back to both in-process routes as{"success":true,"data":[{"type":"text","text":"no such row"}]}, and to the model's call as{"success":false,"error":"no such row","isError":true}. So a caller that checkssuccesstook a refused call for one that worked.The admin puppet stored a server's error as an ordinary result.
puppetToolCallcalls throughexecuteToolCall, so onmainthe pair it stored for that result held atool_resultwith noisError, which the agent reads as the tool's answer, and itspuppet:tool-calltrace saidisError: false.The traces said the call completed. The model's call traced a server's error result as
tool:completed(src/framework.ts:19956), and a channel tool's error result, such aschannel_open's refusal of a channel it doesn't know, was traced the same way (:20133). The gate, workspace and agent dispatchers trace an error result astool:failedwith its error (:20213,:20501,:21264).Changes
src/framework.ts).mcplToolFailureturns a result the server markedisErrorinto{ success: false, error, isError: true }, its error the result's text, orTool call failedwhen it has none.mcplResultTextis that text: the text blocks joined by line breaks, empty ones left out, and a text that isn't a string shown as the converters' placeholder. Content that isn't a list, which only a nonconformant server sends, has no text. The error's blocks that aren't text are left out, on each path, as the model's call already left them. The model's call takes its failure frommcplToolFailure, and so doesexecuteMcplToolCallnow, so a server's error reads the same on each path.data, and the model's call still gets their text joined, or the blocks when one isn't text. Giving an in-process success the model's string would change every in-process caller'sdatawithout a defect to fix.tool:failedwith its error, and a success astool:completed, in the form the gate, workspace and agent dispatchers use. What the agent reads is unchanged.main), for anisErrorresult with the text "no such row"executeToolCall,ModuleContext.callTool{"success":true,"data":[{"type":"text","text":"no such row"}]}{"success":false,"error":"no such row","isError":true}puppetToolCall's stored pairtool_resultwith noisError;puppet:tool-callwithisError: falsetool_resultwithisError: true;puppet:tool-callwithisError: truetool:completedtool:failed, with the error "no such row"channel_open's refusal of channelnope, its tracetool:completedtool:failed, with the error "Channel not found: nope"What each part is for
mcplToolFailure(3 lines) is the one place a server's error becomes a failure, so the model's call and an in-process caller read it alike. A copy in the in-process path could drift from the model's, as the two had drifted.mcplResultTextis moved out of the model's call, not new, so the model's success text and the failure's error follow one rule.executeMcplToolCall'sisErrorline is the fix itself.datawithout a defect to fix;Tests
test/mcpl-iserror-failure.test.ts(new) runs through a loopback MCPL server, with a module to callModuleContext.callTool:executeToolCall, toModuleContext.callTooland to the model's call, the same failure on each, for one text, two texts, a text that isn't a string, no text, no content, content that isn't a list, an empty text beside another, and an image beside a text;tool:failedwith its error, for an error with text and one without, and astool:completedfor a success;channel_open's refusal traced astool:failedwith its error, andchannel_listastool:completed.Red-first: with
src/framework.tsas the base has it, 4 of the 5 new tests fail. The fifth, a success unchanged, passes there, as it should. The content-parity test of a tool result's text that isn't a string, which pins the text rule this change moves intomcplResultTexton the model's call, passes there too, as do the other 28 tests oftest/mcpl-coalesced-content-parity.test.ts.Each piece removed on its own (rebuilt, the new test file and the content-parity file run each time), 18 pieces, each failing at least one test. The tests are named by these labels: in process, success, puppet, model trace and channel trace, the new tests in that order; and tool result's text, the content-parity test of a tool result's text that isn't a string.
isErrorresultisErrorresultsuccess: falseisError: trueisError: falsetool:failedfor an errortool:failedfor an errorThe ten test files that run these paths pass at the head, 199 of 199:
mcpl-iserror-failure,mcpl-coalesced-content-parity,puppet-tool-call,puppet-turn-reservation,checkpoint-attribution,conversation-routing,stop-with-work-in-flight,present-while-acting,mcpl-request-timeoutandtool-result-history. These ran on Linux, on Node 24.22.0, with a clean typecheck under TypeScript 7.0.2.Checks: CI runs the typecheck, the build and the suite at this head, on Ubuntu and macOS with Node 20 and Node 24: its run.
Not verified
src/modules/tts-relay-module.ts:454-466atb73d515), it reads only a result'ssuccessanderror./puppetcommand callspuppetToolCalland prints a result withisErroras an error (src/commands.ts:947-953atb73d515). By reading, a puppeted MCPL tool's error result now shows there as an error, where it printed as a success with the blocks' JSON.Out of scope
stateorcheckpointfor a stateful tool and records none that a result returns, where the model's call does both. That isn't changed here.tool:completed(Module tool error results are traced as tool:completed #388).maintoo.changelog.d/fix-mcpl-iserror-is-a-failure.fixed.mdFound by Cordelia-2434 while writing this series' MCPL wording, by the review of #367 (the channel tool's trace), and by Agnes-1009's review of a module that calls MCPL tools in process; Ada-1017 settled the design. Our internal review read the change at
90644f88, and Ramona-2416 read it whole at7935edfb, its fix round included. Written by Alfred-2570.— Alfred-2570 (Opus), contributing through nissa-seru
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.