Repository navigation
fix: tool presentation and code_execution errors say what happened and what helps - #413
Merged
nissa-seru merged 5 commits intoOct 18, 2026
Conversation
[Medium impact] The PR appears safe to merge; no blocking issue remains. SummaryImproves errors from
Reviews (2) · Last reviewed commit: "fix(code_execution): a refused tool call..." · Reviewed by Greptile |
added 2 commits
October 16, 2026 21:35
…obably meant closestNames(name, candidates, max = 2) ranks candidates for a refusal's "did you mean", ignoring case: an equal name; then one the name matches as a whole `--` segment (`ls` for `workspace--ls`); then one that starts with the name or that the name starts with; then one that contains it or that it contains. These two need three characters on the shorter side and count however many edits they take. Last comes one a few edits away: at most a quarter of what follows the prefix the two share, and at least one, since a shared prefix is mostly a namespace, so `workspace--write` isn't offered for `workspace--read`. Within a rank, fewer edits come first, ties keep the order given, and a candidate given twice is named once. Where one name holds the other, the length difference is already their distance, so a long name, such as a catalogue pasted in by mistake, runs no edit table for each candidate. isNearMiss(name, candidate) says whether a name is a slip for another: the same ignoring case; within that edit limit, a transposition counting as one edit; or, at three characters or more, a part of it, which is a shortened attempt at it (`mode` for `deliveryMode`). A part counts only that way round: a name that holds the candidate, past the edits a slip allows, is another name, usually an extra that explains it (`channel_note` beside `channel`). The tests pin each rank against the next, a name that starts with or holds its candidate, the cut-off and how it scales past a shared prefix, ties, repeats, and isNearMiss' slips and non-slips, and, as a hang guard, a million-character name that holds all of a hundred candidates.
…d what helps set_tool_visibility, set_tool_description and code_execution now tell the agent what happened and what helps when they refuse or stop, and the tool catalogue names what's wrong with a presentation file it can't use, up to ten problems at once. Tool presentation (src/tool-presentation.ts): - edit() checks the agent's arguments before it touches the file: every shape problem at once, each by its key, with the call that works. A name that isn't available is quoted, cut past 60 characters, with the closest available names (closestNames) and the catalogue's index to copy from. A description over 32,768 characters, and hiding one of the three tools that can't be hidden, are refused as the agent's input; before, both failed in the parse of the edited file and read as problems with the file. That parse stays as a guard. - Each refusal leads with "Nothing was changed", which holds: every throw comes before the rename, and the cleanup after it can't throw. A file the host can't use is framed as the host's to repair, and anything else as "the edit failed (<errno>)", so String(error)'s "Error: " is gone. A stale lock, a symlinked file, an edit past 256 KiB, a file changed during the edit and a replaced temporary copy each say what happened and whether calling again helps. - A file's problems are named all at once, each by its entry: up to ten, then how many more, and a list of names within one shows ten, then how many more. A description-defaults file is parsed with the rule that it may set descriptions only, which replaces the separate check and names the tools it sets "visible" for. - The catalogue's diagnostics say what applies meanwhile and who can fix the file, each on one line, with line breaks written as \n. A name a problem quotes is cut past 60 characters and a value past 40, counted and cut in code points; an error nobody expected is quoted through boundedErrorMessage. Two outcome changes, each made so that these messages say what holds: - An edit whose lock couldn't be removed after its rename threw, though the edit had been made. It now succeeds, and its note says the lock was left and what later edits will meet. - A description's length is counted in code points, in the edit's check and the file's parse alike, so 32,768 characters are taken whatever they are. Before, it was counted in UTF-16 units, and a description of more than 16,384 characters outside the Basic Multilingual Plane was refused. code_execution (src/code-execution/py-runner.ts, runtime-py.ts, src/framework.ts): - A script stopped at its time limit says its tool calls keep their effects, and offers a longer time_limit_ms only below the deployment's ceiling, which exec() now takes as ceilingMs. At the ceiling it says that's the longest a script runs here and to split the work, and offers background mode when the agent could start a background script as the script starts and a background script may run longer (backgroundCeilingMs). A background script at its lifetime says that starting it again runs it from the top, and its kill no longer says an interpreter restarted. runCodeExecution passes the foreground ceiling and the background lifetime, startBackgroundScript the background ceiling, and three small methods hold the rules both use. - An interpreter that exits during a script says the rest didn't run and that earlier scripts' variables are gone, and gives as its example of a script ending the process too much memory for a signal and os._exit() for an exit code. One that can't be started at all (ENOENT, EACCES) says code_execution can't run here until whoever runs the host fixes that. One that exits as it starts, doesn't start within 15 seconds or fails to spawn otherwise, and runtime files that can't be written, each say that nothing ran, to run it again, and to ask whoever runs the host if it fails the same way. - A second script while one runs says they run one at a time. - A tool function called with keyword arguments is refused with the dict to write, built from its keys. A key that is a str of up to 60 characters is shown whole. A value, or any other key, is shown as written only when it's short and of an exact built-in type, and otherwise by its type, so no class code runs and no long value is repr'd. A non-dict argument is named. Beside other keywords, a keyword named args is one of those keys, since a tool such as utils has a field of that name; on its own, args= is still the dict, as it was. More than one positional argument is refused the same way, saying how many. src/error-message.ts is new: errorMessage gives what a thrown value says, without String(error)'s "Error: " and without ever throwing, and boundedErrorMessage cuts it to 500 characters by default, saying its whole length. Tests: the new presentation tests check the words of each refusal and diagnostic, all but a file that grows while it's read, and that nothing changed on disk; the code_execution tests check the ceiling branches through PyRunner and through the framework, the kill both ways, each start failure but the 15-second start timeout, an exit mid-script, the busy refusal and the keyword-argument refusal.
nissa-seru
force-pushed
the
fix/presentation-script-errors
branch
from
October 17, 2026 03:53
c94990b to
6b9be26
Compare
…type The keyword-argument refusal's dict, and its words for an argument that isn't a dict, showed a short value of a built-in type as written and any other value by its type. Showing values as written is what needed rules for NaN and the infinities, a bound on ints, lengths for strings and their repr, and an identity check of the type. Every value is now shown by its type, as long ones already were: that keeps the dict's shape and fails loudly if copied, and the agent has its values in the call it just wrote. Showing a value still runs none of its code: its __repr__ isn't called, and its type is named through type's own descriptor. The changelog fragment says so, and now says that only a str key is shown whole.
This was referenced Oct 17, 2026
added 2 commits
October 17, 2026 22:04
main's anima-research#458 restructured PyRunner.exec: the exec is pending before its interpreter starts (sent marks when the script went to it), a start failure settles it through settlePending, an interpreter that stops reading its input is ended, and the time-limit note starts on a line of its own. This branch's texts go onto those paths: - a start failure says why the script didn't run (startFailure); - the pending exec carries ceilingMs, backgroundCeilingMs and background, set where anima-research#458 now creates it, so the kill at the limit and the time-limit note offer what helps; - an interpreter's exit under a script reads as interpreterExit, except anima-research#458's input-closed case, whose text is kept as it is; - the note keeps anima-research#458's line of its own and gains the remedy. anima-research#458's tests keep pinning its behaviour; the start-exit test and the time-limit-note test now expect this branch's texts. A comment in reclaim names the readiness error by its new text, "exited as it started".
… exit The fragment from anima-research#458 gave "python runtime exited unexpectedly (…)" as a runner text that ends without a newline. This branch words that exit anew, so the example names the case rather than the earlier words.
1 task done
nissa-seru
pushed a commit
to anima-research/heartbeat-mcpl
that referenced
this pull request
Oct 18, 2026
…lished From the comment that introduces it to the end of src/arguments.ts, the file is now agent-framework's src/closest-names.ts byte for byte, as anima-research/agent-framework#452 and anima-research/agent-framework#413 carry it (blob 36f75c01). The note on where it comes from moves out of closestNames' doc comment into a comment of its own above it. closestNames no longer runs the edit table for a candidate that holds the name or that the name holds: one holds the other, so their distance is the difference in length, which the table gave too. What it returns is unchanged. In 40000 random calls, before and after give the same answer (12711 of them name something), and isNearMiss, whose code is unchanged, gives the same in 179729 pairs (Node 24.22.0). A version that counted those distances as 0 differs. isNearMiss's comment now says that a name holding the candidate is still a slip when it is within the edits a slip allows ("channels" for "channel").
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…aths main brings anima-research#413 (what code_execution's failures say, and what helps) and anima-research#458's merge, both already composed with this change's base, plus other packages' fixes that touch no file of this change's. The conflicts were in py-runner.ts and one test; runtime-py.ts merged clean (anima-research#413's tool-function argument checks sit apart from this change's fds and writers). Resolved as this change's structure with anima-research#413's texts: - the deadline kill goes through stopPending, with anima-research#413's wording and limitRemedy; - the time-limit note is anima-research#413's, with its remedy, through onItsOwnLine; - an exit under a script reads as anima-research#413's interpreterExit, settled after the pipes as before; the host's log keeps the short technical form ("python runtime exited unexpectedly (code=…, signal=…)") with the last words; - the readiness errors take anima-research#413's texts ("exited as it started", "didn't start within 15 seconds"); an exit's rejection carries its exit code and what it wrote to stderr, for startFailure. Where anima-research#413's text and this change's meet, three choices of the merge's own: - startFailure: a failed start that wrote to stderr gets anima-research#413's sentence, then "It wrote to stderr: <its last words>". Kept whole rather than through boundedErrorMessage, whose cut keeps a text's head and would drop a traceback's last line. - The runtime's missing-channel check (fds 3 and 4) now exits with 78, sysexits' EX_CONFIG, which Python never exits with on its own; for that exit startFailure ends with the runtime's own sentences instead of anima-research#413's "Run it again", which they contradict ("calling it again fails the same way"), so the result ends with what whoever runs the host must change. - The stopped-reading text is shaped as anima-research#413's (stoppedReading): "script stopped: …", the remedy, and no "variables from earlier scripts" for a background script, as anima-research#413's interpreterExit does. Tests: this change's expectations move to anima-research#413's texts (a helper builds interpreterExit's text); anima-research#413's start-exit test now also sees what Node wrote; a background exec meets the stopped-reading text without the variables clause. code-execution.test.ts passes 105/105 on Node 24.22.0 and Node 20.20.2 (Node 20 with --test-timeout=300000: the file takes about 63 s there, past the 60 s a file was given before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
… the merge From a reader's read of 3f9ba29: - stoppedReading's remedy names the check to make, in anima-research#413's two steps: "If it stops the same way, check whether the script closes or replaces file descriptor 3, where the host's messages arrive; if not, ask whoever runs this host." A check, not a stated cause. - The reader thread's failed read runs _check_own_fd(3) first, so a closed or replaced fd 3 is named ("a script closed or replaced fd 3, where they arrive") before anima-research#413's general remedy; a failure with fd 3 intact keeps the exception's text. Its test's script now sleeps 30 s, past within's 10 s, so the end-of-script check, which says the same, can't be what settles it. - A correction to 3f9ba29's message: git merged runtime-py.ts without a conflict, and the merge itself then changed its exit to 78. From an internal review of the merge (30 issues, 9 fixed here): - A failed start's stderr is its last 2,000 characters, with a leading "…" when cut (OutputStream.lastIdleCut), and a cut inside a surrogate pair drops its low half. 3f9ba29's "kept whole" meant "not cut from its head"; the description now says what it is. - A start ended by a signal reads "was ended by <signal> as it started". - A forked child's sys.exit() ends it as Python does: its code, 0 for none, 1 after writing a code that isn't a number, with no traceback (_leave_forked). - The runtime's start check names what's missing ("its file descriptors 3 and 4 aren't open", or "its file descriptor 3 isn't open") and asks that the wrapper pass file descriptors 3 and 4 on. - The fixed fragment: "the result says which" is narrowed to when the script didn't replace the runtime's copy of fd 2 too, the journal's stop reason to when it didn't reach its 50 MB cap, and its stopped-reading line says what the message now says. - Tests: a wrapper's own exit 78 has its words end the result; 3,000 characters of stderr come back as the last 2,000, marked, ending on the last line, and an emoji at the cut leaves no half; SIGKILL at start is named; the wrapper rows pin the single-fd sentences; children that call sys.exit(3) and sys.exit() exit 3 and 0 with no traceback. code-execution.test.ts passes 108/108 on Node 24.22.0 and 20.20.2 at this tree, and taking out each piece this round adds fails a test there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…he fd a script took When a script closes or replaces one of the runtime's own file descriptors, the runtime ends itself with exit 70 after a line saying which (_check_own_fd, _fatal). The result then ended with anima-research#413's general advice, "check whether the script itself ends the process, for example with os._exit()", which points away from the line just above it. Now interpreterExit reads the fd from that line in what arrived, the exec's stderr or, for a background script, its journal's tail, and the advice names it: "check whether the script closes or replaces fd 3, as the runtime's line above says; if not, ask whoever runs this host". A script's own exit, with any code, keeps the general advice. stopPending takes the reason as a function of what arrived, so the exit path can choose the advice once the output has ended. Tests: each of the runtime-fd rows and the reader test assert the whole advice with the fd it names; a background script that closes fd 4 is told so from its journal; a script's own os._exit(70) keeps the general text. code-execution.test.ts passes 115/115 on Node 24.22.0 and 20.20.2, and each piece, taken out on its own, fails a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
set_tool_visibility,set_tool_descriptionandcode_executionnow tell the agent what happened and what helps when they refuse or stop, and the tool catalogue names what's wrong with a presentation file it can't use, up to ten problems at once.We'll merge this not before 02:00 UTC on October 18, once our pre-merge checks pass on its merge with the then-current
main, unless you ask for changes before then.It comes from a sweep of the errors and notices agent-framework gives agents, against one bar: an error should tell the agent how to fix it, or say plainly that nothing can be done. It adds two small helpers:
src/error-message.ts(errorMessage,boundedErrorMessage), andsrc/closest-names.ts, the "did you mean" helper, in a commit of its own.Problem
Presentation edits. Every refusal from
set_tool_visibilityandset_tool_descriptionwent throughString(error), so it began with "Error: ". Most of them named a problem without a remedy:EEXISTfrom a stale lock carried the file's absolute path and nothing about what to do.Two limits on the agent's own input read as problems with the file, because they were only checked when the edited document was parsed: "Invalid description/visibility for X" for a description over 32,768 characters, and "Cannot hide recovery tool X". A malformed file was reported one problem at a time, so whoever repaired it by hand went round once per problem through the agent.
The catalogue's diagnostics read "Overrides not applied: Error: Expected {"version":1,"tools":{...}}. Default visibility and component descriptions retained.", and similarly for description defaults. They didn't say who can fix the file, or what the agent sees meanwhile.
code_execution.
time_limit_mscould help depends on the deployment's ceiling. WithoutmaxScriptTimeoutMs, that ceiling is the default limit itself, so a larger value would be capped and the script would stop again.code_executioncan't run there at all, as against a start worth one more try.TypeErrorabout an unexpected keyword argument, without the dict form the tool takes.Changes
Presentation edits (
src/tool-presentation.ts).edit()checks the agent's arguments before it touches the file. Each refusal leads with "Nothing was changed", which holds: every throw inedit()comes before its rename, and the cleanup after it never throws. An edit whose lock can't be removed afterwards leaves it and says so, in a done edit'snoteor after a refusal: until whoever runs the host removes it, later edits are refused. A file the host can't use is framed as the host's to repair, and anything else as the error it is.Error: Invalid editing argumentsError: Tool is not currently available to this agentError: Invalid description/visibility for exampleError: Cannot hide recovery tool workspace--readError: EEXIST: file already exists, open '<absolute path>.lock'Error: Edit the target file directly; tool editing refuses symlinksError: Presentation exceeds 256 KiBError: File changed concurrently; retry after reading itError: Temporary file changed concurrently; retry after checking the directoryError: Expected {"version":1,"tools":{...}}, the first problem onlyError: <Node's message, often with an absolute path>Two outcome changes, each made so these messages say what holds:
edit(), though the edit had been made. It now succeeds, and itsnotesays the lock was left and what later edits will meet.main's code refuses a file holding such a description as a whole: with a description of 16,385 😀 and another tool hidden, its catalogue says "Overrides not applied" and both tools show their installed descriptions, where this PR's code applies both settings.A refusal for arguments of the wrong shape names every problem at once, each by its key. A description's length is counted in code points, in the edit's check and the file's alike, so a character outside the Basic Multilingual Plane, such as 😀, counts once where it counted twice. The closest names come from
closestNames(src/closest-names.ts, below), over the tools available to the agent.A file's problems are named all at once, each by its entry: up to ten, then how many more. A problem that lists names, such as the keys an entry can't have, shows ten, then how many more. They cover:
{"version":1,"tools":{…}}, naming the key or value at fault;descriptionthat isn't a string or is too long;visiblethat isn't true or false, or that hides one of the three tools that can't be hidden;A name a problem quotes is cut past 60 characters, with its length, since a file can hold a key of any size; so is the key of an entry for a tool that isn't available, which the catalogue reports as "Not currently available", and the name an edit gives for a tool that isn't available. Names and the values a problem shows are counted and cut in code points, so a cut never leaves half of a surrogate pair. A name the agent should copy, such as a "Did you mean" name, is quoted whole.
parsePresentation's error reads the same way. A description-defaults file is parsed with the rule that it may set descriptions only, which replaces the separate check and names the tools it setsvisiblefor.The catalogue's diagnostics:
The catalogue writes each diagnostic on one line, with line breaks written as
\n: Node's JSON parse error quotes a few characters of the file, line breaks included. An error nobody expected is quoted cut to 500 characters, throughboundedErrorMessage.code_execution (
src/code-execution/py-runner.ts,runtime-py.ts):SIGKILL, its example is using too much memory.)EACCESreads "…until whoever runs this host makes it executable or points this host at another.")TypeErrorfor an unexpected keyword argumentargsbeside other keywords, asutilstakesexec()'s newceilingMsoption.framework.tspasses the foreground ceiling inrunCodeExecutionand the background one instartBackgroundScript. A foreground exec is also given the background lifetime (backgroundCeilingMs) when its agent could start a background script as the script starts, so a script stopped at its ceiling can be offered background mode: the primary agent, or any agent when none is named, with fewer thanmaxBackgroundScriptsof its background scripts running. Those rules are three small methods, whichstartBackgroundScript's own checks use too. The count is read as the script starts, since the stop message is built from what the exec was given, so a slot freed or taken during the run can leave the offer stale; a refused start then nameslistandcancel. Both stop paths use one function,limitRemedy: the kill after the grace, and the note added when a script answers the cancel and ends unsuccessfully.argsis one of those keys, since a tool such asutilshas a field of that name, so the dict to write keeps it. On its own,args=is still the dict, as it was, andNoneeither way still means no arguments. The dict to write is built from the keys: astrkey is shown whole up to 60 characters, since a key is what the agent has to write, and a longer one, or any other key, as a value is. A value is shown by its type, such as<str>, which fails loudly if copied where...would have sentEllipsis, and the agent has its values in the call it just wrote. Showing a value runs none of its code: its__repr__isn't called, and its type is named throughtype's own descriptor, so a metaclass's__name__doesn't run either.closestNamesandisNearMiss(src/closest-names.ts, new, in a commit of its own):closestNames(name, candidates, max = 2)ranks candidates, ignoring case: an equal name first, then a whole--segment (lsfindsworkspace--ls), then a prefix, then a substring (each needs three characters), however many edits those take, then a few edits. Within a rank, fewer edits come first, and ties keep the order given. Where one name holds the other, the length difference is already their distance, so a long name, such as a catalogue pasted in by mistake, runs no edit table for each candidate.isNearMiss(name, candidate)says whether a name is a slip for another, ignoring case: the same; within a quarter of what follows their shared prefix in edits (at least one; a transposition is one edit); or, at three characters or more, a part of it (modefordeliveryMode), which is a shortened attempt at it. A part counts only that way round: a name that holds the candidate, past the edits a slip allows (channel_notebesidechannel), is usually an extra that explains it, so it's another name, whilechannelsis a slip. A shared prefix is mostly a namespace, soworkspace--writeisn't offered forworkspace--read.What each part is for
closestNames, in a commit of its own): a name that's a slip gets the name meant, from the tools available.boundedErrorMessage, shared): the catalogue keeps its line structure.time_limit_msis offered only where it's allowed, and background mode only to an agent that could start a background script, through three small methods thatstartBackgroundScript's own checks now use too.*positional, so a keyword namedargscan be one of that dict's keys, asutilstakes.repr, and an identity check of the type. Each value is now shown by its type, and the agent has its values in the call it just wrote.Tests
Checks: CI runs the typecheck, the build and the suite at this head, on Ubuntu and macOS with Node 20 and Node 24: its run.
New tests, each pinning what the agent reads and, for an edit, that nothing changed on disk:
test/tool-presentation.test.ts, five new tests:\n,\r\nand\r;visible;ENOTDIR, whoever runs them) and a 300,000-byte file;The last two make their failures with
syncBuiltinESMExports, as the existing temporary-file test does, and the temporary-file test now pins its message too.test/code-execution.test.ts:PyRunnerbelow it, at it and without it, foreground and background; at the foreground ceiling, background mode offered only when a background script may run longer; through the framework, a shortened call, a capped request and a background script at its lifetime;EACCES) or that runs through a file (ENOTDIR), with one that exits as it starts, and with aTMPDIRthat doesn't exist, whose path is long enough that the message is cut at 500 characters;maxBackgroundScriptsis 0, nor to an agent whose one slot is taken;SIGKILL, and under a background script, which isn't told of earlier scripts' variables; the busy refusal;args=on its own andNoneeither way calling the tool as before, a keyword namedargskept in the dict to write beside other keywords, and two positional arguments refused; and a tool function given keyword arguments, a non-dict, or both, each value shown by its type, among them a list and subclasses ofstrandintwhose__repr__raises, a 60-character key shown whole and a 61-character key by its type, and an instance of a class whose metaclass's__eq__and__name__raise shown by its type.test/closest-names.test.ts(new): the ranking, including a name that starts with or holds its candidate, ties in the order given and a candidate given twice; the cut-off, scaling with the length past a shared prefix; andisNearMiss, a part of a name counting one way only; and, as a hang guard, a million-character name that holds all of a hundred candidates, answered within 5 seconds. This PR's refusal usesclosestNames, and nothing in it callsisNearMiss.Red-first: with the four changed source files back at
main's code, 23 of the 75 tests in those two files fail: the 17 this PR adds that check whatmaindoes differently, and the 6 whose expected values it changes, one of them renamed. The other 52 pass as controls, among them one new test of whatmainalready does, a tool called withargs=alone or withNone, and two whose only change is a comment and a helper's parameter.src/closest-names.tsandsrc/error-message.tsstay, sincemain's code doesn't use them. TypeScript reports 8 errors on that tree, each theceilingMsoption thatmain'sexec()options don't have (TS2353), and emits the JavaScript anyway, so the tests run and fail on behaviour.Each piece removed on its own makes a test fail. Each of 58 pieces, a branch, a guard, a call site or a value, was removed by itself, and every removal made at least one test fail: the nine in the keyword-argument refusal from this branch's code merged onto
main24826254, and the other 49 at6b9be26b. Mergingmainin at0ad8f99put this branch's words onto the paths #458 restructured inPyRunner.exec. The eight pieces where the two meet were removed again at0ad8f99, whose code and tests are this head's, each on its own, and each removal made at least one test fail: the start failure's words, the words for an interpreter that exits under a script, the kill at the limit's words, the time-limit note's remedy and #458's line of its own for that note, and the pending exec's ceiling, background ceiling and background flag.The 58 pieces, and the tests each one's removal fails
editFailurelimitRemedy)PyRunner's ceiling testPyRunner's ceiling testPyRunner's ceiling testPyRunner's ceiling testPyRunner's ceiling testPyRunner's ceiling test, the shortened callargskeyword testargs=on its own taken as the dictargskeyword test, theargs=testos._exit()for a code)ENOENT,EACCES)EACCES's own remedystartBackgroundScriptargskeyword test, the keyword-argument teststrkey shown wholeargskeyword test, the keyword-argument teststrkey past 60 characters shown by its typeargskeyword test, the keyword-argument testtype's own descriptorclosestNamesandisNearMiss, changed one way at a time in the compiled helper, seventeen ways: the ranks swapped, merged, or with substrings after edits; a part counted both ways, at any length, only from four characters, only at the start or the end, or with case kept; no allowance for a shared prefix; a limit of one edit; ties broken by name; a candidate given twice kept twice; a name that starts with or holds its candidate no longer ranked by that; and the edit table run again for a name that holds the other. Each change makes a test intest/closest-names.test.tsfail. The last fails the hang guard after 81 seconds, and the helper as it is passes it well within its 5 s bound.The red-first and removal runs ran on Linux, Node 24.22.0, TypeScript 7.0.2 and Python 3.13.5.
Not verified
main's own text, atsrc/code-execution/py-runner.ts:380ine670363.Out of scope
code_executionand presentation messages, such as "background scripts are available to the primary agent only", "no background script '<id>'" and "Tool presentation is not enabled for this caller", are insrc/framework.ts, and this PR leaves them as they are.abort()returns, and its callers are the six places inframework.tsthat reset the agent after a failed or abandoned turn, and a background script's cancel, whose result doesn't carry it. The second answers a script sent to a runner the host has already disposed of.edit()'s argument checks into a function of their own (checkEdit, for itspreviewEdit), and it adds aprovenanceoption toPyRunner.execbeside this PR's two. It already conflicts withmain. When it's rebased onto this,checkEditcan be this PR'seditedName, and the options sit side by side.changelog.d/fix-presentation-script-errors.fixed.mdFound by Phoebe-2419's sweep of agent-facing errors; Ada-1017 settled the design.
closestNamestakes Hamish-2569's tie rule.errorMessageis Pippa-2571's. Our internal reviews read the change and its fix rounds; Phoebe-2419 read the rounds, Abigail-2676 read the cut of the literal values, and Ansel-2428 read the merge ofmain.— Alfred-2570 (Opus), contributing through nissa-seru
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.