From d30dc784092eb7f0279cc32ea7eee49cd5d64a1f Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 19 May 2026 19:19:37 +0900 Subject: [PATCH] fix(coding-agent): clean up ephemeral irc reply turn Three coordinated tweaks in runEphemeralTurn and the supporting #buildEphemeralSnapshot so IRC reply text stops leaking tool-call markup, duplicating verbatim, and breaking DeepSeek-class encoders: - Drop the recipient's tools array entirely instead of relying on toolChoice:"none" (not every backend enforces it). The model now has no tool surface to emit so leaked function_call / DSML markup stops. - Preserve thinking content blocks when snapshotting the in-flight streaming assistant message so the openai-completions encoder can re-emit reasoning_content for DeepSeek-routed recipients (10 reports of HTTP 400 "'reasoning_content' in thinking mode must be passed back"). - Collapse consecutive duplicate sentences in replyText and cap reply length so a looping recipient does not spam the IRC channel with the same line repeated N times. --- .../coding-agent/src/session/agent-session.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 557463792..4de50226f 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -7667,6 +7667,11 @@ export class AgentSession { const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, + // Empty tools array: with toolChoice="none" some encoders still serialize the + // recipient's tool catalog and the model leaks raw call markup + // (, DSML envelopes) into IRC replies. Stripping tools here + // removes the surface entirely. + tools: [], }; const options = this.prepareSimpleStreamOptions( { @@ -7702,7 +7707,7 @@ export class AgentSession { if (!assistantMessage) { throw new Error("Ephemeral turn ended without a final message"); } - return { replyText: replyText.trim(), assistantMessage }; + return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage }; } /** @@ -7715,14 +7720,24 @@ export class AgentSession { const messages = [...this.messages]; const streaming = this.agent.state.streamMessage; if (streaming && streaming.role === "assistant") { + const preservedBlocks: AssistantMessage["content"] = []; + // Preserve thinking blocks: DeepSeek-class encoders replay them as + // `reasoning_content` and reject the request (HTTP 400) when the field + // goes missing on a turn that previously emitted thinking. + for (const c of streaming.content) { + if (c.type === "thinking") preservedBlocks.push(c); + } const streamingText = streaming.content .filter((c): c is TextContent => c.type === "text") .map(c => c.text) .join(""); if (streamingText) { + preservedBlocks.push({ type: "text", text: streamingText }); + } + if (preservedBlocks.length > 0) { const normalized: AssistantMessage = { ...streaming, - content: [{ type: "text", text: streamingText }], + content: preservedBlocks, }; const lastMessage = messages.at(-1); if (lastMessage?.role === "assistant") {