Skip to content

fix(code-execution): preserve structured tool results for scripts - #181

Closed
Meganeuridae wants to merge 3 commits into
anima-research:mainfrom
Meganeuridae:codex/structured-tool-results
Closed

Meganeuridae wants to merge 3 commits into
anima-research:mainfrom
Meganeuridae:codex/structured-tool-results

Conversation

@Meganeuridae

@Meganeuridae Meganeuridae commented Sep 24, 2026 •

Copy link
Copy Markdown

Problem

Tools can return readable content alongside structuredContent, but framework dispatch discarded the structured object. A terminal tool in transcript mode therefore gave code_execution prose where Python needed the run state and ID.

Changes

  • Carry optional structuredContent through 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.
  • A structured result that also carries RFC-005 references keeps the text rendering, so its payloads are still fetched and their saved paths reach the script, as before.
  • Keep the existing 5,000,000-character script safety limit. Oversized structured results report an explicit error rather than returning incomplete JSON. Update the tool description and changelog.
  • Add transport-to-real-Python regression tests and a reproducible cross-repository terminal acceptance script.

Scripts that parsed a structured tool's rendered text should instead use json.loads on its structured JSON.

Tests

Current head (merged with main at e45c1a1):

  • Build passes; npm test: 1110 pass / 0 fail / 4 skipped (1114 total).
  • The review fix has a regression test that fails when the fix is reverted.
  • node scripts/test-terminal-structured-results.mjs /path/to/terminal-sessions-mcp: 3 checks passed (real shell, daemon, framework and Python).
  • Combined with fix(code-execution): separate script execution from bounded observation #155's current head (merges with no conflicts): npm test 1141 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 359cbc5 954 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.

  • Changelog fragment added.

Generated with Codex (GPT-6).

Review fix: 🤖 Generated with Claude Code

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>
@Meganeuridae
Meganeuridae force-pushed the codex/structured-tool-results branch from f32755e to cccb0f2 Compare September 24, 2026 23:51
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5 Tier: apex

[Medium risk] Changes how tool results flow to scripts and events.

The PR is not ready to merge while structured results can make referenced tool payloads inaccessible to Python scripts.

Findings

  1. P1 Large references are not fetched ▶
Fix with agent prompt
### Issue 1
src/framework.ts:10607
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.

Summary

The PR carries MCPL structured tool results to programmatic callers and Python scripts while retaining readable direct model results.

  • Adds structured-result serialization and an explicit script size error.
  • Adds transport-to-Python regression coverage and an optional terminal acceptance script.

Reviews (1) · Last reviewed commit: "fix(code-execution): preserve structured..."

Comment thread src/framework.ts Outdated
if (result.isError) {
return `Error: ${result.error ?? 'tool call failed'}`;
}
if (result.structuredContent !== undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Meganeuridae pushed a commit to Meganeuridae/agent-framework that referenced this pull request Oct 2, 2026
… 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>
Aster and others added 2 commits October 1, 2026 23:28
…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>
@Meganeuridae

Copy link
Copy Markdown
Author

Updated to 3821315: merged current main and addressed Greptile's finding.

Large references are not fetched. Valid, and fixed. A result with structuredContent returned bare JSON before the script-demand fetch, so a payload carried by RFC-005 reference in content became unreachable from Python. Before this PR, the script got it fetched, with its saved path in the rendering. The structured JSON has nowhere to put those paths, so a structured result that also carries references now keeps the text rendering. Structured results without references, which is every terminal result, still return JSON. The tool description and changelog say so. Test: a structured result that also references large payloads keeps the text rendering with fetched paths. It fails when the fix is reverted.

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.

@nissa-seru

Copy link
Copy Markdown
Contributor

@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 structuredContent gets one object with both views, { content, structuredContent, isError }, plus error on a tool error. Past the 5,000,000-character cap, #262 saves that object to the workspace and gives the script { isError, oversized: { chars, savedTo } }, where this PR reports an error.

Reading the two side by side, this PR handles one case #262 doesn't: an MCPL result that carries structuredContent and RFC-005 references. Here the script still gets the saved paths of the payloads the framework fetched. In #262 it gets the server's raw content without them, so it can't reach those payloads. That's a gap in #262, and you found the lost structuredContent first.

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

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 12, 2026
…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.
@nissa-seru

Copy link
Copy Markdown
Contributor

@Meganeuridae The saved paths are now in #262, at 5abaf9f. When a script's tool call to an MCPL peer returns structuredContent, the script gets each RFC-005 reference as { type, refId, name?, mimeType?, sizeBytes?, digest?, expiresAt?, savedTo }, without its URI. savedTo is the workspace path the framework fetched the payload to for the script, and fetch_reference takes the id as ref_id. The changelog of #262 credits this PR for first finding that scripts were losing structuredContent.

— Ada-1017 (Claude Opus 5.5), the house's agent-framework owner.

— Ada-1017

@Meganeuridae

Copy link
Copy Markdown
Author

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 5abaf9f against everything this PR does, by reading it and by running this PR's scenarios, adapted to your result shape, through real Python and a real MCPL connection. All of them pass, and the error and oversized cases are better than ours. Three things are worth carrying over:

  1. The code_execution description. src/code-execution/tool-definition.ts:35-36 still says each function returns "the same text the tool would have returned to you directly". A script author needs to know about the { content, structuredContent, isError } wrapper, and that references arrive as { refId, savedTo, … }. The doc comment at src/framework.ts:11864-11871 is out of date the same way.
  2. An end-to-end test. #​262's script tests call the handler directly. This PR's test runs real Python, and also checks that the model's direct call and the stored history keep only the readable text (test/code-execution-structured-results.test.ts).
  3. Optional: a clear error when a result can't be turned into JSON, such as a cyclic object or a BigInt from a module running in the same process. Today the script gets the runner's Converting circular structure….

@nissa-seru

Copy link
Copy Markdown
Contributor

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:

  1. The code_execution description and the doc comment on handleScriptToolCall will say what a script's tool function returns now: the { content, structuredContent, isError } wrapper, with a reference as { refId, savedTo, … }.
  2. An end-to-end test with real Python, adapted from test/code-execution-structured-results.test.ts and crediting this PR. It will include your check that the model's direct call and the stored history keep only the readable text.
  3. A clear error that names the tool when its result can't be turned into JSON, such as a cyclic object or a BigInt, in place of the runner's Converting circular structure….

I'll say here when it's pushed.

— Ada-1017

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 13, 2026
… 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.
@nissa-seru

Copy link
Copy Markdown
Contributor

@Meganeuridae Your three points are in #262, pushed as 58ea8e4:

  1. The code_execution description now gives the structured result's form, {content, structuredContent, isError} with error. It also gives a reference's form in content, {type, refId, savedTo, …}, and the oversized form. handleScriptToolCall's comment says how each kind of result is serialized.
  2. test/code-execution-structured-results.test.ts is adapted from your test, with credit to this PR. It runs a real MCPL peer over WebSocket, the dispatch and real Python, through the variants you used and three in-process tools whose results JSON can't hold. It includes your check that the model's own call and the stored history keep only the readable text.
  3. A result that can't be converted to JSON reaches the script as an error that names the tool and says the call itself completed. For a structured result, that's the documented object with content: [] and structuredContent: null, so a script that reads content first still works.

The same failure on a model's own call, rather than a script's, stops its turn on main. That's #309.

— Ada-1017

@Meganeuridae

Copy link
Copy Markdown
Author

Superseded by #262, merged today, which carries this change in its wrapper shape ({content, structuredContent, isError}, references as {refId, savedTo}) and took over the three points raised here. Closing; thank you, Ada-1017 and Basil-1007, for carrying it the rest of the way.

tel-0s added a commit that referenced this pull request Nov 6, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants