From 6ceadaa41f93935787ceee175fe30fd1bebac201 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sun, 22 Feb 2026 00:23:25 -0800 Subject: [PATCH] Agents: add fallback reply for tool-only completions --- CHANGELOG.md | 1 + .../run/payloads.e2e.test.ts | 36 +++++++++++++++++++ src/agents/pi-embedded-runner/run/payloads.ts | 11 +++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5947cdef..f4bbfa997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane. - Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry. - Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson. +- Agents/Replies: emit a default completion acknowledgement (`✅ Done.`) when runs execute tools successfully but return no final assistant text, preventing silent no-reply turns after tool-only completions. (#22834) Thanks @Oldshue. - Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize. - Plugins/Hooks: run legacy `before_agent_start` once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710. - Models/Config: default missing Anthropic provider/model `api` fields to `anthropic-messages` during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123. diff --git a/src/agents/pi-embedded-runner/run/payloads.e2e.test.ts b/src/agents/pi-embedded-runner/run/payloads.e2e.test.ts index 70e41de83..6804f035f 100644 --- a/src/agents/pi-embedded-runner/run/payloads.e2e.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.e2e.test.ts @@ -145,6 +145,42 @@ describe("buildEmbeddedRunPayloads", () => { expect(payloads[0]?.text).toBe("All good"); }); + it("adds completion fallback when tools run successfully without final assistant text", () => { + const payloads = buildPayloads({ + toolMetas: [{ toolName: "write", meta: "/tmp/out.md" }], + lastAssistant: makeAssistant({ + stopReason: "stop", + errorMessage: undefined, + content: [], + }), + }); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.isError).toBeUndefined(); + expect(payloads[0]?.text).toBe("✅ Done."); + }); + + it("does not add completion fallback when the run still has a tool error", () => { + const payloads = buildPayloads({ + toolMetas: [{ toolName: "browser", meta: "open https://example.com" }], + lastToolError: { toolName: "browser", error: "url required" }, + }); + + expect(payloads).toHaveLength(0); + }); + + it("does not add completion fallback when no tools ran", () => { + const payloads = buildPayloads({ + lastAssistant: makeAssistant({ + stopReason: "stop", + errorMessage: undefined, + content: [], + }), + }); + + expect(payloads).toHaveLength(0); + }); + it("adds tool error fallback when the assistant only invoked tools and verbose mode is on", () => { const payloads = buildPayloads({ lastAssistant: makeAssistant({ diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 3939e85bd..8dae31dd2 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -294,7 +294,7 @@ export function buildEmbeddedRunPayloads(params: { } const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice); - return replyItems + const payloads = replyItems .map((item) => ({ text: item.text?.trim() ? item.text.trim() : undefined, mediaUrls: item.media?.length ? item.media : undefined, @@ -314,4 +314,13 @@ export function buildEmbeddedRunPayloads(params: { } return true; }); + if ( + payloads.length === 0 && + params.toolMetas.length > 0 && + !params.lastToolError && + !lastAssistantErrored + ) { + return [{ text: "✅ Done." }]; + } + return payloads; }