diff --git a/src/hooks/shared/prompt-async-gate.test.ts b/src/hooks/shared/prompt-async-gate.test.ts index ce65a2d41..813e79d71 100644 --- a/src/hooks/shared/prompt-async-gate.test.ts +++ b/src/hooks/shared/prompt-async-gate.test.ts @@ -356,6 +356,86 @@ describe("dispatchInternalPrompt shared gate behavior", () => { expect(promptCalls).toBe(0) }) + test("#given latest assistant turn has a tool-calls finish without pending part state #when an internal promptAsync is requested #then no prompt is sent", async () => { + // given + let promptCalls = 0 + const client = { + session: { + status: async () => ({ data: { ses_finish_waiting_tools: { type: "idle" } } }), + messages: async () => ({ + data: [ + { + info: { id: "msg_user", role: "user" }, + parts: [{ type: "text", text: "run work" }], + }, + { + info: { id: "msg_assistant", role: "assistant", finish: "tool-calls" }, + parts: [{ type: "tool_use", id: "toolu_pending" }], + }, + ], + }), + promptAsync: async () => { + promptCalls += 1 + }, + }, + } + + // when + const result = await dispatchInternalPrompt({ + mode: "async", + client, + sessionID: "ses_finish_waiting_tools", + input: { path: { id: "ses_finish_waiting_tools" }, body: { parts: [] } }, + source: "test:finish-waiting-tools", + settleMs: 0, + postDispatchHoldMs: 0, + }) + + // then + expect(result.status).toBe("active") + expect(promptCalls).toBe(0) + }) + + test("#given latest assistant turn has a running tool-call part #when an internal promptAsync is requested #then no prompt is sent", async () => { + // given + let promptCalls = 0 + const client = { + session: { + status: async () => ({ data: { ses_tool_call_part: { type: "idle" } } }), + messages: async () => ({ + data: [ + { + info: { id: "msg_user", role: "user" }, + parts: [{ type: "text", text: "run work" }], + }, + { + info: { id: "msg_assistant", role: "assistant" }, + parts: [{ type: "tool-call", id: "call_pending", state: { status: "running" } }], + }, + ], + }), + promptAsync: async () => { + promptCalls += 1 + }, + }, + } + + // when + const result = await dispatchInternalPrompt({ + mode: "async", + client, + sessionID: "ses_tool_call_part", + input: { path: { id: "ses_tool_call_part" }, body: { parts: [] } }, + source: "test:tool-call-part", + settleMs: 0, + postDispatchHoldMs: 0, + }) + + // then + expect(result.status).toBe("active") + expect(promptCalls).toBe(0) + }) + test("#given internal user tail follows an assistant waiting on tools #when an internal promptAsync is requested #then no prompt is sent", async () => { // given let promptCalls = 0 diff --git a/src/shared/prompt-async-gate.ts b/src/shared/prompt-async-gate.ts index e23d6c849..c9a9ab371 100644 --- a/src/shared/prompt-async-gate.ts +++ b/src/shared/prompt-async-gate.ts @@ -203,6 +203,17 @@ function messageRole(message: unknown): string | undefined { return typeof message.role === "string" ? message.role : undefined } +function messageFinish(message: unknown): string | undefined { + if (!isRecord(message)) { + return undefined + } + const info = message.info + if (isRecord(info) && typeof info.finish === "string") { + return info.finish + } + return typeof message.finish === "string" ? message.finish : undefined +} + function toInternalInitiatorTextPartLike(part: unknown): InternalInitiatorTextPartLike { const result: InternalInitiatorTextPartLike = {} if (!isRecord(part)) { @@ -249,7 +260,12 @@ function partIsWaitingOnTool(part: unknown): boolean { if (!isRecord(part)) { return false } - if (part.type !== "tool" && part.type !== "tool_use") { + if ( + part.type !== "tool" + && part.type !== "tool_use" + && part.type !== "tool-call" + && part.type !== "tool-invocation" + ) { return false } @@ -266,9 +282,9 @@ function latestAssistantTurnIsWaitingOnTools(messages: unknown[]): boolean { const role = messageRole(message) if (role === "assistant") { if (!isRecord(message) || !Array.isArray(message.parts)) { - return false + return messageFinish(message) === "tool-calls" } - return message.parts.some(partIsWaitingOnTool) + return messageFinish(message) === "tool-calls" || message.parts.some(partIsWaitingOnTool) } if (role === "user") { if (messageIsSyntheticOrInternalUser(message)) {