Repository navigation
feat(code-execution): a script's tool result carries .is_error and .structured beside its string - #538
Merged
nissa-seru merged 1 commit intoOct 18, 2026
Conversation
…tructured beside its string
A code_execution script gets each tool result as one string: the tool's
text, an "Error: ..." string for a failure, or a structured result's JSON
object. So a script tells a failure from a success by the text's prefix,
and a successful result whose text happens to start with "Error:" can't be
told from one.
The string is now a str subclass, ToolResult, unchanged as a string, with
two attributes beside it:
- .is_error: True for an error result, False otherwise. The framework sets
it at every return of handleScriptToolCall and scriptStructuredResult:
True for each "Error: ..." string it makes (a failure, a refusal,
code_execution called from a script, a result the script serializer
can't convert) and for a structured result whose isError is true,
including the two forms that couldn't be delivered (past the size cap
and unsaved, and one JSON can't hold); False for text and data results,
and for a result its tool returned with isError false, as the model
reads it. scriptStructuredResult reads each flag from the object its
string is made from.
- .structured: the parsed structuredContent of a structured result's JSON
object, or None. The host sends the string once and marks it as such an
object (structured_json); the runner parses it on first read.
PyRunner's onToolCall may return a ScriptToolResult ({ text, isError,
structuredJson? }), exported, or a plain string as before, for which both
attributes read None. Any other value is sent as before, and a handler
that rejects reaches the script as an "Error: ..." result flagged True.
ToolResult's constructor takes defaults so that copy, deepcopy and pickle,
which rebuild a str subclass from its text, keep working in the script.
The kwargs refusal shows a tool result key as its text, as it shows a str.
The tool description gains a sentence saying what the attributes mean, so
the first request after the upgrade that carries it misses the prompt cache
from its tool block on. Tests pin the flag at each return, through both of
the framework's runners, and the runner's value under a real python3.
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
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…and anima-research#372 main aae86b7 brings anima-research#234, anima-research#528, anima-research#529, anima-research#524, anima-research#538, anima-research#539, anima-research#372 and anima-research#210. One conflict, a comment in src/framework.ts's utils run: anima-research#372 rewrote its first lines (the registry checks the call against the utility resolved here; the model's path takes the result's stored form), and this branch added a paragraph on where the result goes, before the resultMaxChars line. The resolution keeps anima-research#372's lines, then this branch's paragraph and line. The code below it merged as git made it: the utility call carries resultMaxChars, and the registry takes anima-research#372's { definition: def }. anima-research#538 landed first and meets this branch in a test without a textual conflict: handleScriptToolCall now returns a ScriptToolResult, { text, isError }, where it returned the string. The script case of tool-call-result-max-chars.test.ts reads .text, and checks isError is false, as anima-research#538's own tests read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
The edit planner's branch, which this one is stacked on, answered the review of its whole change and that review's run, and merged main at aae86b7 into its head along the way, as 11354ae. Its head is now 39d0151. That brings anima-research#538 and anima-research#372, among others. This branch's changes apply to it as they stand: the merge has no conflict, and its diff from 39d0151 is this branch's diff from 8b03920, 10 files, +1042 -142.
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
main has taken anima-research#234, anima-research#528, anima-research#529, anima-research#524, anima-research#538, the 0.22.3 merge-back (anima-research#539), anima-research#372, anima-research#210 and anima-research#541 since 8d7694f. - changelog.d/403-truncation-image-slots.fixed.md: main deleted it when 0.22.3 took it into the changelog, where this branch had changed one clause of it, to say a spill file goes only to a writable mount without an include. Taken as deleted: 0.22.3's entry stays as released, and this change's own fragment (mount-include.added.md) already says where the framework keeps its files and that a setup whose only writable mount has an include is treated as having none. git merged the rest on its own. Against main, every file the merge changes differs exactly as this branch did against 8d7694f, so no change of main's is undone. Of the files both sides changed, src/framework.ts, src/types/framework.ts and src/types/trace.ts take this branch's spill texts beside main's changes elsewhere in them, and test/workspace-three-way.test.ts takes the nested-directory tests beside anima-research#528's re-attach tests. No use of firstWritableMountName or text about a writable workspace was added on main since 8d7694f. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
main has taken anima-research#234, anima-research#528, anima-research#529, anima-research#524, anima-research#538, the 0.22.3 merge-back (anima-research#539), anima-research#372, anima-research#210 and anima-research#541 since 8d7694f. - changelog.d/403-truncation-image-slots.fixed.md: main deleted it when 0.22.3 took it into the changelog, where this branch had changed one clause of it, to say a spill file goes only to a writable mount without an include. Taken as deleted: 0.22.3's entry stays as released, and this change's own fragment (mount-include.added.md) already says where the framework keeps its files and that a setup whose only writable mount has an include is treated as having none. git merged the rest on its own. Against main, every file the merge changes differs exactly as this branch did against 8d7694f, so no change of main's is undone. Of the files both sides changed, src/framework.ts, src/types/framework.ts and src/types/trace.ts take this branch's spill texts beside main's changes elsewhere in them, and test/workspace-three-way.test.ts takes the nested-directory tests beside anima-research#528's re-attach tests. No use of firstWritableMountName or text about a writable workspace was added on main since 8d7694f. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
…his branch's content arrays main moved from ce1a073 by 82 commits (anima-research#538, anima-research#539's 0.22.3 merge-back, anima-research#524, anima-research#529, anima-research#528, anima-research#372, anima-research#210 and anima-research#541). Two conflicts: - src/framework.ts, the script path's text result: anima-research#538 returns `{ text, isError: false }` where this branch passes the result's `dataIsContent` to toolResultDataToHistoryString. Both are kept. - test/script-structured-references.test.ts, its imports: anima-research#538's `ScriptToolResult` beside this branch's `referenceRegistry`. Read beside the conflicts, as they stand after the merge: anima-research#538's scriptStructuredResult, which still takes the content array the dispatch gave the call, where an MCPL peer's blocks were mapped through referenceBlockForScript, and the reference registry's address sites in start(), stop(), setMcplServerConfig and a server's removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
…, isError } in the on-demand fetch test anima-research#538 made handleScriptToolCall return { text, isError } where it returned the text. This branch's test of a link not fetched for a script on demand typed the call as returning a string, so the merge of main 00eb87a left it matching a regex against the object (J20564's one failure). It now reads `.text` and checks `isError` is false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
Brings in the 34 pull requests merged into main since 9e3e407, among them inbound-only channels (anima-research#350) and the script tool results (anima-research#538). Conflicts: - src/framework.ts: - anima-research#350 refuses speech into a channel its server declared inbound-only. It also kept the turn's destination for the speculative stream: set where a round is admitted, cleared where a target or a delivery is refused, with a `speakable` check in the stream's router. This branch removes the router and the destination, since the stream follows delivery, so that half goes. Its delivery half stays: a refused target, and a failed delivery to a channel declared inbound-only, end the turn's pin, and the comments that named the destination name the pin alone. - anima-research#538: a script's tool call returns a ScriptToolResult. The import and the signature keep this branch's round calls beside it. - src/mcpl/channel-registry.ts: the stream's doc is this branch's. The stream's server lookup keeps anima-research#350's inbound-only check, so nothing streams into such a channel once it's declared, a declaration that lands between a publish and its chunk included. - src/mcpl/prose-stream-router.ts: deleted here, as before. - test/channel-registry-routing.test.ts: main's failure type. - test/explicit-prose-routing.test.ts: the router's own test goes with the router. anima-research#350's refusal tests check the pin alone, and its queued-delivery test holds the first delivery until the second round's tool calls are yielded, since the mock membrane gives a turn's rounds to one stream. Texts: since anima-research#350, a subconscious's speak_in_channel reaches mcpl:speech-routed once it's delivered. This branch's added fragment and the trace's doc say so. Co-Authored-By: Claude Opus 5.5 <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.
A
code_executionscript can test a tool result for an error, and read its structured content, without parsing its string. Each result's string stays as it is, and gains.is_errorand.structuredbeside it. It builds on #262's structured results for scripts.Problem
A
code_executionscript gets each tool result as one string: the tool's text, anError: …string for a failure, or, for a result with structured content, a JSON object holdingcontent,structuredContentandisError(#262). So a script tells a failure from a success by the text's prefix. A successful result whose text happens to start withError:, such as a line read from a file or a log message, reads as a failure, and a failure the script misses reads as a success.What changes
ToolResult, astrsubclass whose string is unchanged. It carries two attributes:.is_error:Truefor an error result,Falseotherwise. It follows the result's error flag at every return ofhandleScriptToolCall:Truefor eachError: …string the framework makes: a failure, a refusal,code_executioncalled from a script, and a result the script serializer can't convert;Truefor a structured result whoseisErroris true. That includes the two forms that couldn't be delivered, one past the size cap that couldn't be saved and one JSON can't hold, whose strings already say the tool may have completed or did. A result past the cap that was saved is delivered as its path, flagged as the tool flagged it;Falsefor text and data results, and for a result its tool returned withisError: false, as the model reads it..structured: the parsedstructuredContentof a structured result's JSON object, orNone.is_errorbeside it, andstructured_json: truewhen the string is a structured result's object. The runner parses.structuredfrom the string on first read.PyRunner'sonToolCallmay return aScriptToolResult({ text, isError, structuredJson? }, exported) or a plain string, as before. A plain string's attributes readNone. Any other value a JavaScript handler resolves with is sent as before, and a handler that rejects reaches the script as anError: …result flaggedTrue.strkey.code_execution's definition, so the first request after the upgrade that carries it misses the prompt cache from its tool block on.Why each part is here
The change has one job: a script can tell an error result from a success, and reach structured content, without parsing the string, while every script written for strings keeps running.
strsubclass keeps the string exactly as it was, so code that uses it as a string keeps working: printing, slicing, comparing,json.loadsandisinstance(r, str). An exact type check such astype(r) is strnow readsFalse, which the changelog names. A separate accessor would need a second call for each result, and returning a pair or an object would break every script that uses the string..is_errorat each return is a field at each ofhandleScriptToolCall's returns andscriptStructuredResult's. The framework decides there whether a result is an error, so the flag is set where that is known. Reading the prefix in the runner would bring back the ambiguity the flag removes.scriptStructuredResultreads each flag from the object its string is made from, so the two can't disagree..structuredread from the string costs a cached property and one marker on the wire. SendingstructuredContentbeside the string would send it twice for each structured result, up to twice the 5 MB script cap on one line, since neither end of the pipe caps a line. Read on first use, it costs nothing to a script that never reads it.PyRunneritself. Its attributes readNone, "not said", sinceFalsewould claim a success the handler never reported. Only an object is read as aScriptToolResult, one condition, so a value a JavaScript handler resolves with that its type rules out is still answered rather than throwing where nothing catches it.ToolResultbesidestr, one comparison, so its promise to show a key whole holds for a tool result.ToolResultkeepsstr'srepr, so showing it runs no script code, which is what the exact check guards.copy,deepcopyandpicklewithin a script, which rebuild astrsubclass from its text alone and then restore its attributes, keep working. A pickle names the runtime's class, so it loads only in the script runtime, where a pickledstrloaded anywhere..is_erroragainst the string's prefix, since the prefix is what's ambiguous;Tests
New or changed for this change, each failing on
mainat this change's base (5227741b) with only theScriptToolResulttype added there, so it compiles:code-execution, under a realpython3: a script reads each form of result. That's a flagged failure (True), text that only starts like an error (False), a structured object (.structuredparsed), an oversized form (None), unmarked text holding astructuredContentkey (None), a plain-string handler (None,None), a handler resolvingundefined(answered with'') and a rejecting handler (True). It also checksjson.loadsof the string,copyandpickle, and a refusal that shows a tool result key as its text. Before this change it stops atAttributeError: 'str' object has no attribute 'is_error'.code-execution, through the framework: the foreground integration test's script reports.is_errorfor its two calls, and the background test's script sends it in its wake. These pin both runners' call sites, since the handler type also takes a plain string. Before this change each script stops at.is_error.mcp-modern-framework: the structured-result tests pinisErrorandstructuredJsonbeside each string, inline, at the cap, past it, and undelivered, and a structured tool error inline readstrue.refusals-are-error-results: a script's refusals readisError: trueand its successfalse, andcode_executioncalled from a script is refused as an error.unconvertible-tool-result: a result its tool didn't flag readsfalse, as the model reads it, even when its text starts withError:, and unusable results readtrue. A new test reaches the two returns that only a result past the dispatch's own check can reach, a result the script serializer can't convert, as text and as a structured object, with the dispatch stubbed.Adapted, not new:
script-structured-referencesand two tests oftool-result-reference-uriread.text, since the handler now returns the string beside its flag. They fail on the old handler only for that reason.Removals: 23, each taking one piece out, or inverting it, on its own:
None, and a non-object result read as aScriptToolResult;.structured's key, and its check of the mark, removed or replaced by atry/except;handleScriptToolCall's returns, one of them taken from the text'sError:prefix instead, andscriptStructuredResult's flag in each direction, and its mark;All ran at this head's tree. Each failed at least one test aimed at it, and the head passed before and after.
Checks
PyRunners use plain-string handlers. CI runs the whole suite on Ubuntu and macOS with Node 20 and 24.Not verified
python3.ToolResultalone, asruntime-py.tsdefines it, passed the same 27 checks on CPython 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13. These coveredisinstance, equality and hashing,json.loadsandjson.dumps, slicing and formatting, the lazy and cached.structured,copy,deepcopy, andpickleat every protocol. The runner already needs 3.8 or later for top-levelawait, ascached_propertydoes.Merge order
mcp-modern-framework'sechocase pins the JSON-quoted string, so by our reading that case fails with both in. Nothing ran the two together, since fix(tool-results): send a string result as its text, not JSON-quoted #400 conflicts withmainitself, insrc/framework.tsandsrc/tool-result-history.ts.src/code-execution/runtime-py.ts: the protocol comment, where it changes theinitandexeclines and this change thetool_resultline, and the imports, where it addsfaulthandlerand this changefunctools. Keeping both sides' lines resolves each.changelog.d/script-result-flags.added.md.Written by Crispin-2713 (Claude Opus 5.5), contributing through nissa-seru, to a design by Ada-1017 (Claude Opus 5.5) and Gladys-2592 (Claude Opus 5.5). William-2616 (Claude Opus 5.5) reviewed it.
Description checked and posted by Lottie-2661 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.