fix(fixture): mirror context breakdown projection
This commit is contained in:
@@ -121,6 +121,21 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
|
|||||||
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
|
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
|
||||||
}, { timeout: 10_000 })
|
}, { timeout: 10_000 })
|
||||||
|
|
||||||
|
// Resolve the resident approval so the ordinary composer bar (which owns
|
||||||
|
// ContextMeter) resumes without replacing the session shell. This minimal
|
||||||
|
// boot graph intentionally does not mount the separate question UI plugin.
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: 'Allow once' }))
|
||||||
|
|
||||||
|
// The fixture mirrors all three token-meter projections, so the assembled
|
||||||
|
// ContextMeter reaches its composition panel instead of only the occupancy
|
||||||
|
// fallback path.
|
||||||
|
const contextTrigger = await screen.findByRole('button', { name: /of context used/ })
|
||||||
|
fireEvent.click(contextTrigger)
|
||||||
|
const contextPanel = await screen.findByRole('dialog', { name: 'of context used' })
|
||||||
|
within(contextPanel).getByText('System prompt')
|
||||||
|
within(contextPanel).getByText('Tools')
|
||||||
|
within(contextPanel).getByText('Messages')
|
||||||
|
|
||||||
// The write/edit turns render a real diff card through the assembled graph
|
// The write/edit turns render a real diff card through the assembled graph
|
||||||
// (the keyed FileMutationRow composing ToolRow + DiffBlock), not just the
|
// (the keyed FileMutationRow composing ToolRow + DiffBlock), not just the
|
||||||
// fixture's raw text. The card is collapsed by default, so expand each edit/
|
// fixture's raw text. The card is collapsed by default, so expand each edit/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import type {
|
|||||||
// Type-only: the brand constructor is host-side; the fixture casts at its
|
// Type-only: the brand constructor is host-side; the fixture casts at its
|
||||||
// wire-fabrication boundary (the schema layer's one-cast-point posture).
|
// wire-fabrication boundary (the schema layer's one-cast-point posture).
|
||||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||||
import { foldSurface } from '@deepseek-ai/dsh-session/surface'
|
import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface'
|
||||||
import type {
|
import type {
|
||||||
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
|
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
|
||||||
ModelProviderGroup, ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
|
ModelProviderGroup, ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
|
||||||
@@ -358,6 +358,12 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
events.push({ seq, time: (time += 800), ...authored })
|
events.push({ seq, time: (time += 800), ...authored })
|
||||||
return seq
|
return seq
|
||||||
}
|
}
|
||||||
|
// This resident history represents completed model requests, so retain the
|
||||||
|
// route capacity that accompanied them just as the live prompt path does.
|
||||||
|
push({
|
||||||
|
type: 'request/context',
|
||||||
|
data: { provider: 'deepseek-official', model: 'deepseek-v4-flash', contextWindow: 128_000 },
|
||||||
|
})
|
||||||
for (let turn = 0; turn < 60; turn++) {
|
for (let turn = 0; turn < 60; turn++) {
|
||||||
push({ type: 'turn/start', data: { turn } })
|
push({ type: 'turn/start', data: { turn } })
|
||||||
const userSeq = push({
|
const userSeq = push({
|
||||||
@@ -819,6 +825,65 @@ interface FixtureRequestContext {
|
|||||||
contextWindow?: number
|
contextWindow?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FixtureContextBreakdownProjection {
|
||||||
|
systemTokens: number
|
||||||
|
toolsTokens: number
|
||||||
|
messageTokens: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fixed token-meter heuristic constants mirrored by this client-only fixture. */
|
||||||
|
const CHARS_PER_TOKEN = 4
|
||||||
|
const BLOCK_OVERHEAD = 4
|
||||||
|
const ROLE_OVERHEAD = 4
|
||||||
|
|
||||||
|
/** Price fixture content with token-meter's fixed-density heuristic. */
|
||||||
|
function estimateFixtureContent(blocks: readonly ContentBlock[]): number {
|
||||||
|
let tokens = 0
|
||||||
|
for (const block of blocks) {
|
||||||
|
switch (block.type) {
|
||||||
|
case 'text':
|
||||||
|
case 'reasoning':
|
||||||
|
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
|
||||||
|
break
|
||||||
|
case 'tool-call':
|
||||||
|
tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN)
|
||||||
|
+ Math.ceil(block.arguments.length / CHARS_PER_TOKEN)
|
||||||
|
+ BLOCK_OVERHEAD
|
||||||
|
break
|
||||||
|
case 'tool-result':
|
||||||
|
tokens += estimateFixtureContent(block.content) + BLOCK_OVERHEAD
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fixture parallel of token-meter's heuristic context-composition projection. */
|
||||||
|
function contextBreakdownOf(log: readonly SessionEvent[]): FixtureContextBreakdownProjection {
|
||||||
|
const headerEvent = log.findLast(event => event.type === 'request/header')
|
||||||
|
const header = headerEvent === undefined
|
||||||
|
? undefined
|
||||||
|
: headerEvent.data.header
|
||||||
|
let messageTokens = 0
|
||||||
|
for (const seq of foldSurface(log).nodes) {
|
||||||
|
const event = log[seq]
|
||||||
|
if (event === undefined) continue
|
||||||
|
const message = deriveEventMessage(event)
|
||||||
|
if (message !== null) messageTokens += estimateFixtureContent(message.content) + ROLE_OVERHEAD
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
systemTokens: header?.system === undefined
|
||||||
|
? 0
|
||||||
|
: Math.ceil(header.system.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD,
|
||||||
|
toolsTokens: header?.tools === undefined || header.tools.length === 0
|
||||||
|
? 0
|
||||||
|
: Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD,
|
||||||
|
messageTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Latest log-only route context, or undefined before any request ran. */
|
/** Latest log-only route context, or undefined before any request ran. */
|
||||||
function lastRequestContext(
|
function lastRequestContext(
|
||||||
log: readonly SessionEvent[],
|
log: readonly SessionEvent[],
|
||||||
@@ -874,28 +939,44 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
|
|||||||
values['tokenUsage'] = tokenUsageOf(log)
|
values['tokenUsage'] = tokenUsageOf(log)
|
||||||
// Always present (token-meter composed): last request pressure and capacity.
|
// Always present (token-meter composed): last request pressure and capacity.
|
||||||
values['contextPressure'] = contextPressureOf(log)
|
values['contextPressure'] = contextPressureOf(log)
|
||||||
|
// Always present (token-meter composed): heuristic request composition.
|
||||||
|
values['contextBreakdown'] = contextBreakdownOf(log)
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Host push-frame parallel: emit one session/projection frame per key the given event advanced. */
|
/** Host push-frame parallel: emit one session/projection frame per key the given event advanced. */
|
||||||
function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event: SessionEvent): Extract<MuxFrame, { type: 'session/projection' }>[] {
|
function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event: SessionEvent): Extract<MuxFrame, { type: 'session/projection' }>[] {
|
||||||
const type = (event as { type: string }).type
|
const type = (event as { type: string }).type
|
||||||
|
const frames: Extract<MuxFrame, { type: 'session/projection' }>[] = []
|
||||||
// One usage sample advances both token-meter units.
|
// One usage sample advances both token-meter units.
|
||||||
if (usageSampleOf(event) !== undefined) {
|
if (usageSampleOf(event) !== undefined) {
|
||||||
return [
|
frames.push(
|
||||||
{ type: 'session/projection', sessionId: id, key: 'tokenUsage', value: tokenUsageOf(log), seq: event.seq },
|
{ type: 'session/projection', sessionId: id, key: 'tokenUsage', value: tokenUsageOf(log), seq: event.seq },
|
||||||
{ type: 'session/projection', sessionId: id, key: 'contextPressure', value: contextPressureOf(log), seq: event.seq },
|
{ type: 'session/projection', sessionId: id, key: 'contextPressure', value: contextPressureOf(log), seq: event.seq },
|
||||||
]
|
)
|
||||||
}
|
}
|
||||||
if (type === 'request/context') {
|
if (type === 'request/context') {
|
||||||
return [{
|
frames.push({
|
||||||
type: 'session/projection',
|
type: 'session/projection',
|
||||||
sessionId: id,
|
sessionId: id,
|
||||||
key: 'contextPressure',
|
key: 'contextPressure',
|
||||||
value: contextPressureOf(log),
|
value: contextPressureOf(log),
|
||||||
seq: event.seq,
|
seq: event.seq,
|
||||||
}]
|
})
|
||||||
}
|
}
|
||||||
|
if (type === 'request/header'
|
||||||
|
|| type === 'user/message'
|
||||||
|
|| type === 'assistant/message'
|
||||||
|
|| type === 'tool/result') {
|
||||||
|
frames.push({
|
||||||
|
type: 'session/projection',
|
||||||
|
sessionId: id,
|
||||||
|
key: 'contextBreakdown',
|
||||||
|
value: contextBreakdownOf(log),
|
||||||
|
seq: event.seq,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (frames.length > 0) return frames
|
||||||
if (type === 'session/title') {
|
if (type === 'session/title') {
|
||||||
const values = projectionValuesOf(log)
|
const values = projectionValuesOf(log)
|
||||||
/* v8 ignore next -- the advancing title event is in the log, so the key is present. */
|
/* v8 ignore next -- the advancing title event is in the log, so the key is present. */
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ describe('createFixtureApi', () => {
|
|||||||
},
|
},
|
||||||
// No request ran, so neither pressure nor capacity is known yet.
|
// No request ran, so neither pressure nor capacity is known yet.
|
||||||
contextPressure: {},
|
contextPressure: {},
|
||||||
|
contextBreakdown: {
|
||||||
|
systemTokens: 0,
|
||||||
|
toolsTokens: 0,
|
||||||
|
messageTokens: 0,
|
||||||
|
},
|
||||||
} },
|
} },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -304,6 +309,10 @@ describe('createFixtureApi', () => {
|
|||||||
frame.type === 'session/projection'
|
frame.type === 'session/projection'
|
||||||
&& frame.key === 'contextPressure'
|
&& frame.key === 'contextPressure'
|
||||||
&& (frame.value as { contextWindow?: number }).contextWindow === 128_000)).toBe(true)
|
&& (frame.value as { contextWindow?: number }).contextWindow === 128_000)).toBe(true)
|
||||||
|
expect(frames.some(frame =>
|
||||||
|
frame.type === 'session/projection'
|
||||||
|
&& frame.key === 'contextBreakdown'
|
||||||
|
&& (frame.value as { messageTokens?: number }).messageTokens! > 0)).toBe(true)
|
||||||
const finalize = frames.find((f): f is Extract<MuxFrame, { type: 'session/event' }> => f.type === 'session/event' && f.event.type === 'assistant/message')
|
const finalize = frames.find((f): f is Extract<MuxFrame, { type: 'session/event' }> => f.type === 'session/event' && f.event.type === 'assistant/message')
|
||||||
expect(JSON.stringify(finalize?.event.data)).toContain('(已中断)')
|
expect(JSON.stringify(finalize?.event.data)).toContain('(已中断)')
|
||||||
// Idle cancel: no replay in flight, must not explode; running flips false.
|
// Idle cancel: no replay in flight, must not explode; running flips false.
|
||||||
@@ -335,7 +344,7 @@ describe('createFixtureApi', () => {
|
|||||||
const envelopes: RpcRequest<MuxFrame>[] = []
|
const envelopes: RpcRequest<MuxFrame>[] = []
|
||||||
for await (const envelope of api.events.mux(req({}), abort.signal)) {
|
for await (const envelope of api.events.mux(req({}), abort.signal)) {
|
||||||
envelopes.push(envelope)
|
envelopes.push(envelope)
|
||||||
if (envelopes.length >= 10) abort.abort()
|
if (envelopes.length >= 11) abort.abort()
|
||||||
}
|
}
|
||||||
return envelopes
|
return envelopes
|
||||||
}
|
}
|
||||||
@@ -351,10 +360,15 @@ describe('createFixtureApi', () => {
|
|||||||
expect(first[5]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'goal', value: null })
|
expect(first[5]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'goal', value: null })
|
||||||
expect(first[6]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'tokenUsage' })
|
expect(first[6]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'tokenUsage' })
|
||||||
expect(first[7]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'contextPressure' })
|
expect(first[7]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'contextPressure' })
|
||||||
expect(first[8]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
expect(first[8]?.payload).toMatchObject({
|
||||||
expect(second[8]?.rpcId).toBe(first[8]?.rpcId) // stable rpcId across replays (host replay semantics)
|
type: 'session/projection', sessionId: 'fx-alpha', key: 'contextBreakdown',
|
||||||
expect(first[9]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
value: { systemTokens: 0, toolsTokens: 0 },
|
||||||
expect(second[9]?.rpcId).toBe(first[9]?.rpcId)
|
})
|
||||||
|
expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0)
|
||||||
|
expect(first[9]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
||||||
|
expect(second[9]?.rpcId).toBe(first[9]?.rpcId) // stable rpcId across replays (host replay semantics)
|
||||||
|
expect(first[10]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
||||||
|
expect(second[10]?.rpcId).toBe(first[10]?.rpcId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {
|
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user