Agents: add fallback reply for tool-only completions

This commit is contained in:
Vignesh Natarajan
2026-02-22 00:23:25 -08:00
parent 8a0a28763e
commit 6ceadaa41f
3 changed files with 47 additions and 1 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -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;
}