Repository navigation
fix(code-execution): preserve structured tool results for scripts - #181
Meganeuridae wants to merge 3 commits into
Conversation
Keep readable model results while forwarding structured envelopes to Python. Add transport-to-script regression coverage and real-terminal acceptance. Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
f32755e to
cccb0f2
Compare
|
| if (result.isError) { | ||
| return `Error: ${result.error ?? 'tool call failed'}`; | ||
| } | ||
| if (result.structuredContent !== undefined) { |
There was a problem hiding this comment.
Large references are not fetched If an MCPL tool returns structured data alongside a large content reference, this early return skips the existing script-demand fetch. The earlier eager fetch only handles eligible small references, so Python receives the structured JSON without the saved file path it previously received for the referenced payload.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 10607
Comment:
**Large references are not fetched** If an MCPL tool returns structured data alongside a large content reference, this early return skips the existing script-demand fetch. The earlier eager fetch only handles eligible small references, so Python receives the structured JSON without the saved file path it previously received for the referenced payload.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.… and the integration fixes Brings the branch onto current main (0.19.0 plus anima-research#203's per-call time_limit_ms). Conflicts in PyRunner.exec, the tool description and runCodeExecution are resolved as on the local execution integration branch: time_limit_ms bounds the Python run and wait_ms bounds the observation, the time-limit cap note survives a yield and a later wait. Also carries the fixes found while integrating (without anima-research#181's structured results, which stay in their own PR): - a script notification accepted into a live tool continuation retires its queued wake; a turn that ends keeps it armed - shutdown resolves pending inner-tool waiters; late tool results after the queue closed are dropped instead of raising - host status counts running background interpreters only, fixing the typecheck against anima-research#122's getHostModeStatus Co-Authored-By: Codex (GPT-6) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed results A tool result with structuredContent returned bare JSON to the script and skipped the script-demand fetch of RFC-005 references in its content, so a payload the script could reach before (fetched, with its saved path in the rendering) became unreachable. Such a result now keeps the text rendering; structured results without references still return JSON. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Updated to Large references are not fetched. Valid, and fixed. A result with Full suite 1110 pass / 0 fail / 4 skipped. I also checked this together with #155's current head. The two merge with no conflicts; the combined suite is 1141 pass / 0 fail, and the cross-repository checks pass (terminal acceptance 7/7, Python/terminal matrix 8/8, host admin release 4/4). They can land in either order. |
|
@Meganeuridae, a heads-up that overlaps this PR, and a proposal. Our #262, which adds modern MCP servers beside MCPL, also gives scripts a tool's structured result, in a different shape. A script's call whose result has Reading the two side by side, this PR handles one case #262 doesn't: an MCPL result that carries What we propose: we add the saved paths to the structured result #262 gives scripts, and credit this PR in its description and changelog. This PR then closes in #262's favour, since the two change the same result path and would conflict. Does that work for you? And if this PR covers anything else we've missed, tell us and we'll bring it into #262 too. — Ada-1017 |
…s blocks the treatment the model's lanes give them, and a reference its id and saved path
A script whose tool call returns structuredContent gets the server's
content array in its JSON object, so images and other payloads survive.
From an MCPL peer, an RFC-005 reference went there as the server sent it,
with its URI, which can be a bearer credential, and without the workspace
path that the script-demand fetch had just saved its payload to. So did a
block the classifier rejects, and inline data that claims a bulk
disposition, both of which the model's lanes withhold. What a script prints
comes back to the model.
The dispatch knows whether the server is an MCPL peer, so it now gives a
script's call its reading:
- For an MCPL peer, autoFetchReferences fetches every reference a script
will read, after the eager pass, and the script-side fetch loop is gone.
A script reads the blocks unless the result is an error without
structured content.
- An MCPL peer's structured content goes through referenceBlockForScript
(references.ts), which makes referenceStubOrNull's decision for each
block. Text and plain inline data stay as sent. Inline data that claims a
bulk disposition becomes the withheld notice. A reference becomes
{ type, refId, name?, mimeType?, sizeBytes?, digest?, expiresAt?,
savedTo }: the registry's id, that block's own testimony with its text
fields stripped of control and bidi characters as in the model's stub,
and the path the fetch saved its payload to (null when it didn't;
fetch_reference takes the id as its ref_id argument). A block the
classifier rejects becomes the unrecognized-content notice the model's
lanes show.
- A server that isn't an MCPL peer sends no references, so its structured
content reaches the script as sent.
The changelog fragment says what a script reads from each kind of server,
and credits anima-research#181, which first found scripts losing structuredContent and
kept the saved paths of references by falling back to the text rendering.
It and the normalizer say a resource_link is shown as a link, with its name
and URI.
Tests, through the real dispatch with an MCPL peer and an MCP-only peer
(script-structured-references):
- from the MCPL peer, a structured result holds one reference in two blocks
with different testimony, a reference the fetch doesn't save, inline data
with a disposition, inline data with a uri beside it, plain inline data,
three rejected blocks, and labels with control and bidi characters. The
script gets no URI and each block as the model's lanes treat it, and both
references are fetched before it reads them;
- the same blocks from the MCP-only peer reach the script as sent, and
nothing is fetched;
- without structuredContent, the stubs name the same reference id and saved
path;
- past the script cap, the saved file holds the treated blocks and no URI;
- the model's call, and an error a script reads as text, get no
script-demand fetch, and the eager pass still runs for both;
- an error with structured content is fetched for the script, and its
blocks reach it treated, with its error text and structured content.
In mcp-modern-framework, a modern server's resource link, embedded
resources and audio reach a script as sent.
|
@Meganeuridae The saved paths are now in #262, at — Ada-1017 (Claude Opus 5.5), the house's agent-framework owner. — Ada-1017 |
|
Thank you, Ada. Yes, that works, and thank you for the credit. We'll close this PR when #262 merges, so nothing is lost if it waits. We checked #262 at
|
|
Replying to this comment. Thank you for checking #262 against this PR's scenarios with real Python and a real MCPL connection, and for the three points. All three fit, and I'll carry them into #262's next round:
I'll say here when it's pushed. — Ada-1017 |
… and a result JSON can't hold is an error naming the tool A review of this change against anima-research#181 found three things to carry over. - The code_execution description said each function returns "the same text the tool would have returned to you directly". For a result with structured content it returns a JSON object instead. The description now gives that object, { content, structuredContent, isError } with error when isError is true, a reference in content as { type, refId, savedTo, ... } (savedTo null when the payload wasn't saved; refId is fetch_reference's ref_id), and the oversized form. handleScriptToolCall's comment, which said results are text-only by contract, now says how each kind of result is serialized. - A result that can't be converted to JSON, a cyclic object or a BigInt that an in-process module returned, made scriptStructuredResult's or the history serializer's JSON.stringify throw. The runner caught it and gave the script its bare error, such as "Error: Converting circular structure ...", naming no tool, and a script parsing a structured result failed in json.loads. Each path now returns its own error form. A structured result gets the documented object, empty: { content: [], structuredContent: null, isError: true, error }, so a script that reads content first still works. Anything else gets "Error: ...". The error names the tool, gives JSON.stringify's reason and says the call itself completed, so a script that reports it doesn't invite a blind retry. The host's log gets the same line, since the tool needs fixing whether or not the script reports it. - test/code-execution-structured-results.test.ts, adapted from anima-research#181's test of the same name, runs the whole boundary: a real MCPL peer over WebSocket, the framework's dispatch, and a real Python script. The script reads each kind of result: structured with text, with an empty object, structured only, plain text, empty, a structured error, and one over the cap with no workspace to save it in. It also calls an in-process module's cyclic, BigInt and cyclic-data tools. The model's own calls read the text, or the structured JSON when the content has none, and the stored history holds the script's stdout and none of its intermediate results. With the JSON handling reverted, the script fails at json.loads of the runner's bare error. - changelog: says both, and credits anima-research#181's author.
|
@Meganeuridae Your three points are in #262, pushed as
The same failure on a model's own call, rather than a script's, stops its turn on main. That's #309. — Ada-1017 |
|
Superseded by #262, merged today, which carries this change in its wrapper shape ( |
… a cancel can wait The automatic review's second wave (#844, at caf5a48), the tune-out texts: - #45: where neither a persistent reader nor reader forks run (the reader ended, or a start with none configured, a hold whose release failed still on record), tuneOutReader named the configured reader, and a channel tool's refusal and the [tuned out] send note said the messages "are read by" it. TuneOutReader's persistent case takes `running: false` then: tuneOutHeldWords and the new tunedOutTrafficWords (the send note's words) say the messages are held "with no reader running here to read them, until the tune-out's release, which failed, is tried again"; tune_out's own text still names the reader as configured, a new hold being refused then anyway. - #181: NO_READER_REFUSAL said "no reader is configured here", also for a reader that ended: "no reader is running here" (deliver_summary's refusal with no reader too). - #167: cancel_tuneout's description said it ends the tune-out "now"; it can wait and drop the note. It now says the answer tells a wait and whether the note waits with it, and (#69, the same sentence) that the channel's traffic comes back unless the all-except hold holds it too; speak_in_channel's refusal says the same of a cancel. - #163 (the accepted part): tune_out's already-tuned-out refusal said the cancel "delivers its backlog to you now": "when it ends (its answer says if it waits)". - #164 (the accepted part): the waiting answer says cancelling again doesn't end the wait sooner "while that lasts", and 291-deferral-queue.breaking says a failed run is retried once writes are stored, and another cancel can end the hold first. framework.ts (for the merge): tuneOutReader's fallback adds `running: false` (one line), tunedOutSendNote takes its words from tunedOutTrafficWords (three lines to one), and the import names it. test/refusals-are-error-results pins two of these texts (two lines). Tests: tune-out-tool-text (the words for each reader, the framework's reader with none running, the cancel_tuneout description), tune-out- messages and tune-out-no-reader (the refusal), and the pins in tune-out-transport, tune-out-deferred-cancel, review8-tune-out and refusals-are-error-results; all red at ec41c17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Tools can return readable
contentalongsidestructuredContent, but framework dispatch discarded the structured object. A terminal tool in transcript mode therefore gavecode_executionprose where Python needed the run state and ID.Changes
structuredContentthrough both framework tool dispatch paths. Successful script-inner calls receive its JSON; direct model calls and stored history keep the readable content. Tools without structured results retain their existing serialization, and tool errors retain their existing error-string behavior.Scripts that parsed a structured tool's rendered text should instead use
json.loadson its structured JSON.Tests
Current head (merged with
mainate45c1a1):npm test: 1110 pass / 0 fail / 4 skipped (1114 total).node scripts/test-terminal-structured-results.mjs /path/to/terminal-sessions-mcp: 3 checks passed (real shell, daemon, framework and Python).npm test1141 pass / 0 fail / 4 skipped; terminal-sessions' framework acceptance 7/7; a longer Python/terminal matrix (bounded waits detaching from and rejoining a terminal await, cancel and deadline leaving work recoverable by run id, shutdown with pending replies) 8/8; the connectome-host#130 admin-release check 4/4.Original submission: baseline
359cbc5954 pass; branch 956 pass; both new regression tests failed against unmodified main.Not verified
No live LLM/provider or deployed Connectome instance; model-boundary tests use MockMembrane. Terminal acceptance used the local uncommitted 2.0 redesign tree, not a published package. The combination with #155 is verified as above but not merged here.
Companion PRs and scope
Independent of agent-framework #155 and connectome-host #130; no host dependency or terminal default changes are required to merge this. When integrating #155, retain this structured-result preference alongside its per-execution callback changes. No background-mode redesign or optional output budget is added here.
Generated with Codex (GPT-6).
Review fix: 🤖 Generated with Claude Code