diff --git a/packages/scout-agent/lib/compaction.test.ts b/packages/scout-agent/lib/compaction.test.ts index 57138fe..7f8de37 100644 --- a/packages/scout-agent/lib/compaction.test.ts +++ b/packages/scout-agent/lib/compaction.test.ts @@ -400,8 +400,8 @@ describe("applyCompactionToMessages", () => { ]); const ids2 = result2.map((m) => m.id); expect(ids2).toContain("1"); - expect(ids2).not.toContain("2"); - expect(ids2).not.toContain("2-assistant"); + expect(ids2).toContain("2"); + expect(ids2).toContain("2-assistant"); expect(ids2).not.toContain("2-assistant2"); expect(ids2).not.toContain("3"); }); @@ -443,7 +443,7 @@ describe("applyCompactionToMessages", () => { test("replaces old messages with summary and excluded messages when compaction complete", () => { const messages: Message[] = [ userMsg("kept", "Will be summarized"), - userMsg("excluded-1", "Will be excluded and restored"), + userMsg("kept-1", "Will be excluded and restored"), assistantMsg("excluded-1-assistant", "Will be excluded and restored"), markerMsg("marker-msg"), summaryMsg("summary-msg", "Summary"), @@ -453,7 +453,7 @@ describe("applyCompactionToMessages", () => { const result = applyCompactionToMessages(messages); const ids = result.map((m) => m.id); expect(ids).not.toContain("kept"); - expect(ids).toContain("excluded-1"); + expect(ids).not.toContain("kept-1"); expect(ids).toContain("excluded-1-assistant"); expect(ids).not.toContain("marker-msg"); expect(ids).not.toContain("summary-msg"); @@ -663,7 +663,7 @@ describe("applyCompactionToMessages", () => { expect(ids).not.toContain("1"); expect(ids).not.toContain("2"); // Messages excluded during compaction request should be restored - expect(ids).toContain("3"); + expect(ids).not.toContain("3"); expect(ids).toContain("4"); expect(ids).toContain("interrupted"); // The interrupted user message is preserved // Markers and summary message itself should be gone diff --git a/packages/scout-agent/lib/compaction.ts b/packages/scout-agent/lib/compaction.ts index 71abcef..704b9e1 100644 --- a/packages/scout-agent/lib/compaction.ts +++ b/packages/scout-agent/lib/compaction.ts @@ -54,11 +54,22 @@ export function isOutOfContextError(error: unknown): boolean { if (!apiError) { return false; } - return OUT_OF_CONTEXT_PATTERNS.some((pattern) => - pattern.test( - apiError.responseBody ?? util.inspect(apiError, { depth: null }) - ) - ); + let textToTest = apiError.responseBody ?? ""; + // even though typings say message is always a string, empirically it's not always a string + if (!textToTest && typeof apiError.message === "string") { + textToTest = apiError.message; + } + if (!textToTest) { + try { + textToTest = JSON.stringify(apiError); + } catch { + // note: util.inspect returns different values in Bun and Node.js + // in Node.js it includes the error message, in Bun it doesn't + // that's why it's the final fallback + textToTest = util.inspect(apiError, { depth: null }); + } + } + return OUT_OF_CONTEXT_PATTERNS.some((pattern) => pattern.test(textToTest)); } /** @@ -157,6 +168,10 @@ function isCompactionMarkerPart(part: Message["parts"][number]): boolean { ); } +function isCompactionMarkerMessage(message: Message): boolean { + return message.parts.some((part) => isCompactionMarkerPart(part)); +} + /** * Check if a message part is a compaction summary. */ @@ -404,8 +419,9 @@ function findExcludedMessagesStartIndex( let lastUserIndex = messages.length; let found = 0; for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - if (message?.role !== "user") { + // biome-ignore lint/style/noNonNullAssertion: we know the message is not null + const message = messages[i]!; + if (isCompactionMarkerMessage(message)) { continue; } lastUserIndex = i; diff --git a/packages/scout-agent/lib/core.test.ts b/packages/scout-agent/lib/core.test.ts index 65b192c..1250cf3 100644 --- a/packages/scout-agent/lib/core.test.ts +++ b/packages/scout-agent/lib/core.test.ts @@ -1091,6 +1091,7 @@ describe("compaction", () => { const scout = new Scout({ agent, logger: noopLogger }); agent.on("chat", async ({ messages }) => { const params = await scout.buildStreamTextParams({ + systemPrompt: "hello", chatID, messages, model, @@ -1455,7 +1456,7 @@ describe("compaction", () => { { id: "msg-3", role: "user", - parts: [{ type: "text", text: "Second message - will be excluded" }], + parts: [{ type: "text", text: "Second message to summarize" }], }, ], }); @@ -1478,6 +1479,7 @@ describe("compaction", () => { // Verify: only 1 marker, so excludeCount=1 const params = await scout.buildStreamTextParams({ chatID, + systemPrompt: "system", messages: helper.messages as Message[], model, }); @@ -1485,7 +1487,7 @@ describe("compaction", () => { expect(allContent).toContain("CONVERSATION SUMMARY"); expect(allContent).toContain("Summary after non-context error"); - expect(allContent).toContain("Second message - will be excluded"); // restored + expect(allContent).not.toContain("Second message to summarize"); // restored expect(allContent).not.toContain("First message to summarize"); // summarized expect(allContent).not.toContain("First response to summarize"); // summarized expect(allContent).toContain("Retry after network error"); // added after diff --git a/packages/scout-agent/package.json b/packages/scout-agent/package.json index be7b219..97ce132 100644 --- a/packages/scout-agent/package.json +++ b/packages/scout-agent/package.json @@ -1,7 +1,7 @@ { "name": "@blink-sdk/scout-agent", "description": "A general-purpose AI agent with GitHub, Slack, web search, and compute capabilities built on Blink SDK.", - "version": "0.0.13", + "version": "0.0.14", "type": "module", "keywords": [ "blink",