diff --git a/.changeset/fix-vscode-context-usage.md b/.changeset/fix-vscode-context-usage.md new file mode 100644 index 0000000000..07c65ed925 --- /dev/null +++ b/.changeset/fix-vscode-context-usage.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Restore live context-window usage updates after switching to the v2 engine. diff --git a/apps/vscode/src/runtime/event-adapter.ts b/apps/vscode/src/runtime/event-adapter.ts index 55c2c44524..68eedfd4b4 100644 --- a/apps/vscode/src/runtime/event-adapter.ts +++ b/apps/vscode/src/runtime/event-adapter.ts @@ -330,7 +330,8 @@ function mapStatusUpdate( sdkEvent: Extract, ): MappedLegacyWireEvent { const payload: StatusUpdate = {}; - if (sdkEvent.contextUsage !== undefined) payload.context_usage = sdkEvent.contextUsage; + const contextUsage = contextUsageRatio(sdkEvent); + if (contextUsage !== undefined) payload.context_usage = contextUsage; if (sdkEvent.planMode !== undefined) payload.plan_mode = sdkEvent.planMode; if (sdkEvent.model !== undefined) payload.model = sdkEvent.model; if (sdkEvent.thinkingEffort !== undefined) payload.thinking_effort = sdkEvent.thinkingEffort; @@ -356,6 +357,22 @@ function mapStatusUpdate( }; } +function contextUsageRatio( + sdkEvent: Extract, +): number | undefined { + if (sdkEvent.contextUsage !== undefined) return sdkEvent.contextUsage; + const { contextTokens, maxContextTokens } = sdkEvent; + if ( + typeof contextTokens !== 'number' || + typeof maxContextTokens !== 'number' || + !Number.isFinite(contextTokens) || + !Number.isFinite(maxContextTokens) + ) { + return undefined; + } + return maxContextTokens > 0 ? contextTokens / maxContextTokens : undefined; +} + function usageDelta(current: AdapterTokenUsage, previous: AdapterTokenUsage | undefined): TokenUsage { return { input_other: delta(current.inputOther, previous?.inputOther), diff --git a/apps/vscode/src/runtime/session-runtime.ts b/apps/vscode/src/runtime/session-runtime.ts index a3bd2b4615..16de802649 100644 --- a/apps/vscode/src/runtime/session-runtime.ts +++ b/apps/vscode/src/runtime/session-runtime.ts @@ -160,6 +160,7 @@ export class SessionRuntime { model: status.model, thinking_effort: status.thinkingEffort, plan_mode: status.planMode, + context_usage: status.contextUsage, }, _sessionId: this.id, }, diff --git a/apps/vscode/test/event-adapter.test.ts b/apps/vscode/test/event-adapter.test.ts index e910edfa3e..fc40ae541f 100644 --- a/apps/vscode/test/event-adapter.test.ts +++ b/apps/vscode/test/event-adapter.test.ts @@ -295,6 +295,50 @@ describe('event adapter (projects SDK events into the legacy Webview contract)', }); }); + it('derives context_usage from the v2 context token pair', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextTokens: 25_600, + maxContextTokens: 256_000, + }); + + expect(result.event).toEqual({ + type: 'StatusUpdate', + payload: { context_usage: 0.1 }, + _sessionId: 'session-1', + }); + }); + + it('preserves an explicit contextUsage field over the context token pair', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextUsage: 0, + contextTokens: 25_600, + maxContextTokens: 256_000, + }); + + expect(result.event).toMatchObject({ + type: 'StatusUpdate', + payload: { context_usage: 0 }, + }); + }); + + it('does not derive a ratio from an invalid context capacity', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextTokens: 25_600, + maxContextTokens: 0, + }); + + expect(result.event).toBeUndefined(); + }); + it('emits only new token usage when SDK status carries cumulative turn usage', () => { const first = adaptSdkEvent(createEventAdapterState(), { type: 'agent.status.updated', diff --git a/apps/vscode/test/kimi-runtime.test.ts b/apps/vscode/test/kimi-runtime.test.ts index 94fb08f994..00474c429a 100644 --- a/apps/vscode/test/kimi-runtime.test.ts +++ b/apps/vscode/test/kimi-runtime.test.ts @@ -440,7 +440,12 @@ describe("Kimi runtime (owns shared SDK sessions for Webviews)", () => { event: Events.StreamEvent, data: { type: "StatusUpdate", - payload: { model: "kimi-test", thinking_effort: "max", plan_mode: true }, + payload: { + model: "kimi-test", + thinking_effort: "max", + plan_mode: true, + context_usage: 0, + }, _sessionId: "saved-1", }, webviewId: "view-1", diff --git a/apps/vscode/test/session-runtime.test.ts b/apps/vscode/test/session-runtime.test.ts index 640277273a..21a63ff147 100644 --- a/apps/vscode/test/session-runtime.test.ts +++ b/apps/vscode/test/session-runtime.test.ts @@ -219,6 +219,22 @@ function turnEnded( } describe("session runtime (adapts one SDK session for subscribed Webviews)", () => { + it("announces the current context usage to a subscribed Webview", async () => { + const { runtime, broadcasts } = createRuntime(); + + await runtime.announceStatus("view-1"); + + expect(streamData(broadcasts)).toContainEqual({ + type: "StatusUpdate", + payload: { + thinking_effort: "off", + plan_mode: false, + context_usage: 0, + }, + _sessionId: "session-1", + }); + }); + it("renders a host-only command without making it a forkable core turn", () => { const { runtime, broadcasts } = createRuntime(); diff --git a/packages/node-sdk/src/v2/session-wiring.ts b/packages/node-sdk/src/v2/session-wiring.ts index eb81bf35ff..ca505d30e5 100644 --- a/packages/node-sdk/src/v2/session-wiring.ts +++ b/packages/node-sdk/src/v2/session-wiring.ts @@ -282,10 +282,18 @@ function withStatusSnapshot(agent: IAgentScopeHandle, event: Event2): Event const contextTokens = tokenCounting.statusSize(); const capabilities = profile.getModelCapabilities(); const maxContextTokens = capabilities.max_input_tokens ?? capabilities.max_context_tokens; + const contextUsage = + Number.isFinite(contextTokens) && + maxContextTokens !== undefined && + Number.isFinite(maxContextTokens) && + maxContextTokens > 0 + ? contextTokens / maxContextTokens + : undefined; return Object.assign({}, event, { usage: usageService.status(), contextTokens, maxContextTokens, + contextUsage, model: profile.getModel(), }) as unknown as Event2; } diff --git a/packages/node-sdk/test/session-event-wiring.test.ts b/packages/node-sdk/test/session-event-wiring.test.ts index dc03dffb4e..abd37fd13e 100644 --- a/packages/node-sdk/test/session-event-wiring.test.ts +++ b/packages/node-sdk/test/session-event-wiring.test.ts @@ -138,6 +138,7 @@ describe('SessionEventWiring status snapshot fold', () => { usage: USAGE, contextTokens: 10, maxContextTokens: 128_000, + contextUsage: 10 / 128_000, model: 'sub-model', }); expect(events[1]).toMatchObject({