diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 00:43:55 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 00:43:55 +0900 |
| commit | e2646ad2b0c28bd64ad4efd6b154f97f8a35e0ad (patch) | |
| tree | 0f739576bbabf19ebb0e254776188b0950c4f489 /packages/kernel/src/runtime/run-turn.test.ts | |
| parent | e7eada4802ceebd86c83bcd6e3eca70152e7f331 (diff) | |
| download | dispatch-e2646ad2b0c28bd64ad4efd6b154f97f8a35e0ad.tar.gz dispatch-e2646ad2b0c28bd64ad4efd6b154f97f8a35e0ad.zip | |
feat(metrics): expose current context size to the frontend
contextSize = the turn's FINAL step inputTokens+outputTokens (true context
occupancy; NOT the aggregate usage, which sums per-step prompts and overcounts
multi-step turns). Stamped on both the live done event (kernel) and persisted
TurnMetrics (session-orchestrator); a client reads the latest turn's value.
- @dispatch/wire 0.4.0->0.5.0: optional contextSize on TurnDoneEvent + TurnMetrics
- @dispatch/transport-contract 0.5.0->0.6.0 (re-export only)
- glossary: context size (reserve 'context window' for the model limit, later)
- FE courier: frontend-context-size-handoff.md
881 vitest pass; tsc -b EXIT 0; biome clean.
Diffstat (limited to 'packages/kernel/src/runtime/run-turn.test.ts')
| -rw-r--r-- | packages/kernel/src/runtime/run-turn.test.ts | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/packages/kernel/src/runtime/run-turn.test.ts b/packages/kernel/src/runtime/run-turn.test.ts index fa2aba4..dcaea7f 100644 --- a/packages/kernel/src/runtime/run-turn.test.ts +++ b/packages/kernel/src/runtime/run-turn.test.ts @@ -2480,4 +2480,101 @@ describe("runTurn", () => { } }); }); + + describe("contextSize", () => { + it("single-step turn: contextSize equals step inputTokens + outputTokens", async () => { + const provider = createFakeProvider([ + [ + { type: "text-delta", delta: "Hello" }, + { type: "usage", usage: { inputTokens: 100, outputTokens: 50 } }, + { type: "finish", reason: "stop" }, + ], + ]); + + const { events, emit } = createCollectingEmit(); + + await runTurn({ + provider, + messages: [userMessage], + tools: [], + dispatch: { maxConcurrent: 1, eager: false }, + conversationId: "conv-1", + turnId: "turn-1", + emit, + }); + + const doneEvt = events.find((e) => e.type === "done"); + expect(doneEvt).toBeDefined(); + if (doneEvt?.type === "done") { + expect(doneEvt.contextSize).toBe(150); + } + }); + + it("multi-step turn: contextSize equals ONLY the last step's inputTokens + outputTokens", async () => { + const tool = createFakeTool("echo", async () => ({ content: "echoed" })); + + const provider = createFakeProvider([ + [ + { type: "tool-call", toolCallId: "tc1", toolName: "echo", input: {} }, + { type: "usage", usage: { inputTokens: 100, outputTokens: 20 } }, + { type: "finish", reason: "tool-calls" }, + ], + [ + { type: "text-delta", delta: "done" }, + { type: "usage", usage: { inputTokens: 300, outputTokens: 80 } }, + { type: "finish", reason: "stop" }, + ], + ]); + + const { events, emit } = createCollectingEmit(); + + await runTurn({ + provider, + messages: [userMessage], + tools: [tool], + dispatch: { maxConcurrent: 1, eager: false }, + conversationId: "conv-1", + turnId: "turn-1", + emit, + }); + + const doneEvt = events.find((e) => e.type === "done"); + expect(doneEvt).toBeDefined(); + if (doneEvt?.type === "done") { + expect(doneEvt.contextSize).toBe(380); + expect(doneEvt.usage).toBeDefined(); + if (doneEvt.usage !== undefined) { + expect(doneEvt.contextSize).not.toBe(doneEvt.usage.inputTokens); + } + } + }); + + it("no usage reported: contextSize is undefined", async () => { + const provider = createFakeProvider([ + [ + { type: "text-delta", delta: "Hello" }, + { type: "finish", reason: "stop" }, + ], + ]); + + const { events, emit } = createCollectingEmit(); + + await runTurn({ + provider, + messages: [userMessage], + tools: [], + dispatch: { maxConcurrent: 1, eager: false }, + conversationId: "conv-1", + turnId: "turn-1", + emit, + }); + + const doneEvt = events.find((e) => e.type === "done"); + expect(doneEvt).toBeDefined(); + if (doneEvt?.type === "done") { + expect(doneEvt.contextSize).toBeUndefined(); + expect(doneEvt.usage).toBeUndefined(); + } + }); + }); }); |
