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:
+9
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createContext, Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
||||
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
||||
import { createChatTransport } from "@/components/chat-transport";
|
||||
import { useStickToBottomContext } from "use-stick-to-bottom";
|
||||
import {
|
||||
@@ -34,7 +34,7 @@ import { QuestionDock } from "@/components/question-dock";
|
||||
import { SidebarToggle } from "@/components/sidebar";
|
||||
import type { ArtifactPartProps, JobRecord, Phase3, QuestionAnswerData, TeacherUIMessage, TurnUsage } from "@/src/core";
|
||||
import { PhaseChip, PhaseDivider, planNodeCount } from "@/components/phase-divider";
|
||||
import { findPendingToolCall, phaseAt3, phaseTimeline } from "@/src/core";
|
||||
import { findPendingToolCall, phaseAt3, phaseTimeline, streamingToolActivity } from "@/src/core";
|
||||
|
||||
/** One width for everything: transcript, cards, and the bottom blob share this column. */
|
||||
/**
|
||||
@@ -262,12 +262,12 @@ export function Chat({
|
||||
// session's current phase.
|
||||
const phases = useMemo(() => phaseTimeline(messages), [messages]);
|
||||
|
||||
// A tool call still streaming its input: the teacher is writing a question.
|
||||
// Shown as a transcript shimmer, exactly like the thinking indicator.
|
||||
const questionStreaming =
|
||||
!pending &&
|
||||
messages.at(-1)?.role === "assistant" &&
|
||||
(messages.at(-1)?.parts.some((p) => isToolUIPart(p) && p.state === "input-streaming") ?? false);
|
||||
// A tool call still streaming its input: the teacher is mid-activity. The
|
||||
// label names the ACTUAL tool (a long grep used to shimmer as “Writing a
|
||||
// question…” for minutes — a part stays input-streaming through its whole
|
||||
// execution, see streamingToolActivity); shown as the transcript shimmer,
|
||||
// exactly like the thinking indicator.
|
||||
const streamingActivity = !pending ? streamingToolActivity(messages) : null;
|
||||
|
||||
const [text, setText] = useState("");
|
||||
// Voice dictation (STT wave, 2026-09-17): finals append at the end of
|
||||
@@ -941,7 +941,7 @@ export function Chat({
|
||||
))}
|
||||
|
||||
{(status === "submitted" || turnInFlight) && <Shimmer className="text-sm">Thinking…</Shimmer>}
|
||||
{questionStreaming && <Shimmer className="text-sm">Writing a question…</Shimmer>}
|
||||
{streamingActivity && <Shimmer className="text-sm">{streamingActivity}</Shimmer>}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm">
|
||||
Something went wrong: {error.message}
|
||||
|
||||
Reference in New Issue
Block a user