fix: clear matched tool errors and dedupe reasoning end

This commit is contained in:
Vignesh Natarajan
2026-02-18 23:33:55 -08:00
committed by Vignesh
parent 221d50bc18
commit 0ff506140d
6 changed files with 87 additions and 4 deletions

View File

@@ -30,6 +30,14 @@ const stripTrailingDirective = (text: string): string => {
return text.slice(0, openIndex);
};
function emitReasoningEnd(ctx: EmbeddedPiSubscribeContext) {
if (!ctx.state.reasoningStreamOpen) {
return;
}
ctx.state.reasoningStreamOpen = false;
void ctx.params.onReasoningEnd?.();
}
export function resolveSilentReplyFallbackText(params: {
text: string;
messagingToolSentTexts: string[];
@@ -83,6 +91,9 @@ export function handleMessageUpdate(
const evtType = typeof assistantRecord?.type === "string" ? assistantRecord.type : "";
if (evtType === "thinking_start" || evtType === "thinking_delta" || evtType === "thinking_end") {
if (evtType === "thinking_start" || evtType === "thinking_delta") {
ctx.state.reasoningStreamOpen = true;
}
const thinkingDelta = typeof assistantRecord?.delta === "string" ? assistantRecord.delta : "";
const thinkingContent =
typeof assistantRecord?.content === "string" ? assistantRecord.content : "";
@@ -101,7 +112,10 @@ export function handleMessageUpdate(
ctx.emitReasoningStream(partialThinking || thinkingContent || thinkingDelta);
}
if (evtType === "thinking_end") {
void ctx.params.onReasoningEnd?.();
if (!ctx.state.reasoningStreamOpen) {
ctx.state.reasoningStreamOpen = true;
}
emitReasoningEnd(ctx);
}
return;
}
@@ -166,9 +180,12 @@ export function handleMessageUpdate(
if (next) {
const wasThinking = ctx.state.partialBlockState.thinking;
const visibleDelta = chunk ? ctx.stripBlockTags(chunk, ctx.state.partialBlockState) : "";
if (!wasThinking && ctx.state.partialBlockState.thinking) {
ctx.state.reasoningStreamOpen = true;
}
// Detect when thinking block ends (</think> tag processed)
if (wasThinking && !ctx.state.partialBlockState.thinking) {
void ctx.params.onReasoningEnd?.();
emitReasoningEnd(ctx);
}
const parsedDelta = visibleDelta ? ctx.consumePartialReplyDirectives(visibleDelta) : null;
const parsedFull = parseReplyDirectives(stripTrailingDirective(next));
@@ -414,4 +431,5 @@ export function handleMessageEnd(
ctx.state.blockState.inlineCode = createInlineCodeState();
ctx.state.lastStreamedAssistant = undefined;
ctx.state.lastStreamedAssistantCleaned = undefined;
ctx.state.reasoningStreamOpen = false;
}

View File

@@ -52,6 +52,7 @@ export type EmbeddedPiSubscribeState = {
emittedAssistantUpdate: boolean;
lastStreamedReasoning?: string;
lastBlockReplyText?: string;
reasoningStreamOpen: boolean;
assistantMessageIndex: number;
lastAssistantTextMessageIndex: number;
lastAssistantTextNormalized?: string;

View File

@@ -251,6 +251,59 @@ describe("subscribeEmbeddedPiSession", () => {
expect(onReasoningEnd).toHaveBeenCalledTimes(1);
});
it("emits reasoning end once when native and tagged reasoning end overlap", () => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {
handler = fn;
return () => {};
},
};
const onReasoningEnd = vi.fn();
subscribeEmbeddedPiSession({
session: session as unknown as Parameters<typeof subscribeEmbeddedPiSession>[0]["session"],
runId: "run",
reasoningMode: "stream",
onReasoningStream: vi.fn(),
onReasoningEnd,
});
handler?.({ type: "message_start", message: { role: "assistant" } });
handler?.({
type: "message_update",
message: { role: "assistant" },
assistantMessageEvent: {
type: "text_delta",
delta: "<think>Checking",
},
});
handler?.({
type: "message_update",
message: {
role: "assistant",
content: [{ type: "thinking", thinking: "Checking" }],
},
assistantMessageEvent: {
type: "thinking_end",
},
});
handler?.({
type: "message_update",
message: { role: "assistant" },
assistantMessageEvent: {
type: "text_delta",
delta: " files</think>\nFinal answer",
},
});
expect(onReasoningEnd).toHaveBeenCalledTimes(1);
});
it("emits delta chunks in agent events for streaming assistant text", () => {
const { emit, onAgentEvent } = createAgentEventHarness();

View File

@@ -55,6 +55,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
emittedAssistantUpdate: false,
lastStreamedReasoning: undefined,
lastBlockReplyText: undefined,
reasoningStreamOpen: false,
assistantMessageIndex: 0,
lastAssistantTextMessageIndex: -1,
lastAssistantTextNormalized: undefined,
@@ -117,6 +118,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
state.lastBlockReplyText = undefined;
state.lastStreamedReasoning = undefined;
state.lastReasoningSent = undefined;
state.reasoningStreamOpen = false;
state.suppressBlockChunks = false;
state.assistantMessageIndex += 1;
state.lastAssistantTextMessageIndex = -1;

View File

@@ -27,7 +27,11 @@ describe("tool mutation helpers", () => {
expect(writeFingerprint).toContain("tool=write");
expect(writeFingerprint).toContain("path=/tmp/demo.txt");
expect(writeFingerprint).toContain("id=42");
expect(writeFingerprint).toContain("meta=write /tmp/demo.txt");
expect(writeFingerprint).not.toContain("meta=write /tmp/demo.txt");
const metaOnlyFingerprint = buildToolActionFingerprint("exec", { command: "ls -la" }, "ls -la");
expect(metaOnlyFingerprint).toContain("tool=exec");
expect(metaOnlyFingerprint).toContain("meta=ls -la");
const readFingerprint = buildToolActionFingerprint("read", { path: "/tmp/demo.txt" });
expect(readFingerprint).toBeUndefined();

View File

@@ -151,6 +151,7 @@ export function buildToolActionFingerprint(
if (action) {
parts.push(`action=${action}`);
}
let hasStableTarget = false;
for (const key of [
"path",
"filePath",
@@ -167,10 +168,14 @@ export function buildToolActionFingerprint(
const value = normalizeFingerprintValue(record?.[key]);
if (value) {
parts.push(`${key.toLowerCase()}=${value}`);
hasStableTarget = true;
}
}
const normalizedMeta = meta?.trim().replace(/\s+/g, " ").toLowerCase();
if (normalizedMeta) {
// Meta text often carries volatile details (for example "N chars").
// Prefer stable arg-derived keys for matching; only fall back to meta
// when no stable target key is available.
if (normalizedMeta && !hasStableTarget) {
parts.push(`meta=${normalizedMeta}`);
}
return parts.join("|");