Merge remote-tracking branch 'origin/master' into codex/status-bar-token-metrics

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/event-producer-consumer.md
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent-loop/tests/request-reconstruction.spec.ts
#	packages/core/agent/README.i18n.yaml
#	packages/llm/llm/README.i18n.yaml
This commit is contained in:
Hypatia May
2026-07-28 19:54:58 +08:00
363 changed files with 5880 additions and 3017 deletions

View File

@@ -1,3 +1,4 @@
import { MessageId, freezeMessage } from '@deepseek-ai/dsh-llm'
/**
* Command/skill RPC handlers and the two new frames over createApiProxy:
* command.list serves the addressed agent's effective catalog (missing
@@ -10,10 +11,10 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentMessage } from '@deepseek-ai/dsh-agent'
import AgentRegistry, {} from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionId, UserMessage } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
@@ -246,9 +247,10 @@ describe('host/commands-changed frame', () => {
})
/** Build one frozen inbox message for the live `agent/inbox/*` events. */
function inboxMessage(id: string, text: string, rpcId?: string): AgentMessage {
return Object.freeze({
id: AgentMessageId(id),
function inboxMessage(id: string, text: string, rpcId?: string): UserMessage {
return freezeMessage({
id: MessageId(id),
role: 'user',
content: [{ type: 'text' as const, text }],
source: rpcId === undefined ? { kind: 'user' as const } : { kind: 'user' as const, rpcId: RpcId(rpcId) },
})
@@ -271,8 +273,8 @@ describe('session/queued frames', () => {
const liveFrames = (await liveCollected).filter(f => f.type === 'session/queued')
expect(liveFrames).toEqual([
{ type: 'session/queued', sessionId: agent.id, content: queued.content, source: { kind: 'user' }, steering: false },
{ type: 'session/queued', sessionId: agent.id, content: steering.content, source: { kind: 'user' }, steering: true },
{ type: 'session/queued', sessionId: agent.id, message: queued, steering: false },
{ type: 'session/queued', sessionId: agent.id, message: steering, steering: true },
])
// A fresh mux connection replays the still-pending entries as its baseline.
@@ -290,8 +292,8 @@ describe('session/queued frames', () => {
const steering = inboxMessage('m-4', 'x', 'r-1')
ctx.emit('agent/inbox/enqueue', agent, queued, 'queued')
ctx.emit('agent/inbox/enqueue', agent, steering, 'steering')
ctx.emit('agent/inbox/dequeue', agent, queued)
ctx.emit('agent/inbox/dequeue', agent, steering)
ctx.emit('agent/inbox/dequeue', agent, queued, 'queued')
ctx.emit('agent/inbox/dequeue', agent, steering, 'steering')
const abort = new AbortController()
const frames = await collect<MuxFrame>(
@@ -299,6 +301,24 @@ describe('session/queued frames', () => {
expect(frames.filter(f => f.type === 'session/queued')).toHaveLength(0)
})
it('retires the matching placement when one message identity is queued and steering', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
const agent = stubAgent(ctx)
const repeated = inboxMessage('m-repeat', 'same prompt')
ctx.emit('agent/inbox/enqueue', agent, repeated, 'queued')
ctx.emit('agent/inbox/enqueue', agent, repeated, 'steering')
ctx.emit('agent/inbox/dequeue', agent, inboxMessage('unknown', 'not queued'), 'queued')
ctx.emit('agent/inbox/dequeue', agent, repeated, 'steering')
const abort = new AbortController()
const frames = await collect<MuxFrame>(
api.events.mux({ rpcId: RpcId('t-mux-repeat'), payload: {} }, abort.signal), 2, abort)
expect(frames.filter(f => f.type === 'session/queued')).toEqual([
{ type: 'session/queued', sessionId: agent.id, message: repeated, steering: false },
])
})
it('retires mirror entries on a batch discard (cancel path)', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
@@ -314,6 +334,6 @@ describe('session/queued frames', () => {
api.events.mux({ rpcId: RpcId('t-mux-swept'), payload: {} }, abort.signal), 2, abort)
const remaining = frames.filter(f => f.type === 'session/queued')
expect(remaining).toHaveLength(1)
expect(remaining[0]).toMatchObject({ content: survivor.content })
expect(remaining[0]).toMatchObject({ message: survivor })
})
})

View File

@@ -12,6 +12,7 @@ import { Context } from 'cordis'
import { z } from 'zod'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -61,7 +62,10 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
/** Append `count` user messages so the log has paginable message boundaries. */
function seedMessages(session: Session, count: number): void {
for (let i = 0; i < count; i++) {
session.append('user/message', { content: [{ type: 'text', text: `m${i}` }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `m${i}` }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
}
}

View File

@@ -14,9 +14,9 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { CallId, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import type { MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
@@ -88,16 +88,35 @@ describe('mux live view computation', () => {
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-term'), name: 'term', arguments: '{"cmd":"echo hi"}' })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-diff'), name: 'diffy', arguments: '{}' })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-call-only'), name: 'call-only', arguments: '{}' })
session.append('tool/result', { turn: 1, step: 1, callId: CallId('c-call-only'), content: [{ type: 'text', text: rawResult }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c-call-only'),
content: [{ type: 'text', text: rawResult }],
isError: false,
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-plain'), name: 'plain', arguments: '{}' })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-boom'), name: 'boom', arguments: '{}' })
session.append('tool/result', { turn: 1, step: 1, callId: CallId('c-gen'), content: [{ type: 'text', text: 'ok' }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c-gen'),
content: [{ type: 'text', text: 'ok' }],
isError: false,
}),
}, { surfaceOp: 'append' })
const frames = await collected
const events = frames.filter(f => f.type === 'session/event')
const byCall = new Map(events
.filter(f => f.event.type === 'tool/call' || f.event.type === 'tool/result')
.map(f => [`${f.event.type}:${(f.event.data as { callId: string }).callId}`, f]))
.map(f => [
`${f.event.type}:${f.event.type === 'tool/call'
? f.event.data.callId
: (f.event.data as SessionEvent<'tool/result'>['data']).message.source.callId}`,
f,
]))
expect(byCall.get('tool/call:c-gen')?.view).toEqual({ for: 'call', view: { card: 'generic', title: 'gen call' } })
expect(byCall.get('tool/call:c-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'echo hi' } })
@@ -130,15 +149,44 @@ describe('mux live view computation', () => {
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-term'), name: 'term', arguments: '{"cmd":"ls"}' })
// meta rides through to presentResult's ToolResult (the spread arm).
session.append('tool/result', { turn: 1, step: 1, callId: CallId('h-term'), content: [{ type: 'text', text: 'ok' }], isError: false, meta: { n: 1 } }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('h-term'),
content: [{ type: 'text', text: 'ok' }],
isError: false,
}),
meta: { n: 1 },
}, { surfaceOp: 'append' })
// Unpaired result: no tool/call with this id anywhere in the page.
session.append('tool/result', { turn: 1, step: 1, callId: CallId('h-orphan'), content: [{ type: 'text', text: 'x' }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('h-orphan'),
content: [{ type: 'text', text: 'x' }],
isError: false,
}),
}, { surfaceOp: 'append' })
// Paired, but the call's stored arguments do not parse: backscan soft-falls.
session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-bad'), name: 'term', arguments: '{broken' })
session.append('tool/result', { turn: 1, step: 1, callId: CallId('h-bad'), content: [{ type: 'text', text: 'y' }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('h-bad'),
content: [{ type: 'text', text: 'y' }],
isError: false,
}),
}, { surfaceOp: 'append' })
// Presenterless tool: pairing succeeds but presentResult is absent.
session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-plain'), name: 'plain', arguments: '{}' })
session.append('tool/result', { turn: 1, step: 1, callId: CallId('h-plain'), content: [{ type: 'text', text: 'z' }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('h-plain'),
content: [{ type: 'text', text: 'z' }],
isError: false,
}),
}, { surfaceOp: 'append' })
const response = await api.sessions.history({ rpcId: RpcId('t-hist'), payload: { sessionId: session.id } })
expect(response.result.ok).toBe(true)
@@ -146,7 +194,12 @@ describe('mux live view computation', () => {
const entries = response.result.value.events
const byKey = new Map(entries
.filter(entry => entry.event.type === 'tool/call' || entry.event.type === 'tool/result')
.map(entry => [`${entry.event.type}:${(entry.event.data as { callId: string }).callId}`, entry]))
.map(entry => [
`${entry.event.type}:${entry.event.type === 'tool/call'
? entry.event.data.callId
: (entry.event.data as SessionEvent<'tool/result'>['data']).message.source.callId}`,
entry,
]))
expect(byKey.get('tool/call:h-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'ls' } })
expect(byKey.get('tool/result:h-term')?.view).toEqual({ for: 'result', view: { card: 'terminal', output: 'done' } })
expect('view' in (byKey.get('tool/result:h-orphan') ?? {})).toBe(false)
@@ -188,7 +241,14 @@ describe('mux live view computation', () => {
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
// The turn/end above cleared the live table; pairing must fall back to
// scanning the session's in-memory events.
session.append('tool/result', { turn: 1, step: 1, callId: CallId('c-late'), content: [{ type: 'text', text: 'ok' }], isError: false }, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c-late'),
content: [{ type: 'text', text: 'ok' }],
isError: false,
}),
}, { surfaceOp: 'append' })
const frames = await collected
const result = frames.find(f => f.type === 'session/event' && f.event.type === 'tool/result')

View File

@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
import AgentRegistry, {} from '@deepseek-ai/dsh-agent'
import type { Agent, AgentFactory } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
@@ -45,10 +45,10 @@ function stubAgent(session: Session): Agent {
status: 'idle',
acceptsNextStep: false,
ctx: new Context(),
followup: () => AgentMessageId('stub'),
steer: () => AgentMessageId('stub'),
inject: () => AgentMessageId('stub'),
send: () => AgentMessageId('stub'),
followup: () => {},
steer: () => {},
inject: () => {},
send: () => {},
cancel() {},
whenIdle: () => Promise.resolve(),
}

View File

@@ -119,9 +119,19 @@ describe('sessions domain schemas', () => {
expect(sessionSummarySchema.parse({ sessionId: 's1', updatedAt: 1, running: true, blank: false, parentSessionId: 'p', cwd: '/x' }).cwd).toBe('/x')
// blank is mandatory: a summary without it fails the parse.
expect(() => sessionSummarySchema.parse({ sessionId: 's1', updatedAt: 1, running: false })).toThrow()
const event = sessionEventSchema.parse({ type: 'user/message', seq: 0, time: 1, data: { any: true } })
const event = sessionEventSchema.parse({
type: 'user/message',
seq: 0,
time: 1,
data: { any: true },
})
expect(event).toMatchObject({ type: 'user/message' })
expect(() => sessionEventSchema.parse({ type: 'user/message', seq: -1, time: 1, data: {} })).toThrow()
expect(() => sessionEventSchema.parse({
type: 'user/message',
seq: -1,
time: 1,
data: {},
})).toThrow()
})
it('validates the per-method request/value pairs', () => {
@@ -358,8 +368,8 @@ describe('events frame schemas', () => {
{ type: 'approval/resolved', sessionId: 's', approvalId: 'a', outcome: 'allowed-once' },
{ type: 'question/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] },
{ type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' },
{ type: 'session/queued', sessionId: 's', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' }, steering: false },
{ type: 'session/queued', sessionId: 's', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' }, steering: true },
{ type: 'session/queued', sessionId: 's', message: { id: 'm1', role: 'user', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' } }, steering: false },
{ type: 'session/queued', sessionId: 's', message: { id: 'm2', role: 'user', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' } }, steering: true },
{ type: 'session/projection', sessionId: 's', key: 'todos', value: [{ content: 'x', status: 'pending' }], seq: 7 },
{ type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } },
]
@@ -380,9 +390,9 @@ describe('events frame schemas', () => {
})
it('rejects a queued frame missing its members', () => {
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: 'x', source: { kind: 'user' } })).toThrow()
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: [], source: { kind: 'user' } })).toThrow()
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: [], source: {}, steering: false })).toThrow()
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: 'x', steering: false })).toThrow()
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: { id: 'm', role: 'user', content: [], source: { kind: 'user' } } })).toThrow()
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: { id: 'm', role: 'user', content: [], source: {} }, steering: false })).toThrow()
})
it('accepts every host frame branch', () => {

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import { affectsSessionMetrics, SessionMetricsProjector } from '../src/session-metrics.ts'
@@ -22,8 +23,10 @@ function assistant(
session.append('assistant/message', {
turn,
step,
content: [{ type: 'text', text: `answer-${turn}-${step}` }],
provenance: { provider: 'test', model: 'alpha' },
message: createAssistantMessage({
content: [{ type: 'text', text: `answer-${turn}-${step}` }],
source: { provider: 'test', model: 'alpha' },
}),
usage,
}, { surfaceOp: 'append' })
}
@@ -45,10 +48,10 @@ describe('SessionMetricsProjector', () => {
header: { config: { provider: 'test', model: 'alpha' } },
reason: 'initial',
})
const surface = session.append('user/message', {
const surface = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'question' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
const plain = session.append('step/start', { turn: 1, step: 1 })
expect(affectsSessionMetrics(text)).toBe(false)
@@ -66,10 +69,10 @@ describe('SessionMetricsProjector', () => {
},
})
const session = new Session(SessionId('metrics-fold'))
const first = session.append('user/message', {
const first = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'large old surface' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
assistant(session, 1, 1, {
inputTokens: 11,
outputTokens: 3,
@@ -88,10 +91,10 @@ describe('SessionMetricsProjector', () => {
const assistantSeq = session.surface.nodes.at(-1)
if (assistantSeq === undefined) throw new Error('assistant surface missing')
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compact summary' }],
source: { kind: 'plugin', plugin: 'test' },
}, {
}), {
surfaceOp: { op: 'replace', start: first.seq, end: assistantSeq },
sourceEventSeqs: [first.seq, assistantSeq],
})