refactor(commands): move the command service to Remote

`CommandService.list` and `execute` carry the wire contract directly through
`@Remote`, and the Client assembly mounts the generated commands
contribution. The legacy API Proxy route, its schemas, the map rows, the
generated client methods and the fixture's command domain are removed, so the
catalog and the admission call have one owner again.

`Session.command()` keeps a result-shaped public face for parity with the
prompt, cancel and attachment neighbours it sits beside, and reads the
generated namespace through one `SessionRemotes` parameter. The Session
cluster declares that face against the owning business package rather than the
generated contribution: the Host compiler aggregate builds this package, and
it runs before any contribution is emitted.

Migrated calls lose the `title-invalid` class of protocol-only error codes and
report `internal`; no production caller branched on them.
This commit is contained in:
imccyu
2026-08-11 19:10:31 +08:00
parent a2981207b0
commit 070a2a7f1e
71 changed files with 648 additions and 1129 deletions

View File

@@ -67,7 +67,7 @@ import type {} from '@deepseek-ai/dsh-session-projection-cache'
// GoalError narrows domain rejections to their stable codes at the wire boundary.
import { GoalError } from '@deepseek-ai/dsh-goal'
import type { GoalRef as CoreGoalRef } from '@deepseek-ai/dsh-goal'
// Type-only edges: resolve `ctx.get('commands')`, the `commands/change` event, and `ctx.get('skills')`.
// Type-only edges: resolve the command-change stream and `ctx.get('skills')`.
import type {} from '@deepseek-ai/dsh-commands'
import type {} from '@deepseek-ai/dsh-skill'
// The settings/credentials seams: brand guards run at this wire boundary; the
@@ -2889,49 +2889,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
},
},
commands: {
// Both methods address one session's agent. agentFor resumes on miss
// and fences every subagent-owned identity with `agent-busy`; the
// api/commands.ts module contract owns that fence's wording, so this
// comment only notes the routing shape: clients send a sessionId for a
// published session, and resume restores an existing entity.
async list(request) {
// Missing service = the deployment omitted dsh-commands from its
// composition, not an empty catalog: fail loud instead of serving [].
const commands = ctx.get('commands')
if (commands === undefined) {
return err(request, { code: 'internal', message: 'command registry is absent: this deployment does not mount @deepseek-ai/dsh-commands in its composition (cordis.yml or explicit assembly)', details: {} })
}
const found = await agentFor(request.payload.sessionId)
if ('error' in found) return err(request, found.error)
return ok(request, { commands: commands.list(found.agent) })
},
async execute(request, signal) {
const commands = ctx.get('commands')
if (commands === undefined) {
return err(request, { code: 'internal', message: 'command registry is absent: this deployment does not mount @deepseek-ai/dsh-commands in its composition (cordis.yml or explicit assembly)', details: {} })
}
const { sessionId, line } = request.payload
const found = await agentFor(sessionId)
if ('error' in found) return err(request, found.error)
try {
// Pure admission: the executor's durable command/run + command/done
// pair (broadcast on the mux stream) carries the outcome; the
// response reports whether the line resolved to a handler, plus the
// minted pairing id so the issuing client can correlate its request
// with the flow node the lifecycle events produce.
const execution = await commands.execute(found.agent, line, signal)
return ok(request, execution === undefined
? { matched: false }
: { matched: true, commandId: execution.commandId })
} catch (error: unknown) {
if (signal.aborted) return err(request, { code: 'cancelled', message: 'command execution was aborted', details: {} })
return err(request, { code: 'internal', message: `command failed: ${String(error)}`, details: {} })
}
},
},
goals: {
// Mutations only — the read side is the 'goal' session projection.
// Every verb resolves the session's agent (agentFor: implicit cold

View File

@@ -1,44 +0,0 @@
/**
* commands domain zod schemas (names derived from map keys: commandListRequestSchema /
* commandListValueSchema / commandExecuteRequestSchema / commandExecuteValueSchema).
*/
import { z } from 'zod'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './sessions.schema.ts'
import type { CommandDescriptor } from './commands.ts'
/** CommandDescriptor row of command.list. */
export const commandDescriptorSchema = z.object({
name: z.string().min(1),
description: z.string(),
input: z.object({ hint: z.string() }).optional(),
}) satisfies z.ZodType<Wire<CommandDescriptor>>
/** command.list request payload. */
export const commandListRequestSchema = z.object({
sessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'command.list'>>>
/** command.list response value. */
export const commandListValueSchema = z.object({
commands: z.array(commandDescriptorSchema),
}) satisfies z.ZodType<Wire<ResponseValue<'command.list'>>>
/** command.execute request payload. */
export const commandExecuteRequestSchema = z.object({
sessionId: sessionIdSchema,
line: z.string(),
}) satisfies z.ZodType<Wire<RequestPayload<'command.execute'>>>
/** 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
* lifecycle events; commandId (present exactly when matched) correlates with them. */
export const commandExecuteValueSchema = z.object({
matched: z.boolean(),
commandId: commandIdSchema.optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'command.execute'>>>

View File

@@ -1,50 +0,0 @@
/**
* commands domain contract: the web catalog/dispatch face of the host command
* registry (`ctx.commands`). Both methods address an ordinary session's Agent
* via `sessionId`, resuming it when cold. Session-backed subagents reject with
* `agent-busy` and retain their dedicated continuation owner.
*/
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/**
* Handler-free command view served to clients. Wire mirror of the host
* registry descriptor (which stays host-side with its cordis dependencies);
* no source field — the host descriptor has none.
*/
export interface CommandDescriptor {
/** Lowercase command name without the leading slash. */
readonly name: string
/** Human-readable summary used in discovery UI. */
readonly description: string
/** Optional free-form input hint advertised to capable clients. */
readonly input?: { readonly hint: string }
}
/** Command-domain unary methods (the map keys command.* of RpcMethodMap). */
export interface CommandsApi {
/**
* Lists the addressed agent's effective command catalog (name-sorted,
* globals plus its scoped shadows). Session-backed subagents reject with
* `agent-busy`.
*/
list(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ commands: readonly CommandDescriptor[] }>>
/**
* Parses and executes one slash-command line against the addressed agent
* without sending it to the model — pure admission semantics. matched=false
* when syntax or name does not resolve (the client falls back to its
* default sink). The handler's outcome does NOT ride the response: the host
* executor durably logs the lifecycle (`command/run`/`command/done`), which
* broadcasts on the mux stream and renders as a persistent flow node.
* `commandId` is present exactly when matched — the minted lifecycle
* pairing id, letting the issuing client correlate this acknowledgment
* with that flow node. The signal rides beside the request, never on the
* wire: the fetch carrier's request signal cancels the running handler.
* Session-backed subagents reject with `agent-busy` before dispatch.
*/
execute(request: RpcRequest<{ sessionId: SessionId; line: string }>, signal: AbortSignal):
Promise<RpcResponse<{ matched: boolean; commandId?: CommandId }>>
}

View File

@@ -7,7 +7,6 @@
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'
@@ -25,7 +24,6 @@ export interface ApiProxy {
subagents: SubagentsApi
host: HostApi
workspace: WorkspaceApi
commands: CommandsApi
skills: SkillsApi
agentPresets: AgentPresetsApi
events: EventsApi
@@ -52,7 +50,6 @@ export type {
} 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'

View File

@@ -7,7 +7,6 @@
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'
@@ -50,8 +49,6 @@ export interface RpcMethodMap {
'workspace.delete': WorkspaceApi['delete']
'workspace.insertSessionBefore': WorkspaceApi['insertSessionBefore']
'workspace.archiveSession': WorkspaceApi['archiveSession']
'command.list': CommandsApi['list']
'command.execute': CommandsApi['execute']
'skill.list': SkillsApi['list']
'agentPreset.list': AgentPresetsApi['list']
'agentPreset.select': AgentPresetsApi['select']

View File

@@ -39,7 +39,6 @@ import {
workspaceListValueSchema,
workspaceRenameValueSchema,
} from '../api/workspace.schema.ts'
import { commandExecuteValueSchema, commandListValueSchema } from '../api/commands.schema.ts'
import { skillListValueSchema } from '../api/skills.schema.ts'
import {
agentPresetCopyValueSchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema,
@@ -120,10 +119,6 @@ export interface IApiClient {
insertSessionBefore(payload: RequestPayload<'workspace.insertSessionBefore'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.insertSessionBefore'>>>
archiveSession(payload: RequestPayload<'workspace.archiveSession'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.archiveSession'>>>
}
commands: {
list(payload: RequestPayload<'command.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'command.list'>>>
execute(payload: RequestPayload<'command.execute'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'command.execute'>>>
}
skills: {
list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'skill.list'>>>
}
@@ -200,8 +195,6 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
'workspace.delete': workspaceDeleteValueSchema,
'workspace.insertSessionBefore': workspaceInsertSessionBeforeValueSchema,
'workspace.archiveSession': workspaceArchiveSessionValueSchema,
'command.list': commandListValueSchema,
'command.execute': commandExecuteValueSchema,
'skill.list': skillListValueSchema,
'agentPreset.list': agentPresetListValueSchema,
'agentPreset.select': agentPresetSelectValueSchema,
@@ -456,15 +449,6 @@ export abstract class AbstractApiClient implements IApiClient {
archiveSession: (payload, signal) => this.callUnary('workspace.archiveSession', payload, signal),
}
readonly commands: IApiClient['commands'] = {
list: (payload, signal) => this.callUnary('command.list', payload, signal),
// Command handlers are user-driven operations and may legitimately exceed
// the transport health deadline. Caller/connection aborts remain.
execute: (payload, signal) => this.callUnary(
'command.execute', payload, signal, 'caller-signal-only',
),
}
readonly skills: IApiClient['skills'] = {
list: (payload, signal) => this.callUnary('skill.list', payload, signal),
}

View File

@@ -42,7 +42,6 @@ import {
workspaceListRequestSchema,
workspaceRenameRequestSchema,
} from '../api/workspace.schema.ts'
import { commandExecuteRequestSchema, commandListRequestSchema } from '../api/commands.schema.ts'
import { skillListRequestSchema } from '../api/skills.schema.ts'
import {
agentPresetCopyRequestSchema, agentPresetListRequestSchema, agentPresetOpenDocumentRequestSchema,
@@ -115,8 +114,6 @@ const UNARY_ROUTES: UnaryRoutes = {
'workspace.delete': { schema: workspaceDeleteRequestSchema, invoke: (api, r) => api.workspace.delete(r) },
'workspace.insertSessionBefore': { schema: workspaceInsertSessionBeforeRequestSchema, invoke: (api, r) => api.workspace.insertSessionBefore(r) },
'workspace.archiveSession': { schema: workspaceArchiveSessionRequestSchema, invoke: (api, r) => api.workspace.archiveSession(r) },
'command.list': { schema: commandListRequestSchema, invoke: (api, r) => api.commands.list(r) },
'command.execute': { schema: commandExecuteRequestSchema, invoke: (api, r, signal) => api.commands.execute(r, signal) },
'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) },
'agentPreset.list': { schema: agentPresetListRequestSchema, invoke: (api, r) => api.agentPresets.list(r) },
'agentPreset.select': { schema: agentPresetSelectRequestSchema, invoke: (api, r) => api.agentPresets.select(r) },

View File

@@ -76,7 +76,6 @@ export class ApiProxyService extends Service implements ApiProxy {
readonly subagents: ApiProxy['subagents']
readonly workspace: ApiProxy['workspace']
readonly host: ApiProxy['host']
readonly commands: ApiProxy['commands']
readonly goals: ApiProxy['goals']
readonly skills: ApiProxy['skills']
readonly agentPresets: ApiProxy['agentPresets']
@@ -102,7 +101,6 @@ export class ApiProxyService extends Service implements ApiProxy {
this.subagents = api.subagents
this.workspace = api.workspace
this.host = api.host
this.commands = api.commands
this.goals = api.goals
this.skills = api.skills
this.agentPresets = api.agentPresets