Repository navigation
fix(tool-results): show each block of a content array, never the array's JSON - #527
nissa-seru wants to merge 19 commits into
Conversation
…y's JSON
A tool result whose content array held one block the tool-result lane
didn't recognize went to history, and to the model, as the whole array's
JSON. That put inline audio's base64, an image's base64 when it had no
media type, and an unknown block's uri (a signed URL's signature
included) into the agent's context, and turned a native image beside it
into base64 text. The push lane showed each of those blocks as a marker.
One per-block function now feeds both copies (toolResultBlocks in
tool-result-history.ts): the stored text and the live copy
(tryNativeToolResultContent) classify each block alike, so the stored
copy's shown ranges stay the live copy's. Each block is itself (text, or
an image with data and a media type), a reference's stub, inline data
withheld for a contradiction, or a marker saying what it was and why it
isn't shown: the push lane's markers, and for inline audio, which this
host's tool results can't carry, "[Audio not shown: this host's tool
results carry only text and images (<type>, <size>).]". An image whose
data or media type is an empty string reads as its marker, as the push
lane reads it.
Which arrays are content: a protocol result's content is, whatever its
blocks' types. ToolResult.dataIsContent says so; the framework sets it
on an MCPL tool's result, from the model's dispatch and from an
in-process call. checkedToolResult keeps it as a flag, read by name, so
it survives the copy the admin puppet stores its pair from, and the
puppet stores what a model's call stores. Readers take it for its truth,
as a result's other flags. Any other array (a module's, a script's, a
direct caller's) is content only when every element is a block of one of
MCP's five types (text, image, audio, resource, resource_link). Any other
element makes it data, written as JSON as before. The miss this leaves:
a list of records that all carry MCP's type names, such as
[{ type: 'image', url }], reads as content.
MCP's resource_link is a reference (RFC-005 §8.1): classifyBlock maps its
uri, name, mimeType and size (as sizeBytes) onto the reference record,
with no disposition, and marks the testimony a link's (fromLink). No uri
reaches context that was kept out of it before:
- A link the host can fetch shows as a stub on every lane, convertBlock's
included, and at any depth in data (governedByRfc005); showsAsItself
lets a render result carry one.
- A tool result's link the host can't fetch, whose JSON showed its uri
before, shows its name, uri and type, "[resource link "<name>": <uri>,
<type>; not fetched]", as anima-research#262 shows a standard server's link: the uri
is the agent's only handle on it, and showing it is §5's MAY for a
reference the host classifies as non-capability-bearing. The tool-result
lane asks for this (StubOptions.unfetchedLinkUri) at both its sites.
- Unless the uri, as it would be shown, may carry a credential
(uriMayCarryCredential): userinfo, or a query or fragment parameter
with a value, named on CREDENTIAL_PARAMETERS, one list with an example
of each family (AWS's, Google Cloud Storage's, Azure's, CloudFront's and
B2's signed URLs, Discord's CDN, OAuth's tokens and secrets, generic
names), compared by letters and digits alone, without case, a fragment
read as a query is, '?' and ';' separating parameters too, a uri parsed
as absolute before it's tried as relative, and a uri that doesn't parse
even as a relative reference counted as one; or a reference withheld it
with disposition never. Such a link gets the stub that says why it can't
be fetched, with no uri. Plain links with a query, such as watch?v=,
and anchors named like a credential, such as #token, which carry no
value, still show.
- On every other lane a link the host can't fetch gets that stub too,
and so does a resource reference on any lane: neither's uri showed
before, and a credential the list misses, such as a token in a path,
stays out of context there.
A link is registered with its server but never fetched eagerly
(autoFetchReferences) or on a script's demand: a tool's links are what
it offers to follow. A uri a link names is fetched only when a block that
isn't a link names it too. A link's testimony never replaces a record's
in the registry, since it claims no digest, expiry or disposition, so a
later link can't lift a reference's expiry or digest check.
A stub's promise is the fetch's own check. anima-research#518's pre-dial check of the
server and expiry becomes dialServer (servers in order, the origin) and
dialCheck (dialServer, then the expiry) in references.ts, pure functions
of a record and the addresses its servers are dialed at. The fetcher
runs them on its own framework's configs, through its resolver, so the
origin it checks is the url of the config whose token it sends, as
anima-research#518 checked it against the binding the token came from; anima-research#518's eager
check joins a fetch under way with dialServer alone. A stub runs
dialCheck on the reference registry's addresses (unfetchableReason),
since the code that makes a stub can't reach its framework: the
framework records each server's address there as it configures it
(setMcplServerConfig, from serverAddressOf) and takes it back when it
removes the server. The registry keeps each framework's entry per id,
the latest in force, and a removal takes back only its own; stop()
takes back all of a framework's entries, so a stopped one is neither
read nor kept reachable, and start() records them again. With one
framework, or frameworks that agree on each id they share, a stub
promises what the fetch does; where two configure one id differently,
only a stub's words can be wrong, never where a token goes. So a
resource stub now says why a fetch would be refused (no address,
unknown server, off its server's origin, a scheme the fetcher refuses,
plaintext from a secure origin, unbound, expired) where it offered
fetch_reference. A third-party origin and a refused scheme read in a
stub without the uri's parts; a server's id is bounded and stripped of
controls, in a stub and in fetch_reference's refusal alike, so a stub
stays within REFERENCE_STUB_MAX_CHARS. The plaintext refusal runs before
the origins are compared, which had made it unreachable, for an http uri
on the secure server's host on the same port or each on its scheme's
default.
sanitizeLabel strips U+2028 and U+2029 too, so no label starts a line
the image ledger would read as a slot. The script-demand loop registers
each reference rather than finding it by uri, so a re-issued
reference's claims replace an earlier link's or reference's before the
fetch reads them.
The empty-link rule is made whole, as anima-research#506's merge note leaves open:
inline data beside uri '' is plain inline data on every lane. A link
that isn't empty still withholds it.
Tests: tool-result-blocks.test.ts pins each shape on both copies, the
content rule with and without dataIsContent, resource_link on each lane
and in each fetch path, a tool result's unfetchable link shown where its
uri can't carry a credential and stubbed where it may (a uri from each
family of the list), the push lane's link never showing its uri, the
render check, a link never replacing a reference's claims, each refusal
a stub gives and the fetcher's own check agreeing with it, a refused
scheme unnamed in a stub, two frameworks that
configure one id at two urls (neither fetch dials the other's origin,
each carries its own token to its own server, and a removal takes back
only its own address), a framework's stop and start, plaintext from a secure
origin, an unparseable server url, a link a reference withheld with
never, a credential name or a scheme split by a control character, a link line with
line separators reading as no slot, a script's demand fetch reading a
reference's claims over a link's, the empty link beside data on both lanes,
and dataIsContent through checkedToolResult, a model's round, and an
MCPL call from the model, in process and through the puppet. Three tests
that pinned the old behaviour are turned round: inline audio beside text
(tool-result-history), a resource_link's uri in data
(tool-result-reference-uri), and the empty link beside audio
(mcpl-agent-words). mcpl-reference-fields, mcpl-reference-binding and
save-recent-image configure their servers as the framework does, and
save-recent-image's stub bound check takes each refusal at its longest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uriMayCarryCredential counts a query or fragment parameter named like a credential only when it has a value, and a '?' or ';' separated parameters too, for routes kept in a fragment. So a value that opens with one of them, as in token=;zz or #access_token=?zz, read as empty, and a tool result's link line showed the uri. The parameters are now read both ways, with '&' alone separating them, as a server reading the query as WHATWG does sees them, and with '?' and ';' as well; a credential-named parameter with a value in either reading counts. Both uris join the carriers test. samePlace's doc states the rule as the code has it: the same port, or neither url giving one, or one giving the other's default. forgetServerAddress's doc no longer says a stopped framework's fetch refuses what its stubs do: a stopped framework keeps its configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes reach tool-result handling and reference fetching across agents that use MCPL tools. Fix the live text cap for image results containing links before merging. Findings
Fix with agent prompt### Issue 1
src/framework.ts:18848-18859
An image beside a large list of `resource_link` blocks now bypasses the live result's text cap. Each link can add a 4,096-character URL, but this loop sends every reference line whole. Previously, this combination used the capped JSON fallback.
`estimateAppendedRoundTokens` still measures the cut saved text, so it can miss enough live text to send a request beyond the model's context limit. Give the native result a shared text budget and estimate request growth from the content actually sent.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryShows protocol result arrays block by block, preserving native images and replacing unsupported blocks with markers.
Reviews (1) · Last reviewed commit: "fix(tool-results): show each block of a ..." · Reviewed by Greptile |
| return blocks.map((block): ToolResultContentBlock => { | ||
| if (block.kind === 'image') { | ||
| return { type: 'image', source: { type: 'base64', data: block.data, mediaType: block.mimeType } }; | ||
| } | ||
| let text = block.text; | ||
| if (block.kind === 'text' && maxChars && text.length > maxChars) { | ||
| text = text.slice(0, toolResultCutAt(text, maxChars)) | ||
| + '\n\n[truncated — original was ' + text.length + ' chars' | ||
| + (spillPath ? `; full serialized result: workspace file ${spillPath}` : '') | ||
| + ']'; | ||
| } | ||
| return { type: 'text', text }; |
There was a problem hiding this comment.
Link results bypass text limits
An image beside a large list of resource_link blocks now bypasses the live result's text cap. Each link can add a 4,096-character URL, but this loop sends every reference line whole. Previously, this combination used the capped JSON fallback.
estimateAppendedRoundTokens still measures the cut saved text, so it can miss enough live text to send a request beyond the model's context limit. Give the native result a shared text budget and estimate request growth from the content actually sent.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 18848-18859
Comment:
**Link results bypass text limits**
An image beside a large list of `resource_link` blocks now bypasses the live result's text cap. Each link can add a 4,096-character URL, but this loop sends every reference line whole. Previously, this combination used the capped JSON fallback.
`estimateAppendedRoundTokens` still measures the cut saved text, so it can miss enough live text to send a request beyond the model's context limit. Give the native result a shared text budget and estimate request growth from the content actually sent.
**Knowledge Base Used:**
- [Tool result safety and integrity](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/tool-result-safety.md)
- [Tool result presentation](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/tool-presentation.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
The live copy sent links' blocks and markers whole beside an image, with no budget across them, where a result holding a resource_link went as the capped stored text before this change. A link line carries its uri whole, so enough of them could carry the live copy far past the cap.
Fixed in adc597e. One function, goesNative, decides for both copies. When a result's links and markers come to more than the cap together, the live copy is the stored text, cut at the cap as it is. Below that, they still go beside the image. The stored copy asks the same function for its shown ranges, so it keeps only the image slots the live copy showed. A reference's stub and a withheld line still go whole beside an image, as #403 designed, and the stored copy keeps past its cut the newest 64 image slots the live copy showed. The tests put an image beside six links with 1,000-character uris against a 2,000-character cap, and beside thirty audio markers against a 1,000-character cap. They also send thirty links through a model's round.
Several large text blocks beside an image, each under the cap, go as they do on main. This change doesn't alter how they're cut.
Written by Beth-2709 (Claude Opus 5.5).
— Fiona-2757
…he cap The live copy of a tool result that holds an image sends its blocks natively: the image as itself, each text block cut at the cap, and every other block whole. Links' blocks and markers came onto that path when each block began to show on its own. Before, a result holding a link, inline audio or a block of a type the lane doesn't know went as its stored text, cut at the cap. Whole and with no budget across them, they could carry the live copy far past the cap: a link's line carries its uri of up to 4,096 characters. goesNative (tool-result-history.ts) decides for both copies: the live copy goes native only when the result holds an image and its links' blocks and markers fit the cap together. Otherwise it's the stored text, cut at the cap as it is, with the image as its placeholder and ledger ref. A link's block is told apart by its own kind, `link`, from the testimony's fromLink. A reference's stub and a withheld line still go whole beside an image whatever their length, as anima-research#403 designed, with the newest 64 image slots the live copy showed kept past the stored copy's cut, so anima-research#403's tests stand unchanged. storedToolResultText's shown ranges ask the same function, so the stored copy keeps only slots the live copy showed. Tests: an image beside six links with 1,000-character uris, against a 2,000-character cap, goes as the stored text, while one such link still goes natively, and so for thirty audio markers against 1,000 characters while two go natively; thirty links through a model's round go as the stored copy, the image's placeholder keeping its ledger ref, and with the image after them its slot isn't kept past the cut. Twenty reference stubs, or five and twelve withheld lines, beside an image still go natively past a 1,000-character cap, and a stub longer than a 40-character cap still goes whole. The marker test's cap is raised so its marker fits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e dialing that references.ts now holds main brought anima-research#531, which words each of the fetcher's refusals as a clause and a remedy, { error, remedy }, while this branch moved the checks before dialing into references.ts (dialServer, dialCheck), with anima-research#518's { reason, ofReference }, so that a stub can ask them. The two conflicted in reference-fetcher.ts and references.ts. In references.ts: - Refusal is { error, remedy, inStub? }: anima-research#531's clause and remedy, and this branch's stub wording where the clause names parts of the uri. - The remedies the checks give (NOTHING_TO_DO, FRESH_REFERENCE, SERVER_BACK) move here from the fetcher, which imports them. - originRefusal takes anima-research#531's checks: a reference address and a server address that don't parse, a user name or password in the address, the uri's own scheme, and the origin, naming the server's address as configured. The plaintext-from-secure check and samePlace go: the origin clause already shows http://h beside wss://h. - dialServer and dialCheck give anima-research#531's clauses for an unknown server, one with no address, an unbound reference and an expired one, each server id through shownServerId. - A stub shows inStub or the clause, never a remedy. In reference-fetcher.ts, beforeDial takes dialCheck, then anima-research#531's limits in anima-research#531's order: the host's ceiling, the budget, then a call's ceiling. Its other clauses name the server through shownServerId too. Tests: this branch's tests take anima-research#531's clauses where they pinned the old reasons, and a stub's ending for an unbound or expired reference reads "it's not tied to any server, ..." and "the reference is expired"; the stub bound test adds a server address that doesn't parse and an address with a user name or password; anima-research#531's refusal table compares error and remedy, since a refusal can carry inStub; the plaintext half of the plaintext test goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plaintext refusal The merge of main took anima-research#531's refusals into dialServer and dialCheck, and dropped the plaintext-from-secure refusal, as anima-research#531 did: an http reference from a server dialed over TLS is refused as off its server's origin, and that clause names both addresses. tool-result-blocks.fixed.md no longer describes the plaintext refusal. It says a stub ends with what fetch_reference would say happened, without what calling again can do, and it adds the refusals anima-research#531 brought to what a stub covers: a server whose address doesn't parse, and a uri with a user name or password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server's id is bounded and stripped of controls in a stub and in fetch_reference's refusals alike (shownServerId). No test gave a server an id that changes, so the fetcher's clauses after dialing, which name the server through it, and its budget refusals were unpinned. One test now sends a server whose id carries a bidi control and runs past 120 characters through an answer's status, a spent budget and a claim past what's left of one, and checks each clause holds the bounded id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s branch's content arrays and links main brought anima-research#262, which serves modern MCP servers beside MCPL ones and splits each tool result's reading by peer: an MCPL peer's keeps the RFC-005 reading, and a standard peer's (a modern server's or an MCP-only legacy one's) goes through the standard normalizer. It also moved the model path's conversion into mcpToolResult, an in-process caller's into directToolResult, and the script-demand fetch into autoFetchReferences' modes. Nine hunks of framework.ts conflicted. - An MCPL peer's passed-through blocks are marked as content (dataIsContent), on the model's path in mcpToolResult and for an in-process caller in executeMcplToolCall, as this branch marked them. A standard peer's results stay as anima-research#262 has them. - autoFetchReferences takes anima-research#262's modes and doc; its `live` list leaves out a uri only links name, so neither the model's eager fetch nor a script's demand fetch fetches a link. - The script path takes anima-research#262's structure and its cap constant. A module's array still skips links and registers each reference before its fetch, and its text is read with the result's dataIsContent. - stop() and a server's removal take back the server's address in the reference registry beside anima-research#262's new steps; setMcplServerConfig stays the one place a config is recorded. - A link in a script's structured result (referenceBlockForScript) now classifies as a reference. It carries `uri` exactly where the model's line for it shows the uri: shownLinkUri, which the tool result's link line asks too, decides both (Ada-1017's call, with Crispin-2406's reason: what a script prints comes back to the model). Where the stub gives a reason the fetcher would refuse, the object carries the same words as `unfetchable` (unfetchableReason), beside anima-research#262's `expired`. A test runs five links through both lanes, and anima-research#262's unreleased mcp-standard-tool-results fragment says both. anima-research#262's script-structured-references test is turned round for them: its harness records the `files` peer's address, as configuring it does, so its link is a reference the host can fetch, with no uri, and its expired reference carries the stub's words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…istered first, one resolver answer per fetch From a whole-PR review of this change at 34418e2. - A tool result's live copy is built from the blocks its stored copy was made from (StoredToolResult.live), never classified again. A link's text reads the reference registry and the clock, and the spill's write lies between the two copies, so two classifications could decide goesNative differently: the live copy could then show an image whose slot the stored copy didn't keep. The path with no stored copy classifies once on its own (tryNativeToolResultContent, nativeContent). - A result's references are registered before any block is shown, in a content array (toolResultBlocks) and at any depth in data (registerReferencesIn, before historyJson), so a disposition:"never" that a later block gives a uri holds for an earlier link to it. - A resource_link at any depth is governed whatever its uri, so one with an empty or non-string uri is its marker there, as on the content lanes, never its fields. - The fetcher reads each server's resolution once per check (chosen), so the address the origin check passes and the token the fetch sends come from one answer; bindingFor, whose refusals that makes unreachable, goes. - An MCPL result with an element that isn't a block, null among them, shows that element's marker: mcplResultText, the model path's non-text scan and the channel-id lookup no longer dereference it. - A script's object for a link gives `expired`, as `unfetchable`, only where the model's line is a stub; a link line that shows its uri gives neither. - CREDENTIAL_PARAMETERS adds auth_token, api_token, session_id and jsessionid (Ada-1017's call). - Docs: shownLinkUri sits above buildReferenceStub's doc, ServerAddress says which servers have no address, and the comments that said no uri reaches the model name the link exception. cutStoredToolResult's doc names goesNative. The fixed fragment says a link at depth reads as in a content array and that a module's array is content by the flag only if it sets it; the changed fragment names MCPL and modules' results and says a standard server's links show their uri. Tests: a later never withholding an earlier link's uri, in a content array and at depth; a link with no usable uri at depth as its marker; the live copy built from the stored pass's blocks; the cap's exact boundary; a result's own dataIsContent over the options'; each credential parameter alone; an audio marker's bounded type; no expiry on a script's object for a shown link; the resolver asked once per check; an MCPL result with a null element; save-recent-image's longest stub reaching every ending; a runtime connect recording its address (mcpl-runtime-lifecycle); the bounded server id pinned literally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
… only where the line is a stub mcp-standard-tool-results.changed.md, unreleased, said an unsaved reference that counts as expired carries `expired`. Since a script's object for a link gives `expired`, as `unfetchable`, only where the model's line for it is a stub, a link whose line shows its uri carries neither. The sentence now says so, and keeps that a reference that counts as expired isn't fetched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d fetch differ dialCheck's doc said that with one framework, or frameworks that agree on each id they share, a stub and the fetch read the same addresses. A stopped framework's addresses are out of the reference registry while its configs still resolve them, so that holds only for running frameworks. The description's sentence on the same point was corrected from the whole-PR review (af-f53-sol I11); this is the same claim beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, 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>
…s; one clock reading per object and stub Ada-1017's call (room-203 #148838), on Desmond-2579's read (#148827): a script's reference object always carries a refId, which a model's line for a link doesn't, so its words say what a fetch_reference with that refId meets rather than mirroring the line. An unsaved reference that counts as expired carries `expired` as anima-research#262 has it, and one the fetcher would refuse carries `unfetchable`, the words a stub gives, a link whose line shows its uri included: the host can't fetch such a link, so its refId can only be refused. `uri` stays the line's one decision (shownLinkUri). This reverts ec8809b's gating of `expired` on the line being a stub (af-f53-sol I30), which went past Ada's "`expired` stays as anima-research#262 has it" (#148223). referenceBlockForScript and buildReferenceStub each read the clock once and pass that reading to shownLinkUri, referenceExpiry and unfetchableReason, so an object or a stub can't disagree with itself across an expiry (Desmond's second point, Ada's yes). anima-research#262's unreleased mcp-standard-tool-results fragment says both words again, and that a link whose line shows its uri carries them. Tests: the five links' objects carry each refusal's exact words, a link whose line shows its uri included; an expired link whose line shows its uri carries `expired` and `unfetchable`; a clock that moves past an expiry after its first reading leaves a reference's and a link's object and stub as of that reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reference marked never The changed fragment, which says what agents stop seeing, named only a link whose uri may carry a credential. Since this change's second round, a link to a uri whose reference, as the host last recorded it, said `disposition: never` shows the same stub with no uri too (shownLinkUri), where the result's JSON showed it, as RFC-005 §5 asks. The fragment now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the file loads against main for red-first The audio marker test from the whole-PR review's fixes (I19) imported toolResultBlocks, which main's tool-result-history doesn't export. Run against main's src for red-first, the file then failed to load whole and none of its tests ran (J20606's red run: 48 not found). The test now reads the marker through toolResultDataToHistoryString, as the file's other tests do, and checks it is the audio marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne classification without a stored copy
From the round review of the whole-PR review's fixes (af-f53-sol-r1).
- A tool result's references are read as one (ResultClaims, Ada-1017's
call): a uri any of them marks `disposition: never` is withheld from
every block that names it, and one any of them gives an expiry that
counts is expired for each, whatever the order of the blocks and
whatever the bounded registry evicted while the result was read. Each
block's decision reads the result's claims with its record's own
(claimedNever, claimedExpiry): a link's uri (shownLinkUri), a stub's
reason (dialCheck through unfetchableReason, StubOptions.claims), a
script's object (referenceBlockForScript), and the module demand fetch,
which now passes over a claimed expiry as autoFetchReferences does with
expiredHere. Across results the registry keeps the latest testimony.
A fetch reads the record alone.
- A content array's claims come from its elements (contentClaims), for
the model's lanes and for an MCPL peer's blocks in a script's result.
- At depth they're read by the walk serialization itself takes
(walkedInHistory under JSON.stringify, claimsInHistory): through
toJSON, into inline data's other fields, and no further than a value
serialization can't write. The walk registers nothing.
- This replaces registering every reference before showing any, which
each block's own registration as it was shown undid.
- Without a stored copy, the live copy classifies once: the blocks
nonErrorResultText made its text from go natively where goesNative
holds (onBlocks), as on the stored path. tryNativeToolResultContent goes.
- Comments: the cut's doc says when goesNative says no; the live copy's
builder is named as toMembraneToolResult's nativeContent; the fallback
and shownInHistory say a link's uri shows only in its line; dialCheck's
doc names another running framework's entry for a stopped one's id.
- The changed fragment says a never from any reference in the same result,
or the latest from an earlier one, withholds a link's uri.
Tests: a never withholding a link's uri in five orders, at depth, in a
script's object, inside an image's _meta, and past 2,001 references; an
expiry read for every block whatever the order; claims read through toJSON
beside a throwing getter; nothing registered past a cycle; the module
demand fetch passing over a claimed expiry; a fetch's token and the eager
join's limit each from one resolver answer; the live copy handed over
through buildStoredToolResultContent, and the cap's boundary there; an
MCPL send whose result holds a null element before its channel's id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irst; the demand fetch and kept fields read them as showing does From the round review of the previous commit (af-f53-sol-r2). - A result's claims are one summary per uri (UriClaims: never, rejected, unparseable, earliest expiry), built once from its references, so each decision is constant time where it rescanned every testimony for the uri. - A decision reads its result's claims for a uri where the result's references say anything of it, and the record's own testimony, the latest an earlier result left, only where they don't. A link shown before its result's own reference no longer reads an earlier result's claims. - The module demand fetch reads a result's claims as showing it does (resultClaimsOf: a content array's elements, other data by the walk serialization takes), where it read a data array's top-level elements, and logs a fetch it doesn't make for a claimed expiry. - A withheld result's kept fields are shown with the whole result's claims (HistorySerializeOptions.claims), so a never elsewhere in it withholds a kept link's uri. - The words of a script's object and a stub give why a fetch_reference would be refused, as the result's references claim it; the fetch itself reads the record (Ada-1017's call). - Docs: contentClaims says an element's own fields don't count, the walk's docs hold for values that answer the same each time, claimsInHistory says toJSON and getters run in both walks, nativeContent's doc names its caller's check, shownInHistory's says a value RFC-005 doesn't govern keeps its fields. The claims walk writes a string as 0, its text being thrown away (Desmond-2579's measurement and suggestion). The changed fragment says when an earlier result's never decides. Tests: each block of a never result arrives, as a stub, a script object with its refId, or a script result's content; an expiry at depth; competing expiries worded by one fixed order; a result's own reference deciding over an earlier result's expiry in both orders; a content array not counting an image's _meta; claims read through toJSON, with the getter it leaves out unread; a withheld result's kept link withheld by a never elsewhere; a result's data read once without a stored copy, natively and as text; the demand fetch passing over an expiry at depth in a module's data, and its log line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ript's text reads its demand fetch's claims From the round review of the previous commit (af-f53-sol-r3). - claimedExpiry takes the first of EXPIRY_REASON's cases a uri's claims give, by EXPIRY_REASON's key order, as its doc says, where the previous commit wrote the order out by hand. - A module's result shown to a script reads the claims its demand fetch read (HistorySerializeOptions.claims), so its data is walked for claims once, and the fetches and the text read the same claims. - Docs: where a result's references name a uri, a plain one included, they decide it, and the record's testimony decides otherwise: a reference is its result's latest word on the uri, as register() keeps it, so an earlier result's never or expiry gives way to it (Ada-1017's call). The comments that said the record's testimony is read with the result's claims now say so. A stub reads an expiry with its result's claims and the fetch with the record, which keeps the latest testimony until it's fetched; the fixed fragment no longer says a stub ends with what the fetch would say. The changed fragment says a link names a uri without claiming anything, and claimsInHistory says where a withheld result meets a value its walk can't write. - The depth test of the demand fetch, committed in the previous commit without a run, expected the stubs to say the reference is expired; a module's reference has no server, so its stub says that first. It now asserts the demand loop's own log line. Tests: competing expiries of all three kinds in every order; a later result's plain reference lifting an earlier never, link first and reference first, and a link alone leaving it; a module's content array whose unknown block names a uri expired is still fetched; a fresh control for the depth test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s its claims walk From the round review af-f53-sol-r3 (its I5), which I'd judged not real by reading; Desmond-2579's probe showed it real. A withheld result's kept fields are shown with the whole result's claims, read by a walk of their own. The guard's audit serializes the whole result first, but JSON reads only own enumerable fields and toJSON, while the walk reads a value's `type` as a plain property. So an inherited getter that throws passed the audit and stopped the walk, whose catch kept the claims read so far, and a kept link showed a uri a later reference marked never. - claimsInHistory is null when a value stops its walk, and so is resultClaimsOf: what the rest of the data claims is unknown. - keptWhenWithheld keeps nothing then, as for a failed result. - Showing the whole result reads no claims then (NO_CLAIMS), since it stops at the same value, and the module demand fetch reads the records. Tests: a withheld result whose data holds a value with an inherited getter that throws, which the result's JSON doesn't read, keeps nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Anarchid @slimepriestess We're asking one of you to review this pull request before we merge it. It changes how the tool-result lane shows a content array: each block on its own, in the copy the history stores and in the one the model reads, never the whole array's JSON. Inline audio's base64, an image's base64 without a media type, and the It has been through eight rounds of our internal review, a colleague's read of each round, and Greptile's review, whose finding is fixed, as the description says. CI passes all six checks at Written by Beth-2709 (Claude Opus 5.5). — Beth-2709 |
A tool result's content array went to the model, and to the agent's history, as the whole array's JSON whenever one of its blocks was one the tool-result lane didn't recognize. For example,
[text, audio]was stored and sent as[{"type":"text",…},{"type":"audio","data":"<base64>",…}]. That put into the agent's context:uriof a block of a type the host doesn't know, such asvideo, a signed URL's signature included;The push lane shows each of these blocks as a marker, or inline audio with a media type as audio. With this change the tool-result lane shows each block on its own too, in both copies of the result: the text history stores and the content the model reads when the result is answered.
What changes
Each block of a content array is one of:
[image: …]placeholder;The markers are the ones the push lane shows. Inline audio has one of its own, because this host's tool results carry only text and images:
[Audio not shown: this host's tool results carry only text and images (audio/wav, ~3KB).]. One function,toolResultBlocks, classifies each block for both copies, so they read each block alike.The live copy of a result with an image sends the image natively beside the other blocks, as before: each text block cut at the cap, and each reference's stub and withheld line whole. Its links and markers go too while they fit the cap together. Past that, the live copy is the stored text, cut at the cap, as a result holding a link or inline audio was before. So no number of links and markers carries a result past the cap.
Which arrays are content. A protocol result's content is content whatever its blocks' types. The new optional
ToolResult.dataIsContentsays so. The framework sets it on an MCPL tool's result, from the model's dispatch and from an in-process call, and a module that returns a content array can set it too. Any other array, such as a module's, a script's or a direct caller's that doesn't set the flag, is content only when every element is a block of one of MCP's types (text,image,audio,resource,resource_link). An array with any other element is data, written as JSON as before, so a list of records of other types isn't hidden behind markers.MCP's
resource_linkis a reference, as RFC-005 §8.1 maps it. Itsuri,name,mimeTypeandsize(assizeBytes) make the reference record, with no disposition. No uri reaches context thatmainkeeps out of it.fetch_reference, on every lane: in a tool result's content, at any depth in its data, and on the push, channel and injection lanes (convertBlock).file://or a server's own; a uri off its server's origin; anhttpuri from a server dialed over a secure connection; a reference from a server with no address, such as a stdio one, or from a server the host doesn't know; and a module's link, which no server delivered.[resource link "report.csv": file:///…/report.csv, text/csv; not fetched]. That's how feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262 shows a standard MCP server's link. A tool result's JSON showed every link's uri onmain, and the uri is the agent's only handle on a link the host can't fetch. RFC-005 §5 allows this (a MAY) for a reference with no disposition, which §8.1 makes every link, when the host's policy classifies it as non-capability-bearing.CREDENTIAL_PARAMETERS. The list covers AWS's, Google Cloud Storage's, Azure's, CloudFront's, Discord's CDN's and Backblaze B2's signed URLs, OAuth's tokens and secrets (OAuth 1.0a's too), generic names such astoken,key,x-api-key,api_tokenandpassword, and a session's id (session_id,jsessionid), which is a bearer credential too. A fragment is read as a query is, so#access_token=…counts, and a?or;inside either may separate parameters too, as in a route kept in a fragment,#/reset?token=…, so the parameters are read both with and without them as separators, and one with a value in either reading counts. Names are compared by their letters and digits alone, soaccessToken,access-tokenandtoken[]count asaccess_tokenandtokendo. Only a parameter with a value counts, since a credential is always a value, so a docs link to an anchor such as#tokenshows. A uri that doesn't parse, even as a relative reference, counts too, since its parts can't be read. Ordinary links with a query, such aswatch?v=…or?usp=sharing, still show.main: the push, channel and injection lanes showed a link as an unknown block.autoFetchReferences), nor on a script's demand, because a tool's links are what it offers to follow, and fetching them would spend the server's fetch budget on bytes nobody asked for. A uri a link names is fetched only when a block of the result that isn't a link names it too.A stub's promise is the fetch's own check. Which server a fetch dials, and whether it may, is one pure function,
dialCheck, of the reference's record and the addresses its servers are dialed at. It's the fetcher's pre-dial check from #518, moved intoreferences.ts, withdialServeras its server part. Aresourcestub asks it too, so a reference whose server or expiry stops a fetch now says why, where it offeredfetch_reference. The stub reads an expiry with its result's claims, where the fetch reads the record alone, so where one result names a uri both expired and fresh, the stub says expired while a fetch can succeed ("One reading of a result's claims", below). Examples are a stdio server's reference, one off its server's origin, or one with a scheme the fetcher refuses. A fetch's own limits, a spent budget or a claim over its ceiling, still apply when it's made, as before.fetch_reference's refusal gives, in its own words where the clause would name the uri's parts, and never with the remedy. A third-party origin in a stub doesn't name the origins, and a refused scheme doesn't name the scheme, thoughfetch_reference's refusal names both. A server's id is bounded and stripped of controls, as a label is, in a stub and infetch_reference's refusal alike. So no stub ends with a refusal longer than a saved copy's path, the longest ending a stub has, and a stub stays withinREFERENCE_STUB_MAX_CHARS.fetch_reference's own refusal can be longer, since it names the origins.save_recent_imagecounts a stub line as an image slot only up to that length.An empty
uribeside inline data is no link. This completes the rule that #506's comment leaves open. Inline data besideuri: ''is now plain inline data on every lane, and a link that isn't empty still withholds it.What callers will notice
fetch_reference, where today it showed its uri in the JSON.fetch_referencereturns its content.user:pass@, or one with a token in its query or fragment, and has no other handle on it here. This is intended, since RFC-005 §5 treats such uris as bearer credentials.disposition: never, any reference in the same result, or, where no reference in the result names the uri, the host's latest record from an earlier one while it keeps it, shows the same stub, with no uri, where today its JSON showed the uri. RFC-005 §5 removes such a uri unconditionally.[Content not shown: this host can't display "resource_link" content.]. Now it shows as a stub where the host can fetch it, and as the stub that says why where it can't, never with its uri.resourcestub says why a fetch would be refused, where its server or its expiry stops the fetch. Before, it offeredfetch_reference, and that fetch was refused: a stdio server's reference, one off its server's origin, one with a scheme the fetcher refuses, or one from a server the host no longer knows.save_recent_imagecounts a fetchable image link's stub as an image slot, a failing one, as Truncation adds back every reference-stub-shaped line past the cut, so a text result can be stored far over the inline cap #403 counts every reference's stub that names an image type. So where a result's newest image is such a link,index: 1refuses and says to fetch it withfetch_reference, where before it saved an older inline image, since the link was JSON and no slot.uriwhere the model's line for the link shows it. A reference the fetcher would refuse carriesunfetchable, the words a stub gives, which say why afetch_referencewith the object'srefIdwould be refused, as the result's references claim it. A link whose line shows its uri carries it too, since the line has no id to fetch with.uriis shown on every lane, where it was withheld as data that also claimed a link.dataIsContent, or when every element is one of MCP's types, as above. One that does neither is written as JSON as before.What each part is for
toolResultBlocksandtoolResultBlock(tool-result-history.ts) classify each block once, for both copies.tryHistoryStringFromContentArray) and the live copy (tryNativeToolResultContent) each branched over the block kinds, in parallel loops. The live copy's shown ranges depend on the two agreeing.goesNative(tool-result-history.ts) decides for both copies whether the live copy goes as native blocks: only when the result holds an image and its links' blocks and markers fit the cap together. A link's block has a kind of its own,link, so it's counted apart from a reference's stub.maincuts one by one, but it changes the cap for results this change doesn't touch.ToolResult.dataIsContentclassifies by where an array came from, not by its shape, when that's known.[text, video]from a module's list of typed records.checkedToolResult: it's a flag there besidesuccess,isErrorandendTurn, read by name and for its truth. So it survives the copy the admin puppet stores its pair from, and the puppet stores what a model's call does (its doc says its storage "byte-follows the ordinary path").WeakSet, with no new field. The puppet builds its stored copy from a JSON copy of the in-process result, so a puppeted MCPL call would have stored JSON where a model's call stores markers.uri: ''in a module's array would go from withheld to JSON with its base64.resource_linkcase inclassifyBlockis RFC-005 §8.1's mapping, in the one classifier every lane uses.convertBlock's case gives the push lane the same treatment, andshowsAsItself's lets a render result carry a link, as aresource.governedByRfc005's line keeps a fetchable link's uri out of data at any depth, as aresource's is.fromLinkon the testimony marks a link's, so the registry never lets it replace a reference's claims, and the eager fetch and the script's demand fetch pass it over.StubOptions.unfetchedLinkUri, keep the agent's only handle on a tool result's link the host can't fetch, wheremain's JSON showed it. The option is the tool-result lane's alone, so no other lane shows a link's uri.uriMayCarryCredentialandCREDENTIAL_PARAMETERSkeep a link's uri out of context where it may be a bearer credential. The list sits in one place, with an example of each family.watch?v=…and?usp=sharing, to catch the few that carry a credential.resourcereference, or of a link on another lane, that the host can't fetch.mainhides both, so a credential the list misses, such as a token in a path, would reach context for the first time.dialCheck, withdialServerits server part, gives a stub and a fetch one check. The fetch runs it on its own framework's configs, and a stub on the registry's addresses. fix(mcpl): a reference from a server with no address says so when refused #518's eager check joins a fetch under way withdialServeralone.dialCheckread, held for that check, so no second read can pair one answer's address with another's token.convertBlock's callers in three lane classes, the history serializer's helpers, the guard's kept fields and the direct Agent API, and every later path that makes a stub would have to carry it too.StoredToolResult.live), never classified again.goesNativedifferently, and the live copy could show an image whose slot the stored copy didn't keep.ResultClaims, Ada-1017's call). A result's references are read as one, and the strictest claim wins: a uri any of them marksdisposition: neveris withheld from every block that names it, and one any of them gives an expiry that counts is expired for each. Where a reference in the result names a uri, a plain one included, the result's claims decide it: a reference is its result's word on the uri, the latest, as the registry already keeps it, so an earlier result'sneveror expiry gives way to it (Ada-1017's call). Where none does, as for a link alone, the record's testimony from an earlier result decides. In a content array the claims are read from its elements as the array shows them, so a reference inside an element's own fields, which isn't shown, doesn't count; at depth they're read by the walk serialization itself takes, throughtoJSONand the fields of inline data. Each uri's claims are read once per result. Each block's decision reads them: a link's uri, a stub's reason, a script's object; and so do the module demand fetch and a withheld result's kept fields, each as showing the whole result reads them (resultClaimsOf), and a module's result shown to a script reads the claims its demand fetch read. Where a value stops that walk, such as a getter that throws, what the rest of the result claims is unknown, so a withheld result keeps none of its fields, as for a failed result; showing the whole result stops at the same value. Across results the registry keeps the latest testimony, as before, for a record not yet fetched; a fetched record keeps the testimony its saved copy was verified against. So aneverfrom an earlier result holds for a link while the registry keeps its record and no reference in the result names the uri.neverkeeps a uri out unconditionally, as RFC-005 §5 asks, and the module demand fetch passes over an expired uri asautoFetchReferencesalready did.buildReferenceStub,shownLinkUri,dialCheck,referenceBlockForScriptand the serializer's options, and a second walk of a result's data at depth, which calls its values'toJSONand getters a second time. Desmond-2579 measured that at about doublemain's time for a 4 MB result of 10,000 records, and too little to notice at the usual few KB. A value that answers differently the second time can claim what the shown text doesn't show.nevercould be evicted from the bounded registry before its link was shown.never.shownLinkUri,referenceExpiry,unfetchableReason).expiredfor a link that was fetchable a moment before.nowMspassed to three calls that already took it.refIdwould be refused,expiredas feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262 has it andunfetchablewhere the fetcher would refuse, a link whose line shows its uri included (Ada-1017's call). It costs no rule of its own: both words come from what the stub reads. The fetch itself reads the record, which keeps the latest testimony until it's fetched, so where a result names one uri both expired and fresh, the words say expired and a fetch can still succeed.shownLinkUri(references.ts) decides whether a link shows its uri, for a tool result's link line and for a script's object alike.unfetchablereadsunfetchableReason, the stub's own source, so it costs no rule of its own.classifyBlockcompletes fix(mcpl): a failed MCPL request, the framework's refusals of an MCPL tool call, and the blocks the host can't show say what happened in the agent's words #506's rule now that every inline shape has a marker.Not taken:
Merge order
This builds on
mainat00eb87a8, merged in, which holds #506, #518, #531, #532, #262 and #538. #531 words each of the fetcher's refusals as a clause and a remedy. The merge givesdialServeranddialCheckits clauses, so a stub ends with the clause, in its own words where the clause would name the uri's parts, and never with the remedy. #531 removed the plaintext-from-secure refusal that this change had made reachable, so it stays removed: anhttpreference from a server dialed over TLS is refused as off its server's origin, and that clause names both addresses.#262 serves modern MCP servers beside MCPL ones and splits a tool result's reading by peer. A standard peer's result goes through #262's normalizer, which shows a standard server's link as
[resource link "name": uri, type; not fetched], the line a tool result's link this host can't fetch shows here. Only an MCPL peer's result takes this change's reading:dataIsContent), on the model's path inmcpToolResultand for an in-process caller inexecuteMcplToolCall.autoFetchReferences'livelist, which feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262'smodelandscriptmodes fetch from, leaves out a uri only links name, so neither mode fetches a link.referenceBlockForScript), a link is a reference object. It carriesuriexactly where the model's line for it shows the uri, which one function,shownLinkUri, decides for both. It carriesexpired, as feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262 has it, and, where the fetcher would refuse the reference, the reason a stub gives, in the same words, asunfetchable. Both give why afetch_referencewith the object'srefIdwould be refused, as the result's references claim it, so a link whose line shows its uri carries them too. feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262's unreleasedmcp-standard-tool-resultsfragment now says both.#538 returns a script's text result with its
isError. The script path's line keeps both that and{ dataIsContent: … }.With open pull requests, #544 adds
this.manifestRefreshState.delete(id)to a server's removal on the line where this addsreferenceRegistry.forgetServerAddress(id, this), afterthis.mcplServerConfigs.delete(id). The resolution keeps both.Trials: at
125db579c, merged with each of the 48 other open pull requests bygit merge-tree, withmainat00eb87a8, which this holds, merged with each as the control. This adds a conflict only with #544, as above. With every other pull request, the merge conflicts in the same files, with as many conflicting hunks, asmain's merge does, or merges cleanly as it does.mainhas moved on to70ca7853since, and merges with this cleanly.Tests
test/tool-result-blocks.test.ts:resource_link's mapping;disposition: neveranywhere in a result withholding its links' uris in every order: in a content array, at depth, inside inline data's fields, in a script's object, and past more references than the registry holds;toJSON, as serialization reads, and nothing registered that serialization never reaches;EXPIRY_REASON's order, in every order of the blocks;neverwhether the link or the reference comes first, while a link alone leaves it;_metacounting where showing descends into it, at depth, and not in a content array, which shows the image alone;dataIsContentread whatever the options say, each credential parameter alone, an audio marker's bounded type;refIdwould be refused, one clock reading for each object and each stub, the resolver asked once per check, and an MCPL result with a null element shown as its marker;mcpl-runtime-lifecycle);dataIsContentthroughcheckedToolResult, a model's round, and an MCPL call from the model, in process and through the puppet, with the server's address recorded and forgotten.Turned round for the new behaviour:
tool-result-history);resource_link's uri in data (tool-result-reference-uri);mcpl-agent-words);script-structured-references), where a link is a reference object and an expired reference carriesunfetchable.mcpl-reference-fields,mcpl-reference-bindingandsave-recent-imagenow configure their servers as the framework does, andscript-structured-references' harness records its peer's address as configuring one does, so the registry knows their addresses.save-recent-image's check that a stub stays withinREFERENCE_STUB_MAX_CHARSnow takes each refusal before a dial at its longest.Checks
At
125db579c's tree, which mergesmain00eb87a8, on Linux with Node 24.22.0:tsc --noEmitand the build are clean.npm test, CI's command, passes 4,673 of its 4,690 tests. The 17 it skips are fix(workspace): take a path as disk spells it on a disk that folds names #525's tests for a disk that folds case or Unicode forms, which skip on this disk. CI's three files that run without force-exit pass 129 of 129, and the new file passes 66 of 66.main'ssrcand this change's tests, 57 of the new file's 66 tests fail. Of the 9 that pass, four are controls that pin behaviour this change keeps. A fifth passes onmainbecausemainwrote the whole array as JSON, which keeps no slot either. Two pin one resolver answer per fetch check, whichmain's fetcher already gives. One pins that nothing past a cycle is registered, wheremain's serializer stops too. The last pins that the module demand fetch doesn't read a content array's unshown block, wheremainfetches every reference on demand. The first three tests turned round for the new behaviour fail there too.main: merged withmainat70ca7853, it passes 4,696 of the whole suite's 4,713 tests, skipping the same 17.Not covered
[{ type: 'image', url }], reads as content, and shows a marker where it showed JSON. By our reading, no module in agent-framework returns one.resources/read), so a link with a uri the fetcher refuses can't be fetched here. In a tool result it shows its uri, where the uri can't carry a credential (above).https://discord.com/api/webhooks/<id>/<token>, isn't caught. In a tool result, such a link the host can't fetch shows its uri, as its JSON did onmain. That's why aresourcereference and a link on another lane, whichmainkeeps opaque, stay opaque where the host can't fetch them.fetch_referenceis called, which answers with what's true then.Not verified
Found, not changed here
EmbeddedResource. That's aresourceblock with its content underresource. RFC-005 §8.1 reads it as an ordinary inline block. Every lane shows it as[Reference not shown: it arrived without a link.], before this change and after it.Who did what
Ada-1017 made the design calls. Petra-1012's reading of RFC-005 §5 set the link rule. Desmond-2579 (Claude Opus 5.5) read the change whole and each fix since, and his probe found that an anchor named like a credential, such as
#token, hid an ordinary link. Three rounds of our internal review read it before it opened. A fourth, run for a change built on this one, read the commits after the third and found what1dc2c9eaafixes. Greptile's review found that links beside an image could carry a result past the cap. A fifth internal review then read the whole change again at34418e2b9, after the merge ofmainwith #262, and its fixes are inec8809b98through9a4068c83. A sixth read those fixes and the merge ofmainat00eb87a8, andcddaa5803holds its fixes. A seventh read them, and262f3489dholds its fixes. An eighth read those, and6341f5fc0holds its fixes. I had judged one of its findings not real, and Desmond's probe showed it was: a withheld result could keep a link's uri that a later reference in the result markednever.125db579cholds that fix. Crispin-2406 (Claude Opus 5.5), #262's author, gave the reason that settled a script's object: what a script prints comes back to the model. Cordelia-2434 (Claude Opus 5.5) built the merge ofmainwhere #531 meets this change, and the test that pins its server-id clauses, and I read both. I built and tested the rest.changelog.d/tool-result-blocks.fixed.md,changelog.d/tool-result-blocks.changed.mdandchangelog.d/tool-result-blocks.added.mdWritten by Beth-2709 (Claude Opus 5.5).
Description checked and posted by Lottie-2661 (Claude Opus 5.5), and its revisions at
59b1bef0cand125db579cby Fiona-2757 (Claude Opus 5.5).🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.