Merge remote-tracking branch 'origin/master' into worktree/skill-invocation-controls

# Conflicts:
#	docs/cordis-catalog/services.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-29 12:58:10 +08:00
313 files changed
+12030 -1864

No files matched your search

@@ -0,0 +1,79 @@
/**
* goals domain zod schemas. Mutation-only shapes: every value schema is a
* `{ ref }` acknowledgement (clear: `{ cleared }`) — the current goal state
* travels exclusively on the 'goal' session projection.
*/
import { z } from 'zod'
import type { Wire } from './rpc.schema.ts'
import type { GoalRef, RequestPayload, ResponseValue } from './index.ts'
/** GoalRef schema. */
export const goalRefSchema = z.object({
id: z.string(),
revision: z.number().int().positive(),
}) as unknown as z.ZodType<Wire<GoalRef>>
/** Shared `{ ref }` acknowledgement value of every non-clear mutation. */
const goalRefValueSchema = z.object({ ref: goalRefSchema })
/** goal.create request payload. */
export const goalCreateRequestSchema = z.object({
sessionId: z.string(),
objective: z.string().min(1),
maxGoalRounds: z.number().int().positive().optional(),
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.create'>>>
/** goal.create response value. */
export const goalCreateValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.create'>>>
/** goal.edit request payload. */
export const goalEditRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
objective: z.string().min(1).optional(),
maxGoalRounds: z.number().int().positive().optional(),
}).refine(value => value.objective !== undefined || value.maxGoalRounds !== undefined, {
message: 'goal.edit requires objective or maxGoalRounds',
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.edit'>>>
/** goal.edit response value. */
export const goalEditValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.edit'>>>
/** goal.pause request payload. */
export const goalPauseRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.pause'>>>
/** goal.pause response value. */
export const goalPauseValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.pause'>>>
/** goal.resume request payload. */
export const goalResumeRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.resume'>>>
/** goal.resume response value. */
export const goalResumeValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.resume'>>>
/** goal.complete request payload. */
export const goalCompleteRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.complete'>>>
/** goal.complete response value. */
export const goalCompleteValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.complete'>>>
/** goal.clear request payload. */
export const goalClearRequestSchema = z.object({
sessionId: z.string(),
ref: goalRefSchema,
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.clear'>>>
/** goal.clear response value. */
export const goalClearValueSchema = z.object({
cleared: z.literal(true),
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.clear'>>>
+50
View File
@@ -0,0 +1,50 @@
/**
* goals domain contract. Method signatures are the source of truth:
* unary methods take the RpcRequest<P> narrow form and the impl echoes rpcId.
*
* Mutations only: the read side is the 'goal' session projection (history
* tail-page projections block + session/projection frames), so there is no
* goal.get and no wire goal view — responses acknowledge with the new CAS
* ref and never feed client state (the committed goal/change event reaches
* every client through the mux stream carrying the same whole value).
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** Identifies one goal across its durable revisions. */
export type GoalId = Branded<'GoalId'>
/** Compare-and-set identity for one exact goal revision. */
export interface GoalRef {
readonly id: GoalId
readonly revision: number
}
/** Goal-domain unary methods (every mutation resolves the session's agent and applies one CAS-guarded verb). */
export interface GoalsApi {
/** Create and arm a goal. */
create(request: RpcRequest<{ sessionId: SessionId; objective: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ ref: GoalRef }>>
/** Edit objective and/or round cap without changing phase. */
edit(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef; objective?: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ ref: GoalRef }>>
/** Pause an active goal and disarm automatic continuation. */
pause(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ ref: GoalRef }>>
/** Resume and arm a stopped goal. */
resume(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ ref: GoalRef }>>
/** Mark a current non-complete goal complete and disarm it. */
complete(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ ref: GoalRef }>>
/** Clear the current goal while retaining a durable tombstone and history. */
clear(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ cleared: true }>>
}
@@ -3,6 +3,7 @@
*/
import { z } from 'zod'
import type { DirectoryEntry } from './host.ts'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
@@ -16,6 +17,8 @@ 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). */
@@ -26,6 +29,41 @@ export const hostPickDirectoryValueSchema = z.object({
path: z.string().nullable(),
}) satisfies z.ZodType<Wire<ResponseValue<'host.pickDirectory'>>>
/** Directory row shared by listing entries and breadcrumb crumbs. */
export const directoryEntrySchema = z.object({
name: z.string(),
path: z.string(),
hidden: z.boolean(),
}) satisfies z.ZodType<Wire<DirectoryEntry>>
/** host.listDirectory request payload; an absent path lists the home directory. */
export const hostListDirectoryRequestSchema = z.object({
path: z.string().optional(),
}) satisfies z.ZodType<Wire<RequestPayload<'host.listDirectory'>>>
/** host.listDirectory response value. */
export const hostListDirectoryValueSchema = z.object({
path: z.string(),
home: z.string(),
crumbs: z.array(directoryEntrySchema),
entries: z.array(directoryEntrySchema),
truncated: z.boolean(),
}) satisfies z.ZodType<Wire<ResponseValue<'host.listDirectory'>>>
/** host.createDirectory request payload: name must be one plain path segment. */
export const hostCreateDirectoryRequestSchema = z.object({
path: z.string(),
name: z.string(),
}).refine(
payload => payload.name.trim() !== '' && payload.name !== '.' && payload.name !== '..'
&& !/[/\\]/.test(payload.name),
{ message: 'host.createDirectory requires a single non-blank path segment name' },
) satisfies z.ZodType<Wire<RequestPayload<'host.createDirectory'>>>
/** host.createDirectory response value: the created directory's absolute path. */
export const hostCreateDirectoryValueSchema = z.object({
path: z.string(),
}) satisfies z.ZodType<Wire<ResponseValue<'host.createDirectory'>>>
/** host.openPath request payload. */
export const hostOpenPathRequestSchema = z.object({
path: z.string().min(1),
+57 -4
View File
@@ -5,6 +5,33 @@
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** One directory row of a listing: a child entry or a breadcrumb ancestor. */
export interface DirectoryEntry {
/** Base name shown in a browser row (a root crumb carries its full path). */
name: string
/** Absolute host path — the client never joins path segments itself. */
path: string
/** Hidden by the host platform's convention (dot-prefixed on POSIX); the client owns whether to show it. */
hidden: boolean
}
/** host.listDirectory response value: one directory level plus its ancestry. */
export interface DirectoryListing {
/** Absolute path of the listed directory. */
path: string
/** The host account's home directory (breadcrumb "Home" rooting). */
home: string
/**
* Ancestor chain from the filesystem root to the listed directory
* inclusive; every crumb is a jump target (crumb `hidden` is always false).
*/
crumbs: DirectoryEntry[]
/** Direct child directories, name-sorted; symlinks to directories included. */
entries: DirectoryEntry[]
/** True when the backend cut `entries` at its complete-result bound (the name-sorted tail is absent). */
truncated: boolean
}
/** Host-level unary methods. */
export interface HostApi {
/**
@@ -13,7 +40,7 @@ export interface HostApi {
* directory (root for session persistence and tool execution); provider/model = the defaults
* applied when a new agent doesn't specify them explicitly, absent when the host configures
* no explicit default (the adapter falls back internally);
* attachedSessions = count of currently attached sessions (those with a live agent).
* attachedSessions = count of currently attached sessions (those with a live agent);
*/
describe(request: RpcRequest<{}>): Promise<RpcResponse<{
version: string
@@ -23,16 +50,42 @@ export interface HostApi {
attachedSessions: number
}>>
/** Open the operating system's single-directory picker; cancellation returns null. */
/**
* Open the operating system's single-directory picker; cancellation returns
* null. Only served under the `native` capability.
*/
pickDirectory(
request: RpcRequest<{}>,
signal: AbortSignal,
): Promise<RpcResponse<{ path: string | null }>>
/**
* List one directory level for the in-app browser; an absent path lists the
* host account's home directory. Only served under the `browse` capability;
* unreadable or missing targets fail with `directory-unreadable`. The
* carrier's request signal follows the caller, stopping the backend's scan
* on disconnect or timeout.
*/
listDirectory(
request: RpcRequest<{ path?: string }>,
signal: AbortSignal,
): Promise<RpcResponse<DirectoryListing>>
/**
* Create one child directory under an existing parent (the browser's
* "New folder"). Only served under the `browse` capability; an existing
* child fails with `directory-exists`, every other filesystem failure with
* `directory-create-failed`.
*/
createDirectory(
request: RpcRequest<{ path: string; name: string }>,
): Promise<RpcResponse<{ path: string }>>
/**
* Open a filesystem path with the operating system's default application
* (Finder / Explorer / xdg-open hand-off). The browser carrier restricts this
* privileged method to loopback, same-origin requests.
* (Finder / Explorer / xdg-open hand-off). The browser carrier's
* prefix-wide trust fence covers this privileged method like every other
* `/api` request.
*/
openPath(
request: RpcRequest<{ path: string }>,
+4 -1
View File
@@ -10,6 +10,7 @@ import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { SkillsApi } from './skills.ts'
import type { EventsApi } from './events.ts'
import type { GoalsApi } from './goals.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. */
@@ -20,6 +21,7 @@ export interface ApiProxy {
commands: CommandsApi
skills: SkillsApi
events: EventsApi
goals: GoalsApi
/** Response entry for server-requests (client-response, echoing their rpcId); not a domain method (four-quadrant model). */
respond(message: ClientResponse): Promise<RpcReceipt>
}
@@ -29,11 +31,12 @@ export type {
HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
ModelReasoningEffort, ModelTarget, SessionModels, SessionProjectionsBlock, SessionsApi, SessionSummary,
} from './sessions.ts'
export type { HostApi } from './host.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 { GoalsApi, GoalId, GoalRef } from './goals.ts'
export type { ApprovalResponsePayload } from './approvals.ts'
export type { QuestionResponsePayload } from './questions.ts'
@@ -9,6 +9,7 @@ import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { SkillsApi } from './skills.ts'
import type { GoalsApi } from './goals.ts'
import type { RpcResponse } from './rpc.ts'
/**
@@ -26,6 +27,8 @@ export interface RpcMethodMap {
'session.cancel': SessionsApi['cancel']
'host.describe': HostApi['describe']
'host.pickDirectory': HostApi['pickDirectory']
'host.listDirectory': HostApi['listDirectory']
'host.createDirectory': HostApi['createDirectory']
'host.openPath': HostApi['openPath']
'workspace.list': WorkspaceApi['list']
'workspace.create': WorkspaceApi['create']
@@ -35,6 +38,12 @@ export interface RpcMethodMap {
'command.list': CommandsApi['list']
'command.execute': CommandsApi['execute']
'skill.list': SkillsApi['list']
'goal.create': GoalsApi['create']
'goal.edit': GoalsApi['edit']
'goal.pause': GoalsApi['pause']
'goal.resume': GoalsApi['resume']
'goal.complete': GoalsApi['complete']
'goal.clear': GoalsApi['clear']
}
/** Business request payload of method K (reaches through the RpcRequest narrow form to payload). */
@@ -42,7 +42,13 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('workspace-invalid-path'), message: z.string(), details: z.object({ path: z.string() }) }),
z.object({ code: z.literal('workspace-name-conflict'), message: z.string(), details: z.object({ name: z.string() }) }),
z.object({ code: z.literal('workspace-move-invalid'), message: z.string(), details: z.object({ workspaceId: z.string(), sessionId: z.string(), beforeSessionId: z.string().optional() }) }),
z.object({ code: z.literal('directory-unreadable'), message: z.string(), details: z.object({ path: z.string() }) }),
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-busy'), message: z.string(), details: z.object({ reason: 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('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>
+8
View File
@@ -39,7 +39,15 @@ export interface RpcErrorDetailsMap {
'workspace-invalid-path': { path: string }
'workspace-name-conflict': { name: string }
'workspace-move-invalid': { workspaceId: string; sessionId: SessionId; beforeSessionId?: SessionId }
'directory-unreadable': { path: string }
'directory-exists': { path: string }
'directory-create-failed': { path: string }
'directory-picker-unavailable': { capability: string }
'agent-busy': { reason: string }
/** 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': {}
'internal': {}
}
@@ -193,9 +193,13 @@ export const sessionPromptRequestSchema = z.object({
content: z.array(contentBlockSchema),
}) as unknown as z.ZodType<RequestPayload<'session.prompt'>>
/** session.prompt response value. */
/** session.prompt response value (the command slot appears only when the prompt dispatched a slash command). */
export const sessionPromptValueSchema = z.object({
accepted: z.literal(true),
command: z.object({
kind: z.literal('success'),
text: z.string().optional(),
}).optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.prompt'>>>
/** session.cancel request payload. */
+16 -7
View File
@@ -132,11 +132,13 @@ export interface SessionSummary {
/** Status of the attached agent; always false for cold (unattached) sessions. */
running: boolean
/**
* Derived emptiness bit: true while the session log holds zero events (no
* user message yet). Clients hide blank sessions from lists and reuse them
* for New Session on the same workspace. Always false for cold sessions —
* lazy persistence keeps a never-appended session out of the store, so a
* listed cold session necessarily has events.
* Derived conversation-not-started bit: true while no turn has run (no
* prompt was accepted yet). Standalone plugin events — command lifecycle
* records, plan/mode, titles, goals — do not open a turn and therefore do
* not clear it. Clients hide blank sessions from lists and reuse them for
* New Session on the same workspace. Always false for cold sessions —
* lazy persistence keeps a never-appended session out of the store, and a
* listed cold session's log holds its turns.
*/
blank: boolean
/** fork/spawn lineage (session.header.parentSession passthrough); absent for root sessions. */
@@ -206,9 +208,16 @@ export interface SessionsApi {
}>):
Promise<RpcResponse<{ selected: ModelTarget }>>
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
/**
* Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer.
* A prompt whose content is exactly one text block starting with '/' is a slash command: the host
* executes it through the command registry (mode-agnostic) and it is never sent to the model. A
* successful command returns ok with the command slot (its success text, when the command produced
* 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.
*/
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
Promise<RpcResponse<{ accepted: true }>>
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>