Skip to content

Commit 20714ed

Browse files
Merge pull request #569 from corbitsdev/cl-5661-make-turn-states-tooldone-handler-symmetric-with
Make tool.done/tool_result awaitingResponse symmetric with inference.done
2 parents 4874198 + f45b76f commit 20714ed

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

src/tui/turn-state.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,49 @@ describe("turnStateFromEvent", () => {
138138
expect(bothDone.activeToolCalls).toHaveLength(0);
139139
});
140140

141+
test("tool.done only sets awaitingResponse once every parallel call has finished", () => {
142+
// Regression for CL-5661: with a fan-out of two outstanding calls, the
143+
// first tool.done must not claim the turn is idle while the second call
144+
// is still running — that falsely tells consumers (stall watchdog,
145+
// status chrome) the model is the only thing left to wait on.
146+
const running = fold([
147+
{ type: "inference.start" },
148+
{ type: "tool.start", data: { call: { id: "call_1", name: "grep" } } },
149+
{ type: "tool.start", data: { call: { id: "call_2", name: "bash" } } },
150+
]);
151+
expect(running.activeToolCalls).toHaveLength(2);
152+
expect(running.awaitingResponse).toBe(false);
153+
154+
const oneDone = turnStateFromEvent(
155+
running,
156+
{ type: "tool.done", data: { result: { callId: "call_1" } } },
157+
200,
158+
);
159+
expect(oneDone.activeToolCalls).toHaveLength(1);
160+
expect(oneDone.awaitingResponse).toBe(false);
161+
162+
const bothDone = turnStateFromEvent(
163+
oneDone,
164+
{ type: "tool.done", data: { result: { callId: "call_2" } } },
165+
201,
166+
);
167+
expect(bothDone.activeToolCalls).toHaveLength(0);
168+
expect(bothDone.awaitingResponse).toBe(true);
169+
});
170+
171+
test("a lone tool.done still sets awaitingResponse", () => {
172+
const running = fold([
173+
{ type: "inference.start" },
174+
{ type: "tool.start", data: { call: { id: "call_1", name: "bash" } } },
175+
]);
176+
const done = turnStateFromEvent(
177+
running,
178+
{ type: "tool.done", data: { result: { callId: "call_1" } } },
179+
200,
180+
);
181+
expect(done.awaitingResponse).toBe(true);
182+
});
183+
141184
test("a second call to the same tool name does not inherit a finished call's id", () => {
142185
const firstDone = fold([
143186
{ type: "inference.start" },

src/tui/turn-state.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -591,22 +591,24 @@ export function turnStateFromEvent(
591591
return {
592592
...state,
593593
...tracking,
594-
awaitingResponse: true,
594+
awaitingResponse: tracking.activeToolCalls.length === 0,
595595
streamingType: null,
596596
currentToolName: null,
597597
lastActivityAt: nowMs,
598598
};
599599
}
600600

601-
case "tool_result":
601+
case "tool_result": {
602+
const activeToolCalls = withoutActiveCall(state.activeToolCalls, event.name ?? "tool");
602603
return {
603604
...state,
604-
awaitingResponse: true,
605+
awaitingResponse: activeToolCalls.length === 0,
605606
streamingType: null,
606607
currentToolName: null,
607608
lastActivityAt: nowMs,
608-
activeToolCalls: withoutActiveCall(state.activeToolCalls, event.name ?? "tool"),
609+
activeToolCalls,
609610
};
611+
}
610612

611613
/**
612614
* A cycle with no active tool calls left is also a turn's real

0 commit comments

Comments
 (0)