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(