Merge worktree/schedule-conversational-after into worktree/schedule-explicit-at

This commit is contained in:
Tianyi Cui
2026-08-11 19:20:38 +08:00
3663 files changed
+86658 -37212

No files matched your search

@@ -0,0 +1,88 @@
/**
* agent-presets domain zod schemas (names derived from map keys:
* agentPresetListRequestSchema / agentPresetListValueSchema).
*/
import { z } from 'zod'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './sessions.schema.ts'
import type { AgentPresetEntry } from './agent-presets.ts'
/** AgentPresetEntry row of agentPreset.list. */
export const agentPresetEntrySchema = z.object({
id: z.string().min(1),
trust: z.union([z.literal('system'), z.literal('user')]),
isDefault: z.boolean(),
name: z.string().optional(),
description: z.string().optional(),
broken: z.string().min(1).optional(),
}) satisfies z.ZodType<Wire<AgentPresetEntry>>
/** agentPreset.list request payload. */
export const agentPresetListRequestSchema = z.object({
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.list'>>>
/** agentPreset.list response value. */
export const agentPresetListValueSchema = z.object({
presets: z.array(agentPresetEntrySchema),
authorable: z.boolean(),
hasDocument: z.boolean(),
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.list'>>>
/** agentPreset.select request payload. */
export const agentPresetSelectRequestSchema = z.object({
sessionId: sessionIdSchema,
agentPreset: z.string().min(1),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.select'>>>
/** agentPreset.select response value. */
export const agentPresetSelectValueSchema = z.object({
agentPreset: z.string(),
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.select'>>>
/** agentPreset.read request payload. */
export const agentPresetReadRequestSchema = z.object({
agentPreset: z.string().min(1),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.read'>>>
/** agentPreset.read response value. */
export const agentPresetReadValueSchema = z.object({
agentPreset: z.string(),
trust: z.union([z.literal('system'), z.literal('user')]),
content: z.string(),
name: z.string().optional(),
description: z.string().optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.read'>>>
/** agentPreset.copy request payload. */
export const agentPresetCopyRequestSchema = z.object({
from: z.string().min(1),
agentPreset: z.string().min(1),
name: z.string().optional(),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.copy'>>>
/** agentPreset.copy response value. */
export const agentPresetCopyValueSchema = z.object({
agentPreset: z.string(),
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.copy'>>>
/** agentPreset.openDocument request payload. */
export const agentPresetOpenDocumentRequestSchema = z.object({
agentPreset: z.string().min(1),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.openDocument'>>>
/** agentPreset.openDocument response value. */
export const agentPresetOpenDocumentValueSchema = z.union([
z.object({ opened: z.literal(true) }),
z.object({ opened: z.literal(false), path: z.string() }),
]) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.openDocument'>>>
/** agentPreset.remove request payload. */
export const agentPresetRemoveRequestSchema = z.object({
agentPreset: z.string().min(1),
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.remove'>>>
/** agentPreset.remove response value. */
export const agentPresetRemoveValueSchema = z.object({
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.remove'>>>
@@ -0,0 +1,116 @@
/**
* agent-presets domain contract: the roster a browser offers when starting a
* session, plus the authoring calls behind it.
*
* `list` is ordinary: it carries ids and trust, and every preset picker needs
* it. The authoring calls are privileged and loopback-pinned — a composition
* names the plugins a session runs, so reading one is reconnaissance, and
* although authoring is copy-only (no caller supplies composition text or a
* path), copying and deleting still rearrange what the deployment offers.
*/
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** One preset the deployment can compose a session's agent from. */
export interface AgentPresetEntry {
/** Stable identifier, also the display name until presets carry metadata. */
readonly id: string
/**
* Whether the preset ships with the deployment or was authored locally.
* A `user` preset is exactly as privileged as the plugins it names, so a
* surface offering one should say so rather than present it as vetted.
*/
readonly trust: 'system' | 'user'
/** Whether a session that names no preset gets this one. */
readonly isDefault: boolean
/**
* Display name the preset published, absent when it published none. A
* surface falls back to {@link id}; it is never a second identity, and it
* never decides trust — a locally authored preset cannot name itself into
* the shipped set.
*/
readonly name?: string
/** One sentence on what the preset is for, when it published one. */
readonly description?: string
/**
* Why this preset cannot compose a session, absent when it can. A broken
* preset stays listed — its directory still occupies the id, so a surface
* must be able to show and delete it — but offering it for selection would
* only defer this reason to a failed session start.
*/
readonly broken?: string
}
/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */
export interface AgentPresetsApi {
/**
* Lists every preset the deployment currently supplies, in root-precedence
* order — the roots as configured, each root's own presets sorted by id,
* and the first root to supply an id wins. The order is not globally
* sorted: a user root's preset sits in that root's block, not among the
* shipped ids.
* An empty roster means the deployment composes no presets at all, and
* every session shares the host composition. `authorable` reports whether
* the deployment configures a root new presets can be written to, and
* `hasDocument` whether `openDocument` can hand a preset directory to a
* native opener — both deployment facts rather than per-preset ones, and
* neither exposes a Host path.
*/
list(request: RpcRequest<{}>):
Promise<RpcResponse<{ presets: readonly AgentPresetEntry[]; authorable: boolean; hasDocument: boolean }>>
/**
* Recompose one session's agent from a different preset.
*
* Allowed only while the session is blank — no turn has run. Once a
* conversation starts, its history was produced under that preset's tools,
* and swapping them would leave logged tool calls the new composition cannot
* make; the attempt answers `agent-preset-locked`.
*/
select(request: RpcRequest<{ sessionId: SessionId; agentPreset: string }>):
Promise<RpcResponse<{ agentPreset: string }>>
/**
* Read one preset's composition text, for the read-only viewer.
*
* Privileged: a composition names the plugins a session runs, so reading
* one is reconnaissance.
*/
read(request: RpcRequest<{ agentPreset: string }>):
Promise<RpcResponse<{
agentPreset: string
trust: 'system' | 'user'
content: string
name?: string
description?: string
}>>
/**
* Create a locally authored preset by copying an existing one whole.
*
* The only authoring write. No composition text and no path crosses the
* wire: `from` and `agentPreset` are ids the Host resolves against its own
* roots, so a copy is exactly as loadable as its source and grants nothing
* the roster did not already carry. The copy keeps the source's description
* (the file is the author's to edit afterwards) but not its name — `name`
* here or the id fallback is what distinguishes the rows.
*/
copy(request: RpcRequest<{ from: string; agentPreset: string; name?: string }>):
Promise<RpcResponse<{ agentPreset: string }>>
/**
* Hand one locally authored preset's DIRECTORY to the platform opener, for
* editing the files that are now the only composition editor. The request
* carries an id, never a path — the Host resolves it — so no browser
* payload can select an arbitrary filesystem target. Where the deployment
* has no native opener (`hasDocument: false` on `list`), the reply carries
* the resolved directory for the surface to show as text instead. Shipped
* presets are refused: their install is not the user's to manage.
*/
openDocument(request: RpcRequest<{ agentPreset: string }>, signal: AbortSignal):
Promise<RpcResponse<{ opened: true } | { opened: false; path: string }>>
/** Delete a locally authored preset. Shipped presets are refused. */
remove(request: RpcRequest<{ agentPreset: string }>): Promise<RpcResponse<{}>>
}
@@ -10,7 +10,7 @@ import type { ApprovalResponsePayload } from './approvals.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './sessions.schema.ts'
/** ApprovalRequestId: one brand cast after shape validation (the only cast point in this domain). */
/** ApprovalRequestId: one brand cast after schema validation (the only cast point in this domain). */
export const approvalRequestIdSchema = z.string().min(1) as unknown as z.ZodType<ApprovalRequestId>
/** Approval answer payload (the result.value slot of a client-response). */
@@ -33,7 +33,7 @@ export const commandExecuteRequestSchema = z.object({
line: z.string(),
}) satisfies z.ZodType<Wire<RequestPayload<'command.execute'>>>
/** CommandId: one brand cast after shape validation (the only cast point in this domain). */
/** CommandId: one brand cast after schema validation (the only cast point in this domain). */
export const commandIdSchema = z.string().min(1) as unknown as z.ZodType<CommandId>
/** command.execute response value: pure admission — outcomes ride the logged
@@ -0,0 +1,26 @@
/**
* downloads domain zod schemas. The GET download surface has no wire
* envelope: the request arrives as query parameters (all strings), so its
* request schema parses the raw query-parameter object into the method's
* exact request shape. SessionId brand cast point: sessionIdSchema, and only
* there (hosted in sessions.schema like every other cast).
*/
import { z } from 'zod'
import type { DownloadsApi } from './downloads.ts'
import { sessionIdSchema } from './sessions.schema.ts'
/**
* session.export query params → the sessionLog request. `includeDescendants`
* accepts exactly `true`/`false`/absent; any other value is rejected (400) so
* a misspelled flag cannot silently under-export.
*/
export const sessionLogQuerySchema = z
.object({
sessionId: sessionIdSchema,
includeDescendants: z.union([z.literal('true'), z.literal('false')]).optional(),
})
.transform(query => ({
sessionId: query.sessionId,
...(query.includeDescendants === 'true' ? { includeDescendants: true } : {}),
})) satisfies z.ZodType<Parameters<DownloadsApi['sessionLog']>[0]>
@@ -0,0 +1,25 @@
/**
* downloads domain contract: host-only download surfaces — the GET-download
* channel family, the mirror of the SSE-stream `events` domain. No wire
* envelope: the carrier's GET routes answer these directly, and the browser
* `IApiClient` never exposes them.
*/
import type { SessionId } from '@deepseek-ai/dsh-session/types'
/** Host-only download surfaces (no wire envelope; absent from IApiClient). */
export interface DownloadsApi {
/**
* Stream one session-log ZIP — the root artifact verbatim plus each subagent
* descendant's — as an attachment response. The carrier's GET route answers
* this directly; the browser never calls it.
* @param request - the root session id and whether to include descendants.
* @param signal - cancellation for the underlying reads.
* @returns the ZIP attachment response; missing services answer 500 and a
* missing root session 404 before any byte is produced.
*/
sessionLog(
request: { sessionId: SessionId; includeDescendants?: boolean },
signal: AbortSignal,
): Promise<Response>
}
@@ -13,9 +13,10 @@ import { approvalRequestIdSchema } from './approvals.schema.ts'
import {
contentBlockSchema, messageIdSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema,
} from './sessions.schema.ts'
import { taskViewSchema } from './tasks.schema.ts'
import { workspaceIdSchema, workspaceViewSchema } from './workspace.schema.ts'
/** Question shape validated strictly against core dsh-user-interaction. */
/** Question fields validated strictly against core dsh-user-interaction. */
export const askUserQuestionItemSchema = z.object({
id: z.string(),
question: z.string(),
@@ -58,6 +59,7 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
message: messageSchema,
})),
}),
z.object({ type: z.literal('session/tasks'), sessionId: sessionIdSchema, tasks: z.array(taskViewSchema) }),
// 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() }),
@@ -73,6 +75,7 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
parentSessionId: sessionIdSchema.optional(),
origin: z.literal('subagent').optional(),
cwd: z.string().optional(),
agentPreset: z.string().optional(),
}),
z.object({ type: z.literal('host/session-removed'), sessionId: sessionIdSchema }),
z.object({ type: z.literal('host/session-status'), sessionId: sessionIdSchema, running: z.boolean() }),
@@ -81,6 +84,7 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('host/workspace-removed'), workspaceId: workspaceIdSchema }),
z.object({ type: z.literal('host/archived-sessions-changed'), archivedSessionIds: z.array(sessionIdSchema) }),
z.object({ type: z.literal('host/commands-changed') }),
z.object({ type: z.literal('host/session-preset-changed'), sessionId: sessionIdSchema, agentPreset: z.string() }),
z.object({ type: z.literal('host/settings-changed'), ns: z.string() }),
z.object({ type: z.literal('host/credentials-changed'), ref: z.string() }),
z.object({ type: z.literal('host/models-changed') }),
+28
View File
@@ -14,6 +14,7 @@ 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'
import type { RpcError, RpcId, RpcRequest } from './rpc.ts'
import type { TaskView } from './tasks.ts'
import type { WorkspaceView } from './workspace.ts'
// Client-side consumers take the render-intent vocabulary from the contract;
@@ -81,6 +82,20 @@ export type MuxFrame =
* in QueueDock, while pending steering renders at the conversation tail.
*/
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
/**
* Complete set of background tasks this session can see, after every registry
* commit that changes it: registration, the stopping transition, settlement,
* and owner-disposal removal. The registry is process-local and holds no
* durable event, so — exactly like `session/queue` — the whole snapshot is
* what makes a start, a kill, a reconnect, and a second tab converge on one
* authoritative value.
*
* Sent as a subscription baseline only for a session that currently has
* tasks; an absent key means an empty set. A change that empties the set
* still sends `[]`, since that transition is the only one absence cannot
* express.
*/
| { type: 'session/tasks'; sessionId: SessionId; tasks: TaskView[] }
/**
* One projection unit's finished value changed (session-projection RFC).
* Live push state, never logged — replay recomputes on the host (the
@@ -116,6 +131,7 @@ export type HostFrame =
parentSessionId?: SessionId
origin?: 'subagent'
cwd?: string
agentPreset?: string
}
| { type: 'host/session-removed'; sessionId: SessionId }
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
@@ -129,6 +145,18 @@ export type HostFrame =
* background rather than diffing.
*/
| { type: 'host/commands-changed' }
/**
* One blank session was recomposed onto another agent preset (the logged
* `agent-preset/selected` commit point, read off the session stream). The
* registry-wide `host/commands-changed` cannot stand in for it: recomposing
* re-parents that agent's scope without registering anything, so a
* preset already mounted for another session produces no registry change
* at all. Clients refetch the catalogs this session's composition decides
* (`command.list`, `skill.list`) for this sessionId alone, and fold the
* preset id into their session row — the RPC echo reaches only the client
* that issued the switch, so the row is where every other one learns it.
*/
| { type: 'host/session-preset-changed'; sessionId: SessionId; agentPreset: string }
/**
* One settings namespace's resolved value changed (`settings/updated`
* passthrough) — an RPC write, an external `settings.yaml` edit, or a
@@ -17,8 +17,6 @@ export const hostDescribeValueSchema = z.object({
provider: z.string().optional(),
model: z.string().optional(),
attachedSessions: z.number().int().nonnegative(),
// Open string, not a literal union: unknown kinds must survive the wire so
// a merge-added capability can advertise (the client hides the affordance).
}) satisfies z.ZodType<Wire<ResponseValue<'host.describe'>>>
/** host.pickDirectory request payload (empty object literal). */
+11 -3
View File
@@ -8,6 +8,7 @@ import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { AgentPresetsApi } from './agent-presets.ts'
import type { SkillsApi } from './skills.ts'
import type { SubagentsApi } from './subagents.ts'
import type { EventsApi } from './events.ts'
@@ -15,9 +16,10 @@ import type { GoalsApi } from './goals.ts'
import type { SettingsApi } from './settings.ts'
import type { CredentialsApi } from './credentials.ts'
import type { LlmApi } from './llm.ts'
import type { DownloadsApi } from './downloads.ts'
import type { ClientResponse, RpcReceipt } from './rpc.ts'
/** Root interface of the unified API surface. New client-request domain = one new file pair + one field here + one map row. */
/** Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row. */
export interface ApiProxy {
sessions: SessionsApi
subagents: SubagentsApi
@@ -25,11 +27,14 @@ export interface ApiProxy {
workspace: WorkspaceApi
commands: CommandsApi
skills: SkillsApi
agentPresets: AgentPresetsApi
events: EventsApi
goals: GoalsApi
settings: SettingsApi
credentials: CredentialsApi
llm: LlmApi
/** Host-only download surfaces (GET, no wire envelope); absent from IApiClient. */
downloads: DownloadsApi
/** Response entry for server-requests (client-response, echoing their rpcId); not a domain method (four-quadrant model). */
respond(message: ClientResponse): Promise<RpcReceipt>
}
@@ -37,22 +42,25 @@ export interface ApiProxy {
// ---- Domain interfaces and payload entities ----
export type {
HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
ModelReasoningEffort, ModelSelection, QueueAction, SessionModels, SessionProjectionsBlock, SessionSearchItem,
SessionsApi, SessionSummary,
ModelReasoningEffort, ModelSelection, PromptContentPart, QueueAction, SessionModels,
SessionProjectionsBlock, SessionSearchItem, SessionsApi, SessionSummary,
} from './sessions.ts'
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
export type {
SubagentAddress, SubagentCatalog, SubagentInterruptReceipt, SubagentListEntry,
SubagentPromptReceipt, SubagentsApi,
} from './subagents.ts'
export type { TaskView } from './tasks.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 { AgentPresetsApi, AgentPresetEntry } from './agent-presets.ts'
export type { EventsApi, MuxFrame, HostFrame, QueuedInboxItem, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
export type { GoalsApi, GoalId, GoalRef } from './goals.ts'
export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'
export type { CredentialsApi, CredentialView } from './credentials.ts'
export type { ConfigurableProviderView, DiscoveredModelView, LlmApi } from './llm.ts'
export type { DownloadsApi } from './downloads.ts'
export type { ApprovalResponsePayload } from './approvals.ts'
export type { QuestionResponsePayload } from './questions.ts'
@@ -8,6 +8,7 @@ import type { SessionsApi } from './sessions.ts'
import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { AgentPresetsApi } from './agent-presets.ts'
import type { SkillsApi } from './skills.ts'
import type { GoalsApi } from './goals.ts'
import type { SettingsApi } from './settings.ts'
@@ -31,6 +32,7 @@ export interface RpcMethodMap {
'session.rename': SessionsApi['rename']
'session.fork': SessionsApi['fork']
'session.prompt': SessionsApi['prompt']
'session.attachment': SessionsApi['attachment']
'session.updateQueue': SessionsApi['updateQueue']
'session.cancel': SessionsApi['cancel']
'subagent.list': SubagentsApi['list']
@@ -51,6 +53,12 @@ export interface RpcMethodMap {
'command.list': CommandsApi['list']
'command.execute': CommandsApi['execute']
'skill.list': SkillsApi['list']
'agentPreset.list': AgentPresetsApi['list']
'agentPreset.select': AgentPresetsApi['select']
'agentPreset.read': AgentPresetsApi['read']
'agentPreset.copy': AgentPresetsApi['copy']
'agentPreset.openDocument': AgentPresetsApi['openDocument']
'agentPreset.remove': AgentPresetsApi['remove']
'goal.create': GoalsApi['create']
'goal.edit': GoalsApi['edit']
'goal.pause': GoalsApi['pause']
+8 -2
View File
@@ -23,8 +23,8 @@ export type Wire<T> = T extends readonly (infer E)[] ? Wire<E>[]
: T
/**
* RpcId: one brand cast after shape validation (the only cast point in this
* file). No min-length: the id is an opaque echo token, and rejecting shapes
* RpcId: one brand cast after schema validation (the only cast point in this
* file). No min-length: the id is an opaque echo token, and rejecting values
* here would only turn a correlatable error report into a client-side parse
* failure (the handler substitutes a sentinel when a request's id is unreadable).
*/
@@ -47,7 +47,13 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }),
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-preset-read-only'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }),
z.object({ code: z.literal('agent-preset-locked'), message: z.string(), details: z.object({ sessionId: z.string(), agentPreset: z.string() }) }),
z.object({ code: z.literal('agent-preset-conflict'), message: z.string(), details: z.object({ sessionId: z.string(), requestedPreset: z.string(), existingPreset: z.string().optional() }) }),
z.object({ code: z.literal('agent-preset-not-found'), message: z.string(), details: z.object({ agentPreset: z.string(), available: z.array(z.string()) }) }),
z.object({ code: z.literal('agent-preset-invalid'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }),
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
z.object({ code: z.literal('attachment-error'), 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('steer-unavailable'), message: z.string(), details: z.object({ itemId: z.string() }) }),
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
+7 -1
View File
@@ -45,7 +45,13 @@ export interface RpcErrorDetailsMap {
'directory-exists': { path: string }
'directory-create-failed': { path: string }
'directory-picker-unavailable': { capability: string }
'agent-preset-read-only': { agentPreset: string; reason: string }
'agent-preset-locked': { sessionId: SessionId; agentPreset: string }
'agent-preset-conflict': { sessionId: SessionId; requestedPreset: string; existingPreset?: string }
'agent-preset-not-found': { agentPreset: string; available: string[] }
'agent-preset-invalid': { agentPreset: string; reason: string }
'agent-busy': { reason: string }
'attachment-error': { reason: string }
'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. */
@@ -111,7 +117,7 @@ export type RpcResult<T> = { ok: true; value: T } | { ok: false; error: RpcError
/**
* Fold a transport exception into the RpcResult error branch (unified error
* surface; 'internal' as the catch-all code). Lives with RpcResult so every
* API; 'internal' as the catch-all code). Lives with RpcResult so every
* carrier consumer folds the same way.
* @param error - the thrown value from the carrier.
* @returns the error branch of an RpcResult.
@@ -1,7 +1,7 @@
/**
* sessions domain zod schemas (names derived from map keys: sessionListRequestSchema /
* sessionListValueSchema). SessionEvent passthrough = strict envelope (type/seq/time) + wide
* data: the merge-extensible event surface keeps an unknown-type branch at the union level,
* data: the merge-extensible event API keeps an unknown-type branch at the union level,
* with no field-level passthrough. SessionId brand cast point: sessionIdSchema, and only there.
*/
@@ -15,6 +15,7 @@ import type {
ModelReasoningEffort, ModelSelection, SessionProjectionsBlock, SessionSearchItem, SessionSummary,
} from './sessions.ts'
import type { ToolEventView } from './events.ts'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { WorkspaceId } from './workspace.ts'
import {
SESSION_SEARCH_RESULT_LIMIT,
@@ -22,7 +23,7 @@ import {
truncateUnicodeCodePoints,
} from './session-search.ts'
/** SessionId: one brand cast after shape validation (the only cast point in this domain). */
/** SessionId: one brand cast after schema validation (the only cast point in this domain). */
export const sessionIdSchema = z.string().min(1) as unknown as z.ZodType<SessionId>
/** MessageId: one brand cast after non-empty string validation. */
@@ -44,6 +45,7 @@ export const sessionEventSchema = z.object({
data: z.unknown(),
sourceEventSeqs: z.array(z.number()).optional(),
surfaceOp: z.unknown().optional(),
ignorable: z.literal(true).optional(),
}) as unknown as z.ZodType<SessionEvent>
/** SessionSummary row of session.list (`projections` reuses the history block's shape and schema). */
@@ -55,6 +57,7 @@ export const sessionSummarySchema = z.object({
parentSessionId: sessionIdSchema.optional(),
origin: z.literal('subagent').optional(),
cwd: z.string().optional(),
agentPreset: z.string().optional(),
projections: z.lazy(() => sessionProjectionsBlockSchema).optional(),
}) as unknown as z.ZodType<Wire<SessionSummary>>
@@ -100,6 +103,7 @@ export const sessionCreateRequestSchema = z.object({
workspaceId: workspaceIdSchema.optional(),
cwd: z.string().optional(),
sessionId: sessionIdSchema.optional(),
agentPreset: z.string().optional(),
}).refine(
payload => payload.workspaceId === undefined || payload.cwd === undefined,
{ message: 'session.create accepts workspaceId or cwd, not both' },
@@ -108,6 +112,7 @@ export const sessionCreateRequestSchema = z.object({
/** session.create response value. */
export const sessionCreateValueSchema = z.object({
sessionId: sessionIdSchema,
agentPreset: z.string().optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.create'>>>
/** session.rename request payload (raw title; host-side normalization decides acceptance). */
@@ -246,11 +251,25 @@ export const sessionSelectModelValueSchema = z.object({
/** ContentBlock passthrough: core is merge-extensible — the type discriminant envelope is strict, the rest stays wide. */
export const contentBlockSchema = z.looseObject({ type: z.string() })
/** Raster image media types accepted by the version-one browser wire. */
export const imageMediaTypeSchema = z.union([
z.literal('image/png'),
z.literal('image/jpeg'),
z.literal('image/webp'),
z.literal('image/gif'),
])
/** Prompt wire content is intentionally narrower than merge-extensible durable core content. */
export const promptContentPartSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('text'), text: z.string() }),
z.object({ type: z.literal('image'), mediaType: imageMediaTypeSchema, data: z.string(), name: z.string().optional() }),
])
/** session.prompt request payload, including optional browser-local request provenance. */
export const sessionPromptRequestSchema = z.object({
sessionId: sessionIdSchema,
mode: z.union([z.literal('queue'), z.literal('steer')]),
content: z.array(contentBlockSchema),
content: z.array(promptContentPartSchema),
clientTimeZone: z.string().optional(),
}) as unknown as z.ZodType<RequestPayload<'session.prompt'>>
@@ -263,6 +282,31 @@ export const sessionPromptValueSchema = z.object({
}).optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.prompt'>>>
/** Opaque attachment id after string-shape validation. */
export const attachmentIdSchema = z.string().min(1) as unknown as z.ZodType<AttachmentIdType>
/** Durable image reference returned from the authenticated session lookup. */
export const imageAttachmentRefSchema = z.object({
attachmentId: attachmentIdSchema,
mediaType: imageMediaTypeSchema,
bytes: z.number().int().positive(),
width: z.number().int().positive(),
height: z.number().int().positive(),
name: z.string().optional(),
}) as unknown as z.ZodType<ImageAttachmentRef>
/** session.attachment request payload. */
export const sessionAttachmentRequestSchema = z.object({
sessionId: sessionIdSchema,
attachmentId: attachmentIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'session.attachment'>>>
/** session.attachment response value. */
export const sessionAttachmentValueSchema = z.object({
attachment: imageAttachmentRefSchema,
data: z.string(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.attachment'>>>
/** session.updateQueue request payload. */
export const sessionUpdateQueueRequestSchema = z.object({
sessionId: sessionIdSchema,
+29 -4
View File
@@ -5,6 +5,7 @@
*/
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { AttachmentIdType, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
// The pure-type outlet: api/ is browser-importable, and the package root's
@@ -54,6 +55,11 @@ export interface SessionProjectionsBlock {
values: Partial<SessionProjectionMap>
}
/** Browser-submitted prompt content; the host promotes image bytes to durable references. */
export type PromptContentPart =
| { type: 'text'; text: string }
| { type: 'image'; mediaType: ImageMediaType; data: string; name?: string }
/** Complete model selection for one session. */
export interface ModelSelection {
/** Registered provider route. */
@@ -166,6 +172,13 @@ export interface SessionSummary {
origin?: 'subagent'
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
cwd?: string
/**
* Agent preset this session's agent was composed from (header passthrough);
* absent when the deployment composes no presets. A surface offering a
* switch reads this to show what the session actually runs rather than what
* the deployment currently defaults to.
*/
agentPreset?: string
/**
* Projection baseline for this row, with zero log loads: attached sessions
* read the registry's live watermark cut; cold sessions read the persisted
@@ -209,9 +222,16 @@ export interface SessionsApi {
* session, while a different cwd fails with `session-conflict`. Workspace
* creation attaches the session after publication; an attach failure
* returns `workspace-attach-failed` with the published session id.
*
* `agentPreset` names the composition the new session's agent is built
* from; omitted, the effective default applies — the user's stored choice
* where one exists, else the deployment's own. The resolved id is stored on
* the session header, so a later resume rebuilds the same agent. An unknown
* id fails with `agent-preset-not-found`, and a preset whose composition
* cannot be mounted fails with `agent-preset-invalid`.
*/
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId }>):
Promise<RpcResponse<{ sessionId: SessionId }>>
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId; agentPreset?: string }>):
Promise<RpcResponse<{ sessionId: SessionId; agentPreset?: string }>>
/**
* Reads a window of history events; page boundaries align to append-origin message
@@ -290,7 +310,8 @@ export interface SessionsApi {
Promise<RpcResponse<{ sessionId: SessionId }>>
/**
* Sends a message to an ordinary session Agent. Browser callers attach their current IANA zone;
* Sends text and temporary image bytes to an ordinary session Agent after durable host admission.
* Browser callers attach their current IANA zone;
* the Host validates, canonicalizes, and records it on that exact user message. Omission remains
* valid for non-browser callers. Session-backed subagents reject with `agent-busy` and use
* `subagent.prompt`.
@@ -298,11 +319,15 @@ export interface SessionsApi {
prompt(request: RpcRequest<{
sessionId: SessionId
mode: 'queue' | 'steer'
content: ContentBlock[]
content: PromptContentPart[]
clientTimeZone?: string
}>):
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
/** Reads one durable image after proving that this session's log references its id. */
attachment(request: RpcRequest<{ sessionId: SessionId; attachmentId: AttachmentIdType }>):
Promise<RpcResponse<{ attachment: ImageAttachmentRef; data: string }>>
/**
* Edits, removes, or strictly steers one pending queued occurrence on an ordinary session.
* Session-backed subagents reject with `agent-busy`.
@@ -0,0 +1,33 @@
/**
* tasks domain zod schemas: the branded task id and the wire view carried by
* `session/tasks` frames.
*/
import { z } from 'zod'
import type { TaskId } from '@deepseek-ai/dsh-tasks/brand'
import type { TaskView } from './tasks.ts'
import type { Wire } from './rpc.schema.ts'
/** TaskId: one brand cast after non-empty string validation. */
export const taskIdSchema = z.string().min(1) as unknown as z.ZodType<TaskId>
/**
* One wire task view. `kind` stays an open string because producer plugins
* extend the registry's kind map by declaration merging, so the closed set is
* not knowable at this boundary.
*/
export const taskViewSchema = z.object({
id: taskIdSchema,
kind: z.string().min(1),
label: z.string().min(1),
status: z.union([
z.literal('running'),
z.literal('stopping'),
z.literal('completed'),
z.literal('killed'),
z.literal('failed'),
]),
detail: z.string().optional(),
startedAt: z.number().int().nonnegative(),
finishedAt: z.number().int().nonnegative().optional(),
}) satisfies z.ZodType<Wire<TaskView>>
+36
View File
@@ -0,0 +1,36 @@
/**
* Browser-safe background-task domain contract. The registry's live records
* never cross the wire; a view is the subset a human list needs, minted fresh
* per push.
*/
import type { TaskId } from '@deepseek-ai/dsh-tasks/brand'
/**
* One background task as the client sees it.
*
* Three registry fields are deliberately absent. `ownerSession` is redundant
* beside the frame's own `sessionId`; `reported` is an internal notice-delivery
* bit with no user meaning; `outputLimitBytes` is producer-owned model
* presentation policy that never reaches a human surface.
*/
export interface TaskView {
/** Registry-issued `<kind>-N` identity, stable for the task's whole life. */
id: TaskId
/**
* Producer kind (`bash`, `pwsh`, `pty-send`, `subagent`, …). Kept as a bare
* string because producer plugins extend the kind map by declaration merging,
* so no client build can enumerate the closed set.
*/
kind: string
/** Producer-supplied one-line label: the command, or the delegation description. */
label: string
/** Current lifecycle state. */
status: 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
/** Kind-specific status detail ('exit code: 3'), present once the producer supplied one. */
detail?: string
/** Epoch ms when the task was registered. */
startedAt: number
/** Epoch ms when the task settled; absent while live. */
finishedAt?: number
}
@@ -31,14 +31,10 @@ export const workspaceListValueSchema = z.object({
archivedSessionIds: z.array(sessionIdSchema),
}) satisfies z.ZodType<Wire<ResponseValue<'workspace.list'>>>
/** workspace.create request payload: exactly one of path/name (the contract's create spellings). */
/** workspace.create request payload: the existing directory to adopt. */
export const workspaceCreateRequestSchema = z.object({
path: z.string().optional(),
name: z.string().optional(),
}).refine(
payload => (payload.path === undefined) !== (payload.name === undefined),
{ message: 'workspace.create requires exactly one of path / name' },
) satisfies z.ZodType<Wire<RequestPayload<'workspace.create'>>>
path: z.string(),
}) satisfies z.ZodType<Wire<RequestPayload<'workspace.create'>>>
/** workspace.create response value. */
export const workspaceCreateValueSchema = z.object({
+7 -12
View File
@@ -46,19 +46,14 @@ export interface WorkspaceApi {
list(request: RpcRequest<{}>): Promise<RpcResponse<{ items: WorkspaceView[]; archivedSessionIds: SessionId[] }>>
/**
* Creates (or idempotently resolves) a workspace. Exactly one of `path` /
* `name` (schema-enforced): `path` registers an EXISTING directory (no
* mkdir — a missing or non-directory path fails with `workspace-invalid-path`);
* `name` is a single path segment the host mkdirs under its default project
* root before registering. Either spelling resolving to a directory already
* owned by a workspace returns that workspace (`created: false`) for the
* existing-folder spelling. Create-by-name rejects an existing title with
* `workspace-name-conflict`; path adoption allows distinct canonical paths
* whose basenames produce the same display title.
* A new name-created workspace uses `name` as both directory name and title;
* a path-created workspace uses the registry's basename title default.
* Creates (or idempotently resolves) a workspace over an EXISTING directory
* (no mkdir — a missing or non-directory path fails with
* `workspace-invalid-path`). A path resolving to a directory already owned
* by a workspace returns that workspace (`created: false`). Adoption allows
* distinct canonical paths whose basenames produce the same display title;
* the registry's basename title default names the new workspace.
*/
create(request: RpcRequest<{ path?: string; name?: string }>):
create(request: RpcRequest<{ path: string }>):
Promise<RpcResponse<{ workspace: WorkspaceView; created: boolean }>>
/**