@@ -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.
|
||||
|
||||
@@ -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<string, { type: "
|
||||
.map(s => 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 {
|
||||
|
||||
@@ -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<void> {
|
||||
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 ?? "");
|
||||
|
||||
@@ -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+",
|
||||
|
||||
@@ -164,14 +164,14 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
||||
|
||||
async read(args: Parameters<NonNullable<ICursorExecHandlers["read"]>>[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<NonNullable<ICursorExecHandlers["ls"]>>[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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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 += '<span class="line-numbers">:' + escapeHtml(String(args.sel)) + '</span>';
|
||||
let html = toolHead('open', pathHtml);
|
||||
let html = toolHead('read', pathHtml);
|
||||
if (result) {
|
||||
html += ctx.renderResultImages();
|
||||
const output = ctx.getResultText();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -342,8 +342,7 @@ export class SessionObserverOverlayComponent extends Container {
|
||||
#formatToolArgs(toolName: string, args: Record<string, unknown>): 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}` : "";
|
||||
|
||||
@@ -637,8 +637,7 @@ class TreeList implements Component {
|
||||
|
||||
#formatToolCall(name: string, args: Record<string, unknown>): 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;
|
||||
|
||||
@@ -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<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
||||
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<AgentSessionEvent, { type: "tool_execution_end" }>): Promise<void> {
|
||||
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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Navigates, clicks, types, scrolls, drags, queries DOM content, and captures screenshots.
|
||||
|
||||
<instruction>
|
||||
- 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
|
||||
|
||||
@@ -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"}, …]}`
|
||||
|
||||
<rules>
|
||||
- **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.
|
||||
</rules>
|
||||
|
||||
@@ -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.
|
||||
</critical>
|
||||
|
||||
<regions>
|
||||
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
|
||||
</ops>
|
||||
|
||||
<examples>
|
||||
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):
|
||||
```
|
||||
|
||||
@@ -21,7 +21,8 @@ Searches files using powerful regex matching.
|
||||
</output>
|
||||
|
||||
<critical>
|
||||
- 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.
|
||||
</critical>
|
||||
|
||||
@@ -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.
|
||||
</critical>
|
||||
</critical>
|
||||
+9
-9
@@ -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**.
|
||||
|
||||
<instruction>
|
||||
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).
|
||||
</instruction>
|
||||
|
||||
<critical>
|
||||
- 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.
|
||||
</critical>
|
||||
+8
-10
@@ -1,11 +1,9 @@
|
||||
Opens and reads the content at the specified path or URL.
|
||||
Reads the content at the specified path or URL.
|
||||
|
||||
<instruction>
|
||||
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).
|
||||
</instruction>
|
||||
|
||||
<critical>
|
||||
- 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.
|
||||
</critical>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -567,7 +567,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1189,7 +1189,7 @@ async function materializeReadUrlCacheEntry(
|
||||
}
|
||||
|
||||
async function persistReadUrlArtifact(session: ToolSession, output: string): Promise<string | undefined> {
|
||||
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;
|
||||
|
||||
@@ -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<string, ToolFactory> = {
|
||||
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<string, ToolFactory> = {
|
||||
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<string, string> = {
|
||||
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<string, ToolFactory> = {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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<typeof openSchema>;
|
||||
export type ReadToolInput = Static<typeof readSchema>;
|
||||
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
readonly name = "open";
|
||||
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
offset: number | undefined,
|
||||
limit: number | undefined,
|
||||
options: {
|
||||
details?: OpenToolDetails;
|
||||
details?: ReadToolDetails;
|
||||
sourcePath?: string;
|
||||
sourceUrl?: string;
|
||||
sourceInternal?: string;
|
||||
entityLabel: string;
|
||||
ignoreResultLimits?: boolean;
|
||||
},
|
||||
): AgentToolResult<OpenToolDetails> {
|
||||
): AgentToolResult<ReadToolDetails> {
|
||||
const displayMode = resolveFileDisplayMode(this.session);
|
||||
const details = options.details ?? {};
|
||||
const allLines = text.split("\n");
|
||||
@@ -701,9 +701,9 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
archivePath: string,
|
||||
subPath: string,
|
||||
limit: number | undefined,
|
||||
details: OpenToolDetails,
|
||||
details: ReadToolDetails,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentToolResult<OpenToolDetails>> {
|
||||
): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
const DEFAULT_LIMIT = 500;
|
||||
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
||||
const entries = archive.listDirectory(subPath);
|
||||
@@ -727,8 +727,8 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
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<OpenToolDetails>(directoryDetails).text(truncation.content);
|
||||
const directoryDetails: ReadToolDetails = { ...details, isDirectory: true };
|
||||
const resultBuilder = toolResult<ReadToolDetails>(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<typeof openSchema, OpenToolDetails> {
|
||||
limit: number | undefined,
|
||||
resolvedArchivePath: ResolvedArchiveReadPath,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentToolResult<OpenToolDetails>> {
|
||||
): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
const entry = await archive.readFile(resolvedArchivePath.archiveSubPath);
|
||||
const text = decodeUtf8Text(entry.bytes);
|
||||
if (text === null) {
|
||||
return toolResult<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(
|
||||
prependSuffixResolutionNotice(
|
||||
`[Cannot read binary archive entry '${entry.path}' (${formatBytes(entry.size)})]`,
|
||||
@@ -799,14 +799,14 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
sel: string | undefined,
|
||||
resolvedSqlitePath: ResolvedSqliteReadPath,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentToolResult<OpenToolDetails>> {
|
||||
): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
);
|
||||
const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
|
||||
details.truncation = truncation.truncated ? truncation : undefined;
|
||||
const resultBuilder = toolResult<OpenToolDetails>(details)
|
||||
const resultBuilder = toolResult<ReadToolDetails>(details)
|
||||
.text(truncation.content)
|
||||
.sourcePath(resolvedSqlitePath.absolutePath)
|
||||
.limits({ resultLimit: listLimit.meta.resultLimit?.reached });
|
||||
@@ -845,7 +845,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
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<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution))
|
||||
.sourcePath(resolvedSqlitePath.absolutePath)
|
||||
.done();
|
||||
@@ -857,7 +857,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
? getRowByKey(db, selector.table, lookup, selector.key)
|
||||
: getRowByRowId(db, selector.table, selector.key);
|
||||
if (!row) {
|
||||
return toolResult<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(
|
||||
prependSuffixResolutionNotice(
|
||||
`No row found in table '${selector.table}' for key '${selector.key}'.`,
|
||||
@@ -867,14 +867,14 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
.sourcePath(resolvedSqlitePath.absolutePath)
|
||||
.done();
|
||||
}
|
||||
return toolResult<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(prependSuffixResolutionNotice(renderRow(row), resolvedSqlitePath.suffixResolution))
|
||||
.sourcePath(resolvedSqlitePath.absolutePath)
|
||||
.done();
|
||||
}
|
||||
case "query": {
|
||||
const page = queryRows(db, selector.table, selector);
|
||||
return toolResult<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(
|
||||
prependSuffixResolutionNotice(
|
||||
renderTable(page.columns, page.rows, {
|
||||
@@ -892,7 +892,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
}
|
||||
case "raw": {
|
||||
const result = executeReadQuery(db, selector.sql);
|
||||
return toolResult<OpenToolDetails>(details)
|
||||
return toolResult<ReadToolDetails>(details)
|
||||
.text(
|
||||
prependSuffixResolutionNotice(
|
||||
renderTable(result.columns, result.rows, {
|
||||
@@ -923,11 +923,11 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
params: OpenParams,
|
||||
params: ReadParams,
|
||||
signal?: AbortSignal,
|
||||
_onUpdate?: AgentToolUpdateCallback<OpenToolDetails>,
|
||||
_onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
|
||||
_toolContext?: AgentToolContext,
|
||||
): Promise<AgentToolResult<OpenToolDetails>> {
|
||||
): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
let { path: readPath, sel, timeout } = params;
|
||||
if (readPath.startsWith("file://")) {
|
||||
readPath = expandPath(readPath);
|
||||
@@ -1071,7 +1071,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
if (suffixResolution) {
|
||||
text = prependSuffixResolutionNotice(text, suffixResolution);
|
||||
}
|
||||
return toolResult<OpenToolDetails>({
|
||||
return toolResult<ReadToolDetails>({
|
||||
resolvedPath: absolutePath,
|
||||
suffixResolution,
|
||||
chunk: chunkResult.chunk,
|
||||
@@ -1083,7 +1083,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
|
||||
// Read the file based on type
|
||||
let content: Array<TextContent | ImageContent>;
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
if (suffixResolution) {
|
||||
text = prependSuffixResolutionNotice(text, suffixResolution);
|
||||
}
|
||||
return toolResult<OpenToolDetails>({
|
||||
return toolResult<ReadToolDetails>({
|
||||
resolvedPath: absolutePath,
|
||||
suffixResolution,
|
||||
chunk: chunkResult.chunk,
|
||||
@@ -1222,7 +1222,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
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<OpenToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
||||
return toolResult<ReadToolDetails>({ 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<typeof openSchema, OpenToolDetails> {
|
||||
* 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<AgentToolResult<OpenToolDetails>> {
|
||||
async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
const internalRouter = this.session.internalRouter!;
|
||||
|
||||
// Check if URL has query extraction (agent:// only).
|
||||
@@ -1355,7 +1355,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
|
||||
// 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<typeof openSchema, OpenToolDetails> {
|
||||
absolutePath: string,
|
||||
limit: number | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentToolResult<OpenToolDetails>> {
|
||||
): Promise<AgentToolResult<ReadToolDetails>> {
|
||||
const DEFAULT_LIMIT = 500;
|
||||
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
||||
|
||||
@@ -1425,7 +1425,7 @@ export class OpenTool implements AgentTool<typeof openSchema, OpenToolDetails> {
|
||||
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<typeof openSchema, OpenToolDetails> {
|
||||
// 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 || "")) {
|
||||
@@ -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<string, ToolRenderer> = {
|
||||
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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 = "<html><body><main><h1>UnixSocketOptions</h1><p>Page-specific docs.</p></main></body></html>";
|
||||
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 = "<html><body><nav>Docs</nav><main><h1>Widget</h1></main></body></html>";
|
||||
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 = "<html><body><main><h1>Parallel Page</h1></main></body></html>";
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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<typeof OpenTool>[0];
|
||||
type SessionLike = ConstructorParameters<typeof ReadTool>[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);
|
||||
});
|
||||
|
||||
|
||||
+244
-79
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user