Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7ff3cce
feat(workspace): a read says what the text is, cuts a long line, and …
Oct 17, 2026
57aff86
feat(workspace): grep matches and shows a CRLF file's lines as read s…
Oct 17, 2026
d62d07a
feat(workspace): a read settles its path against disk first, as a lis…
Oct 17, 2026
b33517e
fix(workspace): a read's cut last line is named in its note, readBina…
Oct 17, 2026
c00b269
docs(workspace): grep follows ripgrep's --crlf in matching a CRLF lin…
Oct 17, 2026
0a4303a
fix(workspace): a read whose disk check gives up says the check didn'…
Oct 18, 2026
83aae28
fix(workspace): the page that ends a file names its cut last line's rest
Oct 18, 2026
cd7f83b
fix(workspace): a read made in process pages by its line limit alone,…
Oct 18, 2026
eec5781
fix(workspace): a read's own disk check names its path, as a file too…
Oct 18, 2026
fd7a44b
docs(framework): executeToolCall's doc names the caller it has
Oct 18, 2026
3727bb2
Merge feat/workspace-edit-planner into feat/workspace-read
Oct 18, 2026
22becb5
fix(workspace): a line page is the longest page that fits its result …
Oct 18, 2026
4bb31cf
fix(workspace): a character page that holds all it was asked for name…
Oct 18, 2026
fb0f764
fix(workspace): a read says disk's copy was taken in only where its t…
Oct 18, 2026
62d223d
test(workspace): the read tests an internal review found missing
Oct 18, 2026
0f02d3e
docs(changelog): grep shows each matched line whole
Oct 18, 2026
db840ca
fix(workspace): a read's conflict note on a read-only mount names onl…
Oct 18, 2026
966ae54
test(workspace): a continued line page ends where its note says
Oct 18, 2026
3481086
docs(workspace): the page bound's figures, and comments a review misread
Oct 18, 2026
60e1748
Merge feat/workspace-edit-planner into feat/workspace-read
Oct 18, 2026
c7cdbbd
fix(workspace): a line whose own text ends in a CR shows its line end…
Oct 19, 2026
8b28652
fix(workspace): a cut line's note gives the limitChars that reads its…
Oct 19, 2026
93216a7
fix(workspace): a read whose disk check gave up says to read again
Oct 19, 2026
94138a9
test(workspace): a first line cut to fit, read from a later offset
Oct 19, 2026
7235800
docs: comments and a tool description an internal review found out of…
Oct 19, 2026
b5e97c5
fix(workspace): a stopped character page gives the limitChars left of…
Oct 19, 2026
eea460b
fix(workspace): a first line cut to fit is the longest cut that fits,…
Oct 19, 2026
bf37981
test(workspace): a script's line page cuts a long line too
Oct 19, 2026
694421a
Merge feat/workspace-edit-planner into feat/workspace-read
Oct 19, 2026
ac741bf
docs(workspace): read and grep say a CR CR LF line keeps its end's CR…
Oct 19, 2026
c9c3903
docs(framework): the utils comments name the callers that reach execu…
Oct 19, 2026
dcb7968
test(workspace): pin a mixed file's kept-CR note, a bare LF line's co…
Oct 19, 2026
1961412
fix(workspace): read and grep keep main's tool descriptions, and the …
Oct 19, 2026
b7a5a1d
feat(workspace): grep says why a pattern for a CR can't find a CRLF e…
Oct 19, 2026
813c60d
fix(workspace): grep's CR note takes the usual escapes for a CR, not …
Oct 19, 2026
456e5db
fix(workspace): grep's note names the workspace's copy, takes a backs…
Oct 19, 2026
8db5820
docs(workspace): the utils comment says a module is trusted, and four…
Oct 19, 2026
7d02af8
fix(workspace): grep's U+FEFF note names only a BOM the workspace's c…
Oct 19, 2026
ac6f05d
docs(framework): the utils comment says an unidentified caller is unr…
Oct 19, 2026
4c6171c
Merge main into feat/workspace-read: a direct mount's read and grep s…
Oct 20, 2026
e62ea55
test(workspace): a direct mount's read and grep give a 'chronicle' mo…
Oct 20, 2026
ac535df
docs(changelog): a direct mount's read and grep show text as a store-…
Oct 20, 2026
2a2b02e
fix(workspace): grep's CR and BOM notes name disk's file on a host wi…
Oct 20, 2026
2226bf1
test(workspace): the direct read test holds every page with a result …
Oct 20, 2026
677715d
docs(workspace): MountConfig.backend says a read takes disk's change …
Oct 20, 2026
24b507d
refactor(workspace): the tools' descriptions and grep's notes ask one…
Oct 20, 2026
766f9fa
docs(workspace): the 'chronicle' backend's doc and the changelog say …
Oct 20, 2026
a40712e
test(workspace): grep's notes on a host with both kinds of mount, and…
Oct 20, 2026
5f89cc8
fix(workspace): grep's note on a pattern for a CR or U+FEFF names the…
Oct 21, 2026
1142d08
docs(changelog): how a grep for \r or ^\uFEFF finds the files it sear…
Oct 21, 2026
786cdc1
test(workspace): grep's notes on a conflict per note, on a recorded d…
Oct 21, 2026
deac513
docs(workspace): grep keeps a conflict's disk version per note, and o…
Oct 21, 2026
dd1dcd6
Merge main (code_execution in the isolate) into the workspace read br…
Oct 21, 2026
754b17f
docs(workspace): a read's limit is what reaches its caller whole, a s…
Oct 21, 2026
107406a
Merge main (a pass that decided nothing, and the round's share of a r…
Oct 22, 2026
4060c27
docs(workspace): a read's limit is the most that can reach its caller…
Oct 22, 2026
8b46eac
refactor(workspace): read decides that its pass decided nothing as th…
Oct 22, 2026
b61d4e5
Merge main (mount-prefixed glob patterns, direct mounts that take wri…
Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.d/feat-workspace-read.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- The workspace's `read` shows a file's text as it is, and says so. A line page shows a CRLF file's lines without the CR before each line feed, and leaves out a BOM. A line whose own text ends in a CR shows its line end's CR too, since `edit` reads a CR before a line feed as part of the line end, so text copied from the line still matches it. Its note says when the file has CRLF ends or a BOM, counting a mix of CRLF and LF ends and the lines that show both CRs. It also says when the file isn't valid UTF-8: each invalid byte sequence shows as U+FFFD, `edit` refuses the file, and `write` replaces it whole. Before, each line of a CRLF file ended in a CR, a BOM came first in the first line as U+FEFF, and invalid bytes showed as U+FFFD with no note. A character page still reads the stored text as it is, BOM and CRs included.
- On a page for a call that carries a `resultMaxChars`, such as a model's or a running script's, a line over 2,000 characters is cut, with a marker that gives the `offsetChars` and `limitChars` that read its rest. Before, a line page showed every line whole, however long.
- A `read` page stops before its result would pass `resultMaxChars`, the most of it that can reach its caller whole. For a model's call that's its agent's inline cap, 24,000 characters by default, within which a result is stored whole unless its round's results together pass the readers' bound; for a running script's call it's what the script's memory takes, 7,456,527 characters at the default 128 MiB. Its note says where to continue: the `offset` after its last line, or for a character page the `offsetChars` it also gives as `nextOffsetChars`, with the `limitChars` left of what it was asked for, so a reader that follows the notes ends where it asked to. Where even a page's first line would pass the limit, that line is cut shorter, at the most the page has room for. Where a page's last line is cut, its note names the `offsetChars` where that line's rest starts and the `limitChars` that reads it to the line's end, before the next line's `offset` if the file goes on. A page always moves on, so where its own fields (the path and its notes) and the least it can show pass a small limit, it passes the limit. At 1,000 characters, a page with no notes does that only for a path of about 680 characters or more. Its notes take room from that: with all three notes on a file's text and a conflict's, a path of 93 to 129 characters does it, and a note that names a symlink's target, as one saying disk couldn't be checked can, takes as much room as the target is long. Before, a page of up to 2,000 lines, or of `limitChars` characters, went out whole, and for a result that passed the limit the framework kept only its start, cut mid-JSON, writing the whole of it to a workspace file where the agent has one. A caller that pages 2,000 lines at a time and starts the next page at line 2,001 now starts where the note says. A running `code_execution` script's call has that larger limit, so its line pages still stop at the line limit, 2,000 by default, and its character pages at `limitChars`, unless a page would pass that. Its line pages cut a line over 2,000 characters and leave out CRs and a BOM, as a model's do, and a character page gives it the stored text exactly. A call made in process, through `handleToolCall`, `executeToolCall` or `ModuleContext.callTool`, carries no `resultMaxChars` unless its caller sets one. Its line pages stop at the line limit alone and show every line whole, as they did before, though now without the CRs and the BOM.
- The workspace's `grep` leaves out a CRLF end's CR, for matching as well as display, as `read` does, so `foo$` finds a CRLF file's line too, as it does under ripgrep's `--crlf`. A match's text and context are its lines as `read` shows them, but each line whole, where a `read` page for a call with a result limit cuts a line over 2,000 characters. A BOM isn't part of the first line, and a file with CRLF ends says so in its result's `lineEndings` (`CRLF`, or `mixed`). Before, a CRLF file's lines ended in a CR, so `foo$` missed them and a match's text copied into an edit carried the CR. A pattern written to find the CR of a CRLF line end no longer matches, except on a line whose own text ends in a CR, which keeps its line end's CR as `read` shows it. When a pattern names a CR, as a literal one or as `\r`, `\x0d`, `\u000d`, `\cM` or `\015`, and a file it searched has CRLF ends, the result's `note` says how many files searched have them and names them, each as `CRLF` or `mixed`, up to 20 and then how many more. It also says that a character `read` shows every CR in the workspace's copy, or in disk's file on a direct mount where the host has one. A pattern that names U+FEFF, over a file that starts with a BOM, gets a note that names the files that start with one and says a BOM isn't part of the first line. So a `grep` for `\r` or `^\uFEFF` still finds which of the files it searches have CRLF ends or a BOM. A CR that doesn't come before a line feed is still part of its line.
- The workspace's `read` first takes in a change on disk to a file the workspace holds, as a listing and an `edit` do, and shows that text. Its note says so when disk's copy had changed since the workspace last agreed with it. It also says when disk's copy couldn't be checked, or disk has none and nothing records whether it was deleted there, so it shows the workspace's copy; where the check gave up because the selected branch kept changing, it says to read again. A file disk deleted answers that disk deleted it and the deletion was taken in, and a file disk made binary or too large answers that it's on disk but not stored. Where the path's stored copy is a draft and disk changed or deleted it too, the read shows the draft, and its note names the conflict and the ways to settle it, a sync, or a forced materialize on a mount that isn't read-only: for a deletion, a sync of the path takes it and removes the workspace's copy. Before, a read of a path the workspace held didn't look at disk, so it could show a copy another writer had since changed; a path it didn't hold was observed, as now.
- A direct mount's `read` and `grep` show a file's text as a store-backed mount's do, with the same pages, cut lines, notes and `lineEndings`. Only the disk check is a store-backed mount's alone, since a direct mount reads disk as it is and keeps no copy of its own.
39 changes: 23 additions & 16 deletions src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3191,8 +3191,8 @@ export class AgentFramework {
onTrace: (listener) => this.onTrace(listener),
// The ONLY entry that confers module origin — a private closure the
// framework hands to its own module registry. The public
// executeToolCall() is shared with model/ephemeral callers and always
// stamps agent origin (see there).
// executeToolCall(), whose callers act for an agent (the puppet, a
// host), always stamps agent origin (see there).
callTool: (call) => this.executeToolCallFrom(call, { kind: 'module' }),
notifyOps: (kind, agentName, message, data) => this.notifyOps(kind, agentName, message, data),
});
Expand Down Expand Up @@ -20436,12 +20436,15 @@ export class AgentFramework {
/**
* Execute a tool call and return the result.
* Routes to the appropriate handler (module registry or MCPL).
* Used by SubagentModule to dispatch tool calls for ephemeral agents.
* Its one caller in the framework is the operator's puppet
* (puppetToolCall), which puts the agent's inline cap on the call as
* resultMaxChars; a host may call it as well.
*
* This public entry always carries AGENT origin. Ephemeral/promise-based
* callers are models, and provenance must identify which internal path a
* call entered through — a public method shared with model callers can
* never confer module trust, whatever the input claims. Module origin
* This public entry always carries AGENT origin. Its callers act for an
* agent, the puppet as the agent and a host for the model it runs, and
* provenance must identify which internal path a call entered through — a
* public method shared with model callers can never confer module trust,
* whatever the input claims. Module origin
* exists only through the private closure handed to ModuleRegistry
* (ctx.callTool → executeToolCallFrom with {kind:'module'}), which is exempt
* from agent allowlists. Registered agents' allowedTools is enforced at
Expand Down Expand Up @@ -20655,9 +20658,10 @@ export class AgentFramework {
}

if (isPresentationTool(call.name)) return this.editToolPresentation(call.callerAgentName ?? '__ephemeral__', call);
// Client-side programmatic tool calling for promise-based callers
// (SubagentModule ephemerals). Keyed by callerAgentName, so each
// ephemeral's scripts are its own.
// Client-side programmatic tool calling for callers that await a tool's
// result: the puppet and a host through executeToolCall, and a module
// through callTool. Keyed by callerAgentName, so each agent's scripts are
// its own.
if (call.name === CODE_EXECUTION_TOOL_NAME && this.codeExecution) {
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
}
Expand Down Expand Up @@ -20693,9 +20697,11 @@ export class AgentFramework {
return this.draftsToolResult(origin.agentName, call.input);
}

// The utils meta-tool — programmatic callers (code_execution scripts,
// ephemerals, ModuleContext.callTool) reach utilities the same way the
// model does.
// The utils meta-tool, for callers that await a tool's result. A call
// through executeToolCall (the puppet, a host) reaches what its agent's
// allowedTools permits, as the model's does, and an unidentified caller
// is unrestricted, as executeToolCall's doc says; a module, through
// callTool, is trusted and reaches every utility.
if (call.name === 'utils') {
return this.handleUtilsToolCall(call.callerAgentName ?? '__ephemeral__', call, { inProcess: true, trustedModule: origin.kind === 'module' });
}
Expand Down Expand Up @@ -29222,9 +29228,10 @@ export class AgentFramework {
}

/** The `utils` meta-tool: list/describe/run over module utilities
* (Module.getUtilities). Shared by the model-facing dispatcher and the
* programmatic path (executeToolCall), so code_execution scripts and
* ephemerals reach utilities too. */
* (Module.getUtilities). Shared by the model-facing dispatcher, which a
* running code_execution script's calls take too, and executeToolCallFrom,
* which the puppet and a host reach through executeToolCall and a module
* through callTool. */
private async handleUtilsToolCall(agentName: string, call: ToolCall, options: { inProcess?: boolean; trustedModule?: boolean } = {}): Promise<ToolResult> {
const input = (call.input ?? {}) as { action?: unknown; name?: unknown; args?: unknown };
const allUtilities = this.moduleRegistry.getAllUtilities();
Expand Down
Loading
Loading