fix(tools): the abort cascade — kill running tool calls, honest activity labels
Root cause of the live-test bugs (2026-09-19): pi passes every run's
AbortSignal to tool execute, but toPiTool dropped it — agent.abort()
only landed at the next step boundary, so a long local grep held its
turn hostage until the dev server's memory restart killed the run
mid-tool, freezing the record at its write-ahead state (the 'went
back in time' rollback).
- lh-tool: execute ctx gains signal (parent + timeout combined); every
call races a 30s system-level timeout with a 250ms cooperative grace —
abort kills a stuck tool but lets a poll-the-signal tool deliver its
partial results. Pre-aborted signals never fire listeners: checked
upfront. toolTimeoutMs is the test seam (pdfModelOverride idiom).
- fs-shared: walkFiles returns {timedOut, aborted} instead of throwing —
abort yields partial results.
- grep: the scan phase was unbounded (the walk's 10s deadline only
bounded collection) — same budget now checked per file; find/grep
check the signal and return partials with a notice.
- pending/chat: streamingToolActivity names the ACTUAL running tool —
a part sits input-streaming through its whole execution, so the old
shimmer called minutes-long greps 'Writing a question…'.
Tests: toPiTool guard (forward, abort, timeout, grace, question
bypass, ToolInputError passthrough), walk mid-abort, find/grep
pre-abort + mid-run abort, activity labels. Gates: 617/617.
This commit is contained in:
@@ -83,4 +83,42 @@ export function findPendingToolCall(messages: UIMessage[]): PendingToolCall | nu
|
||||
return findPending(messages, QUESTION_TOOLS);
|
||||
}
|
||||
|
||||
// ─── Turn activity label (2026-09-19, the “Writing a question…” fix) ──────
|
||||
|
||||
/**
|
||||
* The transcript's turn-activity shimmer text: what the teacher is
|
||||
* doing RIGHT NOW. A tool part sits in `input-streaming` for its whole
|
||||
* execution (the bridge defers `tool-input-available` to
|
||||
* tool_execution_end — the invalid-input gate for the question dock), so
|
||||
* keying the old “Writing a question…” shimmer on ANY streaming tool
|
||||
* part mislabeled minutes-long local greps as question-writing. The label
|
||||
* now names the actual activity; chat.tsx gates it on `!pending` and
|
||||
* renders it as the quiet transcript shimmer.
|
||||
*/
|
||||
const ACTIVITY_LABELS: Record<string, string> = {
|
||||
read: "Reading a file…",
|
||||
ls: "Listing a directory…",
|
||||
find: "Finding files…",
|
||||
grep: "Searching file contents…",
|
||||
research: "Dispatching research…",
|
||||
demo: "Building a demo…",
|
||||
visual: "Making a visual…",
|
||||
pdf: "Reading a PDF…",
|
||||
plan_init: "Drawing up the plan…",
|
||||
probe_done: "Concluding the assessment…",
|
||||
};
|
||||
|
||||
export function streamingToolActivity(messages: UIMessage[]): string | null {
|
||||
const last = messages.at(-1);
|
||||
if (!last || last.role !== "assistant") return null;
|
||||
for (const part of last.parts) {
|
||||
if (isToolUIPart(part) && part.state === "input-streaming") {
|
||||
const tool = part.type.slice("tool-".length);
|
||||
if ((QUESTION_TOOLS as readonly string[]).includes(tool)) return "Writing a question…";
|
||||
return ACTIVITY_LABELS[tool] ?? "Working…";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user