Merge remote-tracking branch 'origin/master' into worktree/pr628-merge-20260727

# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
#	docs/architecture.i18n.yaml
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/llm-streaming.md
#	docs/core-data-structures/llm-streaming.zh.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/headless-agent/tests/headless.snapshot.ts
#	packages/compact/compact-basic/tests/compact-loop-repro.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/examples/agent-spine-demo/README.md
#	packages/llm/README.md
#	packages/llm/llm-deepseek/src/adapter.ts
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-retry/README.md
#	packages/llm/llm/README.md
#	packages/llm/llm/src/index.ts
#	packages/llm/llm/tests/service.spec.ts
#	packages/support/llm-replay/src/index.ts
#	packages/support/llm-replay/tests/llm-replay.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-27 22:44:43 +08:00
2019 changed files with 66577 additions and 16794 deletions

View File

@@ -5,7 +5,11 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { CombinedAutocompleteProvider, type Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { agentEvents, assembleContextFor, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
import { type LlmCallConfig } from '@deepseek-ai/dsh-llm'
import {
ReasoningEffortId,
type LlmCallConfig,
type LlmModelReasoningInfo,
} from '@deepseek-ai/dsh-llm'
import { GOAL_CHANGE_VERSION, GoalId, renderGoalChange, type GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal'
import CommandService, { type CommandInvocation } from '@deepseek-ai/dsh-commands'
import SessionStore, { SessionId, type JsonValue, type SessionEvent, type SessionHeader, type TurnEndReason } from '@deepseek-ai/dsh-session'
@@ -143,7 +147,11 @@ function provideLlmCatalog(ctx: Context): void {
ctx.provide('llm', {
listProviders: () => [],
listModels: () => Promise.resolve([]),
resolveModelContext: () => Promise.resolve(undefined),
resolveModelInfo: (provider: string, model: string) => Promise.resolve({
provider,
id: model,
name: model,
}),
} as never)
}
@@ -157,7 +165,7 @@ describe('TUI config', () => {
maxResumeOptions: 8,
questionDialogWidth: 200,
questionDialogMaxHeight: 20,
modelDialogWidth: 72,
modelDialogWidth: 76,
modelDialogMaxHeight: 20,
fileSearchMaxResults: 20,
fileSearchMaxEntries: 10_000,
@@ -998,8 +1006,9 @@ describe('resume command and /resume', () => {
await tick(); await tick()
result.terminal.send('Fallback target')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('This host cannot hand off in place. Exit and run:')
await vi.waitFor(() => {
expect(result.terminal.output).toContain('This host cannot hand off in place. Exit and run:')
})
expect(result.terminal.output).toContain('dsh --resume fallback-session')
expect(result.terminal.stopped).toBe(0)
await dispose(result)
@@ -1019,8 +1028,9 @@ describe('resume command and /resume', () => {
await tick(); await tick()
result.terminal.send('No fallback target')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Session is resumable, but this host cannot hand it off in place')
await vi.waitFor(() => {
expect(result.terminal.output).toContain('Session is resumable, but this host cannot hand it off in place')
})
await dispose(result)
})
@@ -1155,7 +1165,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(result.terminal.output).toContain('restored answer')
expect(result.terminal.output).toContain('write tests')
expect(result.terminal.output).toContain('↑1.3k ↓42')
// Context resolution is async (resolveModelContext); settle before reading.
// Exact model resolution is async; settle before reading.
await tick()
expect(result.terminal.output).toContain('42% context tools:collapsed')
// Narrow terminals clip the right-hand context/tools segment first; the
@@ -1732,7 +1742,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(result.terminal.output).toContain('main-session')
expect(result.terminal.output).toContain('Inspect status \\x1b]2;unsafe\\x07')
expect(result.terminal.output).toContain('/workspace/status')
expect(result.terminal.output).toContain('deepseek/deepseek-v4-pro (reasoning hidden)')
expect(result.terminal.output).toContain('deepseek/deepseek-v4-pro (effort default; reasoning blocks')
expect(result.terminal.output).toContain('hidden)')
expect(result.terminal.output).toContain('running · 6 events · 1 turn · 1 step · 2 tool calls')
expect(result.terminal.output).toContain('1,250 input + 340 output')
expect(result.terminal.output).toContain('[███████████░░░░░] 67% hit (3,000 read + 250 write)')
@@ -1760,7 +1771,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
catalog: {
providers: [],
models: [],
resolveModelContext: () => Promise.resolve(undefined),
resolveModelInfo: () => Promise.resolve({}),
},
})
result.terminal.send('/status')
@@ -1768,7 +1779,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
await tick()
expect(result.terminal.output).toContain('untitled')
expect(result.terminal.output).toContain('unset (reasoning shown)')
expect(result.terminal.output).toContain('unset (effort unset; reasoning blocks shown)')
expect(result.terminal.output).toContain('idle · 0 events · 0 turns · 0 steps · 0 tool calls')
expect(result.terminal.output).toContain('n/a (0 read + 0 write)')
expect(result.terminal.output).toContain('7 used · capacity unknown')
@@ -2307,6 +2318,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
it('opens a keyboard selector and switches the session model without sending slash text to the agent', async () => {
const initialContext = Promise.withResolvers<{ contextWindow: number }>()
let deferInitialContext = true
const result = await setup({
agentOptions: { provider: 'alpha', model: 'a1' },
contextTokens: 50,
@@ -2318,9 +2330,42 @@ describe('pi-tui chat lifecycle and transcript', () => {
{ provider: 'beta', id: 'b1', name: 'Beta One' },
{ provider: 'beta', id: 'shared', name: 'Beta Shared' },
],
resolveModelContext: (provider, model) => provider === 'alpha' && model === 'a1'
? initialContext.promise
: Promise.resolve({ contextWindow: 200 }),
async resolveModelInfo(provider, model) {
const shouldDeferContext = provider === 'alpha' && model === 'a1' && deferInitialContext
if (shouldDeferContext) deferInitialContext = false
const context = shouldDeferContext
? await initialContext.promise
: { contextWindow: 200 }
let reasoning: LlmModelReasoningInfo | undefined
if (model === 'a1') {
reasoning = {
efforts: [
{ id: ReasoningEffortId('low'), name: 'Low' },
{ id: ReasoningEffortId('high'), name: 'High' },
],
defaultEffort: ReasoningEffortId('low'),
}
} else if (model === 'b1') {
reasoning = {
efforts: [
{ id: ReasoningEffortId('high'), name: 'High' },
{ id: ReasoningEffortId('max'), name: 'Max' },
],
defaultEffort: ReasoningEffortId('high'),
}
} else if (provider === 'alpha' && model === 'shared') {
reasoning = {
efforts: [
{ id: ReasoningEffortId('standard'), name: 'Standard' },
{ id: ReasoningEffortId('ultra'), name: 'Ultra' },
],
}
}
return {
context,
...reasoning === undefined ? {} : { reasoning },
}
},
},
})
@@ -2345,6 +2390,92 @@ describe('pi-tui chat lifecycle and transcript', () => {
result.terminal.send('\x1b')
await tick()
const providerDefaultOutput = result.terminal.output.length
result.terminal.send('/model alpha/shared')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(providerDefaultOutput)).toContain('Reasoning effort: provider default.')
})
result.terminal.send('/model')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(providerDefaultOutput)).toContain('Select model')
})
expect(result.terminal.output.slice(providerDefaultOutput)).toContain('Alpha Shared — provider default')
result.terminal.send('\x1b[Z')
await tick()
expect(result.terminal.output.slice(providerDefaultOutput)).toContain('Alpha Shared — Standard')
result.terminal.send('\r')
await tick()
expect(result.terminal.output.slice(providerDefaultOutput)).toContain('Reasoning effort: Standard.')
const resetDefaultOutput = result.terminal.output.length
result.terminal.send('/model')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(resetDefaultOutput)).toContain('Alpha Shared — Standard — current')
})
result.terminal.send('\x1b[Z')
await tick()
expect(result.terminal.output.slice(resetDefaultOutput)).toContain('Alpha Shared — Ultra — current')
result.terminal.send('\x1b[Z')
await tick()
expect(result.terminal.output.slice(resetDefaultOutput)).toContain('Alpha Shared — provider default')
result.terminal.send('\r')
await tick()
expect(result.terminal.output.slice(resetDefaultOutput)).toContain('Reasoning effort: provider default.')
const explicitResetSeed: LlmCallConfig = {
provider: 'beta',
model: 'b1',
reasoningEffort: ReasoningEffortId('max'),
}
await result.ctx.systemPrompt.assemble(assembleContextFor(result.agent))
await expect(agentEvents(result.ctx, result.agent).waterfall(
'agent/request',
0,
0,
explicitResetSeed,
new AbortController().signal,
() => Promise.resolve(explicitResetSeed),
)).resolves.toEqual({ provider: 'alpha', model: 'shared' })
const nonReasoningOutput = result.terminal.output.length
result.terminal.send('/model')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(nonReasoningOutput)).toContain('Select model')
})
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[Z')
result.terminal.send('\r')
await tick()
expect(result.terminal.output.slice(nonReasoningOutput)).toContain('Model selected: beta/shared.')
result.terminal.send('/model beta/shared')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(nonReasoningOutput)).toContain('Model is already beta/shared.')
})
await result.ctx.systemPrompt.assemble(assembleContextFor(result.agent))
result.terminal.send('/model alpha/a1')
result.terminal.send('\r')
await vi.waitFor(() => {
expect(result.terminal.output.slice(nonReasoningOutput)).toContain('Reasoning effort: Low.')
})
const inheritedEffort: LlmCallConfig = {
provider: 'alpha',
model: 'a1',
reasoningEffort: ReasoningEffortId('max'),
}
await expect(agentEvents(result.ctx, result.agent).waterfall(
'agent/request',
0,
0,
inheritedEffort,
new AbortController().signal,
() => Promise.resolve(inheritedEffort),
)).resolves.toEqual({ provider: 'beta', model: 'shared' })
result.agent.status = 'running'
const runningSelectorOutput = result.terminal.output.length
result.terminal.send('/model')
@@ -2353,13 +2484,18 @@ describe('pi-tui chat lifecycle and transcript', () => {
const output = result.terminal.output.slice(runningSelectorOutput)
expect(output).toContain('Select model')
expect(output).toContain('alpha/a1')
expect(output).toContain('Alpha One — Fast — current')
expect(output).toContain('Alpha One — Fast — Low — current')
expect(output).toContain('Beta One — High')
})
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[Z')
await tick()
expect(result.terminal.output).toContain('Beta One — Max')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Model selected: beta/b1')
expect(result.terminal.output).toContain('Reasoning effort: Max.')
expect(result.agent.sent).toEqual([])
expect(result.agent.steered).toEqual([])
initialContext.resolve({ contextWindow: 100 })
@@ -2378,8 +2514,12 @@ describe('pi-tui chat lifecycle and transcript', () => {
result.agent.status = 'idle'
result.ctx.emit('agent/status', result.agent, 'idle')
await tick()
expect(result.terminal.output).toContain('b1 ')
expect(result.terminal.output).toContain('b1 max ')
expect(result.terminal.output).toContain('25% context tools:collapsed')
result.terminal.send('/status')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('beta/b1 (effort max; reasoning blocks shown)')
const assembly = await result.ctx.systemPrompt.assemble(assembleContextFor(result.agent))
expect(assembly.variables).toMatchObject({ provider: 'beta', model: 'b1' })
@@ -2387,7 +2527,12 @@ describe('pi-tui chat lifecycle and transcript', () => {
const request = await agentEvents(result.ctx, result.agent).waterfall(
'agent/request', 1, 0, seed, new AbortController().signal, () => Promise.resolve(seed),
)
expect(request).toEqual({ provider: 'beta', model: 'b1', temperature: 0.2 })
expect(request).toEqual({
provider: 'beta',
model: 'b1',
reasoningEffort: ReasoningEffortId('max'),
temperature: 0.2,
})
await dispose(result)
})
@@ -2397,7 +2542,13 @@ describe('pi-tui chat lifecycle and transcript', () => {
catalog: { providers: [{ id: 'beta', name: 'Beta' }], models: [] },
beforeMount(session) {
session.append('request/header', {
header: { config: { provider: 'beta', model: 'private' } },
header: {
config: {
provider: 'beta',
model: 'private',
reasoningEffort: ReasoningEffortId('ultra'),
},
},
reason: 'initial',
})
},
@@ -2407,15 +2558,36 @@ describe('pi-tui chat lifecycle and transcript', () => {
await tick()
expect(resumed.terminal.output).toContain('Select model')
expect(resumed.terminal.output).toContain('beta/private')
expect(resumed.terminal.output).toContain('private — current')
expect(resumed.terminal.output).toContain('private — ultra — current')
resumed.terminal.send('\x1b')
await tick()
resumed.terminal.send('/model beta/private')
resumed.terminal.send('\r')
await tick()
expect(resumed.terminal.output).toContain('with reasoning effort ultra')
await dispose(resumed)
const resumedDefault = await setup({
catalog: {
providers: [{ id: 'alpha', name: 'Alpha' }],
models: [{ provider: 'alpha', id: 'default', name: 'Default Model' }],
},
beforeMount(session) {
session.append('request/header', {
header: { config: { provider: 'alpha', model: 'default' } },
reason: 'initial',
})
},
})
expect(resumedDefault.terminal.output).toContain('default • main-session')
await dispose(resumedDefault)
const unset = await setup({
agentOptions: {},
catalog: {
providers: [{ id: 'alpha', name: 'Alpha' }],
models: [{ provider: 'alpha', id: 'a1', name: 'Alpha One' }],
resolveModelContext: () => Promise.resolve(undefined),
resolveModelInfo: () => Promise.resolve({}),
},
})
unset.terminal.send('/model')
@@ -2447,7 +2619,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
models: [],
listModels: () => Promise.reject(new Error('catalog offline')),
resolveModelContext: () => Promise.reject(new Error('capacity offline')),
resolveModelInfo: () => Promise.reject(new Error('capacity offline')),
},
})
failed.terminal.send('/model')
@@ -2457,6 +2629,20 @@ describe('pi-tui chat lifecycle and transcript', () => {
})
expect(failed.terminal.output).toContain('Could not resolve model context: capacity offline')
await dispose(failed)
const reasoningFailed = await setup({
catalog: {
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
models: [{ provider: 'deepseek', id: 'model-1', name: 'Model One' }],
resolveModelInfo: () => Promise.reject(new Error('reasoning metadata offline')),
},
})
reasoningFailed.terminal.send('/model')
reasoningFailed.terminal.send('\r')
await vi.waitFor(() => {
expect(reasoningFailed.terminal.output).toContain('Could not read the model catalog: reasoning metadata offline')
})
await dispose(reasoningFailed)
})
it('does not render a model catalog that resolves after TUI disposal', async () => {
@@ -2498,7 +2684,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
catalog: {
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
models: [],
resolveModelContext: () => context.promise,
resolveModelInfo: () => context.promise.then(value => ({ context: value })),
},
})
await contextResult.controller.dispose()