Merge remote-tracking branch 'github/master' into xtr/trajectory-inspection-ui

# Conflicts:
#	packages/client/runtime/src/client/sessions/fold-adapter.ts
#	packages/compact/compact-basic/src/summarizer.ts
This commit is contained in:
_Kerman
2026-07-28 21:56:02 +08:00
495 changed files with 8443 additions and 5556 deletions

View File

@@ -11,6 +11,7 @@ import {
toolPairingBalancedBefore,
} from '@deepseek-ai/dsh-compact'
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { Message } from '@deepseek-ai/dsh-llm'
import type { TokenMeasurement, TokenMeterService } from '@deepseek-ai/dsh-token-meter'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
@@ -134,10 +135,11 @@ export async function compactSurfaceRegion(
throw new Error('compaction: session surface changed during summarization')
}
const framedSummary = frameSummary(summary)
const framedSummaryTokenCount = dependencies.meter.estimateMessage({
role: 'user',
const checkpointMessage = createUserMessage({
content: framedSummary,
source: COMPACT_CHECKPOINT_SOURCE,
})
const framedSummaryTokenCount = dependencies.meter.estimateMessage(checkpointMessage)
if (framedSummaryTokenCount >= shadowedTokenCount) {
throw new Error(
`summary is not smaller than the shadowed content (${framedSummaryTokenCount} estimated framed tokens >= ${shadowedTokenCount})`,
@@ -155,10 +157,7 @@ export async function compactSurfaceRegion(
...maxTokens === undefined ? {} : { maxTokens },
...usage === undefined ? {} : { usage },
})
session.append('user/message', {
content: framedSummary,
source: COMPACT_CHECKPOINT_SOURCE,
}, {
session.append('user/message', checkpointMessage, {
surfaceOp: { op: 'replace', start, end },
sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
})

View File

@@ -5,7 +5,7 @@
*/
import type { Context } from 'cordis'
import { BlockAssembler } from '@deepseek-ai/dsh-llm'
import { createUserMessage, BlockAssembler } from '@deepseek-ai/dsh-llm'
import type {
ContentBlock, FinishReason, GenerateOptions, Message, TokenUsage, ToolSchema,
} from '@deepseek-ai/dsh-llm'
@@ -134,7 +134,10 @@ export async function summarizeWithLlm(
const assembler = new BlockAssembler()
const messages: Message[] = [
...input.messages,
{ role: 'user', content: [{ type: 'text', text: COMPACTION_INSTRUCTION }] },
createUserMessage({
content: [{ type: 'text', text: COMPACTION_INSTRUCTION }],
source: { kind: 'plugin', plugin: 'dsh-compact-basic' },
}),
]
const options: GenerateOptions = {
provider: target.provider,
@@ -151,7 +154,7 @@ export async function summarizeWithLlm(
const error = finishError(assembler.finish)
if (error !== undefined) throw error
const rawOutput = assembler.message().content
const rawOutput = assembler.blocks()
const summary = textOnly(rawOutput)
if (!summary.some(block => block.text.trim().length > 0)) {
throw new Error('summarization produced no text summary content')

View File

@@ -11,7 +11,7 @@ import {
resolveTargetPolicy,
} from '@deepseek-ai/dsh-compact-basic/src/config.ts'
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@deepseek-ai/dsh-llm'
import LlmService, { createUserMessage, CallId, CONTEXT_WINDOW_EXCEEDED_CODE, createToolResultMessage, LlmAdapter , createMessage } from '@deepseek-ai/dsh-llm'
import type {
ContentBlock,
GenerateOptions,
@@ -95,7 +95,10 @@ function summarizedText(input: SummarizationInput): string {
/** A minimal replayed prefix carrying one user message of the given text. */
function promptInput(text: string): SummarizationInput {
return { messages: [{ role: 'user', content: [{ type: 'text', text }] }] }
return { messages: [createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'test' },
})] }
}
/** Closed two-message turns followed by one open turn for durable compaction events. */
@@ -103,10 +106,10 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session {
const session = new Session(SessionId(`conversation-${turns}`))
for (let turn = 1; turn <= turns; turn += 1) {
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `${text} user ${turn}` }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('step/start', { turn, step: 1 })
if (turn === 1) {
session.append('request/header', {
@@ -115,10 +118,16 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session {
})
}
session.append('assistant/message', {
provenance: { provider: MODEL, model: MODEL },
turn,
step: 1,
content: [{ type: 'text', text: `${text} assistant ${turn}` }],
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: `${text} assistant ${turn}` }],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
@@ -135,10 +144,10 @@ function toolConversation(): Session {
for (let turn = 1; turn <= 3; turn += 1) {
const callId = CallId(`call-${turn}`)
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `request ${turn} `.repeat(300) }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('step/start', { turn, step: 1 })
if (turn === 1) {
session.append('request/header', {
@@ -147,21 +156,29 @@ function toolConversation(): Session {
})
}
session.append('assistant/message', {
provenance: { provider: MODEL, model: MODEL },
turn,
step: 1,
content: [
{ type: 'text', text: `calling ${turn} `.repeat(300) },
{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' },
],
message: createMessage({
role: 'assistant',
content: [
{ type: 'text', text: `calling ${turn} `.repeat(300) },
{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' },
],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn, step: 1, callId, name: 'read', arguments: '{}' })
session.append('tool/result', {
turn,
step: 1,
callId,
content: [{ type: 'text', text: `result ${turn} `.repeat(300) }],
isError: false,
message: createToolResultMessage({
callId,
content: [{ type: 'text', text: `result ${turn} `.repeat(300) }],
isError: false,
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
@@ -176,10 +193,10 @@ function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Sess
const callId = CallId('oversized')
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
if (withCompactablePrompt) {
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'older history '.repeat(200) }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
}
session.append('step/start', { turn: 1, step: 1 })
session.append('request/header', {
@@ -189,16 +206,24 @@ function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Sess
session.append('assistant/message', {
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
provenance: { provider: MODEL, model: MODEL },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 1, callId, name: 'bash', arguments: '{}' })
session.append('tool/result', {
turn: 1,
step: 1,
callId,
content: [{ type: 'text', text: 'X'.repeat(chars) }],
isError: false,
message: createToolResultMessage({
callId,
content: [{ type: 'text', text: 'X'.repeat(chars) }],
isError: false,
}),
meta: { presentation: 'preserved' },
}, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
@@ -551,18 +576,26 @@ describe('pressure measurement and retention', () => {
reason: 'initial',
})
session.append('assistant/message', {
provenance: { provider: MODEL, model: MODEL },
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' }],
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 1, callId, name: 'read', arguments: '{}' })
session.append('tool/result', {
turn: 1,
step: 1,
callId,
content: [{ type: 'text', text: 'result' }],
isError: false,
message: createToolResultMessage({
callId,
content: [{ type: 'text', text: 'result' }],
isError: false,
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
const generation = session.surface.replaceGeneration
@@ -705,18 +738,26 @@ describe('pressure measurement and retention', () => {
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn: 1, step: 1 })
session.append('assistant/message', {
provenance: { provider: MODEL, model: MODEL },
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' }],
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 1, callId, name: 'read', arguments: '{}' })
session.append('tool/result', {
turn: 1,
step: 1,
callId,
content: [{ type: 'text', text: 'result' }],
isError: false,
message: createToolResultMessage({
callId,
content: [{ type: 'text', text: 'result' }],
isError: false,
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
@@ -790,7 +831,7 @@ describe('optional model-free tool-result pruning', () => {
expect(await compactIfNeeded(compact, session)).not.toBeNull()
expect(compact.calls).toHaveLength(1)
const original = session.events.find(event => event.type === 'tool/result')
expect(original?.type === 'tool/result' && original.data.content[0])
expect(original?.type === 'tool/result' && original.data.message.content[0].content[0])
.toEqual({ type: 'text', text: 'X'.repeat(3_000) })
expect(session.events.filter(event =>
event.type === 'tool/result' && event.surfaceOp !== 'append')).toHaveLength(0)
@@ -916,10 +957,10 @@ describe('compaction region transaction', () => {
it('rejects a session with no turn boundary at all', async () => {
const compact = service()
const session = new Session(SessionId('turnless'))
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'orphan' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
const node = session.surface.nodes[0]!
await expect(compact.compactRegion(
@@ -1001,10 +1042,10 @@ describe('compaction region transaction', () => {
const compact = service()
const session = conversation(2)
compact.mutateDuringSummary = () => {
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'concurrent surface mutation' }],
source: { kind: 'plugin', plugin: 'test' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
}
const nodes = session.surface.nodes
@@ -1037,16 +1078,22 @@ describe('compaction region transaction', () => {
const compact = service()
const session = new Session(SessionId('model-less-region'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'history '.repeat(100) }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('step/start', { turn: 1, step: 1 })
session.append('assistant/message', {
provenance: { provider: 'historical', model: 'historical' },
turn: 1,
step: 1,
content: [{ type: 'text', text: 'answer '.repeat(100) }],
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'answer '.repeat(100) }],
source: {
kind: 'model',
...{ provider: 'historical', model: 'historical' },
},
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
const nodes = session.surface.nodes
@@ -1161,7 +1208,10 @@ describe('default one-shot summarizer', () => {
it('replays the conversation prefix and appends the instruction as the final message', async () => {
const { adapter, compact } = await summarizerHarness([{ type: 'text', text: 'summary' }])
const tools = [{ name: 'do_thing', description: 'd', parameters: { type: 'object' } }]
const prefix: Message = { role: 'user', content: [{ type: 'text', text: 'earlier turn' }] }
const prefix: Message = createUserMessage({
content: [{ type: 'text', text: 'earlier turn' }],
source: { kind: 'plugin', plugin: 'test' },
})
await compact.runSummarize({
system: 'REPLAYED SYSTEM',
tools,
@@ -1197,7 +1247,10 @@ describe('default one-shot summarizer', () => {
)
const policyAdapter = new ScriptedAdapter([{ type: 'text', text: 'policy summary' }])
ctx.llm.registerAdapter(['policy-summary'], policyAdapter)
const prefix: Message = { role: 'user', content: [{ type: 'text', text: 'warm prefix' }] }
const prefix: Message = createUserMessage({
content: [{ type: 'text', text: 'warm prefix' }],
source: { kind: 'plugin', plugin: 'test' },
})
const output = await compact.runSummarize({
system: 'WARM SYSTEM',

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, resolveRetryPolicy , createMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
@@ -192,16 +192,22 @@ function overflowHistorySeed(): SessionEvent[] {
turn,
trigger: { kind: 'message', source: { kind: 'user' } },
})
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `${sentinel} ${'old context '.repeat(200)}` }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('step/start', { turn, step: 1 })
session.append('assistant/message', {
provenance: { provider: 'mock', model: 'mock' },
turn,
step: 1,
content: [{ type: 'text', text: `historical response ${turn} ${'detail '.repeat(200)}` }],
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: `historical response ${turn} ${'detail '.repeat(200)}` }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
@@ -220,7 +226,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
provider: 'unconfigured-agent-fallback',
model: 'unconfigured-agent-fallback',
})
agent.followup({ content: [{ type: 'text', text: 'do a routed multi-step task' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do a routed multi-step task' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
expect(agent.session.requestHeader()?.config.model).toBe('mock')
@@ -238,7 +244,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
const { ctx } = await harness(8)
try {
const agent = ctx.agentLoop.create(SessionId('post-step-order'), { provider: 'mock', model: 'mock' })
agent.followup({ content: [{ type: 'text', text: 'do tool work' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do tool work' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const events = [...agent.session.events]
@@ -270,7 +276,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
const { ctx } = await harness(8)
try {
const agent = ctx.agentLoop.create(SessionId('repro'), { provider: 'mock', model: 'mock' })
agent.followup({ content: [{ type: 'text', text: 'do a long multi-step task' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do a long multi-step task' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const events = [...agent.session.events]
@@ -331,7 +337,7 @@ describe('context-overflow recovery across the real loop and compact-basic', ()
},
})
agent.followup({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }))
await agent.whenIdle()
expect(adapter.conversationRequests).toHaveLength(2)
@@ -402,7 +408,7 @@ describe('context-overflow recovery across the real loop and compact-basic', ()
seed: overflowHistorySeed(),
agentOptions: { provider: 'mock', model: 'mock' },
})
agent.followup({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }))
await agent.whenIdle()
expect(adapter.conversationRequests).toHaveLength(3)

View File

@@ -6,8 +6,9 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { freezeMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, ToolResultMessage } from '@deepseek-ai/dsh-session'
import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'
import type {
PrunedEntry,
@@ -132,13 +133,21 @@ export class ToolResultPruneService extends Service {
const pruned: PrunedEntry[] = []
let charsRemoved = 0
for (const { seq, event } of candidates) {
const content = this.pruneContent(event.data.content)
const result = event.data.message.content[0]
const content = this.pruneContent(result.content)
if (content === null) continue
const charsBefore = this.measureContent(event.data.content)
const charsBefore = this.measureContent(result.content)
const charsAfter = this.measureContent(content)
const message = freezeMessage<ToolResultMessage>({
...event.data.message,
content: [{
...result,
content,
}] as [typeof result],
})
const replacement = session.append('tool/result', {
...event.data,
content,
message,
}, {
surfaceOp: { op: 'replace', start: seq, end: seq },
sourceEventSeqs: [seq],
@@ -146,7 +155,7 @@ export class ToolResultPruneService extends Service {
pruned.push({
originalSeq: seq,
replacementSeq: replacement.seq,
callId: event.data.callId,
callId: event.data.message.source.callId,
charsBefore,
charsAfter,
})

View File

@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionStore, {
Session,
SessionId,
} from '@deepseek-ai/dsh-session'
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
@@ -41,16 +44,20 @@ function appendToolStep(
session.append('assistant/message', {
turn,
step: 1,
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
provenance: { provider: MODEL, model: MODEL },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: MODEL, model: MODEL },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
const result = session.append('tool/result', {
turn,
step: 1,
callId,
content,
isError: false,
message: createToolResultMessage({ callId, content, isError: false }),
...extra,
}, { surfaceOp: 'append' })
session.append('step/end', { turn, step: 1 })
@@ -172,15 +179,24 @@ describe('ToolResultPruneService session transaction', () => {
const replacement = session.events[entry.replacementSeq]! as SurfaceEvent
expect(original).toMatchObject({
type: 'tool/result',
data: { content: [{ type: 'text', text: 'x'.repeat(100) }] },
data: {
message: {
content: [{
type: 'tool-result',
content: [{ type: 'text', text: 'x'.repeat(100) }],
}],
},
},
})
expect(replacement).toMatchObject({
type: 'tool/result',
data: {
turn: 1,
step: 1,
callId: CallId('one'),
isError: true,
message: {
source: { kind: 'tool', callId: CallId('one') },
},
error: { name: 'ExitError', code: 'EXIT_1' },
meta: { diff: ['a', 'b'] },
futureField: { nested: true },

View File

@@ -29,7 +29,7 @@ const balanceCacheBySession = new WeakMap<Session, BalanceCache>()
function eventDelta(event: SessionEvent): number {
switch (event.type) {
case 'assistant/message':
return event.data.content.filter(block => block.type === 'tool-call').length
return event.data.message.content.filter(block => block.type === 'tool-call').length
case 'tool/result':
return -1
default:

View File

@@ -1,3 +1,4 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import {
@@ -52,10 +53,10 @@ class StubCompactService extends CompactService {
provider: 'mock',
model: 'stub',
})
session.append('user/message', {
session.append('user/message', createUserMessage({
content: summary,
source: COMPACT_CHECKPOINT_SOURCE,
}, {
}), {
surfaceOp: { op: 'replace', start, end },
sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
})
@@ -103,10 +104,10 @@ describe('CompactService seam', () => {
const ctx = new Context()
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
const original = session.append('user/message', {
const original = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'original' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
const result = await svc.compactRegion(original.seq, original.seq, stubAgent(session, 'm'))
@@ -135,10 +136,10 @@ describe('CompactService seam', () => {
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
const controller = new AbortController()
const original = session.append('user/message', {
const original = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'original' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
await svc.compactRegion(original.seq, original.seq, stubAgent(session, 'm'), controller.signal)
expect(svc.lastSignal).toBe(controller.signal)

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { CallId } from '@deepseek-ai/dsh-llm'
import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -26,22 +26,30 @@ function after(session: Session, type: SessionEvent['type'], nth = 0): boolean {
function closedToolStep(): Session {
const session = new Session(SessionId('closed-tool-step'))
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'go' }],
source: { kind: 'user' },
}, SURFACE)
}), SURFACE)
session.append('assistant/message', {
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, SURFACE)
session.append('tool/result', {
turn: 1,
step: 1,
callId: CallId('c1'),
content: [{ type: 'text', text: 'done' }],
isError: false,
message: createToolResultMessage({
callId: CallId('c1'),
content: [{ type: 'text', text: 'done' }],
isError: false,
}),
}, SURFACE)
return session
}
@@ -60,8 +68,14 @@ describe('tool-pairing boundaries', () => {
open.append('assistant/message', {
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: CallId('open'), name: 'bash', arguments: '{}' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('open'), name: 'bash', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, SURFACE)
expect(toolPairingBalancedAfter(open, open.surface.nodes[0]!)).toBe(false)
})
@@ -71,17 +85,33 @@ describe('tool-pairing boundaries', () => {
session.append('assistant/message', {
turn: 1,
step: 1,
content: [
{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' },
{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' },
],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' },
{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' },
],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, SURFACE)
session.append('tool/result', {
turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
content: [],
isError: false,
}),
}, SURFACE)
session.append('tool/result', {
turn: 1, step: 1, callId: CallId('c2'), content: [], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c2'),
content: [],
isError: false,
}),
}, SURFACE)
expect(after(session, 'tool/result', 0)).toBe(false)
@@ -93,24 +123,35 @@ describe('tool-pairing boundaries', () => {
midStep.append('assistant/message', {
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, SURFACE)
midStep.append('user/message', {
midStep.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'background update' }],
source: { kind: 'plugin', plugin: 'test' },
}, SURFACE)
}), SURFACE)
midStep.append('tool/result', {
turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
content: [],
isError: false,
}),
}, SURFACE)
expect(before(midStep, 'user/message')).toBe(false)
expect(after(midStep, 'user/message')).toBe(false)
const free = new Session(SessionId('neutral-free'))
free.append('user/message', {
free.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'idle injection' }],
source: { kind: 'user' },
}, SURFACE)
}), SURFACE)
expect(before(free, 'user/message')).toBe(true)
expect(after(free, 'user/message')).toBe(true)
})
@@ -123,10 +164,10 @@ describe('tool-pairing surface identity', () => {
expect(toolPairingBalancedAfter(session, staleTail)).toBe(true)
const nodes = session.surface.nodes
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'checkpoint' }],
source: { kind: 'plugin', plugin: 'compact' },
}, {
}), {
surfaceOp: { op: 'replace', start: nodes[0]!, end: nodes.at(-1)! },
sourceEventSeqs: [...nodes],
})
@@ -151,10 +192,10 @@ describe('tool-pairing surface identity', () => {
expect(() => toolPairingBalancedBefore(session, missing)).toThrow(/surface seq 999 not found/)
expect(() => toolPairingBalancedAfter(session, missing)).toThrow(/surface seq 999 not found/)
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'first node after empty cache' }],
source: { kind: 'user' },
}, SURFACE)
}), SURFACE)
expect(toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toBe(true)
})
})
@@ -164,7 +205,9 @@ describe('tool-pairing cache refresh', () => {
const events: SessionEvent[] = [
{
type: 'user/message', seq: 0, time: 0,
data: { content: [{ type: 'text', text: 'user' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'user' }], source: { kind: 'user' },
}),
surfaceOp: 'append',
},
{
@@ -172,14 +215,27 @@ describe('tool-pairing cache refresh', () => {
data: {
turn: 1,
step: 1,
content: [{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
},
surfaceOp: 'append',
},
{
type: 'tool/result', seq: 2, time: 2,
data: { turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false },
data: {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
content: [],
isError: false,
}),
},
surfaceOp: 'append',
},
]
@@ -224,7 +280,9 @@ describe('tool-pairing cache refresh', () => {
events.push({
type: 'user/message', seq: 4, time: 4,
data: { content: [{ type: 'text', text: 'tail' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'tail' }], source: { kind: 'user' },
}),
surfaceOp: 'append',
})
nodes.push(4)
@@ -238,14 +296,27 @@ describe('tool-pairing cache refresh', () => {
data: {
turn: 2,
step: 1,
content: [{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
},
surfaceOp: 'append',
},
{
type: 'tool/result', seq: 6, time: 6,
data: { turn: 2, step: 1, callId: CallId('c2'), content: [], isError: false },
data: {
turn: 2, step: 1,
message: createToolResultMessage({
callId: CallId('c2'),
content: [],
isError: false,
}),
},
surfaceOp: 'append',
},
)
@@ -256,7 +327,9 @@ describe('tool-pairing cache refresh', () => {
events.push({
type: 'user/message', seq: 7, time: 7,
data: { content: [{ type: 'text', text: 'replacement' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'replacement' }], source: { kind: 'user' },
}),
surfaceOp: { op: 'replace', start: 0, end: 6 },
})
nodes.splice(0, nodes.length, 7)
@@ -270,11 +343,15 @@ describe('tool-pairing cache refresh', () => {
const events: SessionEvent[] = [
{
type: 'user/message', seq: 0, time: 0,
data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append',
data: createUserMessage({
content: [], source: { kind: 'user' },
}), surfaceOp: 'append',
},
{
type: 'user/message', seq: 1, time: 1,
data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append',
data: createUserMessage({
content: [], source: { kind: 'user' },
}), surfaceOp: 'append',
},
]
const nodes: number[] = [0, 1]
@@ -292,19 +369,29 @@ describe('tool-pairing corrupt surfaces', () => {
it('throws for an orphan result during a rebuild', () => {
const session = new Session(SessionId('orphan-rebuild'))
session.append('tool/result', {
turn: 1, step: 1, callId: CallId('orphan'), content: [], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('orphan'),
content: [],
isError: false,
}),
}, SURFACE)
expect(() => toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toThrow(/no matching tool-call/)
})
it('retries an orphan result in an appended tail without committing partial cache state', () => {
const session = new Session(SessionId('orphan-tail'))
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'safe head' }], source: { kind: 'user' },
}, SURFACE)
}), SURFACE)
expect(toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toBe(true)
session.append('tool/result', {
turn: 1, step: 1, callId: CallId('orphan'), content: [], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('orphan'),
content: [],
isError: false,
}),
}, SURFACE)
expect(() => toolPairingBalancedAfter(session, session.surface.nodes[1]!)).toThrow(/no matching tool-call/)
expect(() => toolPairingBalancedAfter(session, session.surface.nodes[1]!)).toThrow(/no matching tool-call/)
@@ -315,7 +402,9 @@ describe('tool-pairing corrupt surfaces', () => {
const missing = {
events: [{
type: 'user/message', seq: 0, time: 0,
data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append',
data: createUserMessage({
content: [], source: { kind: 'user' },
}), surfaceOp: 'append',
} satisfies SessionEvent],
surface: { nodes: [missingSeq], replaceGeneration: 0 },
} as unknown as Session
@@ -325,7 +414,9 @@ describe('tool-pairing corrupt surfaces', () => {
const mismatched = {
events: [{
type: 'user/message', seq: 99, time: 0,
data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append',
data: createUserMessage({
content: [], source: { kind: 'user' },
}), surfaceOp: 'append',
} satisfies SessionEvent],
surface: { nodes: [mismatchedSeq], replaceGeneration: 0 },
} as unknown as Session