Merge branch 'claude/web-llm-pi-ai-config-385e24' into claude/pi-ai-model-discovery

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/event-producer-consumer.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/llm/llm/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-06 10:50:20 +08:00
822 changed files with 18341 additions and 14753 deletions
+53 -140
View File
@@ -8,14 +8,12 @@ import { mkdir, stat } from 'node:fs/promises'
import { join } from 'node:path'
import type { Context } from 'cordis'
import { installAgentLlmTarget } from '@deepseek-ai/dsh-agent'
import type {
Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus, InboxItem, InboxItemId,
} from '@deepseek-ai/dsh-agent'
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus } from '@deepseek-ai/dsh-agent'
import { createUserMessage, freezeMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { errorChain } from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import { isAppendSurfaceEvent, lastActivityTime } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionEventMap, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query'
import { SubagentError } from '@deepseek-ai/dsh-subagent'
@@ -30,7 +28,7 @@ import type {} from '@deepseek-ai/dsh-tools'
import type {
ApiProxy, CredentialView, GoalRef, HistoryEntry, HostFrame, ModelCatalogFailure, ModelProviderGroup,
ModelReasoning, MuxFrame, QuestionResponsePayload, SessionProjectionsBlock, SessionSearchItem,
SessionSummary, SettingsNamespaceView, SubagentAddress, ToolEventView,
QueuedInboxItem, SessionSummary, SettingsNamespaceView, SubagentAddress, ToolEventView,
WorkspaceId, WorkspaceView,
} from './api/index.ts'
import {
@@ -85,7 +83,7 @@ const SESSION_SEARCH_PROVIDER_CALL_LIMIT = 100
const COLD_SUMMARY_BATCH_SIZE = 16
/** Conversation message event types (the pagination counting unit). */
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message', 'steering/message'])
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message'])
/** Product settings intentionally exposed beside model-provider namespaces. */
const PRODUCT_SETTINGS_NAMESPACES = new Set(['ui-onboarding'])
@@ -762,128 +760,36 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
})
})
/**
* Per-session pending-occurrence mirror serving live and mux-open
* `session/queue` snapshots. It carries both queued and steering placements.
* Each terminal inbox event retires one matching occurrence, so repeated
* sends of the same identified message remain visible until every occurrence
* is claimed.
*/
const queuedMirror = new Map<SessionId, InboxItem[]>()
type UnseenQueueEvent =
| { readonly kind: 'update'; readonly item: InboxItem }
| { readonly kind: 'terminal' }
const unseenQueueEvents = new Map<SessionId, Map<InboxItemId, UnseenQueueEvent>>()
const rememberUnseen = (sessionId: SessionId, itemId: InboxItemId, event: UnseenQueueEvent): void => {
let events = unseenQueueEvents.get(sessionId)
if (events === undefined) {
events = new Map()
unseenQueueEvents.set(sessionId, events)
/** Project both durable inbox lists, optionally including the splice currently being emitted. */
const queueItems = (
agent: Agent,
splice?: SessionEventMap['agent/inbox/spliced'],
): QueuedInboxItem[] => {
const project = (target: 'next-turn' | 'next-step'): readonly UserMessage[] => {
const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep
return splice?.target === target
? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
: messages
}
events.set(itemId, event)
// Only synchronous re-entrancy may deliver a mutation before its outer
// enqueue observer. Drop unmatched protocol-invalid observations instead
// of retaining process-local ids indefinitely.
queueMicrotask(() => {
const current = unseenQueueEvents.get(sessionId)
if (current?.get(itemId) !== event) return
current.delete(itemId)
if (current.size === 0) unseenQueueEvents.delete(sessionId)
})
}
const takeUnseen = (sessionId: SessionId, itemId: InboxItemId): UnseenQueueEvent | undefined => {
const events = unseenQueueEvents.get(sessionId)
const event = events?.get(itemId)
if (event === undefined) return undefined
events?.delete(itemId)
if (events?.size === 0) unseenQueueEvents.delete(sessionId)
return event
}
const publishQueue = (sessionId: SessionId): void => {
const items = queuedMirror.get(sessionId) ?? []
broadcast({
type: 'session/queue',
sessionId,
items: items.map(item => ({
id: item.id,
placement: item.placement,
message: item.message,
return [
...project('next-turn').map(message => ({ id: message.id, placement: 'queued' as const, message })),
...project('next-step').map(message => ({
id: message.id,
// Only user-origin messages are steering; injected context (approval
// notices, task completion, attached snapshots) is not a user action
// and must not render as a pending steering bubble.
placement: message.source.kind === 'user' ? 'steering' as const : 'context' as const,
message,
})),
})
}
ctx.effect(() => {
const retireKnown = (sessionId: SessionId, itemId: InboxItemId): boolean => {
const entries = queuedMirror.get(sessionId)
if (entries === undefined) return false
const index = entries.findIndex(entry => entry.id === itemId)
if (index === -1) return false
entries.splice(index, 1)
if (entries.length === 0) queuedMirror.delete(sessionId)
return true
}
const retire = (agent: Agent, item: InboxItem): boolean => {
if (retireKnown(agent.id, item.id)) return true
rememberUnseen(agent.id, item.id, { kind: 'terminal' })
return false
}
const disposers = [
ctx.on('agent/inbox/enqueue', (agent: Agent, item: InboxItem) => {
const unseen = takeUnseen(agent.id, item.id)
if (unseen?.kind === 'terminal') return
let entries = queuedMirror.get(agent.id)
if (entries === undefined) {
entries = []
queuedMirror.set(agent.id, entries)
}
entries.push(unseen?.kind === 'update' ? unseen.item : item)
publishQueue(agent.id)
}),
ctx.on('agent/inbox/update', (agent: Agent, item: InboxItem) => {
const entries = queuedMirror.get(agent.id)
if (entries === undefined) {
rememberUnseen(agent.id, item.id, { kind: 'update', item })
return
}
const index = entries.findIndex(entry => entry.id === item.id)
if (index === -1) {
rememberUnseen(agent.id, item.id, { kind: 'update', item })
return
}
entries.splice(index, 1, item)
publishQueue(agent.id)
}),
ctx.on('agent/inbox/dequeue', (agent: Agent, item: InboxItem) => {
if (item.placement === 'steering') {
// AgentLoop appends the durable steering/message synchronously after
// this claim. Retain and retire the mirror row in the following
// microtask so any re-entrant snapshot and the Host's linear mux
// stream keep it visible until the durable event exists.
const present = queuedMirror.get(agent.id)?.some(entry => entry.id === item.id) === true
if (!present) {
retire(agent, item)
return
}
queueMicrotask(() => {
if (retireKnown(agent.id, item.id)) publishQueue(agent.id)
})
} else if (retire(agent, item)) {
// Queued claims have no durable same-message handoff to order.
// Publish retirement synchronously as before.
publishQueue(agent.id)
}
}),
ctx.on('agent/inbox/discard', (agent: Agent, items: InboxItem[]) => {
let changed = false
for (const item of items) changed = retire(agent, item) || changed
if (changed) publishQueue(agent.id)
}),
ctx.on('session/disposed', (session: Session) => {
queuedMirror.delete(session.id)
unseenQueueEvents.delete(session.id)
}),
]
return () => { for (const dispose of disposers) dispose() }
}, 'api-proxy: queued mirror')
}
ctx.on('session/event', (session, event) => {
if (event.type !== 'agent/inbox/spliced') return
const agent = ctx.agents.get(session.id)
if (agent?.session !== session) return
broadcast({ type: 'session/queue', sessionId: session.id, items: queueItems(agent, event.data) })
})
/** Remove a wait before settling it: synchronous deletion makes the first claimant win. */
function claimQuestion(pending: PendingQuestion, outcome: 'answered' | 'cancelled'): void {
@@ -1583,7 +1489,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
page = await sessionQuery.searchSessions({
query: request.payload.query,
eventFilters: [
{ kind: 'type', values: ['user/message', 'assistant/message', 'steering/message'] },
{ kind: 'type', values: ['user/message', 'assistant/message'] },
{ kind: 'surface', values: ['current'] },
],
limit: requestedPageLimit,
@@ -1935,21 +1841,33 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
details: { itemId },
}))
}
const result = agent.updateInbox(itemId, action)
if (result === 'not-found') {
const target = agent.inbox.nextTurn.some(message => message.id === itemId)
? 'next-turn'
: agent.inbox.nextStep.some(message => message.id === itemId) ? 'next-step' : undefined
const message = target === undefined
? undefined
: (target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep)
.find(candidate => candidate.id === itemId)
if (target === undefined || message === undefined) {
return Promise.resolve(err(request, {
code: 'queue-item-not-found',
message: 'queued item is no longer pending',
details: { itemId },
}))
}
if (result === 'steer-unavailable') {
if (action.kind === 'steer' && (target !== 'next-turn' || agent.status !== 'running')) {
return Promise.resolve(err(request, {
code: 'steer-unavailable',
message: 'current turn no longer accepts steering',
details: { itemId },
}))
}
if (action.kind === 'edit') {
agent.inbox.replace(itemId, freezeMessage({ ...message, content: action.content }))
} else {
agent.inbox.remove(itemId)
if (action.kind === 'steer') agent.steer(message)
}
return Promise.resolve(ok(request, { accepted: true as const }))
},
@@ -2636,16 +2554,11 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
// Queue snapshot baseline (pendingQuestions precedent): frames replayed
// in arrival order per session; a reconnecting client rebuilds its
// queue view from these alone.
for (const [sessionId, items] of queuedMirror) {
queue.push(frame({
type: 'session/queue',
sessionId,
items: items.map(item => ({
id: item.id,
placement: item.placement,
message: item.message,
})),
}))
for (const session of ctx.sessions.list()) {
const agent = ctx.agents.get(session.id)
if (agent?.session === session && agent.inbox.hasPending) {
queue.push(frame({ type: 'session/queue', sessionId: session.id, items: queueItems(agent) }))
}
}
// Per-session open-call table for result-view pairing. Bounded by the
// per-turn call count: entries clear on turn/end; a table miss (stream
@@ -11,7 +11,7 @@ import type { Wire } from './rpc.schema.ts'
import { rpcErrorSchema, rpcIdSchema } from './rpc.schema.ts'
import { approvalRequestIdSchema } from './approvals.schema.ts'
import {
contentBlockSchema, inboxItemIdSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema,
contentBlockSchema, messageIdSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema,
} from './sessions.schema.ts'
import { workspaceIdSchema, workspaceViewSchema } from './workspace.schema.ts'
@@ -53,8 +53,8 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
type: z.literal('session/queue'),
sessionId: sessionIdSchema,
items: z.array(z.object({
id: inboxItemIdSchema,
placement: z.union([z.literal('queued'), z.literal('steering')]),
id: messageIdSchema,
placement: z.union([z.literal('queued'), z.literal('steering'), z.literal('context')]),
message: messageSchema,
})),
}),
+5 -5
View File
@@ -9,7 +9,7 @@
import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-interaction/types'
import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-approval/types'
import type { Message } from '@deepseek-ai/dsh-llm/types'
import type { InboxItemId } from '@deepseek-ai/dsh-agent/brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { CallId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
@@ -34,10 +34,10 @@ export type ToolEventView =
/** One pending inbox occurrence in the authoritative `session/queue` snapshot. */
export interface QueuedInboxItem {
/** Agent-owned occurrence identity; queue mutations address only `queued` items. */
id: InboxItemId
/** Agent-resolved FIFO placement; clients render queued and steering items on different surfaces. */
placement: 'queued' | 'steering'
/** Message identity used by inbox mutations. */
id: MessageId
/** Agent-resolved FIFO placement; queued and steering items render on different surfaces, context items stay invisible until claimed. */
placement: 'queued' | 'steering' | 'context'
/** Complete pending message; it is not durable until the Agent claims it. */
message: Message
}
-1
View File
@@ -71,7 +71,6 @@ export type {
// ---- Errors and ids ----
export { RpcId, transportError } from './rpc.ts'
export type { RpcError, RpcErrorCode, RpcErrorDetailsMap, RpcResult } from './rpc.ts'
export type { InboxItemId } from '@deepseek-ai/dsh-agent/brand'
// ---- Fixed session-search product bounds ----
export {
+3 -3
View File
@@ -8,8 +8,8 @@
import type { z as zCore } from 'zod'
type ZodIssue = zCore.core.$ZodIssue
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { InboxItemId } from '@deepseek-ai/dsh-agent/brand'
/**
* Message correlation id: the initiator mints it on a request; a response
@@ -45,8 +45,8 @@ export interface RpcErrorDetailsMap {
'directory-create-failed': { path: string }
'directory-picker-unavailable': { capability: string }
'agent-busy': { reason: string }
'queue-item-not-found': { itemId: InboxItemId }
'steer-unavailable': { itemId: InboxItemId }
'queue-item-not-found': { itemId: MessageId }
'steer-unavailable': { itemId: MessageId }
/** A known slash command reported a usage/state error; the message is the command's own text. */
'command-error': {}
/** A leading-/ prompt named no registered command; the message names the token. */
@@ -7,7 +7,7 @@
import { z } from 'zod'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
import type { InboxItemId } from '@deepseek-ai/dsh-agent/brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import type {
@@ -25,8 +25,8 @@ import {
/** SessionId: one brand cast after shape validation (the only cast point in this domain). */
export const sessionIdSchema = z.string().min(1) as unknown as z.ZodType<SessionId>
/** InboxItemId: one brand cast after non-empty string validation. */
export const inboxItemIdSchema = z.string().min(1) as unknown as z.ZodType<InboxItemId>
/** MessageId: one brand cast after non-empty string validation. */
export const messageIdSchema = z.string().min(1) as unknown as z.ZodType<MessageId>
/**
* WorkspaceId: the workspace domain's one brand cast. Hosted here rather
@@ -264,7 +264,7 @@ export const sessionPromptValueSchema = z.object({
/** session.updateQueue request payload. */
export const sessionUpdateQueueRequestSchema = z.object({
sessionId: sessionIdSchema,
itemId: inboxItemIdSchema,
itemId: messageIdSchema,
action: z.discriminatedUnion('kind', [
z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }),
z.object({ kind: z.literal('remove') }),
+2 -2
View File
@@ -4,8 +4,8 @@
* else references RequestPayload<'session.*'> / ResponseValue<'session.*'>.
*/
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { InboxItemId } from '@deepseek-ai/dsh-agent/brand'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
// The pure-type outlet: api/ is browser-importable, and the package root's
// cordis Context merge (via dsh-agent) must not enter client aggregates.
@@ -287,7 +287,7 @@ export interface SessionsApi {
* Edits, removes, or strictly steers one pending queued occurrence on an ordinary session.
* Session-backed subagents reject with `agent-busy`.
*/
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>):
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: MessageId; action: QueueAction }>):
Promise<RpcResponse<{ accepted: true }>>
/**