Merge origin/master into codex/status-bar-token-metrics

Resolve the agent-loop import conflict by retaining both durable request context and runtime policy context. Refresh the combined session fixtures and regenerate documentation catalogs. Mark PDF artifacts as binary so staged whitespace checks do not parse PDF bytes as text.
This commit is contained in:
Hypatia May
2026-07-31 16:53:03 +08:00
249 changed files with 72476 additions and 2957 deletions

View File

@@ -34,6 +34,7 @@ import {
LlmError,
assertNever,
createAssistantMessage,
createUserMessage,
deepFreeze,
errorChain,
freezeMessage,
@@ -45,7 +46,7 @@ import {
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message, PreparedLlmCall, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
import type { AssistantMessage, EpochHeader, RequestContext, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import { renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tools'
import { executeToolCalls } from './tool-calls.ts'
@@ -54,6 +55,47 @@ type StepOutcome =
| { kind: 'completed'; continueTurn: boolean; concluded: boolean; maxTokens: boolean }
| { kind: 'request-failed'; error: RequestError; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined }
const RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt'
/** Clearing marker kept distinct from every prefixed {@link renderContextSnapshot} result. */
const CLEARED_RUNTIME_CONTEXT = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
/** Whether one user message is owned by runtime-context materialization. */
function isRuntimeContextMessage(message: UserMessage): boolean {
return message.source.kind === 'plugin' && message.source.plugin === RUNTIME_CONTEXT_SOURCE
}
/** Latest retained runtime-context snapshot; `found` distinguishes malformed content from absence. */
function retainedRuntimeContext(session: Session): { found: boolean; text: string | undefined } {
const events = session.events
const nodes = session.surface.nodes
for (let index = nodes.length - 1; index >= 0; index -= 1) {
const event = events[nodes[index] as number]
if (event?.type !== 'user/message' || !isRuntimeContextMessage(event.data)) continue
const [block] = event.data.content
return {
found: true,
text: event.data.content.length === 1 && block?.type === 'text' ? block.text : undefined,
}
}
return { found: false, text: undefined }
}
/** Append a full current snapshot only when it changed or compaction removed it. */
function materializeRuntimeContext(session: Session, current: string): void {
const previous = retainedRuntimeContext(session)
if (!previous.found && current.length === 0) {
const compactedPriorSnapshot = session.surface.replaceGeneration > 0
&& session.events.some(event => event.type === 'user/message' && isRuntimeContextMessage(event.data))
if (!compactedPriorSnapshot) return
}
const snapshot = current.length === 0 ? CLEARED_RUNTIME_CONTEXT : current
if (previous.text === snapshot) return
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: snapshot }],
source: { kind: 'plugin', plugin: RUNTIME_CONTEXT_SOURCE },
}), { surfaceOp: 'append' })
}
/** Remove adapter-derived values before plugins propose the next request config. */
function requestProposal(header: EpochHeader): LlmCallConfig {
if (header.adapterDefaults === undefined) return header.config
@@ -520,10 +562,13 @@ export class ReactLoopAgent implements Agent {
// this request together.
this.drainOutbox(turn)
// Assemble the system prompt fresh each step (it may depend on log state).
// Assemble request-owned prompt inputs fresh each step. Dynamic context is
// committed at the tail before deriving history once, preserving the stable
// system/history cache prefix while keeping every model-visible byte logged.
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
signal.throwIfAborted()
const system = renderPrompt(assembly)
materializeRuntimeContext(session, renderContextSnapshot(assembly))
// Snapshot the exact log prefix: the reconstruction boundary. Appends
// after this synchronous snapshot join the next request.

View File

@@ -254,7 +254,7 @@ describe('agent loop', () => {
// NO system field at all (not an empty string).
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
ctx.on('system-prompt/assemble', async () => ({ sections: [], tools: [], variables: {} }))
ctx.on('system-prompt/assemble', async () => ({ sections: [], contexts: [], tools: [], variables: {} }))
const agent = ctx.agentLoop.create(SessionId('a-no-system'), { provider: 'mock', model: 'mock' })
send(agent, 'hi')
@@ -264,6 +264,178 @@ describe('agent loop', () => {
expect('system' in adapter.requests[0]!).toBe(false)
})
it('materializes changed runtime context at the history tail without rewriting the system header', async () => {
const adapter = new MockAdapter([
textResponse('one'),
textResponse('two'),
textResponse('three'),
textResponse('four'),
textResponse('five'),
])
const ctx = await harness(adapter)
let mode = 'read-only'
const dispose = ctx.systemPrompt.context({ name: 'policy', order: 0, text: () => `Mode: ${mode}.` })
const agent = ctx.agentLoop.create(SessionId('a-runtime-context'), { provider: 'mock', model: 'mock' })
const contextEvents = () => agent.session.events.flatMap(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt'
? [event]
: [])
send(agent, 'first')
await waitForIdle(ctx, agent)
expect(contextEvents()).toHaveLength(1)
expect(contextEvents()[0]?.data.content).toEqual([{
type: 'text',
text: 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nMode: read-only.',
}])
send(agent, 'unchanged')
await waitForIdle(ctx, agent)
expect(contextEvents()).toHaveLength(1)
mode = 'danger-full-access'
send(agent, 'changed')
await waitForIdle(ctx, agent)
expect(contextEvents()).toHaveLength(2)
const changedBlock = contextEvents()[1]?.data.content[0]
expect(changedBlock?.type).toBe('text')
if (changedBlock?.type !== 'text') throw new Error('changed runtime context is not text')
expect(changedBlock.text).toContain('danger-full-access')
dispose()
send(agent, 'cleared')
await waitForIdle(ctx, agent)
expect(contextEvents()).toHaveLength(3)
expect(contextEvents()[2]?.data.content).toEqual([{
type: 'text',
text: 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.',
}])
send(agent, 'still clear')
await waitForIdle(ctx, agent)
expect(contextEvents()).toHaveLength(3)
expect(adapter.requests.map(request => request.system)).toEqual(Array(5).fill(adapter.requests[0]?.system))
expect(agent.session.events.filter(event => event.type === 'request/header')).toHaveLength(1)
})
it('re-emits unchanged runtime context when a surface replacement removed the retained snapshot', async () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
const ctx = await harness(adapter)
ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'Mode: read-only.' })
const agent = ctx.agentLoop.create(SessionId('a-runtime-context-compacted'), { provider: 'mock', model: 'mock' })
send(agent, 'first')
await waitForIdle(ctx, agent)
const contextEvent = agent.session.events.find(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')
if (contextEvent?.type !== 'user/message') throw new Error('first turn did not materialize runtime context')
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'test-compaction' },
}), {
surfaceOp: { op: 'replace', start: contextEvent.seq, end: contextEvent.seq },
sourceEventSeqs: [contextEvent.seq],
})
send(agent, 'after compaction')
await waitForIdle(ctx, agent)
const runtimeContexts = agent.session.events.flatMap(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt'
? [event]
: [])
expect(runtimeContexts).toHaveLength(2)
expect(adapter.requests[1]?.messages.some(message =>
message.source.kind === 'plugin'
&& message.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(true)
})
it('clears compacted runtime context after the active set becomes empty', async () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
const ctx = await harness(adapter)
const dispose = ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'Mode: read-only.' })
const agent = ctx.agentLoop.create(SessionId('a-runtime-context-compacted-clear'), { provider: 'mock', model: 'mock' })
send(agent, 'first')
await waitForIdle(ctx, agent)
const contextEvent = agent.session.events.find(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')
if (contextEvent?.type !== 'user/message') throw new Error('first turn did not materialize runtime context')
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary retaining old mode: read-only' }],
source: { kind: 'plugin', plugin: 'test-compaction' },
}), {
surfaceOp: { op: 'replace', start: contextEvent.seq, end: contextEvent.seq },
sourceEventSeqs: [contextEvent.seq],
})
dispose()
send(agent, 'after compaction')
await waitForIdle(ctx, agent)
const clearing = adapter.requests[1]?.messages.find(message =>
message.source.kind === 'plugin'
&& message.source.plugin === '@deepseek-ai/dsh-system-prompt')
expect(clearing?.content).toEqual([{
type: 'text',
text: 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.',
}])
})
it('does not clear runtime context after an unrelated replacement', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a-runtime-context-unrelated-compaction'), { provider: 'mock', model: 'mock' })
const original = agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'old context' }],
source: { kind: 'plugin', plugin: 'test-context' },
}), { surfaceOp: 'append' })
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'test-compaction' },
}), {
surfaceOp: { op: 'replace', start: original.seq, end: original.seq },
sourceEventSeqs: [original.seq],
})
send(agent, 'after compaction')
await waitForIdle(ctx, agent)
expect(adapter.requests[0]?.messages.some(message =>
message.source.kind === 'plugin'
&& message.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false)
})
it('replaces a malformed retained runtime-context message with the current complete snapshot', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'Mode: read-only.' })
const agent = ctx.agentLoop.create(SessionId('a-runtime-context-malformed'), { provider: 'mock', model: 'mock' })
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'broken' }, { type: 'text', text: 'snapshot' }],
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
}), { surfaceOp: 'append' })
send(agent, 'repair context')
await waitForIdle(ctx, agent)
const runtimeContexts = agent.session.events.flatMap(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt'
? [event]
: [])
expect(runtimeContexts).toHaveLength(2)
expect(runtimeContexts[1]?.data.content).toEqual([{
type: 'text',
text: 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nMode: read-only.',
}])
})
it('records raw chunks for replay as assistant/chunk session events', async () => {
const adapter = new MockAdapter([textResponse('abc')])
const ctx = await harness(adapter)