From 3630ce6cbb399182b955df0bb52dd9621ffda3d6 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Tue, 16 Jun 2026 23:57:28 +0800
Subject: [PATCH] fix(agents): return string assistant content in
getLastAssistantText (#93646)
* fix(agents): handle string assistant content in getLastAssistantText
PR #93456 added an `if (!Array.isArray(message.content)) return false` guard
to hasAssistantToolCallArguments, acknowledging that a persisted/legacy
assistant message can carry a string `content` at runtime even though the
type is declared as an array. buildSessionContext pushes such entries through
unchanged, so the string can reach agent.state.messages.
getLastAssistantText() still assumed an array: iterating a string `content`
yields individual characters, none of which has `type === "text"`, so the
assistant's text was silently dropped and the function returned undefined.
Mirror extractTextContent(): when `content` is a string, treat it as the text
itself; otherwise iterate the content blocks as before. The aborted/empty
check is left untouched because `.length === 0` is already correct for both an
empty array and an empty string.
* fix(agents): safely read persisted assistant text
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
---
src/agents/sessions/agent-session.ts | 39 ++++++++++----
src/agents/sessions/sdk.test.ts | 78 ++++++++++++++++++++++++++++
2 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/src/agents/sessions/agent-session.ts b/src/agents/sessions/agent-session.ts
index 44caa0a0cb79..91489a208c2e 100644
--- a/src/agents/sessions/agent-session.ts
+++ b/src/agents/sessions/agent-session.ts
@@ -122,6 +122,30 @@ function normalizeBranchSummaryResult(
return { error: result.error.message };
}
+function hasPersistedAssistantContent(content: unknown): boolean {
+ return (typeof content === "string" || Array.isArray(content)) && content.length > 0;
+}
+
+function extractPersistedAssistantText(content: unknown): string {
+ if (typeof content === "string") {
+ return content;
+ }
+ if (!Array.isArray(content)) {
+ return "";
+ }
+ let text = "";
+ for (const block of content) {
+ if (!block || typeof block !== "object") {
+ continue;
+ }
+ const candidate = block as { type?: unknown; text?: unknown };
+ if (candidate.type === "text" && typeof candidate.text === "string") {
+ text += candidate.text;
+ }
+ }
+ return text;
+}
+
// ============================================================================
// Skill Block Parsing
// ============================================================================
@@ -3213,9 +3237,8 @@ export class AgentSession {
if (m.role !== "assistant") {
return false;
}
- const msg = m;
- // Skip aborted messages with no content
- if (msg.stopReason === "aborted" && msg.content.length === 0) {
+ const content = (m as { content?: unknown }).content;
+ if (m.stopReason === "aborted" && !hasPersistedAssistantContent(content)) {
return false;
}
return true;
@@ -3225,14 +3248,8 @@ export class AgentSession {
return undefined;
}
- let text = "";
- for (const content of (lastAssistant as AssistantMessage).content) {
- if (content.type === "text") {
- text += content.text;
- }
- }
-
- return text.trim() || undefined;
+ const content = (lastAssistant as { content?: unknown }).content;
+ return extractPersistedAssistantText(content).trim() || undefined;
}
// =========================================================================
diff --git a/src/agents/sessions/sdk.test.ts b/src/agents/sessions/sdk.test.ts
index d5bb11a88a71..fcacdf70ec42 100644
--- a/src/agents/sessions/sdk.test.ts
+++ b/src/agents/sessions/sdk.test.ts
@@ -110,6 +110,84 @@ async function createSessionAndStreamModel(model: Model): Promise[0]);
+}
+
+async function createSessionFromManager(sessionManager: SessionManager) {
+ const { session } = await createAgentSession({
+ model: testModel,
+ resourceLoader: createEmptyResourceLoader(),
+ sessionManager,
+ settingsManager: SettingsManager.inMemory(),
+ modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
+ });
+ return session;
+}
+
+async function createSessionWithPersistedAssistantContent(content: unknown) {
+ const sessionManager = SessionManager.inMemory();
+ appendPersistedAssistantMessage({ sessionManager, content });
+ return await createSessionFromManager(sessionManager);
+}
+
+describe("AgentSession getLastAssistantText", () => {
+ it.each([
+ {
+ name: "legacy string content",
+ content: " legacy assistant text ",
+ expected: "legacy assistant text",
+ },
+ {
+ name: "normal text blocks",
+ content: [
+ { type: "thinking", thinking: "hidden" },
+ { type: "text", text: "visible " },
+ { type: "text", text: "answer" },
+ ],
+ expected: "visible answer",
+ },
+ { name: "null content", content: null, expected: undefined },
+ { name: "object content", content: { type: "text", text: "malformed" }, expected: undefined },
+ ])("reads $name without throwing", async ({ content, expected }) => {
+ const session = await createSessionWithPersistedAssistantContent(content);
+ expect(session.getLastAssistantText()).toBe(expected);
+ });
+
+ it("skips aborted malformed content and returns the preceding assistant text", async () => {
+ const sessionManager = SessionManager.inMemory();
+ appendPersistedAssistantMessage({ sessionManager, content: "previous answer" });
+ appendPersistedAssistantMessage({
+ sessionManager,
+ content: null,
+ stopReason: "aborted",
+ });
+ const session = await createSessionFromManager(sessionManager);
+
+ expect(session.getLastAssistantText()).toBe("previous answer");
+ });
+});
+
describe("createAgentSession attribution headers", () => {
it("tolerates Bedrock models that do not expose baseUrl", async () => {
const options = await createSessionAndStreamModel(