diff --git a/CHANGELOG.md b/CHANGELOG.md index 27f056d0aac..b07777bdbdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - Providers/SDK retry: cap long `Retry-After` sleeps in Stainless-based Anthropic/OpenAI model SDKs so 60s+ retry windows surface immediately for OpenClaw failover instead of blocking the run. (#68474) Thanks @jetd1. - Agents/TTS: preserve spoken text in TTS tool results while defusing reply directives in transcript content, so future turns remember voice replies without treating spoken `MEDIA:` or voice tags as delivery metadata. (#68869) Thanks @zqchris. - Providers/OpenAI: harden Voice Call realtime transcription against OpenAI Realtime session-update drift, forward language and prompt hints, and add live coverage for realtime STT. +- Agents/Pi embedded runs: suppress the "⚠️ Agent couldn't generate a response" warning when the assistant already delivered user-visible content through a messaging tool and the turn ended cleanly (`stopReason=stop`/`end_turn`). Real failure modes (tool errors, provider `stopReason=error`, interrupted tool use) still surface the existing "verify before retrying" warning. Fixes #70396. - Providers/Moonshot: stop strict-sanitizing Kimi's native tool_call IDs (shaped like `functions.:`) on the OpenAI-compatible transport, so multi-turn agentic flows through Kimi K2.6 no longer break after 2-3 tool-calling rounds when the serving layer fails to match mangled IDs against the original tool definitions. Adds a `sanitizeToolCallIds` opt-out to the shared `openai-compatible` replay family helper and wires Moonshot to it. Fixes #62319. (#70030) Thanks @LeoDu0314. - Dependencies/security: override transitive `uuid` to `14.0.0`, clearing the runtime advisory across dependencies. - Codex harness: ignore dynamic tool descriptions when deciding whether to reuse a native app-server thread while still fingerprinting tool schemas, so channel-specific copy changes no longer reset otherwise compatible Codex conversations. (#69976) Thanks @chen-zhang-cs-code. diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index 65db43b2d6d..d2611ae8587 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -292,7 +292,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { ); }); - it("does not retry reasoning-only turns after side effects", async () => { + it("does not retry or warn on reasoning-only turns when a messaging tool already delivered", async () => { mockedClassifyFailoverReason.mockReturnValue(null); mockedRunEmbeddedAttempt.mockResolvedValueOnce( makeAttemptResult({ @@ -300,7 +300,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { didSendViaMessagingTool: true, lastAssistant: { role: "assistant", - stopReason: "end_turn", + stopReason: "stop", provider: "openai", model: "gpt-5.4", content: [ @@ -322,8 +322,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); - expect(result.payloads?.[0]?.isError).toBe(true); - expect(result.payloads?.[0]?.text).toContain("verify before retrying"); + expect(result.payloads).toBeUndefined(); }); it("does not retry reasoning-only turns when the assistant ended in error", async () => { @@ -761,6 +760,49 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expect(incompleteTurnText).toBeNull(); }); + it("suppresses the incomplete-turn warning when a messaging tool delivered and the turn ended cleanly", () => { + const incompleteTurnText = resolveIncompleteTurnPayloadText({ + payloadCount: 0, + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + didSendViaMessagingTool: true, + lastAssistant: { + role: "assistant", + stopReason: "stop", + provider: "ollama", + model: "kimi-k2.6:cloud", + content: [], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(incompleteTurnText).toBeNull(); + }); + + it("still surfaces the incomplete-turn warning after a messaging tool when the provider signalled an error", () => { + const incompleteTurnText = resolveIncompleteTurnPayloadText({ + payloadCount: 0, + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + didSendViaMessagingTool: true, + lastAssistant: { + role: "assistant", + stopReason: "error", + provider: "ollama", + model: "kimi-k2.6:cloud", + errorMessage: "provider failed mid-turn", + content: [], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(incompleteTurnText).toContain("verify before retrying"); + }); + it("does not retry reasoning-only GPT turns after side effects", () => { const retryInstruction = resolveReasoningOnlyRetryInstruction({ provider: "openai", diff --git a/src/agents/pi-embedded-runner/run/incomplete-turn.ts b/src/agents/pi-embedded-runner/run/incomplete-turn.ts index 9e0ad431117..02e1ad863a4 100644 --- a/src/agents/pi-embedded-runner/run/incomplete-turn.ts +++ b/src/agents/pi-embedded-runner/run/incomplete-turn.ts @@ -20,6 +20,7 @@ type IncompleteTurnAttempt = Pick< | "currentAttemptAssistant" | "yieldDetected" | "didSendDeterministicApprovalPrompt" + | "didSendViaMessagingTool" | "lastToolError" | "lastAssistant" | "replayMetadata" @@ -179,6 +180,19 @@ export function resolveIncompleteTurnPayloadText(params: { } const stopReason = params.attempt.lastAssistant?.stopReason; + // If the assistant already delivered user-visible content via a messaging + // tool during this turn and ended cleanly (stopReason=stop), do not surface + // an incomplete-turn warning. The user has received the reply; a follow-up + // "couldn't generate a response" bubble is a false positive. Real failure + // modes (tool errors, provider stopReason=error, tool-use interruption) + // still fall through to the normal incomplete-turn paths below. + if ( + params.attempt.didSendViaMessagingTool && + !params.attempt.lastToolError && + stopReason === "stop" + ) { + return null; + } const incompleteTerminalAssistant = isIncompleteTerminalAssistantTurn({ hasAssistantVisibleText: params.payloadCount > 0, lastAssistant: params.attempt.lastAssistant,