diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f3cc64825..8943c29e3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,13 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Renamed hashline separator configuration from `PI_HASHLINE_SEP` to `PI_HL_SEP` and changed the default payload separator from `\\` to `>` ### Added +- Added inline hashline edit syntax so `< ANCHOR${sep}TEXT` prepended text to an anchored line and `+ ANCHOR${sep}TEXT` appended text to it without requiring a multi-line payload block - Added a `memory.backend` setting (off, local, hindsight) under a new Memory settings tab to control which memory subsystem is active - Added Hindsight memory settings (`hindsight.*`) for API connection, bank identification, and recall/retain policy - Added `retain`, `recall`, and `reflect` tools for direct long-term memory search, retention, and reflection when using the Hindsight backend @@ -12,6 +16,7 @@ ### Changed +- Changed hashline read/search previews and diff output to keep `|` as the anchor-to-text separator while using the separate configured edit payload separator - Mapped invalid `hindsight.scoping` settings back to the default `per-project-tagged` behavior with a warning - Changed `/memory view`, `/memory clear`, and `/memory enqueue` to route through the selected memory backend instead of being hardcoded to local memories - Changed compaction context assembly to include backend-provided recall context when available @@ -30,6 +35,7 @@ ### Fixed +- Fixed inline hashline modify operations to fail fast when combined with a delete or replace on the same target line - Fixed hashline parsing of payload blocks to handle a shared extra leading symbol prefix (such as markdown `>>`) on all payload lines by stripping it as an auto-correction instead of rejecting the edit - Forwarded project scoping tags to `hindsight` retain, recall, and reflect operations so manual memory commands honor the active tagging mode - Fixed legacy migrations by mapping existing `memories.enabled` values to `memory.backend` on load to preserve prior enable/disable behavior @@ -7724,4 +7730,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections +- HTML export with syntax highlighting and collapsible sections \ No newline at end of file diff --git a/packages/coding-agent/src/config/prompt-templates.ts b/packages/coding-agent/src/config/prompt-templates.ts index a10480d2f..6bf3ee509 100644 --- a/packages/coding-agent/src/config/prompt-templates.ts +++ b/packages/coding-agent/src/config/prompt-templates.ts @@ -8,7 +8,7 @@ import { parseFrontmatter, prompt, } from "@oh-my-pi/pi-utils"; -import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash"; +import { computeLineHash, HL_BODY_SEP, HL_EDIT_SEP } from "../edit/line-hash"; import { jtdToTypeScript } from "../tools/jtd-to-typescript"; import { parseCommandArgs, substituteArgs } from "../utils/command-args"; @@ -53,10 +53,10 @@ interface HashlineHelperState { byLine: Map; } -const HASHLINE_HELPER_STATE = Symbol("hashlineHelperState"); +const HL_HELPER_STATE = Symbol("hashlineHelperState"); interface HashlineHelperStateHolder { - [HASHLINE_HELPER_STATE]?: HashlineHelperState; + [HL_HELPER_STATE]?: HashlineHelperState; } function isHelperOptions(value: unknown): value is prompt.HelperOptions { @@ -78,10 +78,10 @@ function getHashlineHelperState(context: unknown, options: prompt.HelperOptions } const holder = holderTarget as HashlineHelperStateHolder; - if (!holder[HASHLINE_HELPER_STATE]) { - holder[HASHLINE_HELPER_STATE] = { byLine: new Map() }; + if (!holder[HL_HELPER_STATE]) { + holder[HL_HELPER_STATE] = { byLine: new Map() }; } - return holder[HASHLINE_HELPER_STATE]; + return holder[HL_HELPER_STATE]; } function isLineNumberArg(value: unknown): boolean { @@ -156,15 +156,15 @@ prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): str const { num, ref, text } = formatHashlineRef(lineNum, content); const state = getHashlineHelperState(this, options); rememberHashlineRef(state, num, ref); - return `${ref}${HASHLINE_CONTENT_SEPARATOR}${text}`; + return `${ref}${HL_BODY_SEP}${text}`; }); /** * {{hsep}} — emit the configured hashline payload separator character. - * Stays in sync with {@link HASHLINE_CONTENT_SEPARATOR} so prompt templates - * never have to hardcode `|` and survive a separator switch automatically. + * Stays in sync with {@link HL_EDIT_SEP} so edit prompt templates + * never have to hardcode the payload separator. */ -prompt.registerHelper("hsep", (): string => HASHLINE_CONTENT_SEPARATOR); +prompt.registerHelper("hsep", (): string => HL_EDIT_SEP); const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/; const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/; diff --git a/packages/coding-agent/src/edit/line-hash.ts b/packages/coding-agent/src/edit/line-hash.ts index 30d2b54cf..442482f37 100644 --- a/packages/coding-agent/src/edit/line-hash.ts +++ b/packages/coding-agent/src/edit/line-hash.ts @@ -15,7 +15,7 @@ * Order is stable forever — changing it would invalidate every saved * `LINE+ID` reference in transcripts and prompts. */ -export const HASHLINE_BIGRAMS = [ +export const HL_BIGRAMS = [ "aa", "ab", "ac", @@ -665,7 +665,7 @@ export const HASHLINE_BIGRAMS = [ "zz", ] as const; -export const HASHLINE_BIGRAMS_COUNT = HASHLINE_BIGRAMS.length; +export const HL_BIGRAMS_COUNT = HL_BIGRAMS.length; /** * Decoration prefix that may precede a `LINE+HASH` anchor in tool output: @@ -675,7 +675,7 @@ export const HASHLINE_BIGRAMS_COUNT = HASHLINE_BIGRAMS.length; * regex stays liberal because anchor-ref parsers accept whatever the model * echoes back. */ -export const HASHLINE_ANCHOR_DECORATION_RE_SRC = `\\s*[>+\\-*]*\\s*`; +export const HL_ANCHOR_DECORATION_RE_RAW = `\\s*[>+\\-*]*\\s*`; /** * Capture-group regex source for a decorated `LINE+HASH` anchor. Group 1 @@ -683,36 +683,36 @@ export const HASHLINE_ANCHOR_DECORATION_RE_SRC = `\\s*[>+\\-*]*\\s*`; * source is intentionally unanchored — anchoring with `^` (or composing into a * larger pattern) is the caller's responsibility. */ -export const HASHLINE_ANCHOR_RE_SRC = `${HASHLINE_ANCHOR_DECORATION_RE_SRC}(\\d+)([a-z]{2})`; +export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)([a-z]{2})`; /** * Bare `LINE+HASH` Lid (no decorations, no captures, no anchors). Use for * embedding inside larger patterns where the line+hash unit appears as a * literal (e.g. range bounds, alternation arms, op-line heuristics). */ -export const HASHLINE_LID_RE_SRC = `[1-9]\\d*[a-z]{2}`; +export const HL_HASH_RE_RAW = `[1-9]\\d*[a-z]{2}`; /** - * Capture-group form of {@link HASHLINE_LID_RE_SRC}: group 1 captures the + * Capture-group form of {@link HL_HASH_RE_RAW}: group 1 captures the * line number, group 2 captures the hash. */ -export const HASHLINE_LID_CAPTURE_RE_SRC = `([1-9]\\d*)([a-z]{2})`; +export const HL_HASH_CAPTURE_RE_RAW = `([1-9]\\d*)([a-z]{2})`; /** Width of a hash in display characters. */ -export const HASHLINE_HASH_WIDTH = 2; +export const HL_HASH_WIDTH = 2; /** * Representative hash suffixes for use in user-facing error messages and * prompt examples. */ -export const HASHLINE_HASH_EXAMPLES = ["sr", "ab", "th"] as const; +export const HL_HASH_EXAMPLES = ["sr", "ab", "th"] as const; /** * Format a comma-separated list of example anchors with an optional line-number * prefix, quoted for inclusion in error messages: `"160sr", "160ab", "160th"`. */ export function describeAnchorExamples(linePrefix = ""): string { - return HASHLINE_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", "); + return HL_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", "); } /** @@ -721,32 +721,43 @@ export function describeAnchorExamples(linePrefix = ""): string { * pass through unchanged. */ export function resolveHashlineGrammarPlaceholders(grammar: string): string { - return grammar.replaceAll("$HFMT$", "[a-z]{2}").replaceAll("$HSEP$", JSON.stringify(HASHLINE_CONTENT_SEPARATOR)); + return grammar.replaceAll("$HFMT$", "[a-z]{2}").replaceAll("$HSEP$", JSON.stringify(HL_EDIT_SEP)); } /** @deprecated Use {@link resolveHashlineGrammarPlaceholders}. */ export const resolveLarkLidPlaceholders = resolveHashlineGrammarPlaceholders; -/** - * Single source of truth for the hashline payload separator. This is the - * character that introduces both the content portion of a formatted hashline - * (`LINE+ID|TEXT`) and every payload line in an edit op block (`|TEXT`). - * - * Override at runtime with the `PI_HASHLINE_SEP` env var (e.g. - * `PI_HASHLINE_SEP=">"`, `PI_HASHLINE_SEP="\\"`). The value is read once at - * module load; the regex source, grammar substitution, prompt helper, and - * doubled-prefix detection in the parser all derive from it. - */ -export const HASHLINE_CONTENT_SEPARATOR = Bun.env.PI_HASHLINE_SEP || "\\"; +const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -/** Regex-escaped form of {@link HASHLINE_CONTENT_SEPARATOR}, safe for embedding inside a regex. */ -export const HASHLINE_CONTENT_SEPARATOR_RE_SRC = HASHLINE_CONTENT_SEPARATOR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +/** + * Single source of truth for the hashline edit payload separator. This is the + * configured separator that starts inserted/replacement payload lines in + * hashline edit input (`TEXT`) and separates inline modify ops from + * their appended/prepended text. + * + * Override at runtime with the `PI_HL_SEP` env var (e.g. + * `PI_HL_SEP=">"`, `PI_HL_SEP="\\"`). The value is read once at + * module load; the edit grammar, prompt helper, and edit parser derive from it. + */ +export const HL_EDIT_SEP = (() => { + const sep = Bun.env.PI_HL_SEP; + return sep?.length === 1 ? sep : ">"; +})(); + +/** Regex-escaped form of {@link HL_EDIT_SEP}, safe for regexes. */ +export const HL_EDIT_SEP_RE_RAW = regexEscape(HL_EDIT_SEP); + +/** Stable separator for read/search/hashline display output. Intentionally not configurable. */ +export const HL_BODY_SEP = "|"; + +/** Regex-escaped form of {@link HL_BODY_SEP}, safe for embedding inside a regex. */ +export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP); const RE_SIGNIFICANT = /[\p{L}\p{N}]/u; /** * Compute a 2-character hash of a single line via xxHash32 mod 647 over - * {@link HASHLINE_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`, + * {@link HL_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`, * bare `{`) mix the line number into the seed so adjacent identical * brace-only lines get distinct hashes; lines with significant content stay * line-number-independent so a line is identifiable across small shifts. @@ -756,7 +767,7 @@ const RE_SIGNIFICANT = /[\p{L}\p{N}]/u; export function computeLineHash(idx: number, line: string): string { line = line.replace(/\r/g, "").trimEnd(); const seed = RE_SIGNIFICANT.test(line) ? 0 : idx; - return HASHLINE_BIGRAMS[Bun.hash.xxHash32(line, seed) % HASHLINE_BIGRAMS_COUNT]; + return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT]; } /** @@ -773,7 +784,7 @@ export function formatLineHash(line: number, lines: string): string { * Returns `LINE+ID|TEXT` (e.g., `42sr|function hi() {`, `3ab|}`). */ export function formatHashLine(lineNumber: number, line: string): string { - return `${lineNumber}${computeLineHash(lineNumber, line)}${HASHLINE_CONTENT_SEPARATOR}${line}`; + return `${lineNumber}${computeLineHash(lineNumber, line)}${HL_BODY_SEP}${line}`; } /** diff --git a/packages/coding-agent/src/edit/modes/hashline.lark b/packages/coding-agent/src/edit/modes/hashline.lark index da99fefd4..8e1163c1d 100644 --- a/packages/coding-agent/src/edit/modes/hashline.lark +++ b/packages/coding-agent/src/edit/modes/hashline.lark @@ -1,4 +1,5 @@ %import common.LF +%import common.WS_INLINE start: section+ @@ -6,17 +7,23 @@ section: file_header line_op* file_header: "@" path LF -line_op: insert_before_op payload+ +line_op: inline_before_op payload* + | inline_after_op payload* + | insert_before_op payload+ | insert_after_op payload+ | replace_op payload* | delete_op | blank +inline_before_op: "<" LID $HSEP$ line_text? LF +inline_after_op: "+" LID $HSEP$ line_text? LF insert_before_op: "<" insert_target LF insert_after_op: "+" insert_target LF replace_op: "=" range LF delete_op: "-" range LF -payload: $HSEP$ /[^\r\n]*/ LF +payload: $HSEP$ line_text? LF + +line_text: /[^\r\n]+/ insert_target: LID | "EOF" | "BOF" range: LID (".." LID)? diff --git a/packages/coding-agent/src/edit/modes/hashline.ts b/packages/coding-agent/src/edit/modes/hashline.ts index 316045942..d04a85523 100644 --- a/packages/coding-agent/src/edit/modes/hashline.ts +++ b/packages/coding-agent/src/edit/modes/hashline.ts @@ -44,10 +44,12 @@ import { computeLineHash, describeAnchorExamples, formatHashLine, - HASHLINE_ANCHOR_RE_SRC, - HASHLINE_CONTENT_SEPARATOR, - HASHLINE_CONTENT_SEPARATOR_RE_SRC, - HASHLINE_LID_CAPTURE_RE_SRC, + HL_ANCHOR_RE_RAW, + HL_BODY_SEP, + HL_BODY_SEP_RE_RAW, + HL_EDIT_SEP, + HL_EDIT_SEP_RE_RAW, + HL_HASH_CAPTURE_RE_RAW, } from "../line-hash"; import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize"; import type { EditToolDetails, LspBatchRequest } from "../renderer"; @@ -76,7 +78,8 @@ type HashlineCursor = export type HashlineEdit = | { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number } - | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string }; + | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string } + | { kind: "modify"; anchor: Anchor; prefix: string; suffix: string; lineNum: number; index: number }; export const hashlineEditParamsSchema = Type.Object({ input: Type.String() }); export type HashlineParams = Static; @@ -132,17 +135,18 @@ const RANGE_INTERIOR_HASH = "**"; /** Header marker introducing a new file section in multi-section input. */ const FILE_HEADER_PREFIX = "@"; -const HASHLINE_CONTENT_SEPARATOR_RE = `[:${HASHLINE_CONTENT_SEPARATOR_RE_SRC}]`; -const HASHLINE_PREFIX_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*(?:[+*]\\s*)?\\d+[a-z]{2}${HASHLINE_CONTENT_SEPARATOR_RE}`); -const HASHLINE_PREFIX_PLUS_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+[a-z]{2}${HASHLINE_CONTENT_SEPARATOR_RE}`); +const HL_EDIT_SEPARATOR_RE = HL_EDIT_SEP_RE_RAW; +const HL_OUTPUT_PREFIX_SEPARATOR_RE = `[:${HL_BODY_SEP_RE_RAW}]`; +const HL_PREFIX_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*(?:[+*]\\s*)?\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`); +const HL_PREFIX_PLUS_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`); const DIFF_PLUS_RE = /^[+](?![+])/; const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bsel=L?\d+/; -const HASHLINE_HASH_HINT_RE = /^[a-z]{2}$/i; -const HASHLINE_ANCHOR_EXAMPLES = describeAnchorExamples("160"); +const HL_HASH_HINT_RE = /^[a-z]{2}$/i; +const HL_ANCHOR_EXAMPLES = describeAnchorExamples("160"); -const PARSE_TAG_RE = new RegExp(`^${HASHLINE_ANCHOR_RE_SRC}`); -const LID_CAPTURE_RE = new RegExp(`^${HASHLINE_LID_CAPTURE_RE_SRC}$`); +const PARSE_TAG_RE = new RegExp(`^${HL_ANCHOR_RE_RAW}`); +const LID_CAPTURE_RE = new RegExp(`^${HL_HASH_CAPTURE_RE_RAW}$`); // ─────────────────────────────────────────────────────────────────────────── // 4. Small string utilities @@ -157,7 +161,7 @@ function stripLeadingHashlinePrefixes(line: string): string { let previous: string; do { previous = result; - result = result.replace(HASHLINE_PREFIX_RE, ""); + result = result.replace(HL_PREFIX_RE, ""); } while (result !== previous); return result; } @@ -194,8 +198,8 @@ function collectLinePrefixStats(lines: string[]): LinePrefixStats { continue; } stats.nonEmpty++; - if (HASHLINE_PREFIX_RE.test(line)) stats.hashPrefixCount++; - if (HASHLINE_PREFIX_PLUS_RE.test(line)) stats.diffPlusHashPrefixCount++; + if (HL_PREFIX_RE.test(line)) stats.hashPrefixCount++; + if (HL_PREFIX_PLUS_RE.test(line)) stats.diffPlusHashPrefixCount++; if (DIFF_PLUS_RE.test(line)) stats.diffPlusCount++; } return stats; @@ -219,8 +223,8 @@ export function stripNewLinePrefixes(lines: string[]): string[] { .map(line => { if (stripHash) return stripLeadingHashlinePrefixes(line); if (stripPlus) return line.replace(DIFF_PLUS_RE, ""); - if (stats.diffPlusHashPrefixCount > 0 && HASHLINE_PREFIX_PLUS_RE.test(line)) { - return line.replace(HASHLINE_PREFIX_RE, ""); + if (stats.diffPlusHashPrefixCount > 0 && HL_PREFIX_PLUS_RE.test(line)) { + return line.replace(HL_PREFIX_RE, ""); } return line; }); @@ -380,14 +384,14 @@ export async function* streamHashLinesFromUtf8( export function formatFullAnchorRequirement(raw?: string): string { const suffix = typeof raw === "string" ? raw.trim() : ""; - const hashOnlyHint = HASHLINE_HASH_HINT_RE.test(suffix) + const hashOnlyHint = HL_HASH_HINT_RE.test(suffix) ? ` It looks like you supplied only the hash suffix (${JSON.stringify(suffix)}). ` + `Copy the full anchor exactly as shown (for example, "160${suffix}").` : ""; const received = raw === undefined ? "" : ` Received ${JSON.stringify(raw)}.`; return ( `the full anchor exactly as shown by read/search output ` + - `(line number + hash, for example ${HASHLINE_ANCHOR_EXAMPLES})${received}${hashOnlyHint}` + `(line number + hash, for example ${HL_ANCHOR_EXAMPLES})${received}${hashOnlyHint}` ); } @@ -528,7 +532,7 @@ export class HashlineMismatchError extends Error { const text = fileLines[lineNum - 1] ?? ""; const hash = computeLineHash(lineNum, text); const marker = mismatchSet.has(lineNum) ? "*" : " "; - lines.push(`${marker}${lineNum}${hash}${HASHLINE_CONTENT_SEPARATOR}${text}`); + lines.push(`${marker}${lineNum}${hash}${HL_BODY_SEP}${text}`); } return lines.join("\n"); } @@ -586,13 +590,13 @@ export function buildCompactHashlineDiffPreview( switch (kind) { case "+": addedLines++; - return `+${lineNumber}${computeLineHash(lineNumber, content)}${HASHLINE_CONTENT_SEPARATOR}${content}`; + return `+${lineNumber}${computeLineHash(lineNumber, content)}${HL_BODY_SEP}${content}`; case "-": removedLines++; - return `-${lineNumber}--${HASHLINE_CONTENT_SEPARATOR}${content}`; + return `-${lineNumber}--${HL_BODY_SEP}${content}`; default: { const newLineNumber = lineNumber + addedLines - removedLines; - return ` ${newLineNumber}${computeLineHash(newLineNumber, content)}${HASHLINE_CONTENT_SEPARATOR}${content}`; + return ` ${newLineNumber}${computeLineHash(newLineNumber, content)}${HL_BODY_SEP}${content}`; } } }); @@ -615,6 +619,8 @@ const INSERT_BEFORE_OP_RE = /^<\s*(\S+)$/; const INSERT_AFTER_OP_RE = /^\+\s*(\S+)$/; const DELETE_OP_RE = /^-\s*(\S+)$/; const REPLACE_OP_RE = /^=\s*(\S+)$/; +const INLINE_BEFORE_OP_RE = new RegExp(`^<\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`); +const INLINE_AFTER_OP_RE = new RegExp(`^\\+\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`); function cloneCursor(cursor: HashlineCursor): HashlineCursor { if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } }; @@ -632,12 +638,12 @@ function collectPayload( let index = startIndex; while (index < lines.length) { const line = stripTrailingCarriageReturn(lines[index]); - if (!line.startsWith(HASHLINE_CONTENT_SEPARATOR)) break; + if (!line.startsWith(HL_EDIT_SEP)) break; payload.push(line.slice(1)); index++; } if (payload.length === 0 && requirePayload) { - throw new Error(`line ${opLineNum}: + and < operations require at least one |TEXT payload line.`); + throw new Error(`line ${opLineNum}: + and < operations require at least one ${HL_EDIT_SEP}TEXT payload line.`); } return { payload, nextIndex: index }; } @@ -664,10 +670,46 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[] i++; continue; } - if (line.startsWith(HASHLINE_CONTENT_SEPARATOR)) { + if (line.startsWith(HL_EDIT_SEP)) { throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`); } + const inlineBeforeMatch = INLINE_BEFORE_OP_RE.exec(line); + if (inlineBeforeMatch) { + const anchor = parseLid(`${inlineBeforeMatch[1]}${inlineBeforeMatch[2]}`, lineNum); + edits.push({ + kind: "modify", + anchor, + prefix: inlineBeforeMatch[3], + suffix: "", + lineNum, + index: editIndex++, + }); + const cursor: HashlineCursor = { kind: "before_anchor", anchor }; + const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false); + for (const text of payload) pushInsert(cursor, text, lineNum); + i = nextIndex; + continue; + } + + const inlineAfterMatch = INLINE_AFTER_OP_RE.exec(line); + if (inlineAfterMatch) { + const anchor = parseLid(`${inlineAfterMatch[1]}${inlineAfterMatch[2]}`, lineNum); + edits.push({ + kind: "modify", + anchor, + prefix: "", + suffix: inlineAfterMatch[3], + lineNum, + index: editIndex++, + }); + const cursor: HashlineCursor = { kind: "after_anchor", anchor }; + const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false); + for (const text of payload) pushInsert(cursor, text, lineNum); + i = nextIndex; + continue; + } + const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line); if (insertBeforeMatch) { const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before"); @@ -718,7 +760,7 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[] } throw new Error( - `line ${lineNum}: unrecognized op. Use < ANCHOR (insert before), + ANCHOR (insert after), - A..B (delete), = A..B (replace), or "${HASHLINE_CONTENT_SEPARATOR}TEXT" payload lines. ` + + `line ${lineNum}: unrecognized op. Use < ANCHOR (insert before), + ANCHOR (insert after), - A..B (delete), = A..B (replace), or "${HL_EDIT_SEP}TEXT" payload lines. ` + `Got ${JSON.stringify(line)}.`, ); } @@ -753,6 +795,7 @@ interface IndexedEdit { function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] { if (edit.kind === "delete") return [edit.anchor]; + if (edit.kind === "modify") return [edit.anchor]; if (edit.cursor.kind === "before_anchor") return [edit.cursor.anchor]; if (edit.cursor.kind === "after_anchor") return [edit.cursor.anchor]; return []; @@ -852,9 +895,11 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map (deleteLine ? "replacement" : "insert")); - if (!deleteLine) origins[origins.length - 1] = lineOrigins[idx] ?? "original"; + if (!deleteLine) { + origins[origins.length - 1] = modified ? "replacement" : (lineOrigins[idx] ?? "original"); + } fileLines.splice(idx, 1, ...replacement); lineOrigins.splice(idx, 1, ...origins); @@ -1002,7 +1065,7 @@ function stripLeadingBlankLines(input: string): string { function containsRecognizableHashlineOperations(input: string): boolean { for (const rawLine of input.split("\n")) { const line = stripTrailingCarriageReturn(rawLine); - if (/^[+<=-]\s+/.test(line) || line.startsWith(HASHLINE_CONTENT_SEPARATOR)) return true; + if (/^[+<=-]\s+/.test(line) || line.startsWith(HL_EDIT_SEP)) return true; } return false; } @@ -1121,6 +1184,7 @@ async function readHashlineFile(absolutePath: string): Promise { if (edit.kind === "delete") return true; + if (edit.kind === "modify") return true; return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor"; }); } diff --git a/packages/coding-agent/src/edit/renderer.ts b/packages/coding-agent/src/edit/renderer.ts index 6d42d6c44..2bfdbfd2b 100644 --- a/packages/coding-agent/src/edit/renderer.ts +++ b/packages/coding-agent/src/edit/renderer.ts @@ -309,7 +309,7 @@ function getCallPreview( } const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'"; -const HASHLINE_INPUT_HEADER_PREFIX = "@"; +const HL_INPUT_HEADER_PREFIX = "@"; function normalizeHashlineInputPreviewPath(rawPath: string): string { const trimmed = rawPath.trim(); @@ -323,8 +323,8 @@ function normalizeHashlineInputPreviewPath(rawPath: string): string { } function parseHashlineInputPreviewHeader(line: string): string | null { - if (!line.startsWith(HASHLINE_INPUT_HEADER_PREFIX)) return null; - const body = line.slice(HASHLINE_INPUT_HEADER_PREFIX.length).trim(); + if (!line.startsWith(HL_INPUT_HEADER_PREFIX)) return null; + const body = line.slice(HL_INPUT_HEADER_PREFIX.length).trim(); const previewPath = normalizeHashlineInputPreviewPath(body); return previewPath.length > 0 ? previewPath : null; } diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md index 6f516d95e..51a85962d 100644 --- a/packages/coding-agent/src/prompts/tools/hashline.md +++ b/packages/coding-agent/src/prompts/tools/hashline.md @@ -55,6 +55,10 @@ This format is purely textual. The tool has NO awareness of language, indentatio + {{hrefr 4}} {{hsep}} if (clean.length === 0) return DEF; +# Append WITHIN a line +@a.ts ++ {{hrefr 4}}{{hsep}} // first run + # Append to end of file @a.ts + EOF diff --git a/packages/coding-agent/src/prompts/tools/read.md b/packages/coding-agent/src/prompts/tools/read.md index e5966ec6d..100a9c979 100644 --- a/packages/coding-agent/src/prompts/tools/read.md +++ b/packages/coding-agent/src/prompts/tools/read.md @@ -14,7 +14,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil |`sel` value|Behavior| |---|---| -|*(omitted)*|Read full file (up to {{DEFAULT_LIMIT}} lines)| +|_(omitted)_|Read full file (up to {{DEFAULT_LIMIT}} lines)| |`50`|Read from line 50 onward| |`50-200`|Read lines 50-200| |`50+150`|Read 150 lines starting at line 50| @@ -22,21 +22,24 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil # Filesystem - Reading a directory path returns a list of dirents. -{{#if IS_HASHLINE_MODE}} + {{#if IS_HL_MODE}} - Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():` -{{else}} -{{#if IS_LINE_NUMBER_MODE}} + {{else}} + {{#if IS_LINE_NUMBER_MODE}} - Reading a file returns lines prefixed with line numbers: `41|def alpha():` -{{/if}} -{{/if}} + {{/if}} + {{/if}} # Inspection + Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images. # Directories & Archives + Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents. # SQLite Databases + For `.sqlite`, `.sqlite3`, `.db`, `.db3`: - `file.db` — list tables with row counts - `file.db:table` — schema + sample rows @@ -46,6 +49,7 @@ 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. diff --git a/packages/coding-agent/src/prompts/tools/search.md b/packages/coding-agent/src/prompts/tools/search.md index fb2745fd7..4d627d3ed 100644 --- a/packages/coding-agent/src/prompts/tools/search.md +++ b/packages/coding-agent/src/prompts/tools/search.md @@ -7,7 +7,7 @@ Searches files using powerful regex matching. -{{#if IS_HASHLINE_MODE}} +{{#if IS_HL_MODE}} - Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint. {{else}} {{#if IS_LINE_NUMBER_MODE}} diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index ef1f6191a..34417c047 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -4239,9 +4239,10 @@ export class AgentSession { * Failures are swallowed: a memory backend going sideways MUST NOT block * compaction (which is itself the recovery path for context overflow). */ - async #collectMemoryBackendContext( - preparation: { messagesToSummarize: AgentMessage[]; turnPrefixMessages: AgentMessage[] }, - ): Promise { + async #collectMemoryBackendContext(preparation: { + messagesToSummarize: AgentMessage[]; + turnPrefixMessages: AgentMessage[]; + }): Promise { const backend = resolveMemoryBackend(this.settings); if (!backend.preCompactionContext) return undefined; const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages); diff --git a/packages/coding-agent/src/tools/ast-edit.ts b/packages/coding-agent/src/tools/ast-edit.ts index 1e0d9166d..7cd23d3a3 100644 --- a/packages/coding-agent/src/tools/ast-edit.ts +++ b/packages/coding-agent/src/tools/ast-edit.ts @@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils"; import { type Static, Type } from "@sinclair/typebox"; -import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash"; +import { computeLineHash, HL_BODY_SEP } from "../edit/line-hash"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import type { Theme } from "../modes/theme/theme"; import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" }; @@ -330,7 +330,7 @@ export class AstEditTool implements AgentTool { this.description = prompt.render(readDescription, { DEFAULT_LIMIT: String(this.#defaultLimit), DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES), - IS_HASHLINE_MODE: displayMode.hashLines, + IS_HL_MODE: displayMode.hashLines, IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers, }); } diff --git a/packages/coding-agent/src/tools/search.ts b/packages/coding-agent/src/tools/search.ts index cf711feb4..0da140a63 100644 --- a/packages/coding-agent/src/tools/search.ts +++ b/packages/coding-agent/src/tools/search.ts @@ -87,7 +87,7 @@ export class SearchTool implements AgentTool { const startSpy = vi .spyOn(pythonKernel.PythonKernel, "start") .mockResolvedValueOnce(unrelatedKernel as unknown as PythonKernelInstance); - vi.spyOn(memories, "startMemoryStartupTask").mockImplementation(() => { - throw new Error("Memory startup failed"); + const throwingRegistry = new AgentRegistry(); + vi.spyOn(throwingRegistry, "register").mockImplementation(() => { + throw new Error("Agent registry failed"); }); await pythonExecutor.executePython("print('unrelated after')", { @@ -216,7 +217,7 @@ describe("AgentSession python cleanup", () => { cwd, agentDir: tempDir, sessionManager: SessionManager.inMemory(cwd), - settings: Settings.isolated({ "python.kernelMode": "session" }), + settings: Settings.isolated({ "python.kernelMode": "session", "memory.backend": "local" }), model: getModel(), disableExtensionDiscovery: true, skills: [], @@ -226,8 +227,9 @@ describe("AgentSession python cleanup", () => { enableMCP: false, enableLsp: false, toolNames: ["eval"], + agentRegistry: throwingRegistry, }), - ).rejects.toThrow("Memory startup failed"); + ).rejects.toThrow("Agent registry failed"); expect(startSpy).toHaveBeenCalledTimes(1); expect(unrelatedKernel.shutdown).not.toHaveBeenCalled(); diff --git a/packages/coding-agent/test/core/hashline.test.ts b/packages/coding-agent/test/core/hashline.test.ts index 5244a07ed..570f58bcf 100644 --- a/packages/coding-agent/test/core/hashline.test.ts +++ b/packages/coding-agent/test/core/hashline.test.ts @@ -10,11 +10,12 @@ import { type ExecuteHashlineSingleOptions, executeHashlineSingle, generateDiffString, - HASHLINE_CONTENT_SEPARATOR, HashlineMismatchError, + HL_BODY_SEP, + HL_BODY_SEP_RE_RAW, + HL_EDIT_SEP, hashlineEditParamsSchema, parseHashline, - parseHashlineWithWarnings, splitHashlineInput, splitHashlineInputs, } from "@oh-my-pi/pi-coding-agent/edit"; @@ -28,10 +29,12 @@ beforeAll(async () => { // Single source of truth for the payload separator under test. Every literal // payload line in this file goes through `pl()` so flipping -// `HASHLINE_CONTENT_SEPARATOR` (e.g. to ">" or "\\") flips the test inputs in +// `HL_EDIT_SEP` (e.g. to ">" or "\\") flips the test inputs in // lockstep without any `|`-vs-`>` churn. -const sep = HASHLINE_CONTENT_SEPARATOR; +const sep = HL_EDIT_SEP; const pl = (text: string): string => `${sep}${text}`; +const outputSep = HL_BODY_SEP; +const outputSepRe = HL_BODY_SEP_RE_RAW; function tag(line: number, content: string): string { return `${line}${computeLineHash(line, content)}`; @@ -117,13 +120,7 @@ describe("hashline parser — block op syntax", () => { }); it("preserves payload text exactly after the first separator", () => { - const diff = [ - `= ${tag(2, "bbb")}`, - pl(""), - pl("# not a header"), - pl("+ not an op"), - pl(" spaced"), - ].join("\n"); + const diff = [`= ${tag(2, "bbb")}`, pl(""), pl("# not a header"), pl("+ not an op"), pl(" spaced")].join("\n"); expect(applyDiff(content, diff)).toBe("aaa\n\n# not a header\n+ not an op\n spaced\nccc"); }); @@ -138,55 +135,63 @@ describe("hashline parser — block op syntax", () => { }); }); -describe("hashline parser — common symbol prefix auto-strip", () => { - const before = "aaa\nbbb\nccc\nddd"; +describe("hashline parser — inline modify syntax", () => { + const content = "alpha\nbeta\ngamma"; - it("strips a doubled separator when every payload line shares it", () => { - const diff = [`= ${tag(2, "bbb")}..${tag(3, "ccc")}`, pl(pl("X")), pl(pl("Y"))].join("\n"); - const { edits, warnings } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe("aaa\nX\nY\nddd"); - expect(warnings.some(w => /auto-stripped extra/.test(w))).toBe(true); + it("prepends text to the anchored line via `< ANCHORTEXT`", () => { + const diff = `< ${tag(2, "beta")}${pl("// ")}`; + expect(applyDiff(content, diff)).toBe("alpha\n// beta\ngamma"); }); - it("strips an arbitrary symbol prefix (e.g. `>>`) shared by every payload line", () => { - // Model legitimately uses the configured separator but tacks on a `>>` - // markdown decoration on every line. Both layers must come off. - const diff = [`= ${tag(2, "bbb")}..${tag(3, "ccc")}`, pl(">>X"), pl(">>Y")].join("\n"); - const { edits, warnings } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe("aaa\nX\nY\nddd"); - expect(warnings.some(w => /auto-stripped extra ">>"/.test(w))).toBe(true); + it("appends text to the anchored line via `+ ANCHORTEXT`", () => { + const diff = `+ ${tag(2, "beta")}${pl(" // tag")}`; + expect(applyDiff(content, diff)).toBe("alpha\nbeta // tag\ngamma"); }); - it("preserves indentation: whitespace-only common prefix is never stripped", () => { - const diff = [`= ${tag(2, "bbb")}..${tag(3, "ccc")}`, pl("\tindented1"), pl("\tindented2")].join("\n"); - const { edits, warnings } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe("aaa\n\tindented1\n\tindented2\nddd"); - expect(warnings).toEqual([]); + it("combines a prepend and an append on the same line", () => { + const diff = [`< ${tag(2, "beta")}${pl("[")}`, `+ ${tag(2, "beta")}${pl("]")}`].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\n[beta]\ngamma"); }); - it("leaves payload alone when payload lines disagree at the first symbol", () => { - const diff = [`= ${tag(2, "bbb")}..${tag(3, "ccc")}`, pl("+first"), pl("*second")].join("\n"); - const { edits, warnings } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe("aaa\n+first\n*second\nddd"); - expect(warnings).toEqual([]); + it("stacks multiple prepends with later edits wrapping earlier ones", () => { + const diff = [`< ${tag(2, "beta")}${pl("A")}`, `< ${tag(2, "beta")}${pl("B")}`].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\nBAbeta\ngamma"); }); - it("leaves a single payload line alone (heuristic requires ≥2 non-empty lines)", () => { - const diff = [`= ${tag(2, "bbb")}`, pl(pl("only"))].join("\n"); - const { edits, warnings } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe(`aaa\n${sep}only\nccc\nddd`); - expect(warnings).toEqual([]); + it("stacks multiple appends with later edits wrapping earlier ones", () => { + const diff = [`+ ${tag(2, "beta")}${pl("A")}`, `+ ${tag(2, "beta")}${pl("B")}`].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\nbetaAB\ngamma"); }); - it("preserves blank payload lines when stripping a shared prefix from non-blank ones", () => { - const diff = [ - `= ${tag(2, "bbb")}..${tag(3, "ccc")}`, - pl(pl("first")), - pl(""), // blank payload — no doubled prefix to share - pl(pl("third")), - ].join("\n"); - const { edits } = parseHashlineWithWarnings(diff); - expect(applyHashlineEdits(before, edits).lines).toBe("aaa\nfirst\n\nthird\nddd"); + it("appends inline AND inserts payload lines after the modified line", () => { + const diff = [`+ ${tag(2, "beta")}${pl(" // tag")}`, pl("inserted-after-1"), pl("inserted-after-2")].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\nbeta // tag\ninserted-after-1\ninserted-after-2\ngamma"); + }); + + it("prepends inline AND inserts payload lines before the modified line", () => { + const diff = [`< ${tag(2, "beta")}${pl("// ")}`, pl("inserted-before-1"), pl("inserted-before-2")].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\ninserted-before-1\ninserted-before-2\n// beta\ngamma"); + }); + + it("allows a block insert-before to coexist with an inline modify on the same line", () => { + const diff = [`< ${tag(2, "beta")}`, pl("// note"), `+ ${tag(2, "beta")}${pl("!")}`].join("\n"); + expect(applyDiff(content, diff)).toBe("alpha\n// note\nbeta!\ngamma"); + }); + + it("rejects combining inline modify with a delete on the same line", () => { + const diff = [`- ${tag(2, "beta")}`, `+ ${tag(2, "beta")}${pl("!")}`].join("\n"); + expect(() => applyDiff(content, diff)).toThrow(/cannot combine inline modify/); + }); + + it("validates the anchor hash for inline modify just like other ops", () => { + const diff = `+ ${mistag(2, "beta")}${pl("!")}`; + expect(() => applyDiff(content, diff)).toThrow(HashlineMismatchError); + }); + + it("treats an empty inline payload as a no-op when nothing else follows", () => { + const diff = `+ ${tag(2, "beta")}${pl("")}`; + const result = applyHashlineEdits(content, parseHashline(diff)); + expect(result.lines).toBe(content); }); }); @@ -275,14 +280,9 @@ describe("hashline executor", () => { const bPath = path.join(tempDir, "b.ts"); await Bun.write(aPath, "aaa\n"); await Bun.write(bPath, "bbb\n"); - const input = [ - "@a.ts", - `= ${tag(1, "aaa")}`, - pl("AAA"), - "@b.ts", - `= ${mistag(1, "bbb")}`, - pl("BBB"), - ].join("\n"); + const input = ["@a.ts", `= ${tag(1, "aaa")}`, pl("AAA"), "@b.ts", `= ${mistag(1, "bbb")}`, pl("BBB")].join( + "\n", + ); await expect(executeHashlineSingle(hashlineExecuteOptions(tempDir, input))).rejects.toThrow( /changed since the last read/, @@ -310,16 +310,14 @@ describe("buildCompactHashlineDiffPreview — anchors track post-edit line numbe const { diff } = generateDiffString(before, after); const preview = buildCompactHashlineDiffPreview(diff); - // Walk the preview and verify every ` LINE+HASH${sep}content` line matches what + // Walk the preview and verify every ` LINE+HASH${outputSep}content` line matches what // the file now has at that line number. const newFileLines = after.split("\n"); for (const line of preview.preview.split("\n")) { if (!line.startsWith(" ")) continue; // Skip context-elision markers ("...") which carry no real file content. - if (line.endsWith(`${sep}...`)) continue; - const match = new RegExp(`^\\s(\\d+)([a-z]{2})${sep.replace(/[.*+?^${}()|[\\\]\\\\]/g, "\\$&")}(.*)$`).exec( - line, - ); + if (line.endsWith(`${outputSep}...`)) continue; + const match = new RegExp(`^\\s(\\d+)([a-z]{2})${outputSepRe}(.*)$`).exec(line); expect(match).not.toBeNull(); if (!match) continue; const lineNum = Number(match[1]); @@ -338,11 +336,11 @@ describe("buildCompactHashlineDiffPreview — anchors track post-edit line numbe const additions = preview.preview.split("\n").filter(line => line.startsWith("+")); expect(additions).toEqual([ - `+2${computeLineHash(2, "DELTA")}${sep}DELTA`, - `+3${computeLineHash(3, "EPSILON")}${sep}EPSILON`, + `+2${computeLineHash(2, "DELTA")}${outputSep}DELTA`, + `+3${computeLineHash(3, "EPSILON")}${outputSep}EPSILON`, ]); const removals = preview.preview.split("\n").filter(line => line.startsWith("-")); - expect(removals).toEqual([`-2--${sep}beta`]); + expect(removals).toEqual([`-2--${outputSep}beta`]); }); }); diff --git a/packages/coding-agent/test/edit-diff.test.ts b/packages/coding-agent/test/edit-diff.test.ts index cf4f70533..09b1efbd3 100644 --- a/packages/coding-agent/test/edit-diff.test.ts +++ b/packages/coding-agent/test/edit-diff.test.ts @@ -9,6 +9,7 @@ import { DEFAULT_FUZZY_THRESHOLD, findMatch, } from "@oh-my-pi/pi-coding-agent/edit"; +import { HL_EDIT_SEP } from "@oh-my-pi/pi-coding-agent/edit/line-hash"; describe("findMatch", () => { describe("exact matching", () => { @@ -245,7 +246,7 @@ describe("computeHashlineDiff", () => { const sourcePath = path.join(tempDir, "source.txt"); await Bun.write(sourcePath, "first\n"); - const result = await computeHashlineDiff({ input: `@${sourcePath}\n+ EOF\n|second` }, tempDir); + const result = await computeHashlineDiff({ input: `@${sourcePath}\n+ EOF\n${HL_EDIT_SEP}second` }, tempDir); expect("diff" in result).toBe(true); if ("diff" in result) { expect(result.diff).toContain("second"); diff --git a/packages/typescript-edit-benchmark/src/runner.ts b/packages/typescript-edit-benchmark/src/runner.ts index 5b57180eb..f11ce13e2 100644 --- a/packages/typescript-edit-benchmark/src/runner.ts +++ b/packages/typescript-edit-benchmark/src/runner.ts @@ -264,7 +264,7 @@ function countEditFailureCategories(runs: TaskRunResult[]): Record { - const counts: Record = Object.fromEntries(HASHLINE_SUBTYPES.map(k => [k, 0])); + const counts: Record = Object.fromEntries(HL_SUBTYPES.map(k => [k, 0])); if (!args || typeof args !== "object") return counts; const edits = (args as { edits?: unknown[] }).edits; if (!Array.isArray(edits)) return counts; for (const edit of edits) { if (!edit || typeof edit !== "object") continue; - for (const key of HASHLINE_SUBTYPES) { + for (const key of HL_SUBTYPES) { if (key in edit) { counts[key]++; break; @@ -984,7 +984,7 @@ async function runSingleTask( editAutocorrects: 0, totalInputChars: 0, }; - const hashlineSubtypes: Record = Object.fromEntries(HASHLINE_SUBTYPES.map(k => [k, 0])); + const hashlineSubtypes: Record = Object.fromEntries(HL_SUBTYPES.map(k => [k, 0])); const logFile = path.join(TMP, `run-${task.id}-${runIndex}.jsonl`); const logEvent = async (event: unknown) => { @@ -1202,7 +1202,7 @@ async function runSingleTask( pendingEdits.delete(e.toolCallId); if (config.editVariant === "hashline" && args) { const counts = countHashlineEditSubtypes(args); - for (const key of HASHLINE_SUBTYPES) { + for (const key of HL_SUBTYPES) { hashlineSubtypes[key] += counts[key]; } } @@ -1384,7 +1384,7 @@ async function _runRpcBenchmarkRun( editAutocorrects: 0, totalInputChars: 0, }; - const hashlineSubtypes: Record = Object.fromEntries(HASHLINE_SUBTYPES.map(k => [k, 0])); + const hashlineSubtypes: Record = Object.fromEntries(HL_SUBTYPES.map(k => [k, 0])); const logFile = path.join(sessionDir, `run-${task.id}-${runIndex}.jsonl`); const logEvent = async (event: unknown) => { @@ -1528,7 +1528,7 @@ async function _runRpcBenchmarkRun( pendingEdits.delete(e.toolCallId); if (config.editVariant === "hashline" && args) { const counts = countHashlineEditSubtypes(args); - for (const key of HASHLINE_SUBTYPES) { + for (const key of HL_SUBTYPES) { hashlineSubtypes[key] += counts[key]; } } @@ -2176,10 +2176,7 @@ export function buildBenchmarkResult(params: { const hashlineEditSubtypes: Record | undefined = params.config.editVariant === "hashline" ? Object.fromEntries( - HASHLINE_SUBTYPES.map(key => [ - key, - allRuns.reduce((sum, r) => sum + (r.hashlineEditSubtypes?.[key] ?? 0), 0), - ]), + HL_SUBTYPES.map(key => [key, allRuns.reduce((sum, r) => sum + (r.hashlineEditSubtypes?.[key] ?? 0), 0)]), ) : undefined;