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.
This commit is contained in:
@@ -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
|
||||
// (<function_calls>, 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") {
|
||||
|
||||
Reference in New Issue
Block a user