fix(ui): cover assistant responses input blocks

This commit is contained in:
Peter Steinberger
2026-06-22 12:56:41 -04:00
parent fc9dd34445
commit bfe4f67ccf
11 changed files with 95 additions and 15 deletions

View File

@@ -580,7 +580,7 @@ function extractAssistantTextForSilentCheck(message: unknown): string | undefine
return undefined;
}
const typed = block as { type?: unknown; text?: unknown };
if ((typed.type !== "text" && typed.type !== "output_text") || typeof typed.text !== "string") {
if (!isAssistantTextContentType(typed.type) || typeof typed.text !== "string") {
return undefined;
}
texts.push(typed.text);
@@ -588,6 +588,10 @@ function extractAssistantTextForSilentCheck(message: unknown): string | undefine
return texts.length > 0 ? texts.join("\n") : undefined;
}
function isAssistantTextContentType(type: unknown): boolean {
return type === "text" || type === "input_text" || type === "output_text";
}
function hasAssistantNonTextContent(message: unknown): boolean {
if (!message || typeof message !== "object") {
return false;
@@ -600,8 +604,7 @@ function hasAssistantNonTextContent(message: unknown): boolean {
(block) =>
block &&
typeof block === "object" &&
(block as { type?: unknown }).type !== "text" &&
(block as { type?: unknown }).type !== "output_text",
!isAssistantTextContentType((block as { type?: unknown }).type),
);
}
@@ -623,7 +626,11 @@ function hasAssistantMixedToolVisibleText(message: unknown): boolean {
if (isToolHistoryBlockType(entry.type)) {
hasToolHistoryBlock = true;
}
if (entry.type === "text" && typeof entry.text === "string" && entry.text.trim()) {
if (
isAssistantTextContentType(entry.type) &&
typeof entry.text === "string" &&
entry.text.trim()
) {
hasText = true;
}
}
@@ -1648,7 +1655,7 @@ function projectEmptyAssistantErrorMessages(
}
const type = (block as { type?: unknown }).type;
return (
type !== "text" &&
!isAssistantTextContentType(type) &&
type !== "thinking" &&
type !== "reasoning" &&
type !== "redacted_thinking"
@@ -1669,7 +1676,7 @@ function projectEmptyAssistantErrorMessages(
continue;
}
const entry = block as { type?: unknown; text?: unknown };
if (entry.type === "text" && typeof entry.text === "string") {
if (isAssistantTextContentType(entry.type) && typeof entry.text === "string") {
visibleTexts.push(entry.text);
}
}

View File

@@ -44,6 +44,20 @@ describe("stripEnvelopeFromMessage", () => {
expect(assistant.content?.[0]?.text).toBe("Assistant body");
});
test("strips internal metadata from assistant input_text blocks", () => {
const assistant = stripEnvelopeFromMessage({
role: "assistant",
content: [
{
type: "input_text",
text: 'Conversation info (untrusted metadata):\n```json\n{"message_id":"123"}\n```\n\nAssistant body',
},
],
}) as { content?: Array<{ text?: string }> };
expect(assistant.content?.[0]?.text).toBe("Assistant body");
});
test("does not strip inline message_id text that is part of a line", () => {
const input = {
role: "user",

View File

@@ -59,7 +59,7 @@ function stripEnvelopeFromContentWithRole(
const isRoleTextBlock =
entry.type === "text" ||
(role === "user" && entry.type === "input_text") ||
(role === "assistant" && entry.type === "output_text");
(role === "assistant" && (entry.type === "input_text" || entry.type === "output_text"));
if (!isRoleTextBlock || typeof entry.text !== "string") {
return item;
}

View File

@@ -941,6 +941,43 @@ describe("projectRecentChatDisplayMessages", () => {
]);
});
it.each([
["output_text", ""],
["output_text", "NO_REPLY"],
["input_text", ""],
["input_text", "NO_REPLY"],
])("projects hidden %s assistant errors %j as a generic safe failure", (type, text) => {
const result = projectRecentChatDisplayMessages([
{
role: "assistant",
content: [{ type, text }],
stopReason: "error",
errorMessage: "Connection error.",
timestamp: 1,
},
]);
expect(result[0]?.content).toEqual([
{ type: "text", text: "The agent run failed before producing a reply." },
]);
});
it("preserves visible output_text from a failed assistant turn", () => {
const result = projectRecentChatDisplayMessages([
{
role: "assistant",
content: [{ type: "output_text", text: "A partial reply before the run failed." }],
stopReason: "error",
errorMessage: "Connection error.",
timestamp: 1,
},
]);
expect(result[0]?.content).toEqual([
{ type: "output_text", text: "A partial reply before the run failed." },
]);
});
it("projects thinking-only assistant errors as a generic safe failure", () => {
const result = projectRecentChatDisplayMessages([
{

View File

@@ -714,9 +714,22 @@ describe("gateway server chat", () => {
content: [{ type: "output_text", text: "visible response" }],
timestamp: 2,
},
{
role: "assistant",
content: [{ type: "input_text", text: "NO_REPLY" }],
timestamp: 3,
},
{
role: "assistant",
content: [{ type: "input_text", text: "visible assistant input" }],
timestamp: 4,
},
]);
expect(collectHistoryTextValues(historyMessages)).toEqual(["visible response"]);
expect(collectHistoryTextValues(historyMessages)).toEqual([
"visible response",
"visible assistant input",
]);
});
test("chat.history mirrors current-session message tool sends before NO_REPLY", async () => {

View File

@@ -147,6 +147,15 @@ describe("extractAssistantVisibleText", () => {
).toBe("Persisted assistant answer");
});
it("extracts persisted Responses assistant input_text blocks", () => {
expect(
extractAssistantVisibleText({
role: "assistant",
content: [{ type: "input_text", text: "Persisted assistant input" }],
}),
).toBe("Persisted assistant input");
});
it("does not mix unphased legacy text into final_answer output", () => {
expect(
extractAssistantVisibleText({

View File

@@ -24,7 +24,7 @@ export function extractFirstTextBlock(message: unknown): string | undefined {
export type AssistantPhase = "commentary" | "final_answer";
function isAssistantTextContentBlockType(value: unknown): boolean {
return value === "text" || value === "output_text";
return value === "text" || value === "input_text" || value === "output_text";
}
/** Narrows unknown phase metadata to assistant text phases that affect visibility. */

View File

@@ -59,7 +59,7 @@ describe("extractTextCached", () => {
).toBe("Persisted assistant answer");
});
it("ignores persisted Responses text blocks from the wrong role", () => {
it("accepts assistant Responses input blocks but ignores user output blocks", () => {
expect(
extractText({
role: "user",
@@ -71,7 +71,7 @@ describe("extractTextCached", () => {
role: "assistant",
content: [{ type: "input_text", text: "User-only block" }],
}),
).toBeNull();
).toBe("User-only block");
});
it("prefers final_answer assistant text over commentary text", () => {

View File

@@ -13,7 +13,7 @@ function isTextContentBlockType(value: unknown, role: string): boolean {
return (
value === "text" ||
(role === "user" && value === "input_text") ||
(role === "assistant" && value === "output_text")
(role === "assistant" && (value === "input_text" || value === "output_text"))
);
}

View File

@@ -111,7 +111,7 @@ describe("message-normalizer", () => {
expect(assistant.content).toEqual([{ type: "text", text: "Persisted assistant answer" }]);
});
it("does not reinterpret persisted Responses text blocks from the wrong role", () => {
it("accepts assistant Responses input blocks but rejects user output blocks", () => {
const user = normalizeMessage({
role: "user",
content: [{ type: "output_text", text: "Assistant-only block" }],
@@ -122,7 +122,7 @@ describe("message-normalizer", () => {
});
expect(user.content).not.toContainEqual({ type: "text", text: "Assistant-only block" });
expect(assistant.content).not.toContainEqual({ type: "text", text: "User-only block" });
expect(assistant.content).toContainEqual({ type: "text", text: "User-only block" });
});
it("normalizes structured base64 audio content blocks as renderable attachments", () => {

View File

@@ -23,7 +23,7 @@ function isTextContentBlock(
typeof item.text === "string" &&
(item.type === "text" ||
(role === "user" && item.type === "input_text") ||
(role === "assistant" && item.type === "output_text"))
(role === "assistant" && (item.type === "input_text" || item.type === "output_text")))
);
}