Files
deepseek-harness/packages/context/session-reference/tests/loader-composition.spec.ts
T
Yichen Jiang 4f36177740 feat(agent): publish step decisions as agent.step and add agent/step-boundary
Each proposed step now dispatches the serial agent/step-boundary event
after claiming input, then agent/model, and publishes the chosen model
selection as the optional agent.step until the step ends or its proposal
is rejected, fails, or is cancelled. agent/pre-step listeners, tool
conditions, prompt sections, and the provider and model variables read
agent.step, so the pre-step payload, AssembleContext, and
ToolActivationContext carry no model selection field.

Plan mode commits pending selections at the step boundary, before model
choice, and still narrates the switch in the first admitted step. The
system prompt assembles once after step/start, including runtime context,
and system-prompt/prepare is removed. The runtime-context snapshot is
admitted after the step's accepted messages, so a changed runtime context
alone no longer opens a step.
2026-10-13 17:04:55 +08:00

145 lines
7.6 KiB
TypeScript

/** Real Loader composition preserves retrievable source text outside the bounded preview. */
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
import * as systemPromptPlugin from '@deepseek-ai/dsh-system-prompt'
import * as toolsPlugin from '@deepseek-ai/dsh-tools'
import * as fsPlugin from '@deepseek-ai/dsh-fs-local'
import * as toolFsPlugin from '@deepseek-ai/dsh-tool-fs'
import * as sessionPlugin from '@deepseek-ai/dsh-session'
import * as sessionProjectionPlugin from '@deepseek-ai/dsh-session-projection'
import * as workingDirectoryPlugin from '@deepseek-ai/dsh-working-directory'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import * as queryPlugin from '@deepseek-ai/dsh-session-query-sqlite'
import * as referencePlugin from '@deepseek-ai/dsh-session-reference'
import * as spillPlugin from '@deepseek-ai/dsh-spill-local'
import { sessionDir } from '@deepseek-ai/dsh-spill-local'
import * as sourcePlugin from './fixtures/source-session.ts'
let context: Context | undefined
let root: string | undefined
afterEach(async () => {
await context?.fiber.dispose()
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
})
describe('session-reference real Loader composition', () => {
it('logs a bounded preview and reads the full immutable spill owned by the target', async () => {
root = await mkdtemp(join(tmpdir(), 'reference-loader-'))
const spillRoot = join(root, 'spills')
const fixture = await readFile(new URL('./fixtures/cordis.yml', import.meta.url), 'utf8')
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, fixture.replace('{{spillRoot}}', spillRoot.replaceAll('\\', '/')))
const ctx = context = new Context()
ctx.baseUrl = pathToFileURL(root).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['@deepseek-ai/dsh-session', sessionPlugin],
['@deepseek-ai/dsh-session-projection', sessionProjectionPlugin],
['@deepseek-ai/dsh-working-directory', workingDirectoryPlugin],
['@deepseek-ai/dsh-system-prompt', systemPromptPlugin],
['@deepseek-ai/dsh-tools', toolsPlugin],
['@deepseek-ai/dsh-fs-local', fsPlugin],
['@deepseek-ai/dsh-tool-fs', toolFsPlugin],
['@deepseek-ai/dsh-session-query-sqlite', queryPlugin],
['@deepseek-ai/dsh-session-reference', referencePlugin],
['@deepseek-ai/dsh-spill-local', spillPlugin],
['./source-session.ts', sourcePlugin],
])
ctx.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (!modules.has(specifier)) throw new Error('Unexpected Loader import: ' + specifier)
return modules.get(specifier)
},
} as unknown as NonNullable<typeof ctx.loader.internal>
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await ctx.loader.await()
const target = ctx.sessions.create(SessionId('reference-target'))
const agent = { id: target.id, ctx, session: target } as Agent
const direct = createUserMessage({
content: [{ type: 'text', text: 'Use ' + referencePlugin.formatSessionReferenceMention({
sessionId: SessionId('reference-source'), label: 'Research',
}) }],
source: { kind: 'user' },
})
const decision = await agentEvents(ctx, agent).waterfall('agent/pre-step', {
messages: [direct], turn: 1, step: 1, signal: new AbortController().signal,
}, () => Promise.resolve({ kind: 'enter' as const, messages: [direct] }))
expect(decision.kind).toBe('enter')
if (decision.kind !== 'enter') throw new Error('Expected admitted reference')
expect(decision.messages).toHaveLength(2)
for (const message of decision.messages) target.append('user/message', message, { surfaceOp: 'append' })
const contextMessage = decision.messages[1]
const block = contextMessage?.content[0]
if (block?.type !== 'text') throw new Error('Expected reference context text')
const preview = JSON.parse(block.text.split('<referenced-sessions>\n')[1]!.split('\n</referenced-sessions>')[0]!) as unknown[]
expect(Buffer.byteLength(JSON.stringify(preview[0]))).toBeLessThanOrEqual(360)
expect(block.text).not.toContain('EARLY_SOURCE_FACT')
expect(block.text).toContain('LATEST_SOURCE_FACT')
const notices = JSON.parse(block.text.split('## Reference omissions\n\n')[1]!.split('\n').slice(1).join('\n')) as Array<{
sessionId: string
capturedThroughSeq: number
omittedMessages: number
omittedBytes: number
fullSnapshot: { status: string; locator: string; bytes: number; retrievalHint: string }
}>
expect(notices).toHaveLength(1)
const notice = notices[0]!
expect(notice).toMatchObject({ sessionId: 'reference-source', capturedThroughSeq: 2, omittedMessages: 1 })
expect(notice.omittedBytes).toBeGreaterThan(0)
expect(notice.fullSnapshot.status).toBe('saved')
expect(notice.fullSnapshot.retrievalHint).toContain('offset/limit')
expect(dirname(notice.fullSnapshot.locator)).toBe(sessionDir(spillRoot, target.id))
const transcript = await readFile(notice.fullSnapshot.locator, 'utf8')
expect(Buffer.byteLength(transcript)).toBe(notice.fullSnapshot.bytes)
expect(transcript).toContain('untrusted, read-only snapshot')
const readLines: string[] = []
let totalLines = Infinity
for (let offset = 1; offset <= totalLines; offset += 7) {
const read = await ctx.tools.execute({
name: 'read', callId: ToolCallId(`read-${offset}`),
arguments: { file_path: notice.fullSnapshot.locator, offset, limit: 7 },
signal: new AbortController().signal,
})
expect(read.isError).toBe(false)
if (read.isError) throw new Error('Expected saved transcript read')
const value = read.value as { lines: { text: string }[]; totalLines: number }
totalLines = value.totalLines
readLines.push(...value.lines.map(line => line.text))
}
expect(readLines.join('\n') + '\n').toBe(transcript)
const messages = readLines.join('\n').split(/### Message \d+: (?:user|assistant)\n\n/).slice(1)
.map(body => body.split('\n').filter(line => line.startsWith('"'))
.map(line => JSON.parse(line) as string).join(''))
expect(messages).toEqual([
'EARLY_SOURCE_FACT\n' + 'Historical detail 界.\n'.repeat(30)
+ 'x'.repeat(4096) + 'GIANT_LINE_MIDDLE_FACT' + 'y'.repeat(4096),
'LATEST_SOURCE_FACT\nThe captured answer is forty-two.',
])
expect(transcript).not.toContain('NESTED_REFERENCE_MUST_NOT_PROPAGATE')
expect(transcript).not.toContain('PRIVATE_REASONING_MUST_NOT_PROPAGATE')
expect(await readdir(spillRoot)).toEqual([dirname(notice.fullSnapshot.locator).split(/[\\/]/).at(-1)])
const captured = target.deriveMessages()
ctx.sessions.get(SessionId('reference-source'))!.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'LATER_SOURCE_MUTATION' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
expect(Session.create(SessionId('replayed-target'), target.snapshotEvents()).deriveMessages()).toEqual(captured)
expect(await readFile(notice.fullSnapshot.locator, 'utf8')).toBe(transcript)
})
})