Skip to content

Commit bb9c795

Browse files
committed
fix(agents): mark tool-use terminal with pre-tool text as abandoned in lifecycle (#76477)
The lifecycle handler's derivedWorkingTerminalState was emitting 'working' for interrupted tool-use turns with pre-tool text because it required !hasAssistantVisibleText for the 'abandoned' state. Update the derivation to also mark as 'abandoned' when incompleteTerminalAssistant is true, so lifecycle consumers see a consistent state with the runner's terminal result.
1 parent 5e35272 commit bb9c795

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,34 @@ describe("handleAgentEnd", () => {
289289
});
290290
});
291291

292+
it("marks tool-use terminal with pre-tool text as abandoned (#76477)", async () => {
293+
const onAgentEvent = vi.fn();
294+
const ctx = createContext(
295+
{
296+
role: "assistant",
297+
stopReason: "toolUse",
298+
content: [
299+
{ type: "text", text: "Initial analysis..." },
300+
{ type: "tool_use", id: "tool_1", name: "read", input: { path: "src/index.ts" } },
301+
],
302+
},
303+
{ onAgentEvent },
304+
);
305+
ctx.state.livenessState = "working";
306+
ctx.state.assistantTexts = ["Initial analysis..."];
307+
308+
await handleAgentEnd(ctx);
309+
310+
expect(onAgentEvent).toHaveBeenCalledWith({
311+
stream: "lifecycle",
312+
data: {
313+
phase: "end",
314+
livenessState: "abandoned",
315+
replayInvalid: true,
316+
},
317+
});
318+
});
319+
292320
it("keeps accumulated deterministic side effects from being marked abandoned", async () => {
293321
const onAgentEvent = vi.fn();
294322
const ctx = createContext(undefined, { onAgentEvent });

src/agents/pi-embedded-subscribe.handlers.lifecycle.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext): void | Promise<
5454
});
5555
const replayInvalid =
5656
ctx.state.replayState.replayInvalid || incompleteTerminalAssistant ? true : undefined;
57+
// Tool-use terminal guard: when the last assistant message ended with a
58+
// tool-call stop reason, the turn is incomplete even when pre-tool text
59+
// exists — mark as abandoned so lifecycle consumers do not see a working
60+
// end state for an interrupted tool chain. (#76477)
5761
const derivedWorkingTerminalState = isError
5862
? "blocked"
59-
: replayInvalid && !hasAssistantVisibleText && !hadDeterministicSideEffect
63+
: replayInvalid &&
64+
!hadDeterministicSideEffect &&
65+
(!hasAssistantVisibleText || incompleteTerminalAssistant)
6066
? "abandoned"
6167
: ctx.state.livenessState;
6268
const livenessState =

0 commit comments

Comments
 (0)