diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 74e80ac9a..ff91e2a2d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added a non-mutating chunk edit `read: true` operation for inspecting chunk content without relying on edit failures or delete previews. diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 65416ad9f..c8f5de886 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -5,7 +5,7 @@ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai"; import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { parseEffort } from "../thinking"; -import { BUILTIN_TOOLS, resolveToolAlias } from "../tools"; +import { BUILTIN_TOOLS } from "../tools"; export type Mode = "text" | "json" | "rpc" | "acp"; @@ -129,8 +129,7 @@ export function parseArgs(args: string[], extensionFlags?: Map s.trim().toLowerCase()) .filter(Boolean); const validTools: string[] = []; - for (const rawName of toolNames) { - const name = resolveToolAlias(rawName); + for (const name of toolNames) { if (name in BUILTIN_TOOLS) { validTools.push(name); } else { diff --git a/packages/coding-agent/src/cli/read-cli.ts b/packages/coding-agent/src/cli/read-cli.ts index bf97749d5..12e8aa9b2 100644 --- a/packages/coding-agent/src/cli/read-cli.ts +++ b/packages/coding-agent/src/cli/read-cli.ts @@ -11,7 +11,7 @@ import { formatChunkedRead, resolveAnchorStyle } from "../edit/modes/chunk"; import { getLanguageFromPath } from "../modes/theme/theme"; import type { ToolSession } from "../tools"; import { parseReadUrlTarget } from "../tools/fetch"; -import { OpenTool } from "../tools/open"; +import { ReadTool } from "../tools/read"; export interface ReadCommandArgs { path: string; @@ -34,7 +34,7 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise { const parsedUrlTarget = parseReadUrlTarget(cmd.path, cmd.sel); if (parsedUrlTarget) { const settings = await Settings.init({ cwd }); - const tool = new OpenTool(createCliReadSession(cwd, settings)); + const tool = new ReadTool(createCliReadSession(cwd, settings)); const result = await tool.execute("cli-read", { path: cmd.path, sel: cmd.sel }); const text = result.content.find((content): content is { type: "text"; text: string } => content.type === "text"); console.log(text?.text ?? ""); diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 67e9e78f1..7ddf86a9e 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -155,8 +155,8 @@ const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {}; export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [ { pattern: "^\\s*(cat|head|tail|less|more)\\s+", - tool: "open", - message: "Use the `open` tool instead of cat/head/tail. It provides better context and handles binary files.", + tool: "read", + message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.", }, { pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+", diff --git a/packages/coding-agent/src/cursor.ts b/packages/coding-agent/src/cursor.ts index 044bbbfb3..6e4fe597f 100644 --- a/packages/coding-agent/src/cursor.ts +++ b/packages/coding-agent/src/cursor.ts @@ -164,14 +164,14 @@ export class CursorExecHandlers implements ICursorExecHandlers { async read(args: Parameters>[0]) { const toolCallId = decodeToolCallId(args.toolCallId); - const toolResultMessage = await executeTool(this.options, "open", toolCallId, { path: args.path }); + const toolResultMessage = await executeTool(this.options, "read", toolCallId, { path: args.path }); return toolResultMessage; } async ls(args: Parameters>[0]) { const toolCallId = decodeToolCallId(args.toolCallId); // Redirect ls to read tool, which handles directories - const toolResultMessage = await executeTool(this.options, "open", toolCallId, { path: args.path }); + const toolResultMessage = await executeTool(this.options, "read", toolCallId, { path: args.path }); return toolResultMessage; } diff --git a/packages/coding-agent/src/edit/renderer.ts b/packages/coding-agent/src/edit/renderer.ts index e34112835..7df451ee8 100644 --- a/packages/coding-agent/src/edit/renderer.ts +++ b/packages/coding-agent/src/edit/renderer.ts @@ -31,6 +31,10 @@ import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./m import type { Operation, PatchEditEntry } from "./modes/patch"; import type { PerFileDiffPreview } from "./streaming"; +// ═══════════════════════════════════════════════════════════════════════════ +// LSP Batching +// ═══════════════════════════════════════════════════════════════════════════ + export { getLspBatchRequest, type LspBatchRequest }; // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js index 7d56c3dde..f2f9c88d9 100644 --- a/packages/coding-agent/src/export/html/template.js +++ b/packages/coding-agent/src/export/html/template.js @@ -394,8 +394,7 @@ function formatToolCall(name, args) { switch (name) { - case 'read': // legacy alias - case 'open': { + case 'read': { const path = shortenPath(String(args.path || args.file_path || '')); const offset = args.offset; const limit = args.limit; @@ -810,7 +809,7 @@ const filePath = str(args.file_path == null ? args.path : args.file_path); let pathHtml = pathDisplay(filePath, args.offset, args.limit); if (args.sel) pathHtml += ':' + escapeHtml(String(args.sel)) + ''; - let html = toolHead('open', pathHtml); + let html = toolHead('read', pathHtml); if (result) { html += ctx.renderResultImages(); const output = ctx.getResultText(); diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index a12ce4e7b..0bc78c5a0 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -48,8 +48,8 @@ import type { FindToolInput, GrepToolDetails, GrepToolInput, - OpenToolDetails, - OpenToolInput, + ReadToolDetails, + ReadToolInput, WriteToolInput, } from "../../tools"; import type { TodoItem } from "../../tools/todo-write"; @@ -668,9 +668,9 @@ export interface BashToolCallEvent extends ToolCallEventBase { input: BashToolInput; } -export interface OpenToolCallEvent extends ToolCallEventBase { - toolName: "open"; - input: OpenToolInput; +export interface ReadToolCallEvent extends ToolCallEventBase { + toolName: "read"; + input: ReadToolInput; } export interface EditToolCallEvent extends ToolCallEventBase { @@ -701,7 +701,7 @@ export interface CustomToolCallEvent extends ToolCallEventBase { /** Fired before a tool executes. Can block. */ export type ToolCallEvent = | BashToolCallEvent - | OpenToolCallEvent + | ReadToolCallEvent | EditToolCallEvent | WriteToolCallEvent | GrepToolCallEvent @@ -721,9 +721,9 @@ export interface BashToolResultEvent extends ToolResultEventBase { details: BashToolDetails | undefined; } -export interface OpenToolResultEvent extends ToolResultEventBase { - toolName: "open"; - details: OpenToolDetails | undefined; +export interface ReadToolResultEvent extends ToolResultEventBase { + toolName: "read"; + details: ReadToolDetails | undefined; } export interface EditToolResultEvent extends ToolResultEventBase { @@ -754,7 +754,7 @@ export interface CustomToolResultEvent extends ToolResultEventBase { /** Fired after a tool executes. Can modify result. */ export type ToolResultEvent = | BashToolResultEvent - | OpenToolResultEvent + | ReadToolResultEvent | EditToolResultEvent | WriteToolResultEvent | GrepToolResultEvent @@ -782,7 +782,7 @@ export type ToolResultEvent = * CustomToolCallEvent.toolName is `string` which overlaps with all literals. */ export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent; -export function isToolCallEventType(toolName: "open", event: ToolCallEvent): event is OpenToolCallEvent; +export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent; export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent; export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent; export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent; diff --git a/packages/coding-agent/src/extensibility/hooks/types.ts b/packages/coding-agent/src/extensibility/hooks/types.ts index eb1eac35c..acc629ace 100644 --- a/packages/coding-agent/src/extensibility/hooks/types.ts +++ b/packages/coding-agent/src/extensibility/hooks/types.ts @@ -21,7 +21,7 @@ import type { SessionEntry, SessionManager, } from "../../session/session-manager"; -import type { BashToolDetails, FindToolDetails, GrepToolDetails, OpenToolDetails } from "../../tools"; +import type { BashToolDetails, FindToolDetails, GrepToolDetails, ReadToolDetails } from "../../tools"; import type { TodoItem } from "../../tools/todo-write"; // Re-export for backward compatibility @@ -477,9 +477,9 @@ export interface BashToolResultEvent extends ToolResultEventBase { } /** Tool result event for read tool */ -export interface OpenToolResultEvent extends ToolResultEventBase { - toolName: "open"; - details: OpenToolDetails | undefined; +export interface ReadToolResultEvent extends ToolResultEventBase { + toolName: "read"; + details: ReadToolDetails | undefined; } /** Tool result event for edit tool */ @@ -519,7 +519,7 @@ export interface CustomToolResultEvent extends ToolResultEventBase { */ export type ToolResultEvent = | BashToolResultEvent - | OpenToolResultEvent + | ReadToolResultEvent | EditToolResultEvent | WriteToolResultEvent | GrepToolResultEvent diff --git a/packages/coding-agent/src/memories/index.ts b/packages/coding-agent/src/memories/index.ts index 096d7dcf5..23f2ff914 100644 --- a/packages/coding-agent/src/memories/index.ts +++ b/packages/coding-agent/src/memories/index.ts @@ -539,7 +539,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean { } if (role !== "toolResult") return false; const toolName = (message as { toolName?: string }).toolName; - if (toolName === "bash" || toolName === "python" || toolName === "open" || toolName === "grep") { + if (toolName === "bash" || toolName === "python" || toolName === "read" || toolName === "grep") { const text = extractMessageText(message); return text.length > 0 && text.length <= 32_000; } diff --git a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts index 1a68ca2e8..43a784acf 100644 --- a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts +++ b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts @@ -94,8 +94,7 @@ const ACP_TEXT_LIMIT = 4_000; export function mapToolKind(toolName: string): ToolKind { switch (toolName) { - case "read": // legacy alias - case "open": + case "read": return "read"; case "write": case "edit": diff --git a/packages/coding-agent/src/modes/components/session-observer-overlay.ts b/packages/coding-agent/src/modes/components/session-observer-overlay.ts index b9519a23b..5959b3c09 100644 --- a/packages/coding-agent/src/modes/components/session-observer-overlay.ts +++ b/packages/coding-agent/src/modes/components/session-observer-overlay.ts @@ -342,8 +342,7 @@ export class SessionObserverOverlayComponent extends Container { #formatToolArgs(toolName: string, args: Record): string { // Show the most relevant arg for common tools switch (toolName) { - case "read": // legacy alias - case "open": + case "read": return args.path ? `path: ${args.path}` : ""; case "write": return args.path ? `path: ${args.path}` : ""; diff --git a/packages/coding-agent/src/modes/components/tree-selector.ts b/packages/coding-agent/src/modes/components/tree-selector.ts index 280e042e4..19649ecb4 100644 --- a/packages/coding-agent/src/modes/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/components/tree-selector.ts @@ -637,8 +637,7 @@ class TreeList implements Component { #formatToolCall(name: string, args: Record): string { switch (name) { - case "read": // legacy alias - case "open": { + case "read": { const path = shortenPath(String(args.path || args.file_path || "")); const offset = args.offset as number | undefined; const limit = args.limit as number | undefined; diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index 2ca4ef956..311f146e4 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -212,7 +212,7 @@ export class EventController { for (const content of this.ctx.streamingMessage.content) { if (content.type !== "toolCall") continue; - if (content.name === "open" || content.name === "read") { + if (content.name === "read") { this.#trackReadToolCall(content.id, content.arguments); const component = this.ctx.pendingTools.get(content.id); if (component) { @@ -309,7 +309,7 @@ export class EventController { async #handleToolExecutionStart(event: Extract): Promise { this.#updateWorkingMessageFromIntent(event.intent); if (!this.ctx.pendingTools.has(event.toolCallId)) { - if (event.toolName === "open" || event.toolName === "read") { + if (event.toolName === "read") { this.#trackReadToolCall(event.toolCallId, event.args); const component = this.ctx.pendingTools.get(event.toolCallId); if (component) { @@ -366,7 +366,7 @@ export class EventController { } async #handleToolExecutionEnd(event: Extract): Promise { - if (event.toolName === "open" || event.toolName === "read") { + if (event.toolName === "read") { if (this.#inlineReadToolImages(event.toolCallId, event.result)) { const component = this.ctx.pendingTools.get(event.toolCallId); if (component) { diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 8c68f58fb..8502baf5f 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -254,7 +254,7 @@ export class UiHelpers { continue; } - if (content.name === "open" || content.name === "read") { + if (content.name === "read") { if (hasErrorStop && errorMessage) { if (!readGroup) { readGroup = new ReadToolGroupComponent({ @@ -315,7 +315,7 @@ export class UiHelpers { } } } else if (message.role === "toolResult") { - if (message.toolName === "open" || message.toolName === "read") { + if (message.toolName === "read") { const assistantComponent = readToolCallAssistantComponents.get(message.toolCallId); const images: ImageContent[] = message.content.filter( (content): content is ImageContent => content.type === "image", diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 42d1603c1..1e2eb0b53 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -180,14 +180,14 @@ If the task may involve external systems, SaaS APIs, chat, tickets, databases, d {{#ifAny (includes tools "python") (includes tools "bash")}} ### Tool priority -1. Use specialized tools first{{#ifAny (includes tools "open") (includes tools "grep") (includes tools "find") (includes tools "edit") (includes tools "lsp")}}: {{#has tools "open"}}`open`, {{/has}}{{#has tools "grep"}}`grep`, {{/has}}{{#has tools "find"}}`find`, {{/has}}{{#has tools "edit"}}`edit`, {{/has}}{{#has tools "lsp"}}`lsp`{{/has}}{{/ifAny}} +1. Use specialized tools first{{#ifAny (includes tools "read") (includes tools "grep") (includes tools "find") (includes tools "edit") (includes tools "lsp")}}: {{#has tools "read"}}`read`, {{/has}}{{#has tools "grep"}}`grep`, {{/has}}{{#has tools "find"}}`find`, {{/has}}{{#has tools "edit"}}`edit`, {{/has}}{{#has tools "lsp"}}`lsp`{{/has}}{{/ifAny}} 2. Python: logic, loops, processing, display 3. Bash: simple one-liners only You **MUST NOT** use Python or Bash when a specialized tool exists. {{/ifAny}} -{{#ifAny (includes tools "open") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}} -{{#has tools "open"}}- Use `open`, not `cat`/`head`/`tail`/`curl`/`wget`.{{/has}} +{{#ifAny (includes tools "read") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}} +{{#has tools "read"}}- Use `read`, not `cat`.{{/has}} {{#has tools "write"}}- Use `write`, not shell redirection.{{/has}} {{#has tools "grep"}}- Use `grep`, not shell regex search.{{/has}} {{#has tools "find"}}- Use `find`, not shell file globbing.{{/has}} @@ -232,7 +232,7 @@ Match commands to the host shell: linux/bash and macos/zsh use Unix commands; wi ### Search before you read {{#has tools "grep"}}- Use `grep` to locate targets.{{/has}} {{#has tools "find"}}- Use `find` to map structure.{{/has}} -{{#has tools "open"}}- Use `open` with offset or limit rather than whole-file reads when practical.{{/has}} +{{#has tools "read"}}- Use `read` with offset or limit rather than whole-file reads when practical.{{/has}} {{#has tools "task"}}- Use `task` for investigate+edit when available.{{/has}} - Do not read a file hoping to find the right thing. diff --git a/packages/coding-agent/src/prompts/tools/bash.md b/packages/coding-agent/src/prompts/tools/bash.md index d53d0f94c..6d7e501fa 100644 --- a/packages/coding-agent/src/prompts/tools/bash.md +++ b/packages/coding-agent/src/prompts/tools/bash.md @@ -33,13 +33,13 @@ You **MUST NOT** use bash for file operations where specialized tools exist: |Instead of (WRONG)|Use (CORRECT)| |---|---| -|`cat file`, `head -n N file`|`open(path="file", limit=N)`| -|`cat -n file \|sed -n '50,150p'`|`open(path="file", offset=50, limit=100)`| +|`cat file`, `head -n N file`|`read(path="file", limit=N)`| +|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`| {{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`| |`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`| |`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}} {{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}} -|`ls dir/`|`open(path="dir/")`| +|`ls dir/`|`read(path="dir/")`| |`cat <<'EOF' > file`|`write(path="file", content="…")`| |`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`| {{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}} diff --git a/packages/coding-agent/src/prompts/tools/browser.md b/packages/coding-agent/src/prompts/tools/browser.md index 180abd6eb..0039d7b49 100644 --- a/packages/coding-agent/src/prompts/tools/browser.md +++ b/packages/coding-agent/src/prompts/tools/browser.md @@ -1,7 +1,7 @@ Navigates, clicks, types, scrolls, drags, queries DOM content, and captures screenshots. -- For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `open` tool with a URL — it returns clean reader-mode text without spinning up a browser. Use this tool only when you need JS execution, authentication, or interactive actions. +- For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL — it returns clean reader-mode text without spinning up a browser. Use this tool only when you need JS execution, authentication, or interactive actions. - `"open"` starts a headless session (or implicitly on first action); `"goto"` navigates to `url`; `"close"` releases the browser - `"observe"` captures a numbered accessibility snapshot — prefer `click_id`/`type_id`/`fill_id` using returned `element_id` values; flags: `include_all`, `viewport_only` - `"click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, `"drag"` for selector-based interactions — prefer ARIA/text selectors (`p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS diff --git a/packages/coding-agent/src/prompts/tools/chunk-edit.md b/packages/coding-agent/src/prompts/tools/chunk-edit.md index 6f3127d57..8d800acd9 100644 --- a/packages/coding-agent/src/prompts/tools/chunk-edit.md +++ b/packages/coding-agent/src/prompts/tools/chunk-edit.md @@ -1,5 +1,5 @@ -Edits files via syntax-aware chunks. Use `open(path="file.ts")` to read and discover chunks before editing. -- `open` is the canonical read path for chunk source and `sel="?"` tree listings. +Edits files via syntax-aware chunks. Use `read(path="file.ts")` to read and discover chunks before editing. +- `read` is the canonical read path for chunk source and `sel="?"` tree listings. - `read:true` is a non-mutating convenience for an already-known selector. - `write` rewrites the entire targeted region — best for most edits. - `insert` adds content before/after a chunk. @@ -8,10 +8,10 @@ Edits files via syntax-aware chunks. Use `open(path="file.ts")` to read and disc Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}` -- **MUST** inspect first with `open`. Never invent chunk paths or IDs. Copy them from the latest `open` output or edit response. +- **MUST** inspect first with `read`. Never invent chunk paths or IDs. Copy them from the latest `read` output or edit response. - `path` format: `file:selector` — e.g. `src/app.ts:fn_foo#ABCD~`. Append `~` for body, `^` for head, or nothing for the whole chunk. Include `#ID` for `write`/`delete`. -- To inspect a known chunk through this tool, use `{"path":"file:chunk#ID","read":true}`. `read:true` is non-mutating, cannot be mixed with write operations in the same entry, and is not a replacement for `open(path="file", sel="?")` when discovering targets. -- If the exact chunk path is unclear, run `open(path="file", sel="?")` and copy a selector from that listing. +- To inspect a known chunk through this tool, use `{"path":"file:chunk#ID","read":true}`. `read:true` is non-mutating, cannot be mixed with write operations in the same entry, and is not a replacement for `read(path="file", sel="?")` when discovering targets. +- If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing. {{#if chunkAutoIndent}} - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0: ``` @@ -38,7 +38,7 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}` - Unsuffixed `write` on a leaf chunk uses your content verbatim after normal replacement; it is not a body-region rewrite. Include the exact indentation and punctuation the leaf needs in the file. - `^` head writes and `~` body writes use the same base-indent model: write content at column 0 relative to the target region, and the tool applies the chunk's file indentation. - `write` and `delete` require the current ID. `prepend`/`append` do not. -- **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `open(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit. +- **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit. - Same-file edit batches are transactional: if any operation in that file fails, no changes from that file's batch are saved. Multi-file edit calls run per file, so a later file error does not roll back earlier files that already succeeded. @@ -47,17 +47,17 @@ You **MUST** use the narrowest region that covers your change. Putting without a **`put` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before using `put` on any chunk's `~`, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent. -**Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). `put` on a group chunk's `~` overwrites **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `open` output — do not `put` the parent group. +**Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). `put` on a group chunk's `~` overwrites **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not `put` the parent group. -In `open` or `read:true` output, lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes/decorators, signature). Lines without `^` are **body** lines. Use this to decide which region to target: +In `read` or `read:true` output, lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes/decorators, signature). Lines without `^` are **body** lines. Use this to decide which region to target: - `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On code leaf chunks, this is rejected because there is no safe body boundary. - `fn_foo#ID^` — head only (decorators, attributes, doc comments, signature, opening delimiter). Body stays untouched. - `fn_foo#ID` — entire chunk including leading trivia. **You must include doc comments and attributes in `content`; omitting them deletes them.** - `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*. Appending to a container without `~` emits a warning because it lands after the closing delimiter, not before it. -**Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. JSDoc directly above a plain function is more likely to be absorbed into that function's `^`. If you need to rewrite a decorated member, run `open(path="file", sel="?")` and check for a sibling `chunk#ID` directly above your target. +**Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. JSDoc directly above a plain function is more likely to be absorbed into that function's `^`. If you need to rewrite a decorated member, run `read(path="file", sel="?")` and check for a sibling `chunk#ID` directly above your target. **Python notes:** Python docstrings are body lines, not head lines. A `~` body write on a function that has a docstring deletes the docstring unless you include the docstring in `content`. Python enum members and nested functions/closures are often opaque inside their parent chunk and may not appear as addressable child chunks; rewrite the parent container body. Python decorated class/function `^` writes and Python `^` deletes are rejected because indentation-sensitive bodies can become attached to the wrong block while still parsing. @@ -76,7 +76,7 @@ Each edit entry has `path` (`file:selector`) plus **exactly one** operation fiel -Given this `open` output for `counter.rs`: +Given this `read` output for `counter.rs`: ``` | counter.rs·62L·rust·#ZRPW | @@ -158,7 +158,7 @@ Given this `open` output for `counter.rs`: 61 |} ``` -**Understanding `^` markers in `open` output:** Lines marked with `^` between the line number and `|` (e.g. ` 3^|`) are **head** lines — doc comments, attributes, and the signature. Lines without `^` (e.g. ` 7 |`) are **body** lines. `~` replaces body lines only, keeping head lines intact. +**Understanding `^` markers in `read` output:** Lines marked with `^` between the line number and `|` (e.g. ` 3^|`) are **head** lines — doc comments, attributes, and the signature. Lines without `^` (e.g. ` 7 |`) are **body** lines. `~` replaces body lines only, keeping head lines intact. **Put body** (`~` — the common case): ``` diff --git a/packages/coding-agent/src/prompts/tools/grep.md b/packages/coding-agent/src/prompts/tools/grep.md index 45f6a8598..a34cb2825 100644 --- a/packages/coding-agent/src/prompts/tools/grep.md +++ b/packages/coding-agent/src/prompts/tools/grep.md @@ -21,7 +21,8 @@ Searches files using powerful regex matching. -- You **MUST** use Grep when searching for content. -- You **MUST NOT** invoke `grep` or `rg` via Bash. -- If the search is open-ended, requiring multiple rounds, you **MUST** use Task tool with explore subagent instead. +- You **MUST** use the built-in Grep tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands. +- Bash `grep`/`rg` returns raw text without chunk paths, loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The Grep tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable. +- If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the search through the Grep tool instead. +- If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining Grep calls yourself. diff --git a/packages/coding-agent/src/prompts/tools/lsp.md b/packages/coding-agent/src/prompts/tools/lsp.md index 2826de8b5..8b78deff2 100644 --- a/packages/coding-agent/src/prompts/tools/lsp.md +++ b/packages/coding-agent/src/prompts/tools/lsp.md @@ -36,4 +36,4 @@ Interacts with Language Server Protocol servers for code intelligence. - You **MUST** use `lsp` for symbol-aware operations (rename, find references, go to definition/implementation, code actions) whenever a language server is available — it is safer and more accurate than text-based alternatives. - You **MUST NOT** perform cross-file renames with `ast_edit`, `sed`, `rsed`, or manual edits when `lsp` `rename` can do it. Text-based renames miss shadowing, re-exports, and usages in other files. - Prefer `lsp` `code_actions` for imports, quick-fixes, and refactors the language server already knows how to apply. - + \ No newline at end of file diff --git a/packages/coding-agent/src/prompts/tools/open-chunk.md b/packages/coding-agent/src/prompts/tools/read-chunk.md similarity index 87% rename from packages/coding-agent/src/prompts/tools/open-chunk.md rename to packages/coding-agent/src/prompts/tools/read-chunk.md index 1cef17f8f..a50a4a960 100644 --- a/packages/coding-agent/src/prompts/tools/open-chunk.md +++ b/packages/coding-agent/src/prompts/tools/read-chunk.md @@ -1,10 +1,10 @@ Reads files using syntax-aware chunks. Also inspects directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**. -The chunk-aware `open` variant returns AST-scoped chunks with current checksum IDs for structural editing, and otherwise behaves like `open` for non-code content. +The chunk-aware `read` variant returns AST-scoped chunks with current checksum IDs for structural editing, and otherwise behaves like `open` for non-code content. - You **MUST** parallelize calls when exploring related files -- For URLs, `open` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `open` — not a browser/puppeteer tool — for fetching and inspecting web content. +- For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content. ## Parameters - `path` — file path or URL; may include `:selector` suffix (required) @@ -28,7 +28,7 @@ Max {{DEFAULT_MAX_LINES}} lines per call. # Chunks Each anchor `@full.chunk.path#CCCC` (with `-` prefixes for nesting depth) in the output identifies a chunk. Use `full.chunk.path#CCCC` as-is to read truncated chunks. -If you need a canonical target list, run `open(path="file", sel="?")`. That listing shows chunk paths with IDs and is the safest structural discovery mode. Summary lines in this listing are orientation hints; follow a selector with `open(path="file", sel="chunk#ID")` or use `raw` when you need exact source. +If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with IDs and is the safest structural discovery mode. Summary lines in this listing are orientation hints; follow a selector with `read(path="file", sel="chunk#ID")` or use `raw` when you need exact source. Line numbers in the gutter are absolute file line numbers. {{#if chunkAutoIndent}} @@ -60,15 +60,15 @@ When used against a SQLite database (`.sqlite`, `.sqlite3`, `.db`, `.db3`), retu - `file.db?q=SELECT …` — read-only SELECT query # URLs -Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `open` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). +Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). -- You **MUST** `open` before editing — never invent chunk names or IDs. - - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `open` or `?` output — never construct them from source identifiers. -- You **MUST** use `open` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. -- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `open` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. +- You **MUST** `read` before editing — never invent chunk names or IDs. + - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers. +- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. +- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. - You **MUST** always include the `path` parameter; never call with `{}`. -- For specific line ranges, use `sel`: `open(path="file", sel="L50-L150")` — not `cat -n file | sed`. +- For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`. - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output. diff --git a/packages/coding-agent/src/prompts/tools/open.md b/packages/coding-agent/src/prompts/tools/read.md similarity index 75% rename from packages/coding-agent/src/prompts/tools/open.md rename to packages/coding-agent/src/prompts/tools/read.md index d84e0b5ce..483599e52 100644 --- a/packages/coding-agent/src/prompts/tools/open.md +++ b/packages/coding-agent/src/prompts/tools/read.md @@ -1,11 +1,9 @@ -Opens and reads the content at the specified path or URL. +Reads the content at the specified path or URL. -The `open` tool is multi-purpose and far more capable than its name might suggest — it inspects files, directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**. - -(Previously named `read`. The old name is still accepted as an alias in agent configs and tool lists, but the canonical name is `open`.) -- You **MUST** parallelize calls when exploring related files -- For URLs, `open` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `open` — not a browser/puppeteer tool — for fetching and inspecting web content. +The `read` tool is multi-purpose and more capable than it looks — inspects files, directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**. +- You **MUST** parallelize reads when exploring related files +- For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content. ## Parameters - `path` — file path or URL (required) @@ -49,13 +47,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`: - `file.db?q=SELECT …` — read-only SELECT query # URLs -Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `open` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). +Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). -- You **MUST** use `open` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. -- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `open` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. +- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. +- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. - You **MUST** always include the `path` parameter; never call with `{}`. -- For specific line ranges, use `sel`: `open(path="file", sel="L50-L150")` — not `cat -n file | sed`. +- For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`. - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output. diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index e8dfc5142..5a1e5b6f3 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -116,11 +116,10 @@ import { isSearchProviderPreference, type LspStartupServerInfo, loadSshTool, - OpenTool, PythonTool, + ReadTool, ResolveTool, renderSearchToolBm25Description, - resolveToolAlias, setPreferredImageProvider, setPreferredSearchProvider, type Tool, @@ -268,8 +267,8 @@ export { GrepTool, HIDDEN_TOOLS, loadSshTool, - OpenTool as ReadTool, PythonTool, + ReadTool, ResolveTool, type ToolSession, WriteTool, @@ -1354,9 +1353,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const toolNamesFromRegistry = Array.from(toolRegistry.keys()); const requestedToolNames = - (options.toolNames - ? [...new Set(options.toolNames.map(name => resolveToolAlias(name.toLowerCase())))] - : undefined) ?? toolNamesFromRegistry; + (options.toolNames ? [...new Set(options.toolNames.map(name => name.toLowerCase()))] : undefined) ?? + toolNamesFromRegistry; const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name)); const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode"); const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false; diff --git a/packages/coding-agent/src/session/compaction/pruning.ts b/packages/coding-agent/src/session/compaction/pruning.ts index 2700d1c58..76ea24437 100644 --- a/packages/coding-agent/src/session/compaction/pruning.ts +++ b/packages/coding-agent/src/session/compaction/pruning.ts @@ -18,7 +18,7 @@ export interface PruneConfig { export const DEFAULT_PRUNE_CONFIG: PruneConfig = { protectTokens: 40_000, minimumSavings: 20_000, - protectedTools: ["skill", "open", "read"], + protectedTools: ["skill", "read"], }; export interface PruneResult { diff --git a/packages/coding-agent/src/session/compaction/utils.ts b/packages/coding-agent/src/session/compaction/utils.ts index 48b861b3f..7da8de00b 100644 --- a/packages/coding-agent/src/session/compaction/utils.ts +++ b/packages/coding-agent/src/session/compaction/utils.ts @@ -44,8 +44,7 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp if (!path) continue; switch (block.name) { - case "read": // legacy alias - case "open": + case "read": fileOps.read.add(path); break; case "write": diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 2a2b34f58..bda4dfebf 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -561,7 +561,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): toolNames = Array.from(tools.keys()); } else { // Use defaults - toolNames = ["open", "bash", "python", "edit", "write"]; // TODO: Why? + toolNames = ["read", "bash", "python", "edit", "write"]; // TODO: Why? } } @@ -573,7 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): })); // Filter skills to only include those with read tool - const hasRead = tools?.has("open"); + const hasRead = tools?.has("read"); const filteredSkills = hasRead ? skills : []; const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [ diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index 53c449139..ea31be1eb 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -567,7 +567,7 @@ export class TaskTool implements AgentTool { } const planModeState = this.session.getPlanModeState?.(); - const planModeTools = ["open", "grep", "find", "ls", "lsp", "web_search"]; + const planModeTools = ["read", "grep", "find", "ls", "lsp", "web_search"]; const effectiveAgent: typeof agent = planModeState?.enabled ? { ...agent, diff --git a/packages/coding-agent/src/tools/bash-interceptor.ts b/packages/coding-agent/src/tools/bash-interceptor.ts index 03c793cce..8fca9e981 100644 --- a/packages/coding-agent/src/tools/bash-interceptor.ts +++ b/packages/coding-agent/src/tools/bash-interceptor.ts @@ -6,7 +6,6 @@ * the specialized tools instead. */ import { type BashInterceptorRule, DEFAULT_BASH_INTERCEPTOR_RULES } from "../config/settings-schema"; -import { resolveToolAlias } from "./index"; export interface InterceptionResult { /** If true, the bash command should be blocked */ @@ -51,7 +50,7 @@ export function checkBashInterception( for (const { rule, regex } of compiled) { // Only block if the suggested tool is actually available - if (!availableTools.some(name => resolveToolAlias(name) === rule.tool)) { + if (!availableTools.includes(rule.tool)) { continue; } diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index e08156492..76a95e0f6 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -1189,7 +1189,7 @@ async function materializeReadUrlCacheEntry( } async function persistReadUrlArtifact(session: ToolSession, output: string): Promise { - const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("open")) ?? {}; + const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("read")) ?? {}; if (!artifactPath) return undefined; await Bun.write(artifactPath, output); return id; diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 3b775ae34..72a58832d 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -43,10 +43,10 @@ import { import { GrepTool } from "./grep"; import { InspectImageTool } from "./inspect-image"; import { NotebookTool } from "./notebook"; -import { OpenTool } from "./open"; import { wrapToolWithMetaNotice } from "./output-meta"; import { PollTool } from "./poll-tool"; import { PythonTool } from "./python"; +import { ReadTool } from "./read"; import { RenderMermaidTool } from "./render-mermaid"; import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue"; import { ResolveTool } from "./resolve"; @@ -82,9 +82,9 @@ export * from "./gh"; export * from "./grep"; export * from "./inspect-image"; export * from "./notebook"; -export * from "./open"; export * from "./poll-tool"; export * from "./python"; +export * from "./read"; export * from "./render-mermaid"; export * from "./report-tool-issue"; export * from "./resolve"; @@ -230,7 +230,7 @@ export const BUILTIN_TOOLS: Record = { grep: s => new GrepTool(s), lsp: LspTool.createIf, notebook: s => new NotebookTool(s), - open: s => new OpenTool(s), + read: s => new ReadTool(s), inspect_image: s => new InspectImageTool(s), browser: s => new BrowserTool(s), checkpoint: CheckpointTool.createIf, @@ -244,20 +244,6 @@ export const BUILTIN_TOOLS: Record = { write: s => new WriteTool(s), }; -/** - * Legacy tool-name aliases. Accepted wherever users supply tool names - * (agent `tools:` fields, `--tools` flags, settings, etc.) and normalized - * to the canonical name before lookup. - */ -export const TOOL_ALIASES: Record = { - read: "open", -}; - -/** Resolve a possibly-aliased tool name to its canonical registry key. */ -export function resolveToolAlias(name: string): string { - return TOOL_ALIASES[name] ?? name; -} - export const HIDDEN_TOOLS: Record = { submit_result: s => new SubmitResultTool(s), report_finding: () => reportFindingTool, @@ -305,9 +291,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P const includeSubmitResult = session.requireSubmitResultTool === true; const enableLsp = session.enableLsp ?? true; const requestedTools = - toolNames && toolNames.length > 0 - ? [...new Set(toolNames.map(name => resolveToolAlias(name.toLowerCase())))] - : undefined; + toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined; if (requestedTools && !requestedTools.includes("exit_plan_mode")) { requestedTools.push("exit_plan_mode"); } diff --git a/packages/coding-agent/src/tools/python.ts b/packages/coding-agent/src/tools/python.ts index ea33121b3..719ae9623 100644 --- a/packages/coding-agent/src/tools/python.ts +++ b/packages/coding-agent/src/tools/python.ts @@ -590,8 +590,7 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string { // Build description based on common fields switch (op) { - case "read": // legacy alias - case "open": + case "read": parts.push(`${data.chars} chars`); if (data.path) parts.push(`from ${shortenPath(String(data.path))}`); break; @@ -795,8 +794,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri case "git_branch": if (data.branches) addItems(data.branches as unknown[], b => String(b)); break; - case "read": // legacy alias - case "open": + case "read": case "cat": case "head": case "tail": diff --git a/packages/coding-agent/src/tools/open.ts b/packages/coding-agent/src/tools/read.ts similarity index 96% rename from packages/coding-agent/src/tools/open.ts rename to packages/coding-agent/src/tools/read.ts index c49087bf7..dd21214fb 100644 --- a/packages/coding-agent/src/tools/open.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -21,8 +21,8 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import { parseInternalUrl } from "../internal-urls/parse"; import type { InternalUrl } from "../internal-urls/types"; import { getLanguageFromPath, type Theme } from "../modes/theme/theme"; -import openDescription from "../prompts/tools/open.md" with { type: "text" }; -import openChunkDescription from "../prompts/tools/open-chunk.md" with { type: "text" }; +import readDescription from "../prompts/tools/read.md" with { type: "text" }; +import readChunkDescription from "../prompts/tools/read-chunk.md" with { type: "text" }; import type { ToolSession } from "../sdk"; import { DEFAULT_MAX_BYTES, @@ -71,7 +71,7 @@ import { import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors"; import { toolResult } from "./tool-result"; -const PROSE_LANGUAGES = new Set(["text", "log", "restructuredtext"]); +const PROSE_LANGUAGES = new Set(["markdown", "text", "log", "asciidoc", "restructuredtext"]); function isProseLanguage(language: string | undefined): boolean { return language !== undefined && PROSE_LANGUAGES.has(language); @@ -368,15 +368,15 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from: return text ? `${notice}\n${text}` : notice; } -const openSchema = Type.Object({ +const readSchema = Type.Object({ path: Type.String({ description: "Path or URL to read" }), sel: Type.Optional(Type.String({ description: "Selector: chunk path, L10-L50, or raw" })), timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 20 })), }); -export type OpenToolInput = Static; +export type ReadToolInput = Static; -export interface OpenToolDetails { +export interface ReadToolDetails { kind?: "file" | "url"; truncation?: TruncationResult; isDirectory?: boolean; @@ -391,7 +391,7 @@ export interface OpenToolDetails { meta?: OutputMeta; } -type OpenParams = OpenToolInput; +type ReadParams = ReadToolInput; /** Parsed representation of the `sel` parameter. */ type ParsedSelector = @@ -465,11 +465,11 @@ function parseSqliteSelectorInput(selector: string | undefined): { subPath: stri * Reads files with support for images, converted documents (via markit), and text. * Directories return a formatted listing with modification times. */ -export class OpenTool implements AgentTool { - readonly name = "open"; +export class ReadTool implements AgentTool { + readonly name = "read"; readonly label = "Read"; readonly description: string; - readonly parameters = openSchema; + readonly parameters = readSchema; readonly nonAbortable = true; readonly strict = true; @@ -487,11 +487,11 @@ export class OpenTool implements AgentTool { this.#inspectImageEnabled = session.settings.get("inspect_image.enabled"); this.description = resolveEditMode(session) === "chunk" - ? prompt.render(openChunkDescription, { + ? prompt.render(readChunkDescription, { anchorStyle: resolveAnchorStyle(session.settings), chunkAutoIndent: resolveChunkAutoIndent(), }) - : prompt.render(openDescription, { + : prompt.render(readDescription, { DEFAULT_LIMIT: String(this.#defaultLimit), DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES), IS_HASHLINE_MODE: displayMode.hashLines, @@ -593,14 +593,14 @@ export class OpenTool implements AgentTool { offset: number | undefined, limit: number | undefined, options: { - details?: OpenToolDetails; + details?: ReadToolDetails; sourcePath?: string; sourceUrl?: string; sourceInternal?: string; entityLabel: string; ignoreResultLimits?: boolean; }, - ): AgentToolResult { + ): AgentToolResult { const displayMode = resolveFileDisplayMode(this.session); const details = options.details ?? {}; const allLines = text.split("\n"); @@ -701,9 +701,9 @@ export class OpenTool implements AgentTool { archivePath: string, subPath: string, limit: number | undefined, - details: OpenToolDetails, + details: ReadToolDetails, signal?: AbortSignal, - ): Promise> { + ): Promise> { const DEFAULT_LIMIT = 500; const effectiveLimit = limit ?? DEFAULT_LIMIT; const entries = archive.listDirectory(subPath); @@ -727,8 +727,8 @@ export class OpenTool implements AgentTool { const output = results.length > 0 ? results.join("\n") : "(empty archive directory)"; const text = prependSuffixResolutionNotice(output, details.suffixResolution); const truncation = truncateHead(text, { maxLines: Number.MAX_SAFE_INTEGER }); - const directoryDetails: OpenToolDetails = { ...details, isDirectory: true }; - const resultBuilder = toolResult(directoryDetails).text(truncation.content); + const directoryDetails: ReadToolDetails = { ...details, isDirectory: true }; + const resultBuilder = toolResult(directoryDetails).text(truncation.content); resultBuilder.sourcePath(archivePath).limits({ resultLimit: limitMeta.resultLimit?.reached }); if (truncation.truncated) { directoryDetails.truncation = truncation; @@ -743,12 +743,12 @@ export class OpenTool implements AgentTool { limit: number | undefined, resolvedArchivePath: ResolvedArchiveReadPath, signal?: AbortSignal, - ): Promise> { + ): Promise> { throwIfAborted(signal); const archive = await openArchive(resolvedArchivePath.absolutePath); throwIfAborted(signal); - const details: OpenToolDetails = { + const details: ReadToolDetails = { resolvedPath: resolvedArchivePath.absolutePath, suffixResolution: resolvedArchivePath.suffixResolution, }; @@ -772,7 +772,7 @@ export class OpenTool implements AgentTool { const entry = await archive.readFile(resolvedArchivePath.archiveSubPath); const text = decodeUtf8Text(entry.bytes); if (text === null) { - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( `[Cannot read binary archive entry '${entry.path}' (${formatBytes(entry.size)})]`, @@ -799,14 +799,14 @@ export class OpenTool implements AgentTool { sel: string | undefined, resolvedSqlitePath: ResolvedSqliteReadPath, signal?: AbortSignal, - ): Promise> { + ): Promise> { throwIfAborted(signal); const selectorInput = sel ? parseSqliteSelectorInput(sel) : { subPath: resolvedSqlitePath.sqliteSubPath, queryString: resolvedSqlitePath.queryString }; const selector = parseSqliteSelector(selectorInput.subPath, selectorInput.queryString); - const details: OpenToolDetails = { + const details: ReadToolDetails = { resolvedPath: resolvedSqlitePath.absolutePath, suffixResolution: resolvedSqlitePath.suffixResolution, }; @@ -826,7 +826,7 @@ export class OpenTool implements AgentTool { ); const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER }); details.truncation = truncation.truncated ? truncation : undefined; - const resultBuilder = toolResult(details) + const resultBuilder = toolResult(details) .text(truncation.content) .sourcePath(resolvedSqlitePath.absolutePath) .limits({ resultLimit: listLimit.meta.resultLimit?.reached }); @@ -845,7 +845,7 @@ export class OpenTool implements AgentTool { const remaining = sampleRows.totalCount - sampleRows.rows.length; output += `\n[${remaining} more rows; use sel="${selector.table}?limit=20&offset=${sampleRows.rows.length}" to continue]`; } - return toolResult(details) + return toolResult(details) .text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution)) .sourcePath(resolvedSqlitePath.absolutePath) .done(); @@ -857,7 +857,7 @@ export class OpenTool implements AgentTool { ? getRowByKey(db, selector.table, lookup, selector.key) : getRowByRowId(db, selector.table, selector.key); if (!row) { - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( `No row found in table '${selector.table}' for key '${selector.key}'.`, @@ -867,14 +867,14 @@ export class OpenTool implements AgentTool { .sourcePath(resolvedSqlitePath.absolutePath) .done(); } - return toolResult(details) + return toolResult(details) .text(prependSuffixResolutionNotice(renderRow(row), resolvedSqlitePath.suffixResolution)) .sourcePath(resolvedSqlitePath.absolutePath) .done(); } case "query": { const page = queryRows(db, selector.table, selector); - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( renderTable(page.columns, page.rows, { @@ -892,7 +892,7 @@ export class OpenTool implements AgentTool { } case "raw": { const result = executeReadQuery(db, selector.sql); - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( renderTable(result.columns, result.rows, { @@ -923,11 +923,11 @@ export class OpenTool implements AgentTool { async execute( _toolCallId: string, - params: OpenParams, + params: ReadParams, signal?: AbortSignal, - _onUpdate?: AgentToolUpdateCallback, + _onUpdate?: AgentToolUpdateCallback, _toolContext?: AgentToolContext, - ): Promise> { + ): Promise> { let { path: readPath, sel, timeout } = params; if (readPath.startsWith("file://")) { readPath = expandPath(readPath); @@ -1071,7 +1071,7 @@ export class OpenTool implements AgentTool { if (suffixResolution) { text = prependSuffixResolutionNotice(text, suffixResolution); } - return toolResult({ + return toolResult({ resolvedPath: absolutePath, suffixResolution, chunk: chunkResult.chunk, @@ -1083,7 +1083,7 @@ export class OpenTool implements AgentTool { // Read the file based on type let content: Array; - let details: OpenToolDetails = {}; + let details: ReadToolDetails = {}; let sourcePath: string | undefined; let truncationInfo: | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } } @@ -1178,7 +1178,7 @@ export class OpenTool implements AgentTool { if (suffixResolution) { text = prependSuffixResolutionNotice(text, suffixResolution); } - return toolResult({ + return toolResult({ resolvedPath: absolutePath, suffixResolution, chunk: chunkResult.chunk, @@ -1222,7 +1222,7 @@ export class OpenTool implements AgentTool { totalFileLines === 0 ? "The file is empty." : `Use sel=L1 to read from the start, or sel=L${totalFileLines} to read the last line.`; - return toolResult({ resolvedPath: absolutePath, suffixResolution }) + return toolResult({ resolvedPath: absolutePath, suffixResolution }) .text(`Line ${startLineDisplay} is beyond end of file (${totalFileLines} lines total). ${suggestion}`) .done(); } @@ -1328,7 +1328,7 @@ export class OpenTool implements AgentTool { * Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://). * Supports pagination via offset/limit but rejects them when query extraction is used. */ - async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise> { + async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise> { const internalRouter = this.session.internalRouter!; // Check if URL has query extraction (agent:// only). @@ -1355,7 +1355,7 @@ export class OpenTool implements AgentTool { // Resolve the internal URL const resource = await internalRouter.resolve(url); - const details: OpenToolDetails = { resolvedPath: resource.sourcePath }; + const details: ReadToolDetails = { resolvedPath: resource.sourcePath }; // If extraction was used, return directly (no pagination) if (hasExtraction) { @@ -1376,7 +1376,7 @@ export class OpenTool implements AgentTool { absolutePath: string, limit: number | undefined, signal?: AbortSignal, - ): Promise> { + ): Promise> { const DEFAULT_LIMIT = 500; const effectiveLimit = limit ?? DEFAULT_LIMIT; @@ -1425,7 +1425,7 @@ export class OpenTool implements AgentTool { const output = results.join("\n"); const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER }); - const details: OpenToolDetails = { + const details: ReadToolDetails = { isDirectory: true, }; @@ -1445,7 +1445,7 @@ export class OpenTool implements AgentTool { // TUI Renderer // ============================================================================= -interface OpenRenderArgs { +interface ReadRenderArgs { path?: string; file_path?: string; sel?: string; @@ -1456,8 +1456,8 @@ interface OpenRenderArgs { raw?: boolean; } -export const openToolRenderer = { - renderCall(args: OpenRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component { +export const readToolRenderer = { + renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component { if (isReadableUrlPath(args.file_path || args.path || "")) { return renderReadUrlCall(args, _options, uiTheme); } @@ -1479,10 +1479,10 @@ export const openToolRenderer = { }, renderResult( - result: { content: Array<{ type: string; text?: string }>; details?: OpenToolDetails }, + result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails }, _options: RenderResultOptions, uiTheme: Theme, - args?: OpenRenderArgs, + args?: ReadRenderArgs, ): Component { const urlDetails = result.details as ReadUrlToolDetails | undefined; if (urlDetails?.kind === "url" || isReadableUrlPath(args?.file_path || args?.path || "")) { diff --git a/packages/coding-agent/src/tools/renderers.ts b/packages/coding-agent/src/tools/renderers.ts index eca427679..cda88dc08 100644 --- a/packages/coding-agent/src/tools/renderers.ts +++ b/packages/coding-agent/src/tools/renderers.ts @@ -21,8 +21,8 @@ import { ghRunWatchToolRenderer } from "./gh-renderer"; import { grepToolRenderer } from "./grep"; import { inspectImageToolRenderer } from "./inspect-image-renderer"; import { notebookToolRenderer } from "./notebook"; -import { openToolRenderer } from "./open"; import { pythonToolRenderer } from "./python"; +import { readToolRenderer } from "./read"; import { resolveToolRenderer } from "./resolve"; import { searchToolBm25Renderer } from "./search-tool-bm25"; import { sshToolRenderer } from "./ssh"; @@ -57,7 +57,7 @@ export const toolRenderers: Record = { lsp: lspToolRenderer as ToolRenderer, notebook: notebookToolRenderer as ToolRenderer, inspect_image: inspectImageToolRenderer as ToolRenderer, - read: openToolRenderer as ToolRenderer, + read: readToolRenderer as ToolRenderer, resolve: resolveToolRenderer as ToolRenderer, search_tool_bm25: searchToolBm25Renderer as ToolRenderer, ssh: sshToolRenderer as ToolRenderer, diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index fdb2bdfec..c17d90dbc 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -215,17 +215,17 @@ describe("parseArgs", () => { test("parses --no-tools with explicit --tools flags", () => { const result = parseArgs(["--no-tools", "--tools", "read,bash"]); expect(result.noTools).toBe(true); - expect(result.tools).toEqual(["open", "bash"]); + expect(result.tools).toEqual(["read", "bash"]); }); test("lowercases tool names passed to --tools", () => { const result = parseArgs(["--tools", "Read,Grep"]); - expect(result.tools).toEqual(["open", "grep"]); + expect(result.tools).toEqual(["read", "grep"]); }); test("parses --tools=value with equals syntax", () => { const result = parseArgs(["--tools=read,bash"]); - expect(result.tools).toEqual(["open", "bash"]); + expect(result.tools).toEqual(["read", "bash"]); }); test("parses --tools=value with single tool", () => { diff --git a/packages/coding-agent/test/block-images.test.ts b/packages/coding-agent/test/block-images.test.ts index e2dcb664c..cec63ea1d 100644 --- a/packages/coding-agent/test/block-images.test.ts +++ b/packages/coding-agent/test/block-images.test.ts @@ -5,7 +5,7 @@ import * as path from "node:path"; import { processFileArguments } from "@oh-my-pi/pi-coding-agent/cli/file-processor"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; +import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; // 1x1 red PNG image as base64 (smallest valid PNG) const TINY_PNG_BASE64 = @@ -71,7 +71,7 @@ describe("blockImages setting", () => { const imagePath = path.join(testDir, "test.png"); fs.writeFileSync(imagePath, Buffer.from(TINY_PNG_BASE64, "base64")); - const tool = new OpenTool( + const tool = new ReadTool( createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false })), ); const result = await tool.execute("test-1", { path: imagePath }); @@ -87,7 +87,7 @@ describe("blockImages setting", () => { const textPath = path.join(testDir, "test.txt"); fs.writeFileSync(textPath, "Hello, world!"); - const tool = new OpenTool(createTestToolSession(testDir)); + const tool = new ReadTool(createTestToolSession(testDir)); const result = await tool.execute("test-2", { path: textPath }); expect(result.content).toHaveLength(1); diff --git a/packages/coding-agent/test/sdk-mcp-discovery.test.ts b/packages/coding-agent/test/sdk-mcp-discovery.test.ts index dd4fdf796..71b957579 100644 --- a/packages/coding-agent/test/sdk-mcp-discovery.test.ts +++ b/packages/coding-agent/test/sdk-mcp-discovery.test.ts @@ -105,7 +105,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { await session.activateDiscoveredMCPTools(["mcp_slack_post_message"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue", "mcp_slack_post_message"]), + expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue", "mcp_slack_post_message"]), ); expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue", "mcp_slack_post_message"]); }); @@ -127,7 +127,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25"], + toolNames: ["read", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -136,7 +136,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { try { expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue"]), + expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue"]), ); expect(session.getActiveToolNames()).not.toContain("mcp_slack_post_message"); } finally { @@ -158,7 +158,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25"], + toolNames: ["read", "search_tool_bm25"], customTools: [createMcpCustomTool("mcp_github_create_issue", "github", "create_issue")], }); @@ -185,7 +185,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25"], + toolNames: ["read", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -221,7 +221,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25"], + toolNames: ["read", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -232,7 +232,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { expect(resumedSession.serviceTier).toBe("priority"); expect(resumedSession.getSelectedMCPToolNames()).toEqual(["mcp_slack_post_message"]); expect(resumedSession.getActiveToolNames()).toEqual( - expect.arrayContaining(["open", "search_tool_bm25", "mcp_slack_post_message"]), + expect.arrayContaining(["read", "search_tool_bm25", "mcp_slack_post_message"]), ); expect(resumedSession.systemPrompt).toContain("mcp_slack_post_message"); expect(fs.readFileSync(sessionFile!, "utf8")).toBe(persistedBeforeResume); @@ -274,7 +274,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25"], + toolNames: ["read", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -285,7 +285,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { expect(session.serviceTier).toBe("priority"); expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue"]), + expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue"]), ); expect(session.sessionManager.buildSessionContext().hasPersistedMCPToolSelection).toBe(false); expect(fs.readFileSync(sessionFile!, "utf8")).toBe(persistedBeforeResume); @@ -310,13 +310,13 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25", "mcp_github_create_issue"], + toolNames: ["read", "search_tool_bm25", "mcp_github_create_issue"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), ], }); - await firstSession.setActiveToolsByName(["open", "search_tool_bm25"]); + await firstSession.setActiveToolsByName(["read", "search_tool_bm25"]); expect(firstSession.getSelectedMCPToolNames()).toEqual([]); const sessionFile = firstSession.sessionFile; expect(sessionFile).toBeDefined(); @@ -337,7 +337,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["open", "search_tool_bm25", "mcp_github_create_issue"], + toolNames: ["read", "search_tool_bm25", "mcp_github_create_issue"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -345,7 +345,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { }); try { expect(resumedSession.getSelectedMCPToolNames()).toEqual([]); - expect(resumedSession.getActiveToolNames()).toEqual(expect.arrayContaining(["open", "search_tool_bm25"])); + expect(resumedSession.getActiveToolNames()).toEqual(expect.arrayContaining(["read", "search_tool_bm25"])); expect(resumedSession.getActiveToolNames()).not.toContain("mcp_github_create_issue"); } finally { await resumedSession.dispose(); diff --git a/packages/coding-agent/test/sdk-tool-activation.test.ts b/packages/coding-agent/test/sdk-tool-activation.test.ts index 8286f5613..f1b67e302 100644 --- a/packages/coding-agent/test/sdk-tool-activation.test.ts +++ b/packages/coding-agent/test/sdk-tool-activation.test.ts @@ -104,7 +104,7 @@ describe("createAgentSession defaultInactive tool activation", () => { try { expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["open", "default_active_tool", "default_inactive_tool"]), + expect.arrayContaining(["read", "default_active_tool", "default_inactive_tool"]), ); expect(session.systemPrompt).toContain("default_inactive_tool"); } finally { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 475ab7b92..7fb9e4f00 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -14,9 +14,9 @@ import { BashTool } from "@oh-my-pi/pi-coding-agent/tools/bash"; import { CancelJobTool } from "@oh-my-pi/pi-coding-agent/tools/cancel-job"; import { FindTool } from "@oh-my-pi/pi-coding-agent/tools/find"; import { GrepTool } from "@oh-my-pi/pi-coding-agent/tools/grep"; -import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; import { wrapToolWithMetaNotice } from "@oh-my-pi/pi-coding-agent/tools/output-meta"; import { PollTool } from "@oh-my-pi/pi-coding-agent/tools/poll-tool"; +import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; import { WriteTool } from "@oh-my-pi/pi-coding-agent/tools/write"; import * as markitUtils from "@oh-my-pi/pi-coding-agent/utils/markit"; import { $which, Snowflake } from "@oh-my-pi/pi-utils"; @@ -216,7 +216,7 @@ function createTestToolContext(toolNames: string[]): AgentToolContext { describe("Coding Agent Tools", () => { let testDir: string; let session: ToolSession; - let readTool: OpenTool; + let readTool: ReadTool; let writeTool: WriteTool; let editTool: EditTool; let bashTool: BashTool; @@ -235,7 +235,7 @@ describe("Coding Agent Tools", () => { // Create tools for this test directory session = createTestToolSession(testDir); - readTool = wrapToolWithMetaNotice(new OpenTool(session)); + readTool = wrapToolWithMetaNotice(new ReadTool(session)); writeTool = wrapToolWithMetaNotice(new WriteTool(session)); editTool = wrapToolWithMetaNotice(new EditTool(session)); bashTool = wrapToolWithMetaNotice(new BashTool(session)); @@ -519,7 +519,7 @@ describe("Coding Agent Tools", () => { fs.writeFileSync(testFile, pngBuffer); const legacyReadTool = wrapToolWithMetaNotice( - new OpenTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false }))), + new ReadTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false }))), ); const result = await legacyReadTool.execute("test-call-img-1", { path: testFile }); @@ -543,7 +543,7 @@ describe("Coding Agent Tools", () => { fs.writeFileSync(testFile, pngBuffer); const inspectModeReadTool = wrapToolWithMetaNotice( - new OpenTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": true }))), + new ReadTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": true }))), ); const result = await inspectModeReadTool.execute("test-call-img-guidance", { path: testFile }); const output = getTextOutput(result); @@ -820,7 +820,7 @@ function b() { undefined, createTestToolContext(["read"]), ), - ).rejects.toThrow(/Use the `open` tool instead of cat\/head\/tail/); + ).rejects.toThrow(/Use the `read` tool instead of cat\/head\/tail/); }); it("should allow an explicit empty interceptor pattern list", async () => { diff --git a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts index 1a447841b..31d5a618e 100644 --- a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts +++ b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts @@ -4,7 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { type SettingPath, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; +import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; import * as imageResize from "@oh-my-pi/pi-coding-agent/utils/image-resize"; import * as toolsManager from "@oh-my-pi/pi-coding-agent/utils/tools-manager"; import * as scrapers from "@oh-my-pi/pi-coding-agent/web/scrapers/types"; @@ -59,7 +59,7 @@ describe("read tool URL selector shorthands", () => { it("supports embedded raw selectors in URL paths", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/embedded-raw"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockImplementation(async requestedUrl => { if (requestedUrl !== pageUrl) { @@ -85,7 +85,7 @@ describe("read tool URL selector shorthands", () => { it("supports embedded line selectors in URL paths", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/embedded-lines"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockImplementation(async requestedUrl => { if (requestedUrl !== pageUrl) { @@ -152,7 +152,7 @@ describe("read tool URL handling", () => { it("returns an image content block when fetching image URLs", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const imageBytes = new Uint8Array([137, 80, 78, 71]); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -196,7 +196,7 @@ describe("read tool URL handling", () => { it("resizes fetched images before emitting image content blocks", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const resizeSpy = vi.spyOn(imageResize, "resizeImage").mockResolvedValue({ buffer: new Uint8Array([1, 2, 3]), mimeType: "image/jpeg", @@ -242,7 +242,7 @@ describe("read tool URL handling", () => { it("keeps markit extracted text for image responses", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const extractedText = "Converted image text content that is definitely longer than fifty characters."; vi.spyOn(imageResize, "resizeImage").mockResolvedValue({ buffer: new Uint8Array([1, 2, 3]), @@ -286,7 +286,7 @@ describe("read tool URL handling", () => { }); it("falls back to text-only output for unsupported image MIME types", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary"); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -309,7 +309,7 @@ describe("read tool URL handling", () => { it("uses binary conversion fallback for unsupported image MIME when extension is convertible", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const convertedText = "Converted image text from markit fallback with sufficient length to pass threshold."; const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({ ok: true, @@ -342,7 +342,7 @@ describe("read tool URL handling", () => { it("does not treat text/html at .png paths as inline images", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({ ok: false, error: "not an image" }); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -364,7 +364,7 @@ describe("read tool URL handling", () => { it("falls back to textual output when inline image refetch fails", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const convertSpy = vi.spyOn(scraperUtils, "convertWithMarkit"); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -390,7 +390,7 @@ describe("read tool URL handling", () => { }); it("falls back to text-only output when image payload bytes are invalid", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, status: 200, @@ -431,7 +431,7 @@ describe("read tool URL handling", () => { }); it("prefers rendered page content over site-wide llms.txt for deep pages", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://bun.com/reference/bun/UnixSocketOptions"; const pageHtml = "

UnixSocketOptions

Page-specific docs.

"; const renderedMarkdown = `# UnixSocketOptions\n\n${"Page-specific API docs. ".repeat(8)}`; @@ -493,7 +493,7 @@ describe("read tool URL handling", () => { it("uses section-scoped llms.txt fallback without requesting the site-wide file", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/docs/reference/widget"; const pageHtml = "

Widget

"; const lowQualityRender = `${"Please enable JavaScript to view this page.\n".repeat(6)}${"navigation\n".repeat(4)}`; @@ -574,7 +574,7 @@ describe("read tool URL handling", () => { it("prefers Parallel extract before other HTML renderers when configured", async () => { process.env.PARALLEL_API_KEY = "test-parallel-key"; const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/parallel-page"; const pageHtml = "

Parallel Page

"; const ensureToolSpy = vi.spyOn(toolsManager, "ensureTool"); @@ -649,7 +649,7 @@ describe("read tool URL handling", () => { it("reuses cached output for repeated plain URL reads", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/repeated-read-cache"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -673,7 +673,7 @@ describe("read tool URL handling", () => { it("supports offset and limit for URL reads using cached output", async () => { const session = createSession(); - const tool = new OpenTool(session); + const tool = new ReadTool(session); const pageUrl = "https://example.com/offset-test"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -702,6 +702,6 @@ describe("read tool URL handling", () => { expect(pagedText?.text).toContain("Line 2"); expect(pagedText?.text).not.toContain("Line 3"); expect(loadPageSpy).not.toHaveBeenCalled(); - expect(fs.readdirSync(path.join(testDir, "session")).some(file => file.endsWith(".open.log"))).toBe(true); + expect(fs.readdirSync(path.join(testDir, "session")).some(file => file.endsWith(".read.log"))).toBe(true); }); }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index eb84f80d5..34ea76f29 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -55,7 +55,7 @@ describe("createTools", () => { // Core tools should always be present expect(names).toContain("python"); expect(names).toContain("bash"); - expect(names).toContain("open"); + expect(names).toContain("read"); expect(names).toContain("edit"); expect(names).toContain("write"); expect(names).toContain("grep"); @@ -130,7 +130,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "lsp", "write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["open", "write", "exit_plan_mode"]); + expect(names).toEqual(["read", "write", "exit_plan_mode"]); }); it("excludes lsp tool when disabled", async () => { @@ -146,7 +146,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["open", "write", "exit_plan_mode"]); + expect(names).toEqual(["read", "write", "exit_plan_mode"]); }); it("ignores vim as an unknown requested tool even when vim edit mode is active", async () => { @@ -158,7 +158,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "vim"]); const names = tools.map(t => t.name); - expect(names).toEqual(["open", "exit_plan_mode"]); + expect(names).toEqual(["read", "exit_plan_mode"]); }); it("lowercases requested tool subset", async () => { @@ -166,7 +166,7 @@ describe("createTools", () => { const tools = await createTools(session, ["Read", "Write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["open", "write", "exit_plan_mode"]); + expect(names).toEqual(["read", "write", "exit_plan_mode"]); }); it("includes hidden tools when explicitly requested", async () => { diff --git a/packages/coding-agent/test/tools/root-path-alias.test.ts b/packages/coding-agent/test/tools/root-path-alias.test.ts index dd87bfd20..9b24ae39a 100644 --- a/packages/coding-agent/test/tools/root-path-alias.test.ts +++ b/packages/coding-agent/test/tools/root-path-alias.test.ts @@ -80,7 +80,7 @@ describe("tool path root alias", () => { it("reads cwd when path is slash", async () => { const tools = await createTools(createTestSession(tempDir)); - const tool = tools.find(entry => entry.name === "open"); + const tool = tools.find(entry => entry.name === "read"); expect(tool).toBeDefined(); if (!tool) throw new Error("Missing read tool"); diff --git a/packages/coding-agent/test/tools/sqlite.test.ts b/packages/coding-agent/test/tools/sqlite.test.ts index 0d6632464..de809d21a 100644 --- a/packages/coding-agent/test/tools/sqlite.test.ts +++ b/packages/coding-agent/test/tools/sqlite.test.ts @@ -5,7 +5,7 @@ import * as os from "node:os"; import * as path from "node:path"; import "../../src/tools/renderers"; import { Settings } from "../../src/config/settings"; -import { OpenTool } from "../../src/tools/open"; +import { ReadTool } from "../../src/tools/read"; import { parseSqlitePathCandidates, parseSqliteSelector, renderTable } from "../../src/tools/sqlite-reader"; import { WriteTool } from "../../src/tools/write"; @@ -13,7 +13,7 @@ type ToolTextResult = { content: Array<{ type: string; text?: string }>; }; -type SessionLike = ConstructorParameters[0]; +type SessionLike = ConstructorParameters[0]; function getText(result: ToolTextResult): string { return result.content @@ -150,7 +150,7 @@ describe("SQLite tool support", () => { let sqlitePath: string; let sqliteDbPath: string; let invalidDbPath: string; - let readTool: OpenTool; + let readTool: ReadTool; let writeTool: WriteTool; let originalEditVariant: string | undefined; @@ -167,7 +167,7 @@ describe("SQLite tool support", () => { await Bun.write(invalidDbPath, "not sqlite\nstill text\n"); const session = createSession(tmpDir); - readTool = new OpenTool(session); + readTool = new ReadTool(session); writeTool = new WriteTool(session); }); diff --git a/scripts/rate-edit-tool.py b/scripts/rate-edit-tool.py index db8e11756..ea158b792 100755 --- a/scripts/rate-edit-tool.py +++ b/scripts/rate-edit-tool.py @@ -34,12 +34,12 @@ from omp_rpc import ( # noqa: E402 MessageUpdateEvent, RpcClient, RpcError, - RpcProcessExitError, RpcNotification, + RpcProcessExitError, TodoAutoClearEvent, TodoItem, - TodoReminderEvent, TodoPhase, + TodoReminderEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, @@ -151,8 +151,9 @@ TODOS = [ "Summarize what was awkward, impossible, ambiguous, or under-documented with concrete examples spanning all fixtures.", ] -TS_FIXTURE = textwrap.dedent( - """\ +TS_FIXTURE = ( + textwrap.dedent( + """\ function sealed(_target: Function): void {} function trace(_label: string) { @@ -302,10 +303,13 @@ TS_FIXTURE = textwrap.dedent( } } """ -).strip() + "\n" + ).strip() + + "\n" +) -RUST_FIXTURE = textwrap.dedent( - """\ +RUST_FIXTURE = ( + textwrap.dedent( + """\ use std::collections::HashMap; use std::fmt; @@ -480,10 +484,13 @@ RUST_FIXTURE = textwrap.dedent( } } """ -).strip() + "\n" + ).strip() + + "\n" +) -GO_FIXTURE = textwrap.dedent( - """\ +GO_FIXTURE = ( + textwrap.dedent( + """\ package main import ( @@ -604,12 +611,14 @@ GO_FIXTURE = textwrap.dedent( fmt.Println(sink.Snapshot()) } """ -).strip() + "\n" + ).strip() + + "\n" +) - -PYTHON_FIXTURE = textwrap.dedent( - """\ +PYTHON_FIXTURE = ( + textwrap.dedent( + """\ from __future__ import annotations from dataclasses import dataclass, field @@ -684,10 +693,13 @@ PYTHON_FIXTURE = textwrap.dedent( def write_report(lines: Iterable[str], target: Path) -> None: target.write_text("\n".join(lines) + "\n", encoding="utf-8") """ -).strip() + "\n" + ).strip() + + "\n" +) -MARKDOWN_FIXTURE = textwrap.dedent( - """\ +MARKDOWN_FIXTURE = ( + textwrap.dedent( + """\ --- title: Tooling Evaluation Notes owner: Fixtures Team @@ -734,7 +746,9 @@ MARKDOWN_FIXTURE = textwrap.dedent( 2. List insertions should not collapse into one paragraph. 3. Deleting this section should not damage the fenced blocks above. """ -).strip() + "\n" + ).strip() + + "\n" +) REFERENCE_FILES = { "PROMPT.md": PROMPT + "\n", @@ -775,8 +789,13 @@ def build_fixture_prompt() -> str: f"- `{fixture_file}` ({FIXTURE_DESCRIPTIONS.get(language, language)})" for language, fixture_file in FIXTURES ] - surface = "Test surface (exercise every file in this workspace):\n" + "\n".join(lines) - return PROMPT.format(FIXTURE_SURFACE=surface) + "\n\nExercise every fixture in one session; do not skip any file type." + surface = "Test surface (exercise every file in this workspace):\n" + "\n".join( + lines + ) + return ( + PROMPT.format(FIXTURE_SURFACE=surface) + + "\n\nExercise every fixture in one session; do not skip any file type." + ) @dataclass @@ -827,7 +846,7 @@ class ModelProgress: todo_items: dict[str, tuple[str, str]] = field(default_factory=dict) -TOOL_WHITELIST = ("open", "edit", "todo_write", "report_tool_issue") +TOOL_WHITELIST = ("read", "edit", "todo_write", "report_tool_issue") MODEL_LABEL_WIDTH = 30 STATUS_WIDTH = 7 TOKENS_WIDTH = 9 @@ -868,14 +887,20 @@ def format_count(value: int | None) -> str: return str(value) -def extract_usage_tokens(message: dict[str, Any]) -> tuple[int | None, int | None, int | None]: +def extract_usage_tokens( + message: dict[str, Any], +) -> tuple[int | None, int | None, int | None]: usage = message.get("usage") if not isinstance(usage, dict): return None, None, None token_input = usage.get("input") token_output = usage.get("output") token_total = usage.get("totalTokens") - if not isinstance(token_total, int) and isinstance(token_input, int) and isinstance(token_output, int): + if ( + not isinstance(token_total, int) + and isinstance(token_input, int) + and isinstance(token_output, int) + ): token_total = token_input + token_output return ( token_input if isinstance(token_input, int) else None, @@ -884,7 +909,9 @@ def extract_usage_tokens(message: dict[str, Any]) -> tuple[int | None, int | Non ) -def build_todo_state_from_phases(phases: tuple[TodoPhase, ...]) -> tuple[list[str], dict[str, tuple[str, str]]]: +def build_todo_state_from_phases( + phases: tuple[TodoPhase, ...], +) -> tuple[list[str], dict[str, tuple[str, str]]]: order: list[str] = [] items: dict[str, tuple[str, str]] = {} for phase in phases: @@ -904,7 +931,9 @@ def seed_todo_state(todos: list[str]) -> tuple[list[str], dict[str, tuple[str, s return order, items -def summarize_todo_state(order: list[str], items: dict[str, tuple[str, str]]) -> tuple[int, int, str | None]: +def summarize_todo_state( + order: list[str], items: dict[str, tuple[str, str]] +) -> tuple[int, int, str | None]: if not order: return 0, 0, None completed = 0 @@ -956,7 +985,15 @@ def apply_todo_ops(progress: ModelProgress, args: Any) -> None: task_id = raw_task.get("id") if not isinstance(task_id, str) or not task_id: task_id = f"task-{task_index}" - tasks.append(TodoItem(id=task_id, content=content, status=status, notes=None, details=None)) + tasks.append( + TodoItem( + id=task_id, + content=content, + status=status, + notes=None, + details=None, + ) + ) phase_id = raw_phase.get("id") name = raw_phase.get("name") if not isinstance(name, str) or not name: @@ -964,14 +1001,24 @@ def apply_todo_ops(progress: ModelProgress, args: Any) -> None: if not isinstance(phase_id, str) or not phase_id: phase_id = f"phase-{phase_index}" phases.append(TodoPhase(id=phase_id, name=name, tasks=tuple(tasks))) - progress.todo_order, progress.todo_items = build_todo_state_from_phases(tuple(phases)) + progress.todo_order, progress.todo_items = build_todo_state_from_phases( + tuple(phases) + ) elif op == "update": task_id = raw_op.get("id") if not isinstance(task_id, str) or task_id not in progress.todo_items: continue content, status = progress.todo_items[task_id] - next_content = raw_op.get("content") if isinstance(raw_op.get("content"), str) else content - next_status = raw_op.get("status") if isinstance(raw_op.get("status"), str) else status + next_content = ( + raw_op.get("content") + if isinstance(raw_op.get("content"), str) + else content + ) + next_status = ( + raw_op.get("status") + if isinstance(raw_op.get("status"), str) + else status + ) progress.todo_items[task_id] = (next_content, next_status) elif op == "add_task": phase = raw_op.get("phase") @@ -980,13 +1027,21 @@ def apply_todo_ops(progress: ModelProgress, args: Any) -> None: continue task_id = task_payload.get("id") if not isinstance(task_id, str) or not task_id: - task_id = raw_op.get("id") if isinstance(raw_op.get("id"), str) else f"task-{len(progress.todo_order) + 1}" + task_id = ( + raw_op.get("id") + if isinstance(raw_op.get("id"), str) + else f"task-{len(progress.todo_order) + 1}" + ) content = task_payload.get("content") status = task_payload.get("status") if not isinstance(content, str) or not isinstance(status, str): continue if task_id not in progress.todo_items: - insert_after = raw_op.get("after") if isinstance(raw_op.get("after"), str) else None + insert_after = ( + raw_op.get("after") + if isinstance(raw_op.get("after"), str) + else None + ) if insert_after in progress.todo_order: index = progress.todo_order.index(insert_after) + 1 progress.todo_order.insert(index, task_id) @@ -998,7 +1053,9 @@ def apply_todo_ops(progress: ModelProgress, args: Any) -> None: if not isinstance(task_id, str): continue progress.todo_items.pop(task_id, None) - progress.todo_order = [candidate for candidate in progress.todo_order if candidate != task_id] + progress.todo_order = [ + candidate for candidate in progress.todo_order if candidate != task_id + ] elif op == "add_phase": raw_tasks = raw_op.get("tasks") if not isinstance(raw_tasks, list): @@ -1016,21 +1073,30 @@ def apply_todo_ops(progress: ModelProgress, args: Any) -> None: progress.todo_order.append(task_id) progress.todo_items[task_id] = (content, status) - progress.todo_completed, progress.todo_total, progress.todo_current = summarize_todo_state( - progress.todo_order, progress.todo_items + progress.todo_completed, progress.todo_total, progress.todo_current = ( + summarize_todo_state(progress.todo_order, progress.todo_items) ) class ProgressPrinter: - def __init__(self, runs: list[tuple[str, str]], *, stream: TextIO | None = None, interactive: bool | None = None) -> None: + def __init__( + self, + runs: list[tuple[str, str]], + *, + stream: TextIO | None = None, + interactive: bool | None = None, + ) -> None: self._lock = threading.Lock() self._stream = sys.stdout if stream is None else stream - self._interactive = self._stream.isatty() if interactive is None else interactive - self._console = Console(file=self._stream, force_terminal=self._interactive, soft_wrap=False) + self._interactive = ( + self._stream.isatty() if interactive is None else interactive + ) + self._console = Console( + file=self._stream, force_terminal=self._interactive, soft_wrap=False + ) self._model_order = [run_id for run_id, _ in runs] self._states = { - run_id: ModelProgress(model=run_id, label=label) - for run_id, label in runs + run_id: ModelProgress(model=run_id, label=label) for run_id, label in runs } self._fixtures_dir: str | None = None self._results_dir: str | None = None @@ -1062,8 +1128,8 @@ class ProgressPrinter: with self._lock: progress = self._states[model] progress.todo_order, progress.todo_items = seed_todo_state(todos) - progress.todo_completed, progress.todo_total, progress.todo_current = summarize_todo_state( - progress.todo_order, progress.todo_items + progress.todo_completed, progress.todo_total, progress.todo_current = ( + summarize_todo_state(progress.todo_order, progress.todo_items) ) progress.last_activity = f"seeded {progress.todo_total} todos" self._refresh_locked() @@ -1077,7 +1143,9 @@ class ProgressPrinter: def mark_turn_end(self, model: str, turns: int) -> None: self._mutate_model(model, turns=turns) - def note_tool_start(self, model: str, tool_name: str, intent: str | None, tool_calls: int, args: Any) -> None: + def note_tool_start( + self, model: str, tool_name: str, intent: str | None, tool_calls: int, args: Any + ) -> None: with self._lock: progress = self._states[model] progress.status = "run" @@ -1096,9 +1164,11 @@ class ProgressPrinter: with self._lock: progress = self._states[model] progress.todo_order = [task.id for task in todos] - progress.todo_items = {task.id: (task.content, task.status) for task in todos} - progress.todo_completed, progress.todo_total, progress.todo_current = summarize_todo_state( - progress.todo_order, progress.todo_items + progress.todo_items = { + task.id: (task.content, task.status) for task in todos + } + progress.todo_completed, progress.todo_total, progress.todo_current = ( + summarize_todo_state(progress.todo_order, progress.todo_items) ) self._refresh_locked() @@ -1127,7 +1197,13 @@ class ProgressPrinter: progress.last_activity = progress.last_text or "drafting" self._refresh_locked() - def note_usage(self, model: str, token_input: int | None, token_output: int | None, token_total: int | None) -> None: + def note_usage( + self, + model: str, + token_input: int | None, + token_output: int | None, + token_total: int | None, + ) -> None: self._mutate_model( model, token_input=token_input, @@ -1136,10 +1212,17 @@ class ProgressPrinter: ) def mark_completed(self, model: str, duration_seconds: float) -> None: - self._mutate_model(model, status="done", duration_seconds=duration_seconds, last_activity="completed") + self._mutate_model( + model, + status="done", + duration_seconds=duration_seconds, + last_activity="completed", + ) def mark_failed(self, model: str, error: str) -> None: - self._mutate_model(model, status="failed", error=error, last_activity=truncate_text(error, 72)) + self._mutate_model( + model, status="failed", error=error, last_activity=truncate_text(error, 72) + ) def finish(self, message: str) -> None: with self._lock: @@ -1168,7 +1251,11 @@ class ProgressPrinter: def _build_renderable_locked(self) -> Group: done = sum(1 for state in self._states.values() if state.status == "done") failed = sum(1 for state in self._states.values() if state.status == "failed") - active = sum(1 for state in self._states.values() if state.status not in {"pending", "done", "failed"}) + active = sum( + 1 + for state in self._states.values() + if state.status not in {"pending", "done", "failed"} + ) summary = Text() summary.append(f"done {done}/{len(self._states)}", style="bold green") @@ -1215,7 +1302,11 @@ class ProgressPrinter: ) if self._final_message: - footer = Panel(self._final_message, border_style="green" if failed == 0 else "red", box=box.ROUNDED) + footer = Panel( + self._final_message, + border_style="green" if failed == 0 else "red", + box=box.ROUNDED, + ) return Group(header, table, footer) return Group(header, table) @@ -1244,7 +1335,11 @@ class ProgressPrinter: @staticmethod def _model_text(state: ModelProgress) -> Text: text = Text(truncate_text(state.label, 34), style="bold") - activity = state.error if state.status == "failed" and state.error else state.last_activity + activity = ( + state.error + if state.status == "failed" and state.error + else state.last_activity + ) if activity and activity not in {"waiting", "completed"}: text.append("\n") text.append(truncate_text(activity, 34), style="dim") @@ -1294,7 +1389,14 @@ def serialize_notification(notification: Any) -> dict[str, Any]: class ModelRunRecorder: - def __init__(self, run_id: str, model: str, fixture: str, printer: ProgressPrinter, jsonl_path: Path) -> None: + def __init__( + self, + run_id: str, + model: str, + fixture: str, + printer: ProgressPrinter, + jsonl_path: Path, + ) -> None: self.run_id = run_id self.model = model self.fixture = fixture @@ -1344,7 +1446,9 @@ class ModelRunRecorder: def record_tool_execution_start(self, event: ToolExecutionStartEvent) -> None: self._touch() self.tool_calls += 1 - self.printer.note_tool_start(self.run_id, event.tool_name, event.intent, self.tool_calls, event.args) + self.printer.note_tool_start( + self.run_id, event.tool_name, event.intent, self.tool_calls, event.args + ) def record_tool_execution_update(self, _event: ToolExecutionUpdateEvent) -> None: self._touch() @@ -1376,7 +1480,9 @@ class ModelRunRecorder: self._consumed_assistant_messages += 1 token_input, token_output, token_total = extract_usage_tokens(message) - if token_total is not None and (self.token_total is None or token_total >= self.token_total): + if token_total is not None and ( + self.token_total is None or token_total >= self.token_total + ): self.token_input = token_input self.token_output = token_output self.token_total = token_total @@ -1390,7 +1496,11 @@ class ModelRunRecorder: if not isinstance(message, dict) or message.get("role") != "assistant": continue text = assistant_text(message) - if assistant_count >= self._consumed_assistant_messages and isinstance(text, str) and text.strip(): + if ( + assistant_count >= self._consumed_assistant_messages + and isinstance(text, str) + and text.strip() + ): self.review_sections.append(text.strip()) assistant_count += 1 @@ -1402,7 +1512,9 @@ class ModelRunRecorder: self.token_input = token_input self.token_output = token_output self.token_total = token_total - self.printer.note_usage(self.run_id, token_input, token_output, token_total) + self.printer.note_usage( + self.run_id, token_input, token_output, token_total + ) break def record_message_update(self, event: MessageUpdateEvent) -> None: @@ -1411,11 +1523,15 @@ class ModelRunRecorder: partial = assistant_event.get("partial") if isinstance(partial, dict): token_input, token_output, token_total = extract_usage_tokens(partial) - if token_total is not None and (self.token_total is None or token_total >= self.token_total): + if token_total is not None and ( + self.token_total is None or token_total >= self.token_total + ): self.token_input = token_input self.token_output = token_output self.token_total = token_total - self.printer.note_usage(self.run_id, token_input, token_output, token_total) + self.printer.note_usage( + self.run_id, token_input, token_output, token_total + ) delta_type = assistant_event.get("type") delta = assistant_event.get("delta") @@ -1430,10 +1546,16 @@ class ModelRunRecorder: def record_todo_reminder(self, event: TodoReminderEvent) -> None: self._touch() - self.todo_completed = sum(1 for task in event.todos if task.status == "completed") + self.todo_completed = sum( + 1 for task in event.todos if task.status == "completed" + ) self.todo_total = len(event.todos) - in_progress = next((task.content for task in event.todos if task.status == "in_progress"), None) - pending = next((task.content for task in event.todos if task.status == "pending"), None) + in_progress = next( + (task.content for task in event.todos if task.status == "in_progress"), None + ) + pending = next( + (task.content for task in event.todos if task.status == "pending"), None + ) self.todo_current = in_progress or pending self.printer.note_todo_reminder(self.run_id, event.todos) @@ -1445,7 +1567,9 @@ class ModelRunRecorder: def sync_final_todos(self, phases: tuple[TodoPhase, ...]) -> None: order, items = build_todo_state_from_phases(phases) - self.todo_completed, self.todo_total, self.todo_current = summarize_todo_state(order, items) + self.todo_completed, self.todo_total, self.todo_current = summarize_todo_state( + order, items + ) flattened = tuple(task for phase in phases for task in phase.tasks) self.printer.note_todo_reminder(self.run_id, flattened) @@ -1469,7 +1593,6 @@ class ModelRunRecorder: handle.write(json.dumps(payload) + "\n") - def run_model_sync( *, model: str, @@ -1488,7 +1611,10 @@ def run_model_sync( review_slug = slugify(shorten_model_name(model)) review_path = results_dir / f"review_{review_slug}.md" - jsonl_path = Path(tempfile.gettempdir()) / f"rate-edit-tool-{results_dir.name}-{model_slug}.jsonl" + jsonl_path = ( + Path(tempfile.gettempdir()) + / f"rate-edit-tool-{results_dir.name}-{model_slug}.jsonl" + ) jsonl_path.unlink(missing_ok=True) jsonl_path.touch() recorder = ModelRunRecorder( @@ -1551,14 +1677,23 @@ def run_model_sync( try: client.wait_for_idle(timeout=min(remaining, 60.0)) if recorder.auto_retry_active: - time.sleep(min(max(recorder.auto_retry_delay_ms / 1000.0, 0.2), 2.0)) + time.sleep( + min( + max(recorder.auto_retry_delay_ms / 1000.0, 0.2), 2.0 + ) + ) continue if recorder.agent_ended: grace = min(0.5, max(deadline - time.monotonic(), 0.0)) if grace > 0: time.sleep(grace) if recorder.auto_retry_active: - time.sleep(min(max(recorder.auto_retry_delay_ms / 1000.0, 0.2), 2.0)) + time.sleep( + min( + max(recorder.auto_retry_delay_ms / 1000.0, 0.2), + 2.0, + ) + ) continue return if recorder.is_effectively_complete(quiet_seconds=2.0): @@ -1586,11 +1721,18 @@ def run_model_sync( stats = client.get_session_stats() todo_phases = client.get_todos() recorder.sync_final_todos(todo_phases) - if (recorder.token_total is None or recorder.token_total <= 0) and stats.tokens.total > 0: + if ( + recorder.token_total is None or recorder.token_total <= 0 + ) and stats.tokens.total > 0: recorder.token_input = stats.tokens.input recorder.token_output = stats.tokens.output recorder.token_total = stats.tokens.total - printer.note_usage(run_id, recorder.token_input, recorder.token_output, recorder.token_total) + printer.note_usage( + run_id, + recorder.token_input, + recorder.token_output, + recorder.token_total, + ) review_path.write_text(review_markdown) provider, model_id = model.split("/", 1) session_state = { @@ -1599,7 +1741,9 @@ def run_model_sync( } status = "ok" except Exception as error: # noqa: BLE001 - error_message = f"{type(error).__name__}: {error}" if str(error) else type(error).__name__ + error_message = ( + f"{type(error).__name__}: {error}" if str(error) else type(error).__name__ + ) printer.mark_failed(run_id, error_message) status = "failed" @@ -1632,6 +1776,7 @@ def run_model_sync( session_state=session_state, ) + def build_oracle_review_prompt(sources: list[tuple[str, str, str]]) -> str: review_sections: list[str] = [] for model, fixture, review_path in sorted(sources): @@ -1659,8 +1804,9 @@ def build_oracle_review_prompt(sources: list[tuple[str, str, str]]) -> str: return ORACLE_REVIEW_PROMPT.replace("{{REVIEWS}}", review_payload) - -def oracle_sources_from_results(results: list[ModelResult]) -> list[tuple[str, str, str]]: +def oracle_sources_from_results( + results: list[ModelResult], +) -> list[tuple[str, str, str]]: return [(r.model, r.fixture, r.review_path) for r in results if r.review_path] @@ -1679,6 +1825,7 @@ def oracle_sources_from_dir(results_dir: Path) -> list[tuple[str, str, str]]: sources.append((model, fixture, str(path))) return sources + def run_oracle_review_sync( *, model: str, @@ -1715,18 +1862,32 @@ def run_oracle_review_sync( (results_dir / "oracle_synthesis.md").write_text(synthesis + "\n", encoding="utf-8") return synthesis + def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Run OpenRouter fixture evaluations through omp RPC mode.") + parser = argparse.ArgumentParser( + description="Run OpenRouter fixture evaluations through omp RPC mode." + ) parser.add_argument("--omp-bin", default=os.environ.get("OMP_BIN")) parser.add_argument("--fixtures-dir", default=os.path.expanduser("~/tmp/fixtures")) parser.add_argument("--results-dir") - parser.add_argument("--timeout", type=float, default=900.0, help="Per run timeout in seconds.") - parser.add_argument("--model", dest="models", action="append", help="Repeat to limit execution to specific models.") - parser.add_argument("--oracle-model", default=ORACLE_MODEL, help="Model used to synthesize findings across all reviews.") parser.add_argument( - "--rerun-oracle", - dest="rerun_oracle", - help="Skip fixture runs and only synthesize against review_*.md files in this existing results dir.", + "--timeout", type=float, default=900.0, help="Per run timeout in seconds." + ) + parser.add_argument( + "--model", + dest="models", + action="append", + help="Repeat to limit execution to specific models.", + ) + parser.add_argument( + "--oracle-model", + default=ORACLE_MODEL, + help="Model used to synthesize findings across all reviews.", + ) + parser.add_argument( + "--rerun-oracle", + dest="rerun_oracle", + help="Skip fixture runs and only synthesize against review_*.md files in this existing results dir.", ) return parser.parse_args() @@ -1766,7 +1927,11 @@ async def run_all(args: argparse.Namespace) -> int: timestamp = time.strftime("%Y%m%d-%H%M%S") tmp_root = Path(tempfile.gettempdir()) - results_dir = Path(args.results_dir) if args.results_dir else tmp_root / f"omp-fixture-runs-{timestamp}" + results_dir = ( + Path(args.results_dir) + if args.results_dir + else tmp_root / f"omp-fixture-runs-{timestamp}" + ) results_dir.mkdir(parents=True, exist_ok=True) workspace_root = tmp_root / f"rate-edit-tool-workspaces-{timestamp}" workspace_root.mkdir(parents=True, exist_ok=True)