Compare commits

...
Author SHA1 Message Date
John Choi 8c66d5358a fix(core): hide synthetic empty assistant content 2026-06-23 14:09:15 -07:00
4 changed files with 57 additions and 6 deletions
@@ -2,8 +2,9 @@ import { EMPTY_CONTENT_TEXT } from "@cline/shared";
import { describe, expect, it } from "vitest";
import {
agentMessageToMessageWithMetadata,
messageToAgentMessages,
messagesToAgentMessages,
messageToAgentMessages,
SYNTHETIC_EMPTY_CONTENT_METADATA_KEY,
} from "./agent-message-codec";
describe("agent message codec", () => {
@@ -21,7 +22,7 @@ describe("agent message codec", () => {
role: "assistant",
content: [{ type: "text", text: EMPTY_CONTENT_TEXT }],
createdAt: 1,
metadata: undefined,
metadata: { [SYNTHETIC_EMPTY_CONTENT_METADATA_KEY]: true },
modelInfo: undefined,
metrics: undefined,
},
@@ -39,7 +40,7 @@ describe("agent message codec", () => {
role: "user",
content: [{ type: "text", text: EMPTY_CONTENT_TEXT }],
createdAt: 1,
metadata: undefined,
metadata: { [SYNTHETIC_EMPTY_CONTENT_METADATA_KEY]: true },
modelInfo: undefined,
metrics: undefined,
},
@@ -57,7 +58,7 @@ describe("agent message codec", () => {
role: "assistant",
content: [{ type: "text", text: EMPTY_CONTENT_TEXT }],
createdAt: 1,
metadata: undefined,
metadata: { [SYNTHETIC_EMPTY_CONTENT_METADATA_KEY]: true },
modelInfo: undefined,
metrics: undefined,
},
@@ -15,6 +15,8 @@ import type {
} from "@cline/shared";
import { EMPTY_CONTENT_TEXT } from "@cline/shared";
export const SYNTHETIC_EMPTY_CONTENT_METADATA_KEY = "syntheticEmptyContent";
export function messageToAgentMessages(
message: MessageWithMetadata,
): AgentMessage[] {
@@ -51,7 +53,10 @@ export function messageToAgentMessages(
role: message.role,
content: [{ type: "text", text: EMPTY_CONTENT_TEXT }],
createdAt: message.ts ?? Date.now(),
metadata: message.metadata,
metadata: {
...message.metadata,
[SYNTHETIC_EMPTY_CONTENT_METADATA_KEY]: true,
},
modelInfo: message.modelInfo,
metrics: metricsToAgentMetrics(message.metrics),
});
@@ -21,7 +21,9 @@ import type {
AgentToolCallPart,
AgentUsage,
} from "@cline/shared";
import { EMPTY_CONTENT_TEXT } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SYNTHETIC_EMPTY_CONTENT_METADATA_KEY } from "../config/agent-message-codec";
import {
RuntimeEventAdapter,
toLegacyAgentEvent,
@@ -290,6 +292,37 @@ describe("RuntimeEventAdapter — assistant-message → content_end", () => {
]);
});
it("suppresses synthetic empty-content placeholder text", () => {
const out = adapter.translate({
type: "assistant-message",
snapshot: makeSnapshot(),
iteration: 1,
message: makeMessage(
{
metadata: { [SYNTHETIC_EMPTY_CONTENT_METADATA_KEY]: true },
},
[{ type: "text", text: EMPTY_CONTENT_TEXT }],
),
finishReason: "stop",
});
expect(out).toEqual([]);
});
it("keeps literal empty-content text when it is not synthetic", () => {
const out = adapter.translate({
type: "assistant-message",
snapshot: makeSnapshot(),
iteration: 1,
message: makeMessage({}, [{ type: "text", text: EMPTY_CONTENT_TEXT }]),
finishReason: "stop",
});
expect(out).toEqual([
{ type: "content_end", contentType: "text", text: EMPTY_CONTENT_TEXT },
]);
});
it("fires content_end(reasoning) when the message has reasoning parts", () => {
const out = adapter.translate({
type: "assistant-message",
@@ -61,6 +61,8 @@ import type {
AgentUsage,
LegacyAgentUsage,
} from "@cline/shared";
import { EMPTY_CONTENT_TEXT } from "@cline/shared";
import { SYNTHETIC_EMPTY_CONTENT_METADATA_KEY } from "../config/agent-message-codec";
// =============================================================================
// Helpers
@@ -106,6 +108,16 @@ function textFromMessage(message: AgentMessage | undefined): string {
return extractTextPart(message) ?? "";
}
function isSyntheticEmptyContentMessage(
message: AgentMessage,
text: string,
): boolean {
return (
message.metadata?.[SYNTHETIC_EMPTY_CONTENT_METADATA_KEY] === true &&
text === EMPTY_CONTENT_TEXT
);
}
function statusToLegacyFinishReason(
status: "completed" | "aborted" | "failed",
): AgentFinishReason {
@@ -266,7 +278,7 @@ export class RuntimeEventAdapter {
private translateAssistantMessage(message: AgentMessage): AgentEvent[] {
const out: AgentEvent[] = [];
const text = extractTextPart(message);
if (text !== undefined) {
if (text !== undefined && !isSyntheticEmptyContentMessage(message, text)) {
out.push({ type: "content_end", contentType: "text", text });
}
const reasoning = extractReasoningPart(message);