fold: hashline_edit + hashline_read → Edit({match}) + Read({format}) modes
Per operator R-entry sf-mp9wo7e3-sdxqss + no-compat directive. - Edit gains `match: "substring"|"anchor"` arg; anchor mode routes to the existing applyHashlineEdits logic. Substring stays default. - Read gains `format: "plain"|"tagged"` arg; tagged mode emits LINE#HASH prefixes via formatHashLines. - Delete hashline-edit.ts, hashline-read.ts. KEEP hashline.ts (helpers are now Edit/Read internals). - tools/index.ts: drop the two tools + the createHashlineCodingTools preset. - agent-session.ts: setEditMode no longer swaps tool instances (single tool surface; mode preserved for system-prompt context only). - sdk.ts + index.ts: remove hashline tool re-exports. - headless-ui.ts + test: remove hashline_edit case. Net agent-visible tool surface: -2 tools. Capability preserved as modes. No backward-compat alias for the removed tool names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d03758d803
commit
ffdec0feee
9 changed files with 47 additions and 755 deletions
|
|
@ -980,33 +980,12 @@ export class AgentSession {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch edit mode between standard (text-match) and hashline (LINE#ID anchors).
|
* Switch edit mode between standard (text-match) and hashline (LINE#ID anchors).
|
||||||
* Swaps the active read/edit tools and rebuilds the system prompt.
|
* Both modes now use the same Read/Edit tools; the mode setting is persisted for
|
||||||
|
* system-prompt context only. Read({format:"tagged"}) and Edit({match:"anchor"})
|
||||||
|
* are the caller-controlled arguments.
|
||||||
*/
|
*/
|
||||||
setEditMode(mode: "standard" | "hashline"): void {
|
setEditMode(mode: "standard" | "hashline"): void {
|
||||||
this.settingsManager.setEditMode(mode);
|
this.settingsManager.setEditMode(mode);
|
||||||
|
|
||||||
// Get current active tool registry keys
|
|
||||||
const currentKeys = new Set<string>();
|
|
||||||
for (const [key, tool] of this._toolRegistry.entries()) {
|
|
||||||
if (this.agent.state.tools.includes(tool)) {
|
|
||||||
currentKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap read tools
|
|
||||||
if (mode === "hashline") {
|
|
||||||
currentKeys.delete("Read");
|
|
||||||
currentKeys.add("hashline_read");
|
|
||||||
currentKeys.delete("Edit");
|
|
||||||
currentKeys.add("hashline_edit");
|
|
||||||
} else {
|
|
||||||
currentKeys.delete("hashline_read");
|
|
||||||
currentKeys.add("Read");
|
|
||||||
currentKeys.delete("hashline_edit");
|
|
||||||
currentKeys.add("Edit");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setActiveToolsByName([...currentKeys]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current edit mode */
|
/** Current edit mode */
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,6 @@ import {
|
||||||
createEditTool,
|
createEditTool,
|
||||||
createFindTool,
|
createFindTool,
|
||||||
createGrepTool,
|
createGrepTool,
|
||||||
createHashlineCodingTools,
|
|
||||||
createHashlineEditTool,
|
|
||||||
createHashlineReadTool,
|
|
||||||
createLsTool,
|
createLsTool,
|
||||||
createReadOnlyTools,
|
createReadOnlyTools,
|
||||||
createReadTool,
|
createReadTool,
|
||||||
|
|
@ -92,9 +89,6 @@ import {
|
||||||
editTool,
|
editTool,
|
||||||
findTool,
|
findTool,
|
||||||
grepTool,
|
grepTool,
|
||||||
hashlineCodingTools,
|
|
||||||
hashlineEditTool,
|
|
||||||
hashlineReadTool,
|
|
||||||
lsTool,
|
lsTool,
|
||||||
readOnlyTools,
|
readOnlyTools,
|
||||||
readTool,
|
readTool,
|
||||||
|
|
@ -180,9 +174,6 @@ export {
|
||||||
createEditTool,
|
createEditTool,
|
||||||
createFindTool,
|
createFindTool,
|
||||||
createGrepTool,
|
createGrepTool,
|
||||||
createHashlineCodingTools,
|
|
||||||
createHashlineEditTool,
|
|
||||||
createHashlineReadTool,
|
|
||||||
createLsTool,
|
createLsTool,
|
||||||
createReadOnlyTools,
|
createReadOnlyTools,
|
||||||
createReadTool,
|
createReadTool,
|
||||||
|
|
@ -190,10 +181,6 @@ export {
|
||||||
editTool,
|
editTool,
|
||||||
findTool,
|
findTool,
|
||||||
grepTool,
|
grepTool,
|
||||||
// Hashline edit mode
|
|
||||||
hashlineCodingTools,
|
|
||||||
hashlineEditTool,
|
|
||||||
hashlineReadTool,
|
|
||||||
lsTool,
|
lsTool,
|
||||||
readOnlyTools,
|
readOnlyTools,
|
||||||
// Pre-built tools (use process.cwd())
|
// Pre-built tools (use process.cwd())
|
||||||
|
|
@ -358,20 +345,16 @@ export async function createAgentSession(
|
||||||
thinkingLevel = "off";
|
thinkingLevel = "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
const editMode = settingsManager.getEditMode();
|
const defaultActiveToolNames: ToolName[] = [
|
||||||
const defaultActiveToolNames: ToolName[] =
|
"Read",
|
||||||
editMode === "hashline"
|
|
||||||
? [
|
|
||||||
"hashline_read",
|
|
||||||
"Grep",
|
"Grep",
|
||||||
"Glob",
|
"Glob",
|
||||||
"LS",
|
"LS",
|
||||||
"Bash",
|
"Bash",
|
||||||
"hashline_edit",
|
"Edit",
|
||||||
"Write",
|
"Write",
|
||||||
"lsp",
|
"lsp",
|
||||||
]
|
];
|
||||||
: ["Read", "Grep", "Glob", "LS", "Bash", "Edit", "Write", "lsp"];
|
|
||||||
const initialActiveToolNames: ToolName[] = options.tools
|
const initialActiveToolNames: ToolName[] = options.tools
|
||||||
? options.tools
|
? options.tools
|
||||||
.map((t) => t.name)
|
.map((t) => t.name)
|
||||||
|
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
/**
|
|
||||||
* Hashline edit tool — applies file edits using line-hash anchors.
|
|
||||||
*
|
|
||||||
* The model references lines by `LINE#ID` tags from read output.
|
|
||||||
* Each tag uniquely identifies a line, so edits remain stable even when lines shift.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { constants } from "node:fs";
|
|
||||||
import {
|
|
||||||
access as fsAccess,
|
|
||||||
readFile as fsReadFile,
|
|
||||||
unlink as fsUnlink,
|
|
||||||
writeFile as fsWriteFile,
|
|
||||||
} from "node:fs/promises";
|
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
|
||||||
import type { AgentTool } from "@singularity-forge/agent-core";
|
|
||||||
import {
|
|
||||||
detectLineEnding,
|
|
||||||
generateDiffString,
|
|
||||||
normalizeToLF,
|
|
||||||
restoreLineEndings,
|
|
||||||
stripBom,
|
|
||||||
} from "./edit-diff.js";
|
|
||||||
import {
|
|
||||||
type Anchor,
|
|
||||||
applyHashlineEdits,
|
|
||||||
type HashlineEdit,
|
|
||||||
parseHashlineText,
|
|
||||||
parseTag,
|
|
||||||
} from "./hashline.js";
|
|
||||||
import { resolveToCwd } from "./path-utils.js";
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// Schema
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
const hashlineEditItemSchema = Type.Object(
|
|
||||||
{
|
|
||||||
op: Type.Union([
|
|
||||||
Type.Literal("replace"),
|
|
||||||
Type.Literal("append"),
|
|
||||||
Type.Literal("prepend"),
|
|
||||||
]),
|
|
||||||
pos: Type.Optional(
|
|
||||||
Type.String({ description: 'Anchor tag (e.g. "5#QQ")' }),
|
|
||||||
),
|
|
||||||
end: Type.Optional(
|
|
||||||
Type.String({ description: "End anchor for range replace" }),
|
|
||||||
),
|
|
||||||
lines: Type.Union([
|
|
||||||
Type.Array(Type.String(), { description: "Replacement content lines" }),
|
|
||||||
Type.String(),
|
|
||||||
Type.Null(),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const hashlineEditSchema = Type.Object(
|
|
||||||
{
|
|
||||||
path: Type.String({ description: "Path to the file to edit" }),
|
|
||||||
edits: Type.Array(hashlineEditItemSchema, {
|
|
||||||
description:
|
|
||||||
"Edits to apply (referenced by LINE#ID tags from read output)",
|
|
||||||
}),
|
|
||||||
delete: Type.Optional(
|
|
||||||
Type.Boolean({ description: "If true, delete the file" }),
|
|
||||||
),
|
|
||||||
move: Type.Optional(
|
|
||||||
Type.String({ description: "If set, move/rename the file to this path" }),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
export type HashlineEditInput = Static<typeof hashlineEditSchema>;
|
|
||||||
export type HashlineEditItem = Static<typeof hashlineEditItemSchema>;
|
|
||||||
|
|
||||||
export interface HashlineEditToolDetails {
|
|
||||||
/** Unified diff of the changes made */
|
|
||||||
diff: string;
|
|
||||||
/** Line number of the first change in the new file */
|
|
||||||
firstChangedLine?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pluggable operations for the hashline edit tool.
|
|
||||||
*/
|
|
||||||
export interface HashlineEditOperations {
|
|
||||||
readFile: (absolutePath: string) => Promise<Buffer>;
|
|
||||||
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
|
||||||
access: (absolutePath: string) => Promise<void>;
|
|
||||||
unlink: (absolutePath: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHashlineEditOperations: HashlineEditOperations = {
|
|
||||||
readFile: (path) => fsReadFile(path),
|
|
||||||
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
||||||
access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
|
|
||||||
unlink: (path) => fsUnlink(path),
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface HashlineEditToolOptions {
|
|
||||||
operations?: HashlineEditOperations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse a tag, returning undefined instead of throwing on garbage. */
|
|
||||||
function tryParseTag(raw: string): Anchor | undefined {
|
|
||||||
try {
|
|
||||||
return parseTag(raw);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map flat tool-schema edits into typed HashlineEdit objects.
|
|
||||||
*/
|
|
||||||
function resolveEditAnchors(edits: HashlineEditItem[]): HashlineEdit[] {
|
|
||||||
const result: HashlineEdit[] = [];
|
|
||||||
for (const edit of edits) {
|
|
||||||
const lines = parseHashlineText(edit.lines);
|
|
||||||
const tag = edit.pos ? tryParseTag(edit.pos) : undefined;
|
|
||||||
const end = edit.end ? tryParseTag(edit.end) : undefined;
|
|
||||||
|
|
||||||
const op =
|
|
||||||
edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
|
|
||||||
switch (op) {
|
|
||||||
case "replace": {
|
|
||||||
if (tag && end) {
|
|
||||||
result.push({ op: "replace", pos: tag, end, lines });
|
|
||||||
} else if (tag || end) {
|
|
||||||
result.push({ op: "replace", pos: tag || end!, lines });
|
|
||||||
} else {
|
|
||||||
throw new Error("Replace requires at least one anchor (pos or end).");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "append": {
|
|
||||||
result.push({ op: "append", pos: tag ?? end, lines });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "prepend": {
|
|
||||||
result.push({ op: "prepend", pos: end ?? tag, lines });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HASHLINE_EDIT_DESCRIPTION = `Edit a file by referencing LINE#ID tags from read output. Each tag uniquely identifies a line via content hash, so edits remain stable even when lines shift.
|
|
||||||
|
|
||||||
Read the file first to get fresh tags. Submit one edit call per file with all operations batched.
|
|
||||||
|
|
||||||
Operations:
|
|
||||||
- replace: Replace line(s) at pos (and optionally through end) with lines content
|
|
||||||
- append: Insert lines after pos (omit pos for end of file)
|
|
||||||
- prepend: Insert lines before pos (omit pos for beginning of file)
|
|
||||||
|
|
||||||
Set lines to null or [] to delete lines. Set delete:true to delete the file.`;
|
|
||||||
|
|
||||||
export function createHashlineEditTool(
|
|
||||||
cwd: string,
|
|
||||||
options?: HashlineEditToolOptions,
|
|
||||||
): AgentTool<typeof hashlineEditSchema> {
|
|
||||||
const ops = options?.operations ?? defaultHashlineEditOperations;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "hashline_edit",
|
|
||||||
label: "hashline_edit",
|
|
||||||
description: HASHLINE_EDIT_DESCRIPTION,
|
|
||||||
parameters: hashlineEditSchema,
|
|
||||||
execute: async (
|
|
||||||
_toolCallId: string,
|
|
||||||
params: HashlineEditInput,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
) => {
|
|
||||||
const { path, edits, delete: deleteFile, move } = params;
|
|
||||||
const absolutePath = resolveToCwd(path, cwd);
|
|
||||||
|
|
||||||
return new Promise<{
|
|
||||||
content: Array<{ type: "text"; text: string }>;
|
|
||||||
details: HashlineEditToolDetails | undefined;
|
|
||||||
}>((resolve, reject) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
reject(new Error("Operation aborted"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let aborted = false;
|
|
||||||
const onAbort = () => {
|
|
||||||
aborted = true;
|
|
||||||
reject(new Error("Operation aborted"));
|
|
||||||
};
|
|
||||||
if (signal) {
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
// Handle delete
|
|
||||||
if (deleteFile) {
|
|
||||||
let fileExists = true;
|
|
||||||
try {
|
|
||||||
await ops.access(absolutePath);
|
|
||||||
} catch {
|
|
||||||
fileExists = false;
|
|
||||||
}
|
|
||||||
if (fileExists) {
|
|
||||||
await ops.unlink(absolutePath);
|
|
||||||
}
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
resolve({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: fileExists
|
|
||||||
? `Deleted ${path}`
|
|
||||||
: `File not found, nothing to delete: ${path}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: { diff: "" },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle file creation (no existing file, anchorless appends/prepends)
|
|
||||||
let fileExists = true;
|
|
||||||
try {
|
|
||||||
await ops.access(absolutePath);
|
|
||||||
} catch {
|
|
||||||
fileExists = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileExists) {
|
|
||||||
const lines: string[] = [];
|
|
||||||
for (const edit of edits) {
|
|
||||||
if (
|
|
||||||
(edit.op === "append" || edit.op === "prepend") &&
|
|
||||||
!edit.pos &&
|
|
||||||
!edit.end
|
|
||||||
) {
|
|
||||||
if (edit.op === "prepend") {
|
|
||||||
lines.unshift(...parseHashlineText(edit.lines));
|
|
||||||
} else {
|
|
||||||
lines.push(...parseHashlineText(edit.lines));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`File not found: ${path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await ops.writeFile(absolutePath, lines.join("\n"));
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
resolve({
|
|
||||||
content: [{ type: "text", text: `Created ${path}` }],
|
|
||||||
details: { diff: "" },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
// Read file
|
|
||||||
const rawContent = (await ops.readFile(absolutePath)).toString(
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
const { bom, text } = stripBom(rawContent);
|
|
||||||
const originalEnding = detectLineEnding(text);
|
|
||||||
const originalNormalized = normalizeToLF(text);
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
// Resolve and apply edits
|
|
||||||
const anchorEdits = resolveEditAnchors(edits);
|
|
||||||
const result = applyHashlineEdits(originalNormalized, anchorEdits);
|
|
||||||
|
|
||||||
if (originalNormalized === result.lines && !move) {
|
|
||||||
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
||||||
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
||||||
const details = result.noopEdits
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
`Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.current}`,
|
|
||||||
)
|
|
||||||
.join("\n");
|
|
||||||
diagnostic += `\n${details}`;
|
|
||||||
diagnostic +=
|
|
||||||
"\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
|
|
||||||
}
|
|
||||||
throw new Error(diagnostic);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
// Write result
|
|
||||||
const finalContent =
|
|
||||||
bom + restoreLineEndings(result.lines, originalEnding);
|
|
||||||
const writePath = move ? resolveToCwd(move, cwd) : absolutePath;
|
|
||||||
|
|
||||||
// Prevent silent overwrite when moving to an existing file
|
|
||||||
if (move && writePath !== absolutePath) {
|
|
||||||
try {
|
|
||||||
await ops.access(writePath);
|
|
||||||
// If access succeeds, the file exists — refuse the move
|
|
||||||
throw new Error(
|
|
||||||
`Destination file already exists: ${writePath}. Use a different path or delete the existing file first.`,
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
// Re-throw our own error; swallow only "file not found"
|
|
||||||
if (err.message?.startsWith("Destination file already exists:"))
|
|
||||||
throw err;
|
|
||||||
// File doesn't exist — safe to proceed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ops.writeFile(writePath, finalContent);
|
|
||||||
|
|
||||||
// If moved, delete original
|
|
||||||
if (move && writePath !== absolutePath) {
|
|
||||||
await ops.unlink(absolutePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
|
|
||||||
const diffResult = generateDiffString(
|
|
||||||
originalNormalized,
|
|
||||||
result.lines,
|
|
||||||
);
|
|
||||||
const resultText = move
|
|
||||||
? `Moved ${path} to ${move}`
|
|
||||||
: `Updated ${path}`;
|
|
||||||
const warningsBlock = result.warnings?.length
|
|
||||||
? `\nWarnings:\n${result.warnings.join("\n")}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `${resultText}${warningsBlock}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
diff: diffResult.diff,
|
|
||||||
firstChangedLine:
|
|
||||||
result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
if (!aborted) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Default hashline edit tool using process.cwd() */
|
|
||||||
export const hashlineEditTool = createHashlineEditTool(process.cwd());
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
/**
|
|
||||||
* Hashline read tool — reads files with LINE#ID prefix on each line.
|
|
||||||
*
|
|
||||||
* Produces output like:
|
|
||||||
* 1#QQ:function hello() {
|
|
||||||
* 2#KX: return 42;
|
|
||||||
* 3#NW:}
|
|
||||||
*
|
|
||||||
* These tags are used by the hashline_edit tool to address lines precisely.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { constants } from "node:fs";
|
|
||||||
import { access as fsAccess, readFile as fsReadFile } from "node:fs/promises";
|
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
|
||||||
import type { AgentTool } from "@singularity-forge/agent-core";
|
|
||||||
import type { ImageContent, TextContent } from "@singularity-forge/ai";
|
|
||||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
|
||||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
||||||
import { formatHashLines } from "./hashline.js";
|
|
||||||
import { resolveReadPath } from "./path-utils.js";
|
|
||||||
import {
|
|
||||||
DEFAULT_MAX_BYTES,
|
|
||||||
DEFAULT_MAX_LINES,
|
|
||||||
formatSize,
|
|
||||||
type TruncationResult,
|
|
||||||
truncateHead,
|
|
||||||
} from "./truncate.js";
|
|
||||||
|
|
||||||
const readSchema = Type.Object({
|
|
||||||
path: Type.String({
|
|
||||||
description: "Path to the file to read (relative or absolute)",
|
|
||||||
}),
|
|
||||||
offset: Type.Optional(
|
|
||||||
Type.Number({
|
|
||||||
description: "Line number to start reading from (1-indexed)",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
limit: Type.Optional(
|
|
||||||
Type.Number({ description: "Maximum number of lines to read" }),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type HashlineReadToolInput = Static<typeof readSchema>;
|
|
||||||
|
|
||||||
export interface HashlineReadToolDetails {
|
|
||||||
truncation?: TruncationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pluggable operations for the hashline read tool.
|
|
||||||
*/
|
|
||||||
export interface HashlineReadOperations {
|
|
||||||
readFile: (absolutePath: string) => Promise<Buffer>;
|
|
||||||
access: (absolutePath: string) => Promise<void>;
|
|
||||||
detectImageMimeType?: (
|
|
||||||
absolutePath: string,
|
|
||||||
) => Promise<string | null | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultReadOperations: HashlineReadOperations = {
|
|
||||||
readFile: (path) => fsReadFile(path),
|
|
||||||
access: (path) => fsAccess(path, constants.R_OK),
|
|
||||||
detectImageMimeType: detectSupportedImageMimeTypeFromFile,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface HashlineReadToolOptions {
|
|
||||||
autoResizeImages?: boolean;
|
|
||||||
operations?: HashlineReadOperations;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createHashlineReadTool(
|
|
||||||
cwd: string,
|
|
||||||
options?: HashlineReadToolOptions,
|
|
||||||
): AgentTool<typeof readSchema> {
|
|
||||||
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
||||||
const ops = options?.operations ?? defaultReadOperations;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "Read",
|
|
||||||
label: "Read",
|
|
||||||
description: `Read a file with LINE#ID hash anchors on each line. These anchors are used by hashline_edit for precise edits. Output format: LINENUM#HASH:CONTENT. Supports text files and images. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. Use offset/limit for large files.`,
|
|
||||||
parameters: readSchema,
|
|
||||||
execute: async (
|
|
||||||
_toolCallId: string,
|
|
||||||
{
|
|
||||||
path,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
}: { path: string; offset?: number; limit?: number },
|
|
||||||
signal?: AbortSignal,
|
|
||||||
) => {
|
|
||||||
const absolutePath = resolveReadPath(path, cwd);
|
|
||||||
|
|
||||||
return new Promise<{
|
|
||||||
content: (TextContent | ImageContent)[];
|
|
||||||
details: HashlineReadToolDetails | undefined;
|
|
||||||
}>((resolve, reject) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
reject(new Error("Operation aborted"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let aborted = false;
|
|
||||||
const onAbort = () => {
|
|
||||||
aborted = true;
|
|
||||||
reject(new Error("Operation aborted"));
|
|
||||||
};
|
|
||||||
if (signal) {
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await ops.access(absolutePath);
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
const mimeType = ops.detectImageMimeType
|
|
||||||
? await ops.detectImageMimeType(absolutePath)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let content: (TextContent | ImageContent)[];
|
|
||||||
let details: HashlineReadToolDetails | undefined;
|
|
||||||
|
|
||||||
if (mimeType) {
|
|
||||||
// Image handling (identical to standard read tool)
|
|
||||||
const buffer = await ops.readFile(absolutePath);
|
|
||||||
const base64 = buffer.toString("base64");
|
|
||||||
|
|
||||||
if (autoResizeImages) {
|
|
||||||
const resized = await resizeImage({
|
|
||||||
type: "image",
|
|
||||||
data: base64,
|
|
||||||
mimeType,
|
|
||||||
});
|
|
||||||
const dimensionNote = formatDimensionNote(resized);
|
|
||||||
let textNote = `Read image file [${resized.mimeType}]`;
|
|
||||||
if (dimensionNote) {
|
|
||||||
textNote += `\n${dimensionNote}`;
|
|
||||||
}
|
|
||||||
content = [
|
|
||||||
{ type: "text", text: textNote },
|
|
||||||
{
|
|
||||||
type: "image",
|
|
||||||
data: resized.data,
|
|
||||||
mimeType: resized.mimeType,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
content = [
|
|
||||||
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
||||||
{ type: "image", data: base64, mimeType },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Text file — format with hashline prefixes
|
|
||||||
const buffer = await ops.readFile(absolutePath);
|
|
||||||
const textContent = buffer.toString("utf-8");
|
|
||||||
const allLines = textContent.split("\n");
|
|
||||||
const totalFileLines = allLines.length;
|
|
||||||
|
|
||||||
let startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
||||||
|
|
||||||
// Clamp offset to file bounds instead of throwing (#3007)
|
|
||||||
let offsetClamped = false;
|
|
||||||
if (startLine >= allLines.length) {
|
|
||||||
startLine = Math.max(0, allLines.length - 1);
|
|
||||||
offsetClamped = true;
|
|
||||||
}
|
|
||||||
const startLineDisplay = startLine + 1;
|
|
||||||
|
|
||||||
let selectedContent: string;
|
|
||||||
let userLimitedLines: number | undefined;
|
|
||||||
if (limit !== undefined) {
|
|
||||||
const endLine = Math.min(startLine + limit, allLines.length);
|
|
||||||
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
||||||
userLimitedLines = endLine - startLine;
|
|
||||||
} else {
|
|
||||||
selectedContent = allLines.slice(startLine).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply truncation
|
|
||||||
const truncation = truncateHead(selectedContent);
|
|
||||||
|
|
||||||
let outputText: string;
|
|
||||||
|
|
||||||
if (truncation.firstLineExceedsLimit) {
|
|
||||||
const firstLineSize = formatSize(
|
|
||||||
Buffer.byteLength(allLines[startLine], "utf-8"),
|
|
||||||
);
|
|
||||||
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
||||||
details = { truncation };
|
|
||||||
} else if (truncation.truncated) {
|
|
||||||
const endLineDisplay =
|
|
||||||
startLineDisplay + truncation.outputLines - 1;
|
|
||||||
const nextOffset = endLineDisplay + 1;
|
|
||||||
|
|
||||||
// Format with hashline prefixes
|
|
||||||
outputText = formatHashLines(
|
|
||||||
truncation.content,
|
|
||||||
startLineDisplay,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (truncation.truncatedBy === "lines") {
|
|
||||||
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
|
||||||
} else {
|
|
||||||
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
|
||||||
}
|
|
||||||
details = { truncation };
|
|
||||||
} else if (
|
|
||||||
userLimitedLines !== undefined &&
|
|
||||||
startLine + userLimitedLines < allLines.length
|
|
||||||
) {
|
|
||||||
const remaining =
|
|
||||||
allLines.length - (startLine + userLimitedLines);
|
|
||||||
const nextOffset = startLine + userLimitedLines + 1;
|
|
||||||
|
|
||||||
outputText = formatHashLines(
|
|
||||||
truncation.content,
|
|
||||||
startLineDisplay,
|
|
||||||
);
|
|
||||||
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
|
||||||
} else {
|
|
||||||
outputText = formatHashLines(
|
|
||||||
truncation.content,
|
|
||||||
startLineDisplay,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepend clamp notice so the agent knows offset was adjusted
|
|
||||||
if (offsetClamped) {
|
|
||||||
outputText = `[Offset ${offset} beyond end of file (${totalFileLines} lines). Clamped to line ${startLineDisplay}.]\n\n${outputText}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = [{ type: "text", text: outputText }];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aborted) return;
|
|
||||||
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
resolve({ content, details });
|
|
||||||
} catch (error: any) {
|
|
||||||
if (signal) signal.removeEventListener("abort", onAbort);
|
|
||||||
if (!aborted) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Default hashline read tool using process.cwd() */
|
|
||||||
export const hashlineReadTool = createHashlineReadTool(process.cwd());
|
|
||||||
|
|
@ -65,23 +65,6 @@ export {
|
||||||
stripNewLinePrefixes,
|
stripNewLinePrefixes,
|
||||||
validateLineRef,
|
validateLineRef,
|
||||||
} from "./hashline.js";
|
} from "./hashline.js";
|
||||||
export {
|
|
||||||
createHashlineEditTool,
|
|
||||||
type HashlineEditInput,
|
|
||||||
type HashlineEditItem,
|
|
||||||
type HashlineEditOperations,
|
|
||||||
type HashlineEditToolDetails,
|
|
||||||
type HashlineEditToolOptions,
|
|
||||||
hashlineEditTool,
|
|
||||||
} from "./hashline-edit.js";
|
|
||||||
export {
|
|
||||||
createHashlineReadTool,
|
|
||||||
type HashlineReadOperations,
|
|
||||||
type HashlineReadToolDetails,
|
|
||||||
type HashlineReadToolInput,
|
|
||||||
type HashlineReadToolOptions,
|
|
||||||
hashlineReadTool,
|
|
||||||
} from "./hashline-read.js";
|
|
||||||
export {
|
export {
|
||||||
createLsTool,
|
createLsTool,
|
||||||
type LsOperations,
|
type LsOperations,
|
||||||
|
|
@ -130,8 +113,6 @@ import { type BashToolOptions, bashTool, createBashTool } from "./bash.js";
|
||||||
import { createEditTool, editTool } from "./edit.js";
|
import { createEditTool, editTool } from "./edit.js";
|
||||||
import { createFindTool, findTool } from "./find.js";
|
import { createFindTool, findTool } from "./find.js";
|
||||||
import { createGrepTool, grepTool } from "./grep.js";
|
import { createGrepTool, grepTool } from "./grep.js";
|
||||||
import { createHashlineEditTool, hashlineEditTool } from "./hashline-edit.js";
|
|
||||||
import { createHashlineReadTool, hashlineReadTool } from "./hashline-read.js";
|
|
||||||
import { createInsertAroundSymbolTool } from "./insert-around-symbol.js";
|
import { createInsertAroundSymbolTool } from "./insert-around-symbol.js";
|
||||||
import { createLsTool, lsTool } from "./ls.js";
|
import { createLsTool, lsTool } from "./ls.js";
|
||||||
import { createReadTool, type ReadToolOptions, readTool } from "./read.js";
|
import { createReadTool, type ReadToolOptions, readTool } from "./read.js";
|
||||||
|
|
@ -166,8 +147,6 @@ export const allTools = {
|
||||||
Glob: findTool,
|
Glob: findTool,
|
||||||
LS: lsTool,
|
LS: lsTool,
|
||||||
lsp: lspTool,
|
lsp: lspTool,
|
||||||
hashline_edit: hashlineEditTool,
|
|
||||||
hashline_read: hashlineReadTool,
|
|
||||||
// Serena-style AST tools: tree-sitter-anchored, language-aware editing.
|
// Serena-style AST tools: tree-sitter-anchored, language-aware editing.
|
||||||
// More durable than substring Edit / line-anchor hashline for code in
|
// More durable than substring Edit / line-anchor hashline for code in
|
||||||
// languages with a tree-sitter grammar.
|
// languages with a tree-sitter grammar.
|
||||||
|
|
@ -176,18 +155,6 @@ export const allTools = {
|
||||||
AstGrep: createAstGrepTool(process.cwd()),
|
AstGrep: createAstGrepTool(process.cwd()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hashline-mode coding tools — read with hash anchors, edit with hash references
|
|
||||||
export const hashlineCodingTools: Tool[] = [
|
|
||||||
hashlineReadTool,
|
|
||||||
grepTool,
|
|
||||||
findTool,
|
|
||||||
lsTool,
|
|
||||||
bashTool,
|
|
||||||
hashlineEditTool,
|
|
||||||
writeTool,
|
|
||||||
lspTool,
|
|
||||||
];
|
|
||||||
|
|
||||||
export type ToolName = keyof typeof allTools;
|
export type ToolName = keyof typeof allTools;
|
||||||
|
|
||||||
export interface ToolsOptions {
|
export interface ToolsOptions {
|
||||||
|
|
@ -244,30 +211,8 @@ export function createAllTools(
|
||||||
Glob: createFindTool(cwd),
|
Glob: createFindTool(cwd),
|
||||||
LS: createLsTool(cwd),
|
LS: createLsTool(cwd),
|
||||||
lsp: createLspTool(cwd),
|
lsp: createLspTool(cwd),
|
||||||
hashline_edit: createHashlineEditTool(cwd),
|
|
||||||
hashline_read: createHashlineReadTool(cwd, options?.read),
|
|
||||||
ReplaceSymbol: createReplaceSymbolTool(cwd),
|
ReplaceSymbol: createReplaceSymbolTool(cwd),
|
||||||
InsertAroundSymbol: createInsertAroundSymbolTool(cwd),
|
InsertAroundSymbol: createInsertAroundSymbolTool(cwd),
|
||||||
AstGrep: createAstGrepTool(cwd),
|
AstGrep: createAstGrepTool(cwd),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create hashline-mode coding tools configured for a specific working directory.
|
|
||||||
* Uses hashline read (LINE#ID prefixed output) and hashline edit (hash-anchor based edits).
|
|
||||||
*/
|
|
||||||
export function createHashlineCodingTools(
|
|
||||||
cwd: string,
|
|
||||||
options?: ToolsOptions,
|
|
||||||
): Tool[] {
|
|
||||||
return [
|
|
||||||
createHashlineReadTool(cwd, options?.read),
|
|
||||||
createGrepTool(cwd),
|
|
||||||
createFindTool(cwd),
|
|
||||||
createLsTool(cwd),
|
|
||||||
createBashTool(cwd, options?.bash),
|
|
||||||
createHashlineEditTool(cwd),
|
|
||||||
createWriteTool(cwd),
|
|
||||||
createLspTool(cwd),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { AgentTool } from "@singularity-forge/agent-core";
|
||||||
import type { ImageContent, TextContent } from "@singularity-forge/ai";
|
import type { ImageContent, TextContent } from "@singularity-forge/ai";
|
||||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
||||||
|
import { formatHashLines } from "./hashline.js";
|
||||||
import { resolveReadPath } from "./path-utils.js";
|
import { resolveReadPath } from "./path-utils.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAX_BYTES,
|
DEFAULT_MAX_BYTES,
|
||||||
|
|
@ -27,6 +28,13 @@ const readSchema = Type.Object({
|
||||||
limit: Type.Optional(
|
limit: Type.Optional(
|
||||||
Type.Number({ description: "Maximum number of lines to read" }),
|
Type.Number({ description: "Maximum number of lines to read" }),
|
||||||
),
|
),
|
||||||
|
format: Type.Optional(
|
||||||
|
Type.Union([Type.Literal("plain"), Type.Literal("tagged")], {
|
||||||
|
default: "plain",
|
||||||
|
description:
|
||||||
|
'Output format. "plain" (default): raw file text. "tagged": prefix each line with LINE#HASH for use with Edit({match:"anchor"}).',
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ReadToolInput = Static<typeof readSchema>;
|
export type ReadToolInput = Static<typeof readSchema>;
|
||||||
|
|
@ -166,7 +174,7 @@ export function createReadTool(
|
||||||
return {
|
return {
|
||||||
name: "Read",
|
name: "Read",
|
||||||
label: "Read",
|
label: "Read",
|
||||||
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete. Use format="tagged" to get LINE#HASH prefixes on each line (for anchor-mode editing with Edit({match:"anchor"})).`,
|
||||||
parameters: readSchema,
|
parameters: readSchema,
|
||||||
execute: async (
|
execute: async (
|
||||||
_toolCallId: string,
|
_toolCallId: string,
|
||||||
|
|
@ -174,7 +182,8 @@ export function createReadTool(
|
||||||
path,
|
path,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
}: { path: string; offset?: number; limit?: number },
|
format,
|
||||||
|
}: { path: string; offset?: number; limit?: number; format?: "plain" | "tagged" },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
const absolutePath = resolveReadPath(path, cwd);
|
const absolutePath = resolveReadPath(path, cwd);
|
||||||
|
|
@ -298,7 +307,11 @@ export function createReadTool(
|
||||||
effectiveStartLine + truncation.outputLines - 1;
|
effectiveStartLine + truncation.outputLines - 1;
|
||||||
const nextOffset = endLineDisplay + 1;
|
const nextOffset = endLineDisplay + 1;
|
||||||
|
|
||||||
outputText = truncation.content;
|
const bodyText =
|
||||||
|
format === "tagged"
|
||||||
|
? formatHashLines(truncation.content, effectiveStartLine)
|
||||||
|
: truncation.content;
|
||||||
|
outputText = bodyText;
|
||||||
|
|
||||||
if (truncation.truncatedBy === "lines") {
|
if (truncation.truncatedBy === "lines") {
|
||||||
outputText += `\n\n[Showing lines ${effectiveStartLine}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
outputText += `\n\n[Showing lines ${effectiveStartLine}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
||||||
|
|
@ -315,10 +328,17 @@ export function createReadTool(
|
||||||
const remaining =
|
const remaining =
|
||||||
totalFileLines - (effectiveStartLine + lines.length - 1);
|
totalFileLines - (effectiveStartLine + lines.length - 1);
|
||||||
|
|
||||||
outputText = truncation.content;
|
const bodyText =
|
||||||
|
format === "tagged"
|
||||||
|
? formatHashLines(truncation.content, effectiveStartLine)
|
||||||
|
: truncation.content;
|
||||||
|
outputText = bodyText;
|
||||||
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
||||||
} else {
|
} else {
|
||||||
outputText = truncation.content;
|
outputText =
|
||||||
|
format === "tagged"
|
||||||
|
? formatHashLines(truncation.content, effectiveStartLine)
|
||||||
|
: truncation.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetClamped) {
|
if (offsetClamped) {
|
||||||
|
|
@ -340,7 +360,11 @@ export function createReadTool(
|
||||||
const endLineDisplay = truncation.outputLines;
|
const endLineDisplay = truncation.outputLines;
|
||||||
const nextOffset = endLineDisplay + 1;
|
const nextOffset = endLineDisplay + 1;
|
||||||
|
|
||||||
outputText = truncation.content;
|
const bodyText =
|
||||||
|
format === "tagged"
|
||||||
|
? formatHashLines(truncation.content, 1)
|
||||||
|
: truncation.content;
|
||||||
|
outputText = bodyText;
|
||||||
|
|
||||||
if (truncation.truncatedBy === "lines") {
|
if (truncation.truncatedBy === "lines") {
|
||||||
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
||||||
|
|
@ -349,7 +373,10 @@ export function createReadTool(
|
||||||
}
|
}
|
||||||
_details = { truncation };
|
_details = { truncation };
|
||||||
} else {
|
} else {
|
||||||
outputText = truncation.content;
|
outputText =
|
||||||
|
format === "tagged"
|
||||||
|
? formatHashLines(truncation.content, 1)
|
||||||
|
: truncation.content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -324,9 +324,6 @@ export {
|
||||||
checkBashInterception,
|
checkBashInterception,
|
||||||
codingTools,
|
codingTools,
|
||||||
compileInterceptor,
|
compileInterceptor,
|
||||||
createHashlineCodingTools,
|
|
||||||
createHashlineEditTool,
|
|
||||||
createHashlineReadTool,
|
|
||||||
DEFAULT_BASH_INTERCEPTOR_RULES,
|
DEFAULT_BASH_INTERCEPTOR_RULES,
|
||||||
DEFAULT_MAX_BYTES,
|
DEFAULT_MAX_BYTES,
|
||||||
DEFAULT_MAX_LINES,
|
DEFAULT_MAX_LINES,
|
||||||
|
|
@ -348,16 +345,6 @@ export {
|
||||||
getAllToolCompatibility,
|
getAllToolCompatibility,
|
||||||
getToolCompatibility,
|
getToolCompatibility,
|
||||||
grepTool,
|
grepTool,
|
||||||
type HashlineEditInput,
|
|
||||||
type HashlineEditToolDetails,
|
|
||||||
type HashlineEditToolOptions,
|
|
||||||
type HashlineReadToolDetails,
|
|
||||||
type HashlineReadToolInput,
|
|
||||||
type HashlineReadToolOptions,
|
|
||||||
hashlineCodingTools,
|
|
||||||
// Hashline edit mode tools
|
|
||||||
hashlineEditTool,
|
|
||||||
hashlineReadTool,
|
|
||||||
type LsOperations,
|
type LsOperations,
|
||||||
type LsToolDetails,
|
type LsToolDetails,
|
||||||
type LsToolInput,
|
type LsToolInput,
|
||||||
|
|
|
||||||
|
|
@ -202,8 +202,6 @@ export function summarizeToolArgs(
|
||||||
case "Edit":
|
case "Edit":
|
||||||
case "edit":
|
case "edit":
|
||||||
return filePath();
|
return filePath();
|
||||||
case "hashline_edit":
|
|
||||||
return filePath();
|
|
||||||
case "Bash":
|
case "Bash":
|
||||||
case "bash": {
|
case "bash": {
|
||||||
const cmd = String(input.command ?? "");
|
const cmd = String(input.command ?? "");
|
||||||
|
|
|
||||||
|
|
@ -513,13 +513,6 @@ describe("summarizeToolArgs", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("extracts path for hashline_edit", () => {
|
|
||||||
assert.equal(
|
|
||||||
summarizeToolArgs("hashline_edit", { path: "src/main.ts" }),
|
|
||||||
"src/main.ts",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("extracts agent and task for subagent", () => {
|
it("extracts agent and task for subagent", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
summarizeToolArgs("subagent", {
|
summarizeToolArgs("subagent", {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue