Skip to content

Commit 7d13176

Browse files
authored
fix(agents): use current assistant final payloads (#82850)
1 parent b77077f commit 7d13176

5 files changed

Lines changed: 71 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Docs: https://docs.openclaw.ai
8787
- QQBot: treat only explicit truthy `QQBOT_DEBUG` values as enabling debug logs, so false-like values such as `0` no longer expose debug output. Fixes #82644. (#82697) Thanks @leno23.
8888
- Agents/session_status: resolve implicit no-arg status lookups against the live run session, so `/think` changes report the current thinking level instead of stale sandbox state. Fixes #82669. (#82696) Thanks @leno23.
8989
- Discord: keep progress drafts visible for message-tool-only guild replies under the default coding tool profile. Fixes #82747. Thanks @eliranwong.
90+
- Agents: prefer current structured assistant final answers when assembling final reply payloads, reducing reliance on streamed preview fragments after channel transcript recovery. (#82850) Thanks @joshavant.
9091
- Discord: keep unmentioned room-event history until a visible Discord send succeeds, so quiet ambient context does not disappear before message-tool delivery. (#82573) Thanks @obviyus.
9192
- CLI/setup: order the model/auth provider picker as OpenAI, Anthropic, xAI, Google, then the remaining providers alphabetically.
9293
- Diagnostics/usage/voice-call: treat explicit zero and non-finite limits as empty results and reject invalid voice-call numeric CLI flags. Fixes #82646, #82650, #82651, and #82653. (#82679) Thanks @leno23.

src/agents/pi-embedded-runner/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,7 @@ export async function runEmbeddedPiAgent(
25922592
assistantTexts: attempt.assistantTexts,
25932593
toolMetas: attempt.toolMetas,
25942594
lastAssistant: attempt.lastAssistant,
2595+
currentAssistant: currentAttemptAssistant ?? null,
25952596
lastToolError: attempt.lastToolError,
25962597
config: params.config,
25972598
isCronTrigger: params.trigger === "cron",

src/agents/pi-embedded-runner/run/payloads.test-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export function buildPayloads(overrides: Partial<BuildPayloadParams> = {}) {
99
assistantTexts: [],
1010
toolMetas: [],
1111
lastAssistant: undefined,
12+
currentAssistant:
13+
overrides.currentAssistant === undefined
14+
? overrides.lastAssistant
15+
: overrides.currentAssistant,
1216
isCronTrigger: false,
1317
sessionKey: "session:telegram",
1418
inlineToolResultsAllowed: false,

src/agents/pi-embedded-runner/run/payloads.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,56 @@ describe("buildEmbeddedRunPayloads tool-error warnings", () => {
9090
expectSinglePayloadText(payloads, "Fixed.");
9191
});
9292

93+
it("uses the final assistant answer when streamed text was an incomplete preview", () => {
94+
const payloads = buildPayloads({
95+
assistantTexts: ["Long answer, part one"],
96+
lastAssistant: {
97+
role: "assistant",
98+
stopReason: "stop",
99+
content: [
100+
{
101+
type: "text",
102+
text: "Long answer, part one\nLong answer, part two\nLong answer, part three",
103+
textSignature: JSON.stringify({
104+
v: 1,
105+
id: "item_final",
106+
phase: "final_answer",
107+
}),
108+
},
109+
],
110+
} as AssistantMessage,
111+
});
112+
113+
expectSinglePayloadText(
114+
payloads,
115+
"Long answer, part one\nLong answer, part two\nLong answer, part three",
116+
);
117+
});
118+
119+
it("keeps a current one-chunk reply when only a stale transcript assistant is available", () => {
120+
const payloads = buildPayloads({
121+
assistantTexts: ["Current room event reply."],
122+
currentAssistant: null,
123+
lastAssistant: {
124+
role: "assistant",
125+
stopReason: "stop",
126+
content: [
127+
{
128+
type: "text",
129+
text: "Previous transcript reply.",
130+
textSignature: JSON.stringify({
131+
v: 1,
132+
id: "item_previous",
133+
phase: "final_answer",
134+
}),
135+
},
136+
],
137+
} as AssistantMessage,
138+
});
139+
140+
expectSinglePayloadText(payloads, "Current room event reply.");
141+
});
142+
93143
it("delivers only the final assistant answer when accumulated text includes pre-tool progress", () => {
94144
const payloads = buildPayloads({
95145
assistantTexts: ["I'll inspect that first.", "Done."],

src/agents/pi-embedded-runner/run/payloads.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export function buildEmbeddedRunPayloads(params: {
182182
assistantTexts: string[];
183183
toolMetas: ToolMetaEntry[];
184184
lastAssistant: AssistantMessage | undefined;
185+
currentAssistant?: AssistantMessage | null;
185186
lastToolError?: ToolErrorSummary;
186187
config?: OpenClawConfig;
187188
isCronTrigger?: boolean;
@@ -264,24 +265,28 @@ export function buildEmbeddedRunPayloads(params: {
264265
const useMarkdown = params.toolResultFormat === "markdown";
265266
const suppressAssistantArtifacts =
266267
params.didSendDeterministicApprovalPrompt === true || hasSourceReplyPayload;
267-
const lastAssistantStopReason = params.lastAssistant?.stopReason;
268+
const nonEmptyAssistantTexts = params.assistantTexts.filter((text) => text.trim().length > 0);
269+
const currentAssistant = params.currentAssistant ?? undefined;
270+
const assistantForPayload =
271+
currentAssistant ?? (nonEmptyAssistantTexts.length === 1 ? undefined : params.lastAssistant);
272+
const lastAssistantStopReason = assistantForPayload?.stopReason;
268273
const lastAssistantErrored = lastAssistantStopReason === "error";
269274
const lastAssistantAborted = lastAssistantStopReason === "aborted";
270275
const runAborted = params.runAborted === true || lastAssistantAborted;
271276
const lastAssistantNeedsErrorSurface = lastAssistantErrored || lastAssistantAborted;
272277
const errorText =
273-
params.lastAssistant && lastAssistantNeedsErrorSurface
278+
assistantForPayload && lastAssistantNeedsErrorSurface
274279
? suppressAssistantArtifacts
275280
? undefined
276-
: formatAssistantErrorText(params.lastAssistant, {
281+
: formatAssistantErrorText(assistantForPayload, {
277282
cfg: params.config,
278283
sessionKey: params.sessionKey,
279284
provider: params.provider,
280285
model: params.model,
281286
})
282287
: undefined;
283288
const rawErrorMessage = lastAssistantNeedsErrorSurface
284-
? normalizeOptionalString(params.lastAssistant?.errorMessage)
289+
? normalizeOptionalString(assistantForPayload?.errorMessage)
285290
: undefined;
286291
const rawErrorFingerprint = rawErrorMessage
287292
? getApiErrorPayloadFingerprint(rawErrorMessage)
@@ -333,17 +338,17 @@ export function buildEmbeddedRunPayloads(params: {
333338
const reasoningText =
334339
suppressAssistantArtifacts || runAborted
335340
? ""
336-
: params.lastAssistant && params.reasoningLevel === "on" && params.thinkingLevel !== "off"
337-
? extractAssistantThinking(params.lastAssistant)
341+
: assistantForPayload && params.reasoningLevel === "on" && params.thinkingLevel !== "off"
342+
? extractAssistantThinking(assistantForPayload)
338343
: "";
339344
if (reasoningText) {
340345
replyItems.push({ text: reasoningText, isReasoning: true });
341346
}
342347

343-
const fallbackAnswerText = params.lastAssistant
344-
? extractAssistantVisibleText(params.lastAssistant)
348+
const fallbackAnswerText = assistantForPayload
349+
? extractAssistantVisibleText(assistantForPayload)
345350
: "";
346-
const fallbackRawAnswerText = resolveRawAssistantAnswerText(params.lastAssistant);
351+
const fallbackRawAnswerText = resolveRawAssistantAnswerText(assistantForPayload);
347352
const shouldSuppressRawErrorText = (text: string) => {
348353
if (!lastAssistantNeedsErrorSurface) {
349354
return false;
@@ -403,7 +408,6 @@ export function buildEmbeddedRunPayloads(params: {
403408
const parsed = parseReplyDirectives(text);
404409
return (parsed.mediaUrls?.length ?? 0) > 0 || parsed.audioAsVoice;
405410
});
406-
const nonEmptyAssistantTexts = params.assistantTexts.filter((text) => text.trim().length > 0);
407411
const normalizedAssistantTexts = normalizeTextForComparison(nonEmptyAssistantTexts.join("\n\n"));
408412
const normalizedRawAnswerText = normalizeTextForComparison(rawAnswerDirectiveState?.text ?? "");
409413
const shouldPreferRawAnswerText =
@@ -418,7 +422,7 @@ export function buildEmbeddedRunPayloads(params: {
418422
? normalizeReplyTextForComparison(fallbackAnswerSourceText)
419423
: "";
420424
const shouldUseCanonicalFinalAnswer =
421-
nonEmptyAssistantTexts.length > 1 &&
425+
!lastAssistantNeedsErrorSurface &&
422426
fallbackAnswerSourceText.length > 0 &&
423427
normalizedFallbackAnswerSourceText.length > 0;
424428
const hasAssistantTextPayload = nonEmptyAssistantTexts.length > 0;

0 commit comments

Comments
 (0)