feat(agent): align the document tools with the harness-trained shape — ls, read(path), grep(pattern, path?)
This commit is contained in:
+53
-23
@@ -56,21 +56,24 @@
|
||||
* phase 34 task 02) clears the key + the list back to the empty state.
|
||||
*
|
||||
* Agent tool calls (phase 37, PLAN §4 extension; phase 68 added
|
||||
* search_documents): a grounded turn may call the three server-side
|
||||
* document tools (list_documents / read_document / search_documents,
|
||||
* bounded only by the round cap — phases 45/68). Each call streams a
|
||||
* `tool` SSE frame, and the UI shows the "calling tool" state IN
|
||||
* ADDITION to "thinking": the UI state itself stays "thinking" (the
|
||||
* button stays the enabled "Stop" control — phase 48 — never stale,
|
||||
* PLAN §7.4) while the STATUS LABELS change — the #send-status +
|
||||
* typing-indicator labels say what Brain is doing ("…is listing
|
||||
* documents" / "…is reading source/path" / "…is searching for
|
||||
* pattern" — the name prefix resolves from window.BOR_BRAND at call
|
||||
* time, phase 39) — the button no longer relabels to "Calling tool…"
|
||||
* (phase 48, owner-locked: it stays "Stop" for the whole turn) — and a
|
||||
* visible `.tool-call` line (own icon + accent color, distinct from
|
||||
* the brand-ink Thinking block) is appended above the answer, one per
|
||||
* call, in order.
|
||||
* search_documents; phase 70 remapped the surface to the harness
|
||||
* names ls / read(path) / grep(pattern, path?)): a grounded turn may
|
||||
* call the three server-side document tools, bounded only by the
|
||||
* round cap (phases 45/68). Each call streams a `tool` SSE frame, and
|
||||
* the UI shows the "calling tool" state IN ADDITION to "thinking": the
|
||||
* UI state itself stays "thinking" (the button stays the enabled
|
||||
* "Stop" control — phase 48 — never stale, PLAN §7.4) while the STATUS
|
||||
* LABELS change — the #send-status + typing-indicator labels say what
|
||||
* Brain is doing ("…is listing documents" / "…is reading source/path"
|
||||
* / "…is searching for pattern" — the name prefix resolves from
|
||||
* window.BOR_BRAND at call time, phase 39) — the button no longer
|
||||
* relabels to "Calling tool…" (phase 48, owner-locked: it stays
|
||||
* "Stop" for the whole turn) — and a visible `.tool-call` line (own
|
||||
* icon + accent color, distinct from the brand-ink Thinking block) is
|
||||
* appended above the answer, one per call, in order. Phase 70: the
|
||||
* line/label branches key off the NEW names and still carry the legacy
|
||||
* ones (list_documents / read_document / search_documents) — persisted
|
||||
* turns from before the remap render exactly as before (no migration).
|
||||
* Append-only like thinking: frames are tolerated in any interleaving
|
||||
* (a frame after the first delta just appends — the agent loop never
|
||||
* emits one, but it must not crash). The turn record persists an
|
||||
@@ -810,9 +813,22 @@ function closeThinkingBlock(wrap) {
|
||||
* interleaving with thinking frames, even after the first delta (the
|
||||
* agent loop never emits one, but a late frame must not crash) — just
|
||||
* append another line, in order. The SAME helper re-renders the
|
||||
* persisted lines on restore (phase 14 convention): the path/pattern
|
||||
* argument goes through textContent, so nothing HTML-shaped can come
|
||||
* from storage. Lines are not interactive (no focus targets). */
|
||||
* persisted lines on restore (phase 14 convention): every argument
|
||||
* (path / pattern / source scope) goes through textContent, so nothing
|
||||
* HTML-shaped can come from storage. Lines are not interactive (no
|
||||
* focus targets).
|
||||
*
|
||||
* Phase 70 (owner permission 2026-09-03): the server tools were remapped
|
||||
* to the harness-aligned surface — ls / read(path) / grep(pattern,
|
||||
* path?) — so the NEW names get their own lines (read → the Reading
|
||||
* line, grep → the Searching-for line, ls → the Listing-documents line,
|
||||
* a scoped ls → the Listing-documents-in-<scope> line), and the
|
||||
* pre-phase-70 names (list_documents / read_document /
|
||||
* search_documents) still render EXACTLY as before: persisted turns
|
||||
* (phase 14) carry the old names, so both generations render — no
|
||||
* migration. The content marks (the read/search/list glyphs) stay the
|
||||
* exact tool-line template literals — the frontend emoji guard strips
|
||||
* precisely those in this file. */
|
||||
function appendToolLine(wrap, name, argument) {
|
||||
const body = wrap?.querySelector?.(".msg-body");
|
||||
if (!body) return;
|
||||
@@ -829,16 +845,24 @@ function appendToolLine(wrap, name, argument) {
|
||||
const line = document.createElement("span");
|
||||
line.className = "tool-call";
|
||||
line.setAttribute("role", "listitem");
|
||||
if (name === "read_document" && argument) {
|
||||
// Phase 70: new names first, legacy names kept — a restored turn saved
|
||||
// before the remap (read_document / search_documents / list_documents)
|
||||
// renders byte-identical to before (no migration).
|
||||
if ((name === "read" || name === "read_document") && argument) {
|
||||
line.textContent = "📄 Reading ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the path is data, never markup
|
||||
line.appendChild(code);
|
||||
} else if (name === "search_documents" && argument) {
|
||||
} else if ((name === "grep" || name === "search_documents") && argument) {
|
||||
line.textContent = "🔎 Searching for ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the pattern is data, never markup
|
||||
line.appendChild(code);
|
||||
} else if (name === "ls" && argument) {
|
||||
line.textContent = "🔎 Listing documents in ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the source scope is data, never markup
|
||||
line.appendChild(code);
|
||||
} else {
|
||||
line.textContent = "🔎 Listing documents";
|
||||
}
|
||||
@@ -1940,12 +1964,18 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
toolAcc.push({ name, argument });
|
||||
clearTurnTimeout(); // the stream is alive — a frame arrived
|
||||
if (!wrap) wrap = addMessage("brain", "");
|
||||
// Phase 70: the harness-aligned names (read/grep/ls) map to the
|
||||
// same status copy as their legacy counterparts (read_document /
|
||||
// search_documents) — a pre-remap frame keeps its label; the
|
||||
// scoped ls mirrors the scoped tool line.
|
||||
const toolStatus =
|
||||
name === "read_document" && argument
|
||||
(name === "read" || name === "read_document") && argument
|
||||
? `${brand()} is reading ${argument}`
|
||||
: name === "search_documents" && argument
|
||||
: (name === "grep" || name === "search_documents") && argument
|
||||
? `${brand()} is searching for ${argument}`
|
||||
: `${brand()} is listing documents`;
|
||||
: name === "ls" && argument
|
||||
? `${brand()} is listing documents in ${argument}`
|
||||
: `${brand()} is listing documents`;
|
||||
if (uiState === UI_STATE.thinking) {
|
||||
sendStatus.textContent = toolStatus;
|
||||
document
|
||||
|
||||
@@ -133,14 +133,22 @@ function addThinkingBlock(wrap, thinking) {
|
||||
body.insertBefore(block, body.querySelector(".bubble"));
|
||||
}
|
||||
|
||||
/* Tool-call lines (phase 37) — the local copy of the chat page's
|
||||
* appendToolLine: one visible "calling tool" row per saved
|
||||
* {name, argument} record, in saved order, above the answer. The
|
||||
* path argument goes through textContent, so nothing HTML-shaped can
|
||||
* come from storage. Lines are not interactive (no focus targets).
|
||||
* The two content marks (the read glyph / the list glyph) are the
|
||||
* exact app.js template strings — the frontend emoji guard strips
|
||||
* precisely those two literals in this file, as in app.js. */
|
||||
/* Tool-call lines (phase 37; phase 70 remapped the tool names to the
|
||||
* harness surface ls / read(path) / grep(pattern, path?)) — the local
|
||||
* copy of the chat page's appendToolLine: one visible "calling tool"
|
||||
* row per saved {name, argument} record, in saved order, above the
|
||||
* answer. Every argument (path / pattern / source scope) goes through
|
||||
* textContent, so nothing HTML-shaped can come from storage. Lines
|
||||
* are not interactive (no focus targets). Phase 70: the NEW names
|
||||
* render (read → the Reading line, grep → the Searching-for line,
|
||||
* ls → the Listing-documents line, scoped ls → the
|
||||
* Listing-documents-in-<scope> line), and the pre-phase-70 names
|
||||
* (read_document / search_documents / list_documents) still render
|
||||
* exactly as before — a row saved before the remap keeps its exact
|
||||
* line (no migration). The content marks are the exact app.js
|
||||
* template strings — the frontend emoji guard (tests/integration/
|
||||
* test_api.py) strips precisely those literals in this file, as in
|
||||
* app.js. */
|
||||
function addToolLines(wrap, tools) {
|
||||
if (!Array.isArray(tools) || !tools.length) return;
|
||||
const body = wrap.querySelector(".msg-body");
|
||||
@@ -156,11 +164,25 @@ function addToolLines(wrap, tools) {
|
||||
line.setAttribute("role", "listitem");
|
||||
const argument =
|
||||
typeof t.argument === "string" && t.argument ? t.argument : null;
|
||||
if (t.name === "read_document" && argument) {
|
||||
// Phase 70: new names first, legacy names kept — a conversation
|
||||
// saved before the remap renders byte-identical (no migration).
|
||||
if ((t.name === "read" || t.name === "read_document") && argument) {
|
||||
line.textContent = "📄 Reading ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the path is data, never markup
|
||||
line.appendChild(code);
|
||||
} else if (
|
||||
(t.name === "grep" || t.name === "search_documents") && argument
|
||||
) {
|
||||
line.textContent = "🔎 Searching for ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the pattern is data, never markup
|
||||
line.appendChild(code);
|
||||
} else if (t.name === "ls" && argument) {
|
||||
line.textContent = "🔎 Listing documents in ";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the source scope is data, never markup
|
||||
line.appendChild(code);
|
||||
} else {
|
||||
line.textContent = "🔎 Listing documents";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user