mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:20:44 +00:00
fix(pi-embedded-runner): suppress incomplete-turn warning after clean messaging-tool delivery
The agent runner was surfacing a '⚠️ Agent couldn't generate a response' warning even when the assistant had already sent user-visible content through a messaging tool and the turn ended cleanly. Treat that path as a successful delivery and skip the warning while keeping real failure modes (tool errors, stopReason=error, interrupted tool use) intact. Fixes #70396.
This commit is contained in:
committed by
Peter Steinberger
parent
93a1f5b3fa
commit
6cd9136f2d
@@ -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.
|
- 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.
|
- 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.
|
- 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.<name>:<index>`) 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.
|
- Providers/Moonshot: stop strict-sanitizing Kimi's native tool_call IDs (shaped like `functions.<name>:<index>`) 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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);
|
mockedClassifyFailoverReason.mockReturnValue(null);
|
||||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||||
makeAttemptResult({
|
makeAttemptResult({
|
||||||
@@ -300,7 +300,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
|
|||||||
didSendViaMessagingTool: true,
|
didSendViaMessagingTool: true,
|
||||||
lastAssistant: {
|
lastAssistant: {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
stopReason: "end_turn",
|
stopReason: "stop",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
content: [
|
content: [
|
||||||
@@ -322,8 +322,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
|
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
|
||||||
expect(result.payloads?.[0]?.isError).toBe(true);
|
expect(result.payloads).toBeUndefined();
|
||||||
expect(result.payloads?.[0]?.text).toContain("verify before retrying");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not retry reasoning-only turns when the assistant ended in error", async () => {
|
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();
|
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", () => {
|
it("does not retry reasoning-only GPT turns after side effects", () => {
|
||||||
const retryInstruction = resolveReasoningOnlyRetryInstruction({
|
const retryInstruction = resolveReasoningOnlyRetryInstruction({
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type IncompleteTurnAttempt = Pick<
|
|||||||
| "currentAttemptAssistant"
|
| "currentAttemptAssistant"
|
||||||
| "yieldDetected"
|
| "yieldDetected"
|
||||||
| "didSendDeterministicApprovalPrompt"
|
| "didSendDeterministicApprovalPrompt"
|
||||||
|
| "didSendViaMessagingTool"
|
||||||
| "lastToolError"
|
| "lastToolError"
|
||||||
| "lastAssistant"
|
| "lastAssistant"
|
||||||
| "replayMetadata"
|
| "replayMetadata"
|
||||||
@@ -179,6 +180,19 @@ export function resolveIncompleteTurnPayloadText(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stopReason = params.attempt.lastAssistant?.stopReason;
|
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({
|
const incompleteTerminalAssistant = isIncompleteTerminalAssistantTurn({
|
||||||
hasAssistantVisibleText: params.payloadCount > 0,
|
hasAssistantVisibleText: params.payloadCount > 0,
|
||||||
lastAssistant: params.attempt.lastAssistant,
|
lastAssistant: params.attempt.lastAssistant,
|
||||||
|
|||||||
Reference in New Issue
Block a user