diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index 49b3a51ce8..ecc6e599c2 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -54,6 +54,24 @@ function toolStatus(entry: MakaPiToolEntry | undefined): string | undefined { } describe('Maka Pi TUI transcript', () => { + test('renders provider dropping guidance from durable declaration state', () => { + const state = createMakaPiTranscriptState(); + replaceTranscriptWithStoredMessages(state, [ + { + type: 'system_note', + id: 'drop-1', + turnId: 't1', + ts: 1, + kind: 'context_provider_dropping', + data: { inputTokens: 100, priorInputTokens: 100, contextWindowDeclared: true }, + }, + ]); + assert.match( + renderMakaPiTranscript(state, meta(), 120).map(stripAnsi).join('\n'), + /already declared/, + ); + }); + test('renders manual compaction from the typed terminal outcome', async () => { for (const [outcome, expected] of [ [{ kind: 'compacted' as const, checkpointId: 'checkpoint-1' }, 'Context compacted.'], diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index d7af4da7df..3f2390db43 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -1355,14 +1355,24 @@ function systemNoteText(message: SystemNoteMessage): string | undefined { return 'Context summary failed; the session continued without a new summary.'; case 'context_provider_dropping': { const data = message.data as - | { inputTokens?: unknown; priorInputTokens?: unknown } + | { inputTokens?: unknown; priorInputTokens?: unknown; contextWindowDeclared?: unknown } | undefined; const used = typeof data?.inputTokens === 'number' ? data.inputTokens : undefined; const prior = typeof data?.priorInputTokens === 'number' ? data.priorInputTokens : undefined; - if (used === undefined || prior === undefined) { - return 'The provider is dropping or rewriting context: content was appended but its reported usage did not grow. Declare a context window for this model so Maka compacts first.'; - } - return `The provider is dropping or rewriting context: content was appended, and it counted ${used} input tokens against ${prior} before, which is no growth. Declare a context window for this model so Maka compacts first.`; + const declared = + typeof data?.contextWindowDeclared === 'boolean' ? data.contextWindowDeclared : undefined; + if (used === undefined || prior === undefined) + return declared === true + ? 'The provider is dropping or rewriting context, but a context window is already declared for this model; Maka will compact first.' + : declared === false + ? 'The provider is dropping or rewriting context: content was appended but its reported usage did not grow. Declare a context window for this model so Maka compacts first.' + : 'The provider is dropping or rewriting context: content was appended but its reported usage did not grow.'; + const detail = `The provider is dropping or rewriting context: content was appended, and it counted ${used} input tokens against ${prior} before, which is no growth.`; + return declared === true + ? `${detail} A context window is already declared for this model, so Maka will compact first.` + : declared === false + ? `${detail} Declare a context window for this model so Maka compacts first.` + : detail; } case 'context_overflow_after_compaction': return 'History was compacted and the provider still called this request too large. What remains also carries the system prompt, the tool schemas, the summary and the recent tail; shortening this message is the part you control.'; diff --git a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts index a1f9df154b..34759106aa 100644 --- a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts +++ b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts @@ -1323,7 +1323,11 @@ function defineMidTurnSuite(consumer: ConsumerMode): void { (message): message is { type: 'system_note'; kind: string; data?: unknown } => (message as { kind?: string }).kind === 'context_provider_dropping', ); - assert.deepEqual(note?.data, { inputTokens: 3_716, priorInputTokens: 3_716 }); + assert.deepEqual(note?.data, { + inputTokens: 3_716, + priorInputTokens: 3_716, + contextWindowDeclared: false, + }); }); test('does not report dropping across the boundary when the input grew', async () => { @@ -1399,10 +1403,15 @@ function defineMidTurnSuite(consumer: ConsumerMode): void { await runFixtureTurn(fixture, consumer); const note = fixture.messages.find( - (message): message is { type: 'system_note'; kind: string } => + (message): message is { type: 'system_note'; kind: string; data?: unknown } => (message as { type?: string }).type === 'system_note', ); assert.equal(note?.kind, 'context_provider_dropping'); + assert.deepEqual(note?.data, { + inputTokens: 100, + priorInputTokens: 100, + contextWindowDeclared: true, + }); }); test('records provider context dropping only for an unshaped usage decrease', async () => { @@ -1414,10 +1423,15 @@ function defineMidTurnSuite(consumer: ConsumerMode): void { await runFixtureTurn(fixture, consumer); const note = fixture.messages.find( - (message): message is { type: 'system_note'; kind: string } => + (message): message is { type: 'system_note'; kind: string; data?: unknown } => (message as { type?: string }).type === 'system_note', ); assert.equal(note?.kind, 'context_provider_dropping'); + assert.deepEqual(note?.data, { + inputTokens: 50, + priorInputTokens: 100, + contextWindowDeclared: true, + }); }); test('does not call provider context dropping when active pruning explains the decrease', async () => { diff --git a/packages/runtime/src/ai-sdk-turn.ts b/packages/runtime/src/ai-sdk-turn.ts index b9a436d850..bb9a7e3910 100644 --- a/packages/runtime/src/ai-sdk-turn.ts +++ b/packages/runtime/src/ai-sdk-turn.ts @@ -1734,6 +1734,7 @@ export class AiSdkTurn { await this.recordSystemNote('context_provider_dropping', turnId, { inputTokens: stepUsage.inputTokens, priorInputTokens: priorInput, + contextWindowDeclared: midTurnState.capacity !== undefined, }); } // Fail closed: reset on every step boundary so a missing final diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts index 047e08f677..1576a49ac6 100644 --- a/packages/ui/src/__tests__/materialize.test.ts +++ b/packages/ui/src/__tests__/materialize.test.ts @@ -207,6 +207,23 @@ describe("steering timeline", () => { }); describe("materializeChat message metadata", () => { + test("renders provider dropping guidance from durable declaration state", () => { + const base = { + type: "system_note" as const, + id: "drop", + turnId: "t1", + ts: 1, + kind: "context_provider_dropping" as const, + data: { inputTokens: 98_247, priorInputTokens: 124_832 }, + }; + const declared = materializeChat([{ ...base, data: { ...base.data, contextWindowDeclared: true } }], "en")[0]?.text; + const undeclared = materializeChat([{ ...base, data: { ...base.data, contextWindowDeclared: false } }], "en")[0]?.text; + const legacy = materializeChat([base], "en")[0]?.text; + assert.match(declared ?? "", /already declared/); + assert.match(undeclared ?? "", /Declare a context window/); + assert.doesNotMatch(legacy ?? "", /Declare a context window/); + }); + test("localizes visible system notes", () => { const messages: StoredMessage[] = [ { diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 1bf10a43b4..64b24211f7 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -329,7 +329,7 @@ export interface ConversationCopy { contextCompacting: string; contextCompacted: string; contextCompactionFailedOpen: string; - contextProviderDropping: (used: number, prior: number) => string; + contextProviderDropping: (used: number, prior: number, contextWindowDeclared?: boolean) => string; contextWindowSuggestion: (tokens: number, declared: number | undefined) => string; contextWindowOverrun: (used: number, declared: number) => string; contextReportedWindowExceeded: (used: number, reported: number) => string; @@ -561,8 +561,11 @@ const CONVERSATION_COPY = { contextCompacting: '正在压缩上下文…', contextCompacted: '已压缩较早的上下文。', contextCompactionFailedOpen: '上下文压缩失败。', - contextProviderDropping: (used, prior) => - `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。在连接设置里为该模型声明上下文窗口,让 Maka 先行压缩。`, + contextProviderDropping: (used, prior, declared) => declared === true + ? `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。该模型已声明上下文窗口,Maka 会先行压缩。` + : declared === false + ? `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。在连接设置里为该模型声明上下文窗口,让 Maka 先行压缩。` + : `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。`, contextWindowSuggestion: (tokens, declared) => declared === undefined ? `供应商拒绝了这次请求。该模型未声明上下文窗口;上次成功的用量约 ${tokens} tokens,可将窗口设为该值让 Maka 先行压缩。` @@ -724,8 +727,11 @@ const CONVERSATION_COPY = { contextCompacting: '正在壓縮上下文…', contextCompacted: '已壓縮較早的上下文。', contextCompactionFailedOpen: '上下文壓縮失敗。', - contextProviderDropping: (used, prior) => - `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。在連線設定裡為該模型宣告上下文視窗,讓 Maka 先行壓縮。`, + contextProviderDropping: (used, prior, declared) => declared === true + ? `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。該模型已宣告上下文視窗,Maka 會先行壓縮。` + : declared === false + ? `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。在連線設定裡為該模型宣告上下文視窗,讓 Maka 先行壓縮。` + : `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。`, contextWindowSuggestion: (tokens, declared) => declared === undefined ? `供應商拒絕了這次請求。該模型未宣告上下文視窗;上次成功的用量約 ${tokens} tokens,可將視窗設為該值讓 Maka 先行壓縮。` @@ -913,8 +919,11 @@ const CONVERSATION_COPY = { contextCompacting: 'Compacting context…', contextCompacted: 'Earlier context compacted.', contextCompactionFailedOpen: 'Context compaction failed.', - contextProviderDropping: (used, prior) => - `The provider is dropping or rewriting context: content was appended, and it counted ${used.toLocaleString('en-US')} input tokens against ${prior.toLocaleString('en-US')} before, which is no growth. Declare a context window for this model in the connection settings so Maka compacts first.`, + contextProviderDropping: (used, prior, declared) => declared === true + ? `The provider is dropping or rewriting context: content was appended, and it counted ${used.toLocaleString('en-US')} input tokens against ${prior.toLocaleString('en-US')} before, which is no growth. A context window is already declared for this model, so Maka will compact first.` + : declared === false + ? `The provider is dropping or rewriting context: content was appended, and it counted ${used.toLocaleString('en-US')} input tokens against ${prior.toLocaleString('en-US')} before, which is no growth. Declare a context window for this model in the connection settings so Maka compacts first.` + : `The provider is dropping or rewriting context: content was appended, and its reported input usage did not grow.`, contextWindowSuggestion: (tokens, declared) => declared === undefined ? `The provider rejected this request. No context window is declared for this model; the last accepted usage was about ${tokens} tokens — set the window to that value so Maka compacts first.` diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts index 23262feedb..4fa8e8567c 100644 --- a/packages/ui/src/materialize.ts +++ b/packages/ui/src/materialize.ts @@ -146,10 +146,11 @@ function systemNoteLabel(kind: string, data: unknown, locale: UiLocale): string if (kind === "context_compacted") return copy.contextCompacted; if (kind === "context_compaction_failed_open") return copy.contextCompactionFailedOpen; if (kind === "context_provider_dropping") { - const dropping = data as { inputTokens?: unknown; priorInputTokens?: unknown } | undefined; + const dropping = data as { inputTokens?: unknown; priorInputTokens?: unknown; contextWindowDeclared?: unknown } | undefined; const used = typeof dropping?.inputTokens === "number" ? dropping.inputTokens : 0; const prior = typeof dropping?.priorInputTokens === "number" ? dropping.priorInputTokens : 0; - return copy.contextProviderDropping(used, prior); + const declared = typeof dropping?.contextWindowDeclared === "boolean" ? dropping.contextWindowDeclared : undefined; + return copy.contextProviderDropping(used, prior, declared); } if (kind === "context_overflow_after_compaction") return copy.contextOverflowAfterCompaction; if (kind === "context_reported_window_exceeded") {