Merge remote-tracking branch 'origin/master' into mergebot/pr711
# Conflicts: # apps/cli/README.i18n.yaml # apps/cli/README.md # apps/cli/README.zh.md # apps/cli/cordis.yml # apps/cli/package.json # docs/config-catalog.md # packages/client/runtime/README.i18n.yaml # packages/client/runtime/src/client/contract/sessions.ts # packages/client/test-runtime/src/sessions.ts # packages/client/ui-workspace/README.i18n.yaml # packages/client/ui-workspace/README.md # packages/client/ui-workspace/README.zh.md # packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx # packages/client/ui-workspace/src/client/tree.ts # packages/client/ui-workspace/tests/apply.spec.ts # packages/client/ui-workspace/tests/tree.spec.ts # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/tests/client-handler.spec.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts # pnpm-lock.yaml
This commit is contained in:
980 files changed
+30720
-7127
No files matched your search
@@ -9,11 +9,12 @@ import { join } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import { installAgentLlmTarget } from '@deepseek-ai/dsh-agent'
|
||||
import type {
|
||||
Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus, InboxPlacement,
|
||||
Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus, InboxItem, InboxItemId,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import type { MessageId, MessageSource } 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 { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
|
||||
import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query'
|
||||
@@ -71,7 +72,7 @@ const SESSION_SEARCH_PROVIDER_CALL_LIMIT = 100
|
||||
/** Bound cold-log stat fan-out and settle each started batch before cancellation returns. */
|
||||
const COLD_SUMMARY_BATCH_SIZE = 16
|
||||
|
||||
/** Surface message event types (the pagination counting unit). */
|
||||
/** Conversation message event types (the pagination counting unit). */
|
||||
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message', 'steering/message'])
|
||||
|
||||
/** Read live abort state across awaits without treating it as synchronously immutable. */
|
||||
@@ -80,10 +81,14 @@ function isAborted(signal: AbortSignal): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Message-boundary pagination: count maxMessages surface messages backwards from
|
||||
* the window tail; the cut is the starting seq of the oldest message group
|
||||
* (chunks group via sourceEventSeqs — never cut mid-message). The tail page
|
||||
* naturally includes the in-progress partial.
|
||||
* Message-boundary pagination: count maxMessages append-origin messages
|
||||
* backwards from the window tail. Replacement copies never entered the
|
||||
* conversation a reader sees — they restate a shadowed range for the model
|
||||
* alone — so they consume no quota; the page stays one contiguous raw range,
|
||||
* which keeps a compaction's log-only provenance on the same page as its
|
||||
* replacement. The cut is the starting seq of the oldest message group (chunks
|
||||
* group via sourceEventSeqs — never cut mid-message). The tail page naturally
|
||||
* includes the in-progress partial.
|
||||
*/
|
||||
function paginate(
|
||||
events: readonly SessionEvent[],
|
||||
@@ -95,7 +100,7 @@ function paginate(
|
||||
let cut = 0
|
||||
for (let i = window.length - 1; i >= 0; i--) {
|
||||
const event = window[i] as SessionEvent
|
||||
if (!MESSAGE_TYPES.has(event.type)) continue
|
||||
if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event)) continue
|
||||
count++
|
||||
const sources = (event as { sourceEventSeqs?: number[] }).sourceEventSeqs
|
||||
const groupStart = sources !== undefined && sources.length > 0 ? Math.min(event.seq, ...sources) : event.seq
|
||||
@@ -181,7 +186,9 @@ function sessionBlank(session: Session): boolean {
|
||||
function summarize(session: Session, running: boolean): SessionSummary {
|
||||
return {
|
||||
sessionId: session.id,
|
||||
updatedAt: session.events.at(-1)?.time ?? session.header.createdAt,
|
||||
// Excludes end-seed: a resumed-but-untouched session
|
||||
// must not sort as freshly worked in.
|
||||
updatedAt: lastActivityTime(session.events) ?? session.header.createdAt,
|
||||
running,
|
||||
blank: sessionBlank(session),
|
||||
...session.header.parentSession === undefined ? {} : { parentSessionId: session.header.parentSession },
|
||||
@@ -532,46 +539,106 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
})
|
||||
|
||||
/**
|
||||
* Per-session inbox occurrence mirror serving the mux-open queue snapshot
|
||||
* Per-session queued-occurrence mirror serving the mux-open queue snapshot
|
||||
* (the same refresh-recovery baseline as pending questions). Each terminal
|
||||
* inbox event retires one matching occurrence, so repeated sends of the same
|
||||
* queue 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, { message: UserMessage; steering: boolean }[]>()
|
||||
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)
|
||||
}
|
||||
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,
|
||||
message: item.message,
|
||||
})),
|
||||
})
|
||||
}
|
||||
ctx.effect(() => {
|
||||
const retire = (agent: Agent, id: MessageId, placement?: InboxPlacement): void => {
|
||||
const retire = (agent: Agent, item: InboxItem): boolean => {
|
||||
const entries = queuedMirror.get(agent.id)
|
||||
if (entries === undefined) return
|
||||
const index = entries.findIndex(entry =>
|
||||
entry.message.id === id
|
||||
&& (placement === undefined || entry.steering === (placement === 'steering')))
|
||||
if (index !== -1) entries.splice(index, 1)
|
||||
if (entries === undefined) {
|
||||
rememberUnseen(agent.id, item.id, { kind: 'terminal' })
|
||||
return false
|
||||
}
|
||||
const index = entries.findIndex(entry => entry.id === item.id)
|
||||
if (index === -1) {
|
||||
rememberUnseen(agent.id, item.id, { kind: 'terminal' })
|
||||
return false
|
||||
}
|
||||
entries.splice(index, 1)
|
||||
if (entries.length === 0) queuedMirror.delete(agent.id)
|
||||
return true
|
||||
}
|
||||
const disposers = [
|
||||
ctx.on('agent/inbox/enqueue', (agent: Agent, message: UserMessage, placement) => {
|
||||
ctx.on('agent/inbox/enqueue', (agent: Agent, item: InboxItem) => {
|
||||
if (item.placement !== 'queued') return
|
||||
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)
|
||||
}
|
||||
const steering = placement === 'steering'
|
||||
entries.push({ message, steering })
|
||||
broadcast({
|
||||
type: 'session/queued',
|
||||
sessionId: agent.id,
|
||||
message,
|
||||
steering,
|
||||
})
|
||||
entries.push(unseen?.kind === 'update' ? unseen.item : item)
|
||||
publishQueue(agent.id)
|
||||
}),
|
||||
ctx.on('agent/inbox/dequeue', (agent: Agent, message: UserMessage, placement) => {
|
||||
retire(agent, message.id, placement)
|
||||
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/discard', (agent: Agent, messages: UserMessage[]) => {
|
||||
for (const message of messages) retire(agent, message.id)
|
||||
ctx.on('agent/inbox/dequeue', (agent: Agent, item: InboxItem) => {
|
||||
if (retire(agent, item)) 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() }
|
||||
@@ -1268,6 +1335,75 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
},
|
||||
|
||||
async fork(request) {
|
||||
const { sessionId, atSeq } = request.payload
|
||||
const found = await agentFor(sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
const source = found.agent.session
|
||||
const events = source.events
|
||||
// An in-log anchor belongs to the turn containing it and must never
|
||||
// clip backward to an earlier completed turn. Omitted and past-end
|
||||
// anchors retain the last-completed-turn shortcut.
|
||||
const lastSeq = events.at(-1)?.seq ?? -1
|
||||
const anchoredBoundary = atSeq === undefined
|
||||
? undefined
|
||||
: events.find(e => e.type === 'turn/end' && e.seq >= atSeq)
|
||||
const boundary = anchoredBoundary
|
||||
?? (atSeq === undefined || atSeq > lastSeq
|
||||
? events.findLast(e => e.type === 'turn/end')
|
||||
: undefined)
|
||||
if (boundary === undefined) {
|
||||
return err(request, {
|
||||
code: 'fork-unavailable',
|
||||
message: atSeq !== undefined && atSeq <= lastSeq
|
||||
? `session "${sessionId}" has not completed the turn containing event ${String(atSeq)}`
|
||||
: `session "${sessionId}" has no completed turn to fork from`,
|
||||
details: { sessionId },
|
||||
})
|
||||
}
|
||||
// Extend the cut through trailing out-of-band appends (session/title,
|
||||
// injections) up to the next turn/start: they are standalone events, so
|
||||
// the seed stays balanced, and the child inherits a title generated
|
||||
// right after the boundary turn.
|
||||
let cut = boundary.seq + 1
|
||||
while (cut < events.length && events[cut]?.type !== 'turn/start') cut++
|
||||
const childId = `session-${randomUUID()}` as SessionId
|
||||
try {
|
||||
await ctx.agents.create({
|
||||
sessionId: childId,
|
||||
seed: events.slice(0, cut),
|
||||
meta: {
|
||||
...source.header.cwd === undefined ? {} : { cwd: source.header.cwd },
|
||||
parentSession: source.id,
|
||||
seedLength: cut,
|
||||
},
|
||||
agentOptions,
|
||||
setup: installTarget,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
return err(request, {
|
||||
code: 'internal',
|
||||
message: `failed to fork session "${sessionId}": ${String(error)}`,
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
// Keep the child in the source's Workspace so the list nests it under
|
||||
// its parent; the child is already published if the attach fails.
|
||||
const workspace = ctx.workspace.list().find(w => w.sessionIds.includes(source.id))
|
||||
if (workspace !== undefined) {
|
||||
try {
|
||||
await workspace.attachSession(childId)
|
||||
} catch (error: unknown) {
|
||||
return err(request, {
|
||||
code: 'workspace-attach-failed',
|
||||
message: `session "${childId}" was forked but could not attach to workspace "${workspace.id}": ${String(error)}`,
|
||||
details: { sessionId: childId, workspaceId: workspace.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
return ok(request, { sessionId: childId })
|
||||
},
|
||||
|
||||
async prompt(request) {
|
||||
const { sessionId, mode, content } = request.payload
|
||||
const found = await agentFor(sessionId)
|
||||
@@ -1286,6 +1422,19 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
return ok(request, { accepted: true as const })
|
||||
},
|
||||
|
||||
updateQueue(request) {
|
||||
const { sessionId, itemId, action } = request.payload
|
||||
const agent = ctx.agents.get(sessionId)
|
||||
if (agent === undefined || agent.updateInbox(itemId, action) === 'not-found') {
|
||||
return Promise.resolve(err(request, {
|
||||
code: 'queue-item-not-found',
|
||||
message: 'queued item is no longer pending',
|
||||
details: { itemId },
|
||||
}))
|
||||
}
|
||||
return Promise.resolve(ok(request, { accepted: true as const }))
|
||||
},
|
||||
|
||||
cancel(request) {
|
||||
const { sessionId } = request.payload
|
||||
const agent = ctx.agents.get(sessionId)
|
||||
@@ -1681,15 +1830,15 @@ 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, entries] of queuedMirror) {
|
||||
for (const entry of entries) {
|
||||
queue.push(frame({
|
||||
type: 'session/queued',
|
||||
sessionId,
|
||||
message: entry.message,
|
||||
steering: entry.steering,
|
||||
}))
|
||||
}
|
||||
for (const [sessionId, items] of queuedMirror) {
|
||||
queue.push(frame({
|
||||
type: 'session/queue',
|
||||
sessionId,
|
||||
items: items.map(item => ({
|
||||
id: item.id,
|
||||
message: item.message,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -10,7 +10,9 @@ import type { HostFrame, MuxFrame } from './events.ts'
|
||||
import type { Wire } from './rpc.schema.ts'
|
||||
import { rpcErrorSchema, rpcIdSchema } from './rpc.schema.ts'
|
||||
import { approvalRequestIdSchema } from './approvals.schema.ts'
|
||||
import { contentBlockSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema } from './sessions.schema.ts'
|
||||
import {
|
||||
contentBlockSchema, inboxItemIdSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema,
|
||||
} from './sessions.schema.ts'
|
||||
import { workspaceIdSchema, workspaceViewSchema } from './workspace.schema.ts'
|
||||
|
||||
/** Question shape validated strictly against core dsh-user-interaction. */
|
||||
@@ -21,6 +23,11 @@ export const askUserQuestionItemSchema = z.object({
|
||||
detail: z.string().optional(),
|
||||
options: z.array(z.object({ label: z.string(), description: z.string().optional() })).optional(),
|
||||
multiSelect: z.boolean().optional(),
|
||||
// Presentation intent: a tagged union on the wire, so an unknown tag is a
|
||||
// rejected frame rather than a silently generic render.
|
||||
intent: z.discriminatedUnion('kind', [
|
||||
z.object({ kind: z.literal('plan-review'), approve: z.string() }),
|
||||
]).optional(),
|
||||
}) satisfies z.ZodType<Wire<AskUserQuestionItem>>
|
||||
|
||||
/** Unified message envelope carried by transient queue frames. */
|
||||
@@ -42,7 +49,14 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
|
||||
// and must fail loud here, not reach the composer.
|
||||
z.object({ type: z.literal('question/requested'), sessionId: sessionIdSchema, questions: z.array(askUserQuestionItemSchema).min(1) }),
|
||||
z.object({ type: z.literal('question/resolved'), sessionId: sessionIdSchema, questionRpcId: rpcIdSchema, outcome: z.union([z.literal('answered'), z.literal('cancelled')]) }),
|
||||
z.object({ type: z.literal('session/queued'), sessionId: sessionIdSchema, message: messageSchema, steering: z.boolean() }),
|
||||
z.object({
|
||||
type: z.literal('session/queue'),
|
||||
sessionId: sessionIdSchema,
|
||||
items: z.array(z.object({
|
||||
id: inboxItemIdSchema,
|
||||
message: messageSchema,
|
||||
})),
|
||||
}),
|
||||
// value stays wide: it already passed its unit's own schema on the host,
|
||||
// and deep-validating here would import every domain's schema into the carrier.
|
||||
z.object({ type: z.literal('session/projection'), sessionId: sessionIdSchema, key: z.string().min(1), value: z.unknown(), seq: z.number().int().nonnegative() }),
|
||||
|
||||
@@ -9,6 +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 { 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'
|
||||
@@ -31,6 +32,14 @@ export type ToolEventView =
|
||||
| { for: 'call'; view: ToolCallView }
|
||||
| { for: 'result'; view: ToolResultView }
|
||||
|
||||
/** One pending queued occurrence in an authoritative queue snapshot. */
|
||||
export interface QueuedInboxItem {
|
||||
/** Agent-owned occurrence identity used by queue mutations. */
|
||||
id: InboxItemId
|
||||
/** Complete pending message; it is not durable until the Agent claims it. */
|
||||
message: Message
|
||||
}
|
||||
|
||||
/** Streaming face of the contract: the two SSE stream openers (mux + host). */
|
||||
export interface EventsApi {
|
||||
/**
|
||||
@@ -62,18 +71,13 @@ export type MuxFrame =
|
||||
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
|
||||
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
|
||||
/**
|
||||
* A message entered the addressed agent's inbox. A queued message is not
|
||||
* model-visible, so there is no session event to carry it; this transient
|
||||
* frame is the only wire signal. On stream open the
|
||||
* host replays the current queue snapshot for every attached session (same
|
||||
* refresh-recovery baseline as pending questions); queue clearing on cancel
|
||||
* has no dedicated frame — clients fold it from the status flip.
|
||||
* `steering` is the host's acceptance-time queue classification and remains
|
||||
* authoritative in reconnect snapshots. `message.source` carries the prompt's rpcId
|
||||
* when the message came over this wire (the client's provisional-echo
|
||||
* reconciliation key).
|
||||
* Complete transient queue state after every enqueue, mutation, claim, or
|
||||
* discard. Pending work is not model-visible and therefore has no durable
|
||||
* session event; the whole snapshot makes edit, deletion, cancel, and
|
||||
* reconnect converge through one authoritative signal. Pending steering is
|
||||
* outside this Web queue projection.
|
||||
*/
|
||||
| { type: 'session/queued'; sessionId: SessionId; message: Message; steering: boolean }
|
||||
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
|
||||
/**
|
||||
* One projection unit's finished value changed (session-projection RFC).
|
||||
* Live push state, never logged — replay recomputes on the host (the
|
||||
|
||||
@@ -29,14 +29,14 @@ export interface ApiProxy {
|
||||
// ---- Domain interfaces and payload entities ----
|
||||
export type {
|
||||
HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
ModelReasoningEffort, ModelTarget, SessionModels, SessionProjectionsBlock, SessionSearchItem,
|
||||
ModelReasoningEffort, ModelTarget, QueueAction, SessionModels, SessionProjectionsBlock, SessionSearchItem,
|
||||
SessionsApi, SessionSummary,
|
||||
} from './sessions.ts'
|
||||
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
|
||||
export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
|
||||
export type { CommandsApi, CommandDescriptor } from './commands.ts'
|
||||
export type { SkillsApi, SkillEntry } from './skills.ts'
|
||||
export type { EventsApi, MuxFrame, HostFrame, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
|
||||
export type { EventsApi, MuxFrame, HostFrame, QueuedInboxItem, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
|
||||
export type { GoalsApi, GoalId, GoalRef } from './goals.ts'
|
||||
export type { ApprovalResponsePayload } from './approvals.ts'
|
||||
export type { QuestionResponsePayload } from './questions.ts'
|
||||
@@ -57,6 +57,7 @@ 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 {
|
||||
|
||||
@@ -25,7 +25,9 @@ export interface RpcMethodMap {
|
||||
'session.models': SessionsApi['models']
|
||||
'session.selectModel': SessionsApi['selectModel']
|
||||
'session.rename': SessionsApi['rename']
|
||||
'session.fork': SessionsApi['fork']
|
||||
'session.prompt': SessionsApi['prompt']
|
||||
'session.updateQueue': SessionsApi['updateQueue']
|
||||
'session.cancel': SessionsApi['cancel']
|
||||
'host.describe': HostApi['describe']
|
||||
'host.pickDirectory': HostApi['pickDirectory']
|
||||
|
||||
@@ -47,9 +47,11 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
|
||||
z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }),
|
||||
z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }),
|
||||
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
|
||||
z.object({ code: z.literal('queue-item-not-found'), message: z.string(), details: z.object({ itemId: z.string() }) }),
|
||||
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
|
||||
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
|
||||
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
||||
z.object({ code: z.literal('fork-unavailable'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
||||
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
|
||||
]) as unknown as z.ZodType<RpcError>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { z as zCore } from 'zod'
|
||||
type ZodIssue = zCore.core.$ZodIssue
|
||||
import type { Branded } from '@deepseek-ai/dsh-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
|
||||
@@ -44,11 +45,13 @@ export interface RpcErrorDetailsMap {
|
||||
'directory-create-failed': { path: string }
|
||||
'directory-picker-unavailable': { capability: string }
|
||||
'agent-busy': { reason: string }
|
||||
'queue-item-not-found': { itemId: InboxItemId }
|
||||
/** 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. */
|
||||
'unknown-command': {}
|
||||
'title-invalid': { sessionId: SessionId }
|
||||
'fork-unavailable': { sessionId: SessionId }
|
||||
'internal': {}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +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 { RequestPayload, ResponseValue } from './rpc-map.ts'
|
||||
import type { Wire } from './rpc.schema.ts'
|
||||
import type {
|
||||
@@ -24,6 +25,9 @@ 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>
|
||||
|
||||
/**
|
||||
* WorkspaceId: the workspace domain's one brand cast. Hosted here rather
|
||||
* than in workspace.schema because session.create references it while
|
||||
@@ -117,6 +121,17 @@ export const sessionRenameValueSchema = z.object({
|
||||
seq: z.number().int().nonnegative(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.rename'>>>
|
||||
|
||||
/** session.fork request payload (atSeq anchors the completed-turn cut). */
|
||||
export const sessionForkRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
atSeq: z.number().int().nonnegative().optional(),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'session.fork'>>>
|
||||
|
||||
/** session.fork response value (the child session id). */
|
||||
export const sessionForkValueSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.fork'>>>
|
||||
|
||||
/** session.history request payload (beforeSeq/maxMessages page backwards from the window tail). */
|
||||
export const sessionHistoryRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
@@ -246,6 +261,21 @@ export const sessionPromptValueSchema = z.object({
|
||||
}).optional(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.prompt'>>>
|
||||
|
||||
/** session.updateQueue request payload. */
|
||||
export const sessionUpdateQueueRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
itemId: inboxItemIdSchema,
|
||||
action: z.discriminatedUnion('kind', [
|
||||
z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }),
|
||||
z.object({ kind: z.literal('remove') }),
|
||||
]),
|
||||
}) as unknown as z.ZodType<RequestPayload<'session.updateQueue'>>
|
||||
|
||||
/** session.updateQueue response value. */
|
||||
export const sessionUpdateQueueValueSchema = z.object({
|
||||
accepted: z.literal(true),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.updateQueue'>>>
|
||||
|
||||
/** session.cancel request payload. */
|
||||
export const sessionCancelRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
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.
|
||||
@@ -124,10 +125,19 @@ export interface SessionModels {
|
||||
failures: ModelCatalogFailure[]
|
||||
}
|
||||
|
||||
/** A client-requested mutation of one still-pending queue item. */
|
||||
export type QueueAction =
|
||||
| { kind: 'edit'; content: ContentBlock[] }
|
||||
| { kind: 'remove' }
|
||||
|
||||
/** Session list entry (v1 builds no index: list does readdir+stat). */
|
||||
export interface SessionSummary {
|
||||
sessionId: SessionId
|
||||
/** Persisted file mtime. */
|
||||
/**
|
||||
* Last activity. Attached: the last non-`session/end-seed` event, since a
|
||||
* pickup is not activity. Cold: the log's mtime, or `createdAt` for a backend
|
||||
* with no per-session file (README Known Limitations covers the skew).
|
||||
*/
|
||||
updatedAt: number
|
||||
/** Status of the attached agent; always false for cold (unattached) sessions. */
|
||||
running: boolean
|
||||
@@ -193,9 +203,11 @@ export interface SessionsApi {
|
||||
Promise<RpcResponse<{ sessionId: SessionId }>>
|
||||
|
||||
/**
|
||||
* Reads a window of history events; page boundaries align to message boundaries: one page =
|
||||
* all raw events owned by a whole number of messages (including their chunk / tool events),
|
||||
* never cut mid-message. The tail page (beforeSeq absent) additionally carries the in-flight
|
||||
* Reads a window of history events; page boundaries align to append-origin message
|
||||
* boundaries: one page = all raw events owned by a whole number of such messages (including
|
||||
* their chunk / tool events), never cut mid-message. Model-only replacement copies consume no
|
||||
* `maxMessages`, so a compaction's provenance stays on the page of its replacement. The tail
|
||||
* page (beforeSeq absent) additionally carries the in-flight
|
||||
* partial — chunk events already emitted for the last unfinalized message.
|
||||
* Each entry pairs the raw SessionEvent with the host-computed view (tool events whose
|
||||
* presenter produced one, evaluated against the registry at pagination time); the client
|
||||
@@ -243,9 +255,30 @@ export interface SessionsApi {
|
||||
* one — carried for future rendering; the state change is the feedback). A usage/state error is an
|
||||
* RPC error with code command-error; an unrecognized name is an RPC error with code unknown-command.
|
||||
*/
|
||||
/**
|
||||
* Forks a new session from a completed-turn prefix of the source. `atSeq`
|
||||
* anchors the cut: the boundary is the first `turn/end` at or after it
|
||||
* (a message's fork button passes the message seq, so the fork includes
|
||||
* that whole turn); a boundary past the log end, or an omitted `atSeq`,
|
||||
* falls back to the source's last completed turn. An in-log anchor whose
|
||||
* turn is still open fails with `fork-unavailable` instead of clipping to
|
||||
* an earlier turn. The child inherits the source cwd, latest logged model
|
||||
* target, workspace attachment, and `parentSessionId` lineage; the seed
|
||||
* prefix carries the source title.
|
||||
*/
|
||||
fork(request: RpcRequest<{ sessionId: SessionId; atSeq?: number }>):
|
||||
Promise<RpcResponse<{ sessionId: SessionId }>>
|
||||
|
||||
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
|
||||
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
|
||||
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
|
||||
|
||||
/**
|
||||
* Edits or removes one pending queued occurrence.
|
||||
*/
|
||||
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>):
|
||||
Promise<RpcResponse<{ accepted: true }>>
|
||||
|
||||
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
|
||||
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
sessionCancelValueSchema,
|
||||
sessionCreateValueSchema,
|
||||
sessionForkValueSchema,
|
||||
sessionHistoryValueSchema,
|
||||
sessionListValueSchema,
|
||||
sessionModelsValueSchema,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
sessionRenameValueSchema,
|
||||
sessionSearchValueSchema,
|
||||
sessionSelectModelValueSchema,
|
||||
sessionUpdateQueueValueSchema,
|
||||
} from '../api/sessions.schema.ts'
|
||||
import {
|
||||
workspaceCreateValueSchema,
|
||||
@@ -70,7 +72,9 @@ export interface IApiClient {
|
||||
models(payload: RequestPayload<'session.models'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.models'>>>
|
||||
selectModel(payload: RequestPayload<'session.selectModel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.selectModel'>>>
|
||||
rename(payload: RequestPayload<'session.rename'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.rename'>>>
|
||||
fork(payload: RequestPayload<'session.fork'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.fork'>>>
|
||||
prompt(payload: RequestPayload<'session.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.prompt'>>>
|
||||
updateQueue(payload: RequestPayload<'session.updateQueue'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.updateQueue'>>>
|
||||
cancel(payload: RequestPayload<'session.cancel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.cancel'>>>
|
||||
}
|
||||
host: {
|
||||
@@ -122,7 +126,9 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'session.models': sessionModelsValueSchema,
|
||||
'session.selectModel': sessionSelectModelValueSchema,
|
||||
'session.rename': sessionRenameValueSchema,
|
||||
'session.fork': sessionForkValueSchema,
|
||||
'session.prompt': sessionPromptValueSchema,
|
||||
'session.updateQueue': sessionUpdateQueueValueSchema,
|
||||
'session.cancel': sessionCancelValueSchema,
|
||||
'host.describe': hostDescribeValueSchema,
|
||||
'host.pickDirectory': hostPickDirectoryValueSchema,
|
||||
@@ -335,7 +341,9 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
models: (payload, signal) => this.callUnary('session.models', payload, signal),
|
||||
selectModel: (payload, signal) => this.callUnary('session.selectModel', payload, signal),
|
||||
rename: (payload, signal) => this.callUnary('session.rename', payload, signal),
|
||||
fork: (payload, signal) => this.callUnary('session.fork', payload, signal),
|
||||
prompt: (payload, signal) => this.callUnary('session.prompt', payload, signal),
|
||||
updateQueue: (payload, signal) => this.callUnary('session.updateQueue', payload, signal),
|
||||
cancel: (payload, signal) => this.callUnary('session.cancel', payload, signal),
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { clientRequestSchema, clientResponseSchema } from '../api/rpc.schema.ts'
|
||||
import {
|
||||
sessionCancelRequestSchema,
|
||||
sessionCreateRequestSchema,
|
||||
sessionForkRequestSchema,
|
||||
sessionHistoryRequestSchema,
|
||||
sessionListRequestSchema,
|
||||
sessionModelsRequestSchema,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
sessionRenameRequestSchema,
|
||||
sessionSearchRequestSchema,
|
||||
sessionSelectModelRequestSchema,
|
||||
sessionUpdateQueueRequestSchema,
|
||||
} from '../api/sessions.schema.ts'
|
||||
import {
|
||||
hostCreateDirectoryRequestSchema, hostDescribeRequestSchema,
|
||||
@@ -73,7 +75,9 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'session.models': { schema: sessionModelsRequestSchema, invoke: (api, r) => api.sessions.models(r) },
|
||||
'session.selectModel': { schema: sessionSelectModelRequestSchema, invoke: (api, r) => api.sessions.selectModel(r) },
|
||||
'session.rename': { schema: sessionRenameRequestSchema, invoke: (api, r) => api.sessions.rename(r) },
|
||||
'session.fork': { schema: sessionForkRequestSchema, invoke: (api, r) => api.sessions.fork(r) },
|
||||
'session.prompt': { schema: sessionPromptRequestSchema, invoke: (api, r) => api.sessions.prompt(r) },
|
||||
'session.updateQueue': { schema: sessionUpdateQueueRequestSchema, invoke: (api, r) => api.sessions.updateQueue(r) },
|
||||
'session.cancel': { schema: sessionCancelRequestSchema, invoke: (api, r) => api.sessions.cancel(r) },
|
||||
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
|
||||
'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) },
|
||||
|
||||
Reference in New Issue
Block a user