Q20: read_file and read_attachment page by lines, columns, and search
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b32e784469
commit
1a0da25202
5 files changed
+153
-14
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import type { JsonRecord } from "./provider-http";
|
||||
|
||||
const PAGE_CHAR_LIMIT = 24_000;
|
||||
const SEARCH_MATCH_LIMIT = 200;
|
||||
|
||||
export interface PageRequest {
|
||||
startLine: number;
|
||||
startColumn: number;
|
||||
lineCount: number | null;
|
||||
search: string | null;
|
||||
}
|
||||
|
||||
export const PAGE_TOOL_PROPERTIES = {
|
||||
start_line: { type: "number", description: "First line to read (1-based). Use the start_line a truncated read tells you to continue." },
|
||||
line_count: { type: "number", description: "How many lines to read from start_line." },
|
||||
start_column: { type: "number", description: "Character in start_line to begin at (1-based), for continuing a cut line." },
|
||||
search: { type: "string", description: "Return only the lines containing this text (case-insensitive), with line numbers." },
|
||||
} as const;
|
||||
|
||||
export function readPageArgs(args: JsonRecord): PageRequest {
|
||||
const search = typeof args.search === "string" && args.search.trim() ? args.search.trim() : null;
|
||||
return {
|
||||
startLine: positiveInteger(args.start_line, "start_line") ?? 1,
|
||||
startColumn: positiveInteger(args.start_column, "start_column") ?? 1,
|
||||
lineCount: positiveInteger(args.line_count, "line_count"),
|
||||
search,
|
||||
};
|
||||
}
|
||||
|
||||
/** A window of the text that fits one tool result, with directions to the rest. */
|
||||
export function pageText(name: string, content: string, request: PageRequest): string {
|
||||
const lines = content.split("\n");
|
||||
if (request.search) return searchLines(name, lines, request.search);
|
||||
if (request.startLine > lines.length) throw new Error(`${name} has only ${lines.length} lines.`);
|
||||
const last = request.lineCount === null ? lines.length : Math.min(lines.length, request.startLine + request.lineCount - 1);
|
||||
const picked: string[] = [];
|
||||
let size = 0;
|
||||
let cut = false;
|
||||
let lineNumber = request.startLine;
|
||||
for (; lineNumber <= last; lineNumber += 1) {
|
||||
const offset = lineNumber === request.startLine ? request.startColumn - 1 : 0;
|
||||
const line = lines[lineNumber - 1].slice(offset);
|
||||
const separator = picked.length > 0 ? 1 : 0;
|
||||
if (size + separator + line.length > PAGE_CHAR_LIMIT) {
|
||||
if (picked.length === 0) {
|
||||
const nextColumn = offset + PAGE_CHAR_LIMIT + 1;
|
||||
picked.push(`${line.slice(0, PAGE_CHAR_LIMIT)}\n[line ${lineNumber} continues: pass start_line=${lineNumber} and start_column=${nextColumn}]`);
|
||||
cut = true;
|
||||
lineNumber += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
picked.push(line);
|
||||
size += separator + line.length;
|
||||
}
|
||||
const end = lineNumber - 1;
|
||||
if (request.startLine === 1 && request.startColumn === 1 && end === lines.length && !cut) return content;
|
||||
const more = end < lines.length ? `\n[more: pass start_line=${end + 1} to continue]` : "";
|
||||
return `[lines ${request.startLine}-${end} of ${lines.length} in ${name}]\n${picked.join("\n")}${more}`;
|
||||
}
|
||||
|
||||
function searchLines(name: string, lines: string[], search: string): string {
|
||||
const needle = search.toLowerCase();
|
||||
const matches: string[] = [];
|
||||
let total = 0;
|
||||
let size = 0;
|
||||
lines.forEach((line, index) => {
|
||||
if (!line.toLowerCase().includes(needle)) return;
|
||||
total += 1;
|
||||
const entry = `${index + 1}: ${line.slice(0, 500)}`;
|
||||
if (matches.length >= SEARCH_MATCH_LIMIT || size + entry.length + 1 > PAGE_CHAR_LIMIT) return;
|
||||
matches.push(entry);
|
||||
size += entry.length + 1;
|
||||
});
|
||||
if (total === 0) return `No lines in ${name} contain "${search}".`;
|
||||
const cut = total > matches.length ? `\n[${total - matches.length} more matches not shown; narrow the search]` : "";
|
||||
return `[${total} matching lines for "${search}" in ${name}]\n${matches.join("\n")}${cut}`;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, field: string): number | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throw new Error(`"${field}" must be a positive whole number.`);
|
||||
return value;
|
||||
}
|
||||
Reference in new issue
Block a user