fix(client): gate the inspect catalog

This commit is contained in:
imccyu
2026-08-23 23:06:39 +08:00
parent d80419f4ea
commit a050b3d4f1
8 changed files with 232 additions and 220 deletions
+1
View File
@@ -115,6 +115,7 @@
"verify-cordis-api": "tsx scripts/gen-cordis-api.ts --check",
"gen-client-catalog": "tsx scripts/gen-client-catalog.ts",
"gen-cordis-inspect-catalog": "tsx scripts/gen-cordis-inspect-catalog.ts",
"verify-cordis-inspect-catalog": "tsx scripts/gen-cordis-inspect-catalog.ts --check",
"verify-client-catalog": "tsx scripts/gen-client-catalog.ts --check",
"verify-export-jsdoc": "tsx scripts/verify-export-jsdoc.ts",
"gen-tool-catalog": "tsx scripts/gen-tool-catalog.ts",
@@ -1,7 +1,7 @@
/**
* Generated by scripts/gen-cordis-api.ts — do not edit by hand; run
* `pnpm run gen-cordis-api` to regenerate (freshness-gated by
* `pnpm run verify-cordis-api` in doc-sync).
* Generated by scripts/gen-cordis-inspect-catalog.ts — do not edit by hand; run
* `pnpm run gen-cordis-inspect-catalog` to regenerate (freshness-gated by
* `pnpm run verify-cordis-inspect-catalog` in doc-sync).
*
* The machine-readable cordis API catalog `cordis_inspect` serves to the
* model: harness services (summary + structured public method contracts),
@@ -184,7 +184,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
returns: 'completion of the current or newly started refresh.',
},
{
signature: 'search( query: string, signal: AbortSignal, ): Promise<RpcResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>>',
signature: 'search( query: string, signal: AbortSignal, ): Promise<ClientResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>>',
description: 'Search the Host\'s visible message-content index. Results stay request-local; the list snapshot remains the metadata authority.',
parameters: [{ name: 'query', description: 'non-blank literal phrase.' }, { name: 'signal', description: 'cancellation for a superseded search.' }],
returns: 'bounded results, or a business/transport error.',
@@ -297,71 +297,83 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
],
},
{
key: 'workspaces',
summary: 'The workspaces-service face injected as `ctx.workspaces`.',
description: 'The workspaces-service face injected as `ctx.workspaces`.',
key: 'uiWorkspace',
summary: 'Workspace archive and directory operations consumed by Client UI domains.',
description: 'Workspace archive and directory operations consumed by Client UI domains.',
methods: [
{
signature: 'connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId>',
description: 'Connect a Workspace to its reusable or freshly created blank session.',
parameters: [{ name: 'workspaceId', description: 'target workspace.' }],
returns: 'the connected session id.',
description: 'Resolve the reusable or newly created blank Session for a Workspace.',
parameters: [{ name: 'workspaceId', description: 'target Workspace.' }],
returns: 'a Session already addressable through the Session Controller.',
},
{
signature: 'startSession(workspaceId?: WorkspaceId): void',
description: 'The New Session flow: connect the explicit, current-Session, or recent Workspace and open the resulting session; failures surface on the session list state.',
parameters: [{ name: 'workspaceId', description: 'explicit target; omitted inherits the current Session\'s Workspace before falling back to the recency projection.' }],
description: 'Start a New Session flow and navigate to its Session.',
parameters: [{ name: 'workspaceId', description: 'explicit target; absent inherits the current or most recent Workspace.' }],
},
{
signature: 'create(input: { path: string }): Promise<WorkspaceView>',
description: 'Register an existing path as a Workspace.',
parameters: [{ name: 'input', description: 'the Host create payload.' }],
returns: 'the created or idempotently resolved Workspace.',
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
description: 'Archive a Session and clear it when it is the current selection.',
parameters: [{ name: 'sessionId', description: 'Session to archive.' }],
},
{
signature: 'pickDirectory(): Promise<string | null>',
description: 'Open the Host\'s native directory picker.',
description: 'Open the Host-native directory picker.',
parameters: [],
returns: 'the selected path, or null when the user cancelled.',
returns: 'the selected directory, or null when cancelled.',
},
{
signature: 'listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing>',
description: 'List one directory level through the Host\'s `browse` capability.',
parameters: [{ name: 'path', description: 'absolute directory to list; absent lists the Host home directory.' }, { name: 'signal', description: 'aborts the wire request (and the Host\'s scan) when the caller supersedes it.' }],
returns: 'the level\'s listing with breadcrumb ancestry.',
description: 'List one Host directory level.',
parameters: [{ name: 'path', description: 'directory path; absent selects the Host home.' }, { name: 'signal', description: 'cancellation for a superseded scan.' }],
returns: 'directory entries and breadcrumb ancestry.',
},
{
signature: 'createDirectory(path: string, name: string): Promise<string>',
description: 'Create one child directory through the Host\'s `browse` capability.',
parameters: [{ name: 'path', description: 'absolute existing parent directory.' }, { name: 'name', description: 'single non-blank path segment.' }],
returns: 'the created directory\'s absolute path.',
description: 'Create a child directory.',
parameters: [{ name: 'path', description: 'existing parent directory.' }, { name: 'name', description: 'child directory name.' }],
returns: 'created absolute path.',
},
{
signature: 'openPath(path: string): Promise<void>',
description: 'Open a filesystem path with the Host operating system\'s default application.',
parameters: [{ name: 'path', description: 'absolute or host-resolvable path.' }],
description: 'Open a path with the Host operating system.',
parameters: [{ name: 'path', description: 'absolute or Host-resolvable path.' }],
},
],
},
{
key: 'workspaces',
summary: 'Workspace Controller\'s Client service face.',
description: 'Workspace Controller\'s Client service face.',
methods: [
{
signature: 'create(input: { path: string }): Promise<WorkspaceView>',
description: 'Register an existing path as a Workspace.',
parameters: [{ name: 'input', description: 'Host create payload.' }],
returns: 'the created or idempotently resolved Workspace.',
},
{
signature: 'rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>',
description: 'Rename a Workspace.',
parameters: [{ name: 'workspaceId', description: 'target workspace.' }, { name: 'title', description: 'the new display title.' }],
returns: 'the updated Workspace view.',
parameters: [{ name: 'workspaceId', description: 'target Workspace.' }, { name: 'title', description: 'new display title.' }],
returns: 'the renamed Workspace.',
},
{
signature: 'delete(workspaceId: WorkspaceId): Promise<void>',
description: 'Delete a Workspace (its sessions fall back to the unaccounted group).',
parameters: [{ name: 'workspaceId', description: 'target workspace.' }],
},
{
signature: 'insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise<WorkspaceView>',
description: 'Move an accounted session within/into a Workspace\'s ordered list.',
parameters: [{ name: 'workspaceId', description: 'target workspace.' }, { name: 'sessionId', description: 'accounted session to move.' }, { name: 'beforeSessionId', description: 'accounted anchor to insert before; omitted appends.' }],
returns: 'the updated Workspace view.',
description: 'Delete a Workspace registration without deleting Sessions or files.',
parameters: [{ name: 'workspaceId', description: 'target Workspace.' }],
},
{
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
description: 'Archive a session into the registry-global set (hidden from grouping surfaces; session log and accounting slot remain). Archiving the current session clears the selection into the New Session view state.',
parameters: [{ name: 'sessionId', description: 'session to archive.' }],
description: 'Archive a Session from Workspace grouping surfaces.',
parameters: [{ name: 'sessionId', description: 'Session to archive.' }],
},
{
signature: 'insertSessionBefore( workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId, ): Promise<WorkspaceView>',
description: 'Move a Session within one Workspace account.',
parameters: [{ name: 'workspaceId', description: 'owning Workspace.' }, { name: 'sessionId', description: 'Session to move.' }, { name: 'beforeSessionId', description: 'anchor Session; omitted appends.' }],
returns: 'the changed Workspace.',
},
],
},
@@ -373,8 +385,8 @@ export const EVENT_API: readonly EventApiEntry[] = [
name: 'connection/reset',
mode: 'emit',
signature: '\'connection/reset\'(): void',
summary: 'A connection generation was (re-)established.',
description: 'A connection generation was (re-)established. Wire-derived caches must treat their state as stale and repull (commands directory; the queue mirrors reset themselves through the session resync path).',
summary: 'A connection generation was established.',
description: 'A connection generation was established. Wire-derived caches must repull; long-lived streams own their own resume and baseline lifecycle.',
parameters: [],
},
{
@@ -389,9 +401,9 @@ export const EVENT_API: readonly EventApiEntry[] = [
name: 'slots/changed',
mode: 'emit',
signature: '\'slots/changed\'(key: string): void',
summary: 'A slot\'s definition or registration set changed.',
description: 'A slot\'s definition or registration set changed.',
parameters: [{ name: 'key', description: 'the mutated SlotMap key.' }],
summary: 'A slot declaration or registration set changed.',
description: 'A slot declaration or registration set changed.',
parameters: [{ name: 'key', description: 'mutated SlotMap key.' }],
},
{
name: 'theme/change',
@@ -411,27 +423,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'AgentContext',
declaration: 'export type AgentContext = Omit<Context, \'remote\'> & {\n readonly remote: TypertClientRemote & TypertRemoteScopeApi<\'agent\'>;\n};',
},
{
name: 'AssistantBlock',
declaration: 'export type AssistantBlock = {\n kind: \'text\';\n text: string;\n} | {\n kind: \'reasoning\';\n text: string;\n} | {\n kind: \'image\';\n attachment: ImageAttachmentRef;\n} | {\n kind: \'tool-call\';\n callId: string;\n name: string;\n argsRaw: string;\n} | {\n kind: \'other\';\n block: unknown;\n};',
},
{
name: 'AssistantMessageNode',
declaration: 'export interface AssistantMessageNode {\n kind: \'assistant\';\n seq: number;\n messageId?: MessageId;\n time: number;\n turn: number;\n step: number;\n blocks: readonly AssistantBlock[];\n usage?: unknown;\n provenance?: AssistantProvenanceView;\n requestConfig?: AssistantRequestConfig;\n timing?: AssistantTiming;\n interrupted?: true;\n}',
},
{
name: 'AssistantProvenanceView',
declaration: 'export interface AssistantProvenanceView {\n provider: string;\n model: string;\n}',
},
{
name: 'AssistantRequestConfig',
declaration: 'export interface AssistantRequestConfig {\n provider: string;\n model: string;\n purpose?: string;\n thinking?: string;\n reasoningEffort?: string;\n temperature?: number;\n maxTokens?: number;\n stop?: readonly string[];\n}',
},
{
name: 'AssistantTiming',
declaration: 'export interface AssistantTiming {\n stepStartTime: number | null;\n firstTokenTime: number | null;\n completedTime: number;\n}',
declaration: 'export type AgentContext = Omit<Context, \'remote\'> & {\n readonly remote: ClientRemote & TypertRemoteScopeApi<\'agent\'>;\n};',
},
{
name: 'BakedActions',
@@ -447,99 +439,55 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ChainRenderOpts',
declaration: 'export interface ChainRenderOpts {\n fallback?: ReactNode;\n overlay?: boolean;\n}',
},
{
name: 'ChatConversationViewNode',
declaration: 'export interface ChatConversationViewNode extends ConversationViewNode {\n readonly target: \'chat\';\n readonly anchorSeq: number;\n readonly location: ConversationLocation;\n readonly visibility: \'visible\' | \'hidden\';\n}',
},
{
name: 'ChatLocationNodeIndex',
declaration: 'export interface ChatLocationNodeIndex {\n getTurn(turn: number): readonly string[];\n getStep(turn: number, step: number): readonly string[];\n}',
},
{
name: 'ChatNodeStore',
declaration: 'export interface ChatNodeStore {\n get(key: string): ChatConversationViewNode | undefined;\n values(): readonly ChatConversationViewNode[];\n}',
},
{
name: 'ChatSnapshot',
declaration: 'export interface ChatSnapshot {\n readonly order: readonly string[];\n readonly nodes: ChatNodeStore;\n readonly locations: ChatLocationNodeIndex;\n readonly timeline: ConversationTimelineSnapshot;\n readonly legacy: LegacyConversationSlice;\n}',
declaration: 'export interface ChainRenderOpts {\n fallback?: ReactNode;\n fallbackOnly?: boolean;\n overlay?: boolean;\n}',
},
{
name: 'ChildrenDecl',
declaration: 'export type ChildrenDecl = {\n [P in keyof SlotMap & string]?: SlotSpec<SlotMap[P]>;\n};',
},
{
name: 'CommandNode',
declaration: 'export interface CommandNode {\n kind: \'command\';\n seq: number;\n time: number;\n commandId: CommandId;\n name: string | null;\n args: string | null;\n outcome: {\n kind: \'success\' | \'error\';\n text?: string;\n sourceEventSeq?: number;\n } | null;\n}',
name: 'ClientConnectionRpc',
declaration: 'export interface ClientConnectionRpc {\n call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<ConnectionRpcResult<unknown>>;\n readonly open?: (channel: string, endpoint: string, payload: unknown, signal: AbortSignal) => AsyncIterable<unknown>;\n}',
},
{
name: 'ClientRemote',
declaration: 'export interface ClientRemote extends TypertClientRemote {\n $stream<Item>(options: RemoteStreamOptions<Item>): RemoteStream<Item>;\n}',
},
{
name: 'CommonKeyOf',
declaration: 'export type CommonKeyOf = LocaleNamespaceMap extends {\n common: infer C;\n} ? C & string : never;',
},
{
name: 'CompactionSummaryNode',
declaration: 'export interface CompactionSummaryNode {\n kind: \'compaction\';\n seq: number;\n time: number;\n summary: string | null;\n summaryEventSeq: number | null;\n shadowedItemCount: number | null;\n shadowedTokenCount: number | null;\n}',
},
{
name: 'ComposedProps',
declaration: 'export type ComposedProps<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K>, S extends keyof SlotMap & string, H, I extends object, M = never, N = undefined> = PropsRuntime<K, EntryKey> & PropsRenderSlots<S> & PropsStore<H> & InjectFace<I> & MatchedShare<SlotMap[K], M> & PropsLocale<N>;',
},
{
name: 'ComposerPhase',
declaration: 'export type ComposerPhase = \'blank\' | \'engaging\' | \'active\';',
name: 'ConnectionConfig',
declaration: 'export interface ConnectionConfig {\n backoffBaseMs?: number;\n backoffFactor?: number;\n backoffMaxMs?: number;\n generationReadyTimeoutMs?: number;\n}',
},
{
name: 'ContextMessageNode',
declaration: 'export interface ContextMessageNode {\n kind: \'context\';\n seq: number;\n time: number;\n content: readonly ContentBlock[];\n source: unknown;\n provenance: ContextProvenanceView;\n form: KnownContextForm | null;\n}',
name: 'ConnectionGenerationSource',
declaration: 'export type ConnectionGenerationSource = (signal: AbortSignal, ready: () => void) => Promise<void>;',
},
{
name: 'ContextProvenanceView',
declaration: 'export interface ContextProvenanceView {\n role: ContextRole;\n label: string | null;\n}',
name: 'ConnectionHandle',
declaration: 'export interface ConnectionHandle {\n readonly api: IApiClient;\n readonly isLoopback: boolean;\n readonly hostDescription: HostDescriptionSource;\n readonly rpc: ClientConnectionRpc;\n registerGenerationSource(source: ConnectionGenerationSource): () => void;\n start(sinks: ConnectionSinks, config?: ConnectionConfig): {\n stop(): void;\n };\n}',
},
{
name: 'ContextRole',
declaration: 'export type ContextRole = \'inject\' | \'recall\';',
name: 'ConnectionRpcFailure',
declaration: 'export interface ConnectionRpcFailure {\n readonly code: string;\n readonly message: string;\n readonly details: object;\n}',
},
{
name: 'ConversationLocation',
declaration: 'export type ConversationLocation = {\n readonly kind: \'session\';\n} | {\n readonly kind: \'turn\';\n readonly turn: TurnLocation;\n} | {\n readonly kind: \'step\';\n readonly turn: TurnLocation;\n readonly step: StepLocation;\n} | {\n readonly kind: \'unresolved\';\n};',
name: 'ConnectionRpcResult',
declaration: 'export type ConnectionRpcResult<T> = {\n readonly ok: true;\n readonly value: T;\n} | {\n readonly ok: false;\n readonly error: ConnectionRpcFailure;\n};',
},
{
name: 'ConversationLocationDataStore',
declaration: 'export interface ConversationLocationDataStore<DataMap extends object> {\n get<Key extends keyof DataMap & string>(key: Key): Readonly<DataMap[Key]> | undefined;\n}',
name: 'ConnectionSinks',
declaration: 'export interface ConnectionSinks {\n onConnected?: (description: HostDescription) => void;\n onStateChange?: (state: ConnectionState) => void;\n}',
},
{
name: 'ConversationNode',
declaration: 'export type ConversationNode = UserMessageNode | AssistantMessageNode | SteeringMessageNode | ContextMessageNode | ModelRetryNode | TurnErrorNode | TurnMaxTokensNode | ToolResultNode | CommandNode | CompactionSummaryNode | UnknownSurfaceNode;',
},
{
name: 'ConversationSnapshot',
declaration: 'export interface ConversationSnapshot {\n sessionId: SessionId;\n views: ConversationViewSnapshotStore;\n chat: ChatSnapshot;\n nodes: readonly ConversationNode[];\n turnTimings: ReadonlyMap<number, {\n readonly startTime: number;\n readonly endTime?: number;\n }>;\n turnEnds: ReadonlyMap<number, number>;\n partial: PartialAssistant | null;\n runningCalls: readonly RunningToolCall[];\n pending: readonly PendingInteraction[];\n queue: readonly QueuedMessage[];\n running: boolean;\n subagent: {\n address: SubagentAddress;\n parentAvailable: boolean;\n } | null;\n composerPhase: ComposerPhase;\n removed: boolean;\n openState: OpenState;\n openError: RpcError | null;\n hasMore: boolean;\n loadingOlder: boolean;\n promptError: PromptError | null;\n blank: boolean;\n lastAgentError: string | null;\n}',
},
{
name: 'ConversationStepDataMap',
declaration: 'export interface ConversationStepDataMap {\n}',
},
{
name: 'ConversationTimelineSnapshot',
declaration: 'export interface ConversationTimelineSnapshot {\n readonly turnOrder: readonly number[];\n readonly turns: ReadonlyMap<number, TurnLocation>;\n}',
},
{
name: 'ConversationTurnDataMap',
declaration: 'export interface ConversationTurnDataMap {\n}',
},
{
name: 'ConversationViewNode',
declaration: 'export interface ConversationViewNode {\n readonly key: string;\n readonly kind: string;\n readonly id: string;\n readonly target: string;\n readonly data: unknown;\n}',
},
{
name: 'ConversationViewSnapshotMap',
declaration: 'export interface ConversationViewSnapshotMap {\n}',
},
{
name: 'ConversationViewSnapshotStore',
declaration: 'export interface ConversationViewSnapshotStore {\n get<Target extends Extract<keyof ConversationViewSnapshotMap, string>>(target: Target): ConversationViewSnapshotMap[Target] | undefined;\n}',
name: 'ConnectionState',
declaration: 'export type ConnectionState = \'connected\' | \'reconnecting\';',
},
{
name: 'EntryKeyOf',
@@ -557,9 +505,17 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'HooksSources',
declaration: 'export type HooksSources = Record<string, HostObservable<unknown>>;',
},
{
name: 'HostDescription',
declaration: 'export type HostDescription = import(\'@deepseek-ai/dsh-host-apiproxy/api\').ResponseValue<\'host.describe\'>;',
},
{
name: 'HostDescriptionSource',
declaration: 'export interface HostDescriptionSource {\n getSnapshot(): HostDescription | undefined;\n subscribe(listener: () => void): () => void;\n}',
},
{
name: 'HostObservable',
declaration: 'export interface HostObservable<T> {\n getSnapshot(): T;\n subscribe(fn: () => void): () => void;\n}',
declaration: 'export type HostObservable<T> = ObservableSnapshot<T>;',
},
{
name: 'InjectFace',
@@ -571,20 +527,12 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ISession',
declaration: 'export interface ISession {\n readonly sessionId: SessionId;\n readonly projections: ProjectionsFace;\n prompt(content: PromptContentPart[], mode: \'queue\' | \'steer\', signal?: AbortSignal): Promise<RpcResult<{\n accepted: true;\n }>>;\n readAttachment(attachmentId: AttachmentIdType): Promise<RpcResult<{\n attachment: ImageAttachmentRef;\n data: Uint8Array;\n }>>;\n updateQueue(itemId: MessageId, action: QueueAction): Promise<RpcResult<{\n accepted: true;\n }>>;\n cancel(): Promise<RpcResult<{\n accepted: true;\n }>>;\n rename(title: string): Promise<RpcResult<{\n title: string;\n seq: number;\n }>>;\n loadOlder(): Promise<void>;\n command(line: string): Promise<RemoteResult<{\n matched: boolean;\n }>>;\n}',
declaration: 'export interface ISession {\n readonly sessionId: SessionId;\n readonly projections: ProjectionsFace;\n prompt(content: PromptContentPart[], mode: \'queue\' | \'steer\', signal?: AbortSignal): Promise<ClientResult<{\n accepted: true;\n }>>;\n readAttachment(attachmentId: AttachmentIdType): Promise<ClientResult<{\n attachment: ImageAttachmentRef;\n data: Uint8Array;\n }>>;\n updateQueue(itemId: MessageId, action: QueueAction): Promise<ClientResult<{\n accepted: true;\n }>>;\n cancel(): Promise<ClientResult<{\n accepted: true;\n }>>;\n rename(title: string): Promise<ClientResult<{\n title: string;\n seq: number;\n }>>;\n loadOlder(): Promise<void>;\n command(line: string): Promise<RemoteResult<{\n matched: boolean;\n }>>;\n}',
},
{
name: 'KeyPropsOf',
declaration: 'export type KeyPropsOf<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K>> = SlotMap[K] extends {\n kind: \'keyed\';\n keyProps: infer P extends object;\n} ? EntryKey extends keyof P ? P[EntryKey] extends object ? P[EntryKey] : never : never : object;',
},
{
name: 'KnownContextForm',
declaration: 'export type KnownContextForm = typeof KNOWN_FORMS[number];',
},
{
name: 'LegacyConversationSlice',
declaration: 'export interface LegacyConversationSlice {\n readonly nodes: readonly ConversationNode[];\n readonly turnTimings: ReadonlyMap<number, {\n readonly startTime: number;\n readonly endTime?: number;\n }>;\n readonly turnEnds: ReadonlyMap<number, number>;\n readonly partial: PartialAssistant | null;\n readonly runningCalls: readonly RunningToolCall[];\n}',
},
{
name: 'LocaleDefinition',
declaration: 'export interface LocaleDefinition {\n id: LocaleId;\n label: string;\n}',
@@ -617,10 +565,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'MatchedShare',
declaration: 'export type MatchedShare<E extends SlotEntryDef, M> = E[\'kind\'] extends \'chain\' ? {\n matched: M;\n} : object;',
},
{
name: 'ModelRetryNode',
declaration: 'export type ModelRetryNode = LlmRetryEventData & {\n kind: \'model-retry\';\n seq: number;\n time: number;\n retryState: \'scheduled\' | \'started\' | \'cancelled\';\n};',
},
{
name: 'ObservableSnapshot',
declaration: 'export interface ObservableSnapshot<T> {\n getSnapshot(): T;\n subscribe(fn: () => void): () => void;\n}',
@@ -633,33 +577,17 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'OwnerOf',
declaration: 'export type OwnerOf<K extends keyof SlotMap & string> = SlotMap[K] extends {\n owner: infer O extends object;\n} ? O : object;',
},
{
name: 'PartialAssistant',
declaration: 'export interface PartialAssistant {\n turn: number;\n step: number;\n blocks: readonly AssistantBlock[];\n}',
},
{
name: 'PendingInteraction',
declaration: 'export type PendingInteraction = {\n [K in PendingKind]: PendingWait<K>;\n}[PendingKind];',
},
{
name: 'PendingKind',
declaration: 'export type PendingKind = keyof PendingPayloads;',
},
{
name: 'PendingPayloads',
declaration: 'export interface PendingPayloads {\n approval: Omit<Extract<MuxFrame, {\n type: \'approval/requested\';\n }>, \'type\' | \'sessionId\'>;\n question: Omit<Extract<MuxFrame, {\n type: \'question/requested\';\n }>, \'type\' | \'sessionId\'>;\n}',
},
{
name: 'PendingWait',
declaration: 'export class PendingWait<K extends PendingKind = PendingKind> {\n readonly kind: K;\n readonly key: string;\n readonly sessionId: SessionId;\n readonly payload: PendingPayloads[K];\n constructor(kind: K, rpcId: RpcId, sessionId: SessionId, payload: PendingPayloads[K], respond: (message: ClientResponse) => Promise<RpcReceipt>);\n respond(result: ClientResponse[\'result\']): Promise<RpcReceipt>;\n markSettled(): void;\n}',
},
{
name: 'ProjectionsFace',
declaration: 'export interface ProjectionsFace {\n faceOf(key: string): ObservableSnapshot<unknown>;\n}',
},
{
name: 'PromptContentPart',
declaration: 'export type PromptContentPart = {\n readonly type: \'text\';\n readonly text: string;\n} | {\n readonly type: \'image\';\n readonly mediaType: ImageMediaType;\n readonly data: string;\n readonly name?: string;\n};',
},
{
name: 'PromptError',
declaration: 'export interface PromptError {\n op: \'send\' | \'stop\';\n error: RpcError;\n}',
declaration: 'export interface PromptError {\n readonly op: \'send\' | \'stop\';\n readonly error: ClientFailure;\n}',
},
{
name: 'PropsHooks',
@@ -686,12 +614,20 @@ export const TYPE_API: readonly TypeApiEntry[] = [
declaration: 'export type PropsStore<H> = H extends StoreHandle<infer T, infer A> ? {\n useStore: SnapshotSelectorHook<T>;\n actions: BakedActions<T, A>;\n} : object;',
},
{
name: 'QueueAction',
declaration: 'export type QueueAction = Parameters<SessionFace[\'updateQueue\']>[1];',
name: 'RemoteStream',
declaration: 'export class RemoteStream<Item> implements AsyncIterable<RemoteStreamItem<Item>> {\n constructor(private readonly connection: Pick<ConnectionHandle, \'hostDescription\'>, private readonly options: RemoteStreamOptions<Item>);\n get signal(): AbortSignal;\n restart(): void;\n dispose(): Promise<void>;\n [Symbol.asyncIterator](): AsyncIterator<RemoteStreamItem<Item>>;\n}',
},
{
name: 'RunningToolCall',
declaration: 'export interface RunningToolCall {\n callId: string;\n name: string;\n argsRaw: string;\n turn: number;\n step: number;\n time: number;\n callView: ToolCallView | null;\n subCalls: readonly ToolCallBlock[];\n}',
name: 'RemoteStreamCarrierError',
declaration: 'export class RemoteStreamCarrierError extends Error {\n constructor(message: string, options?: ErrorOptions);\n}',
},
{
name: 'RemoteStreamItem',
declaration: 'export interface RemoteStreamItem<Item> {\n readonly generation: number;\n readonly value: Item;\n readonly signal: AbortSignal;\n accept(): void;\n}',
},
{
name: 'RemoteStreamOptions',
declaration: 'export interface RemoteStreamOptions<Item> {\n readonly name: string;\n readonly open: (signal: AbortSignal) => AsyncIterable<Item>;\n readonly ended: (accepted: boolean) => Error;\n readonly carrierFailed?: (error: RemoteStreamCarrierError) => void;\n}',
},
{
name: 'ScopeOf',
@@ -699,15 +635,31 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SessionAreaProps',
declaration: 'export interface SessionAreaProps {\n empty?: (() => ReactNode) | undefined;\n children: (sessionId: SessionIdOf) => ReactNode;\n}',
declaration: 'export interface SessionAreaProps {\n empty?: (() => ReactNode) | undefined;\n children: ReactNode;\n}',
},
{
name: 'SessionBinding',
declaration: 'export interface SessionBinding {\n readonly sessionId: SessionId;\n readonly session: SessionFace;\n readonly ctx: AgentContext;\n}',
declaration: 'export interface SessionBinding {\n readonly sessionId: SessionId;\n readonly session: SessionFace;\n readonly eventSource: SessionEventSource;\n readonly ctx: AgentContext;\n}',
},
{
name: 'SessionEventChange',
declaration: 'export type SessionEventChange = {\n readonly kind: \'replace\';\n readonly entries: readonly SessionEventEntry[];\n} | {\n readonly kind: \'prepend\';\n readonly entries: readonly SessionEventEntry[];\n} | {\n readonly kind: \'append\';\n readonly entries: readonly SessionEventEntry[];\n};',
},
{
name: 'SessionEventEntry',
declaration: 'export interface SessionEventEntry {\n readonly event: SessionWireEvent;\n readonly view?: SessionToolView;\n}',
},
{
name: 'SessionEventSource',
declaration: 'export type SessionEventSource = ObservableSnapshot<SessionEventWindow>;',
},
{
name: 'SessionEventWindow',
declaration: 'export interface SessionEventWindow {\n readonly entries: readonly SessionEventEntry[];\n readonly hasMore: boolean;\n readonly revision: number;\n readonly change: SessionEventChange;\n}',
},
{
name: 'SessionFace',
declaration: 'export type SessionFace = ISession & ObservableSnapshot<ConversationSnapshot>;',
declaration: 'export type SessionFace = ISession & ObservableSnapshot<SessionSnapshot>;',
},
{
name: 'SessionIdOf',
@@ -725,10 +677,26 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SessionSearchResultItem',
declaration: 'export interface SessionSearchResultItem {\n sessionId: SessionId;\n snippet: string;\n}',
},
{
name: 'SessionSnapshot',
declaration: 'export interface SessionSnapshot {\n readonly sessionId: SessionId;\n readonly queue: readonly QueuedMessage[];\n readonly running: boolean;\n readonly subagent: {\n readonly address: SubagentAddress;\n readonly parentAvailable: boolean;\n } | null;\n readonly removed: boolean;\n readonly openState: OpenState;\n readonly openError: ClientFailure | null;\n readonly hasMore: boolean;\n readonly loadingOlder: boolean;\n readonly promptError: PromptError | null;\n readonly blank: boolean;\n readonly lastAgentError: string | null;\n readonly promptAttempted: boolean;\n readonly awaitingFirstTurn: boolean;\n}',
},
{
name: 'SessionStandardProps',
declaration: 'export interface SessionStandardProps {\n}',
},
{
name: 'SessionToolCallView',
declaration: 'export type SessionToolCallView = (Omit<GenericCallView, \'rawInput\'> & {\n readonly rawInput?: JsonValue;\n}) | TerminalCallView | DiffCallView;',
},
{
name: 'SessionToolView',
declaration: 'export type SessionToolView = {\n readonly for: \'call\';\n readonly view: SessionToolCallView;\n} | {\n readonly for: \'result\';\n readonly view: ToolResultView;\n};',
},
{
name: 'SessionWireEvent',
declaration: 'export interface SessionWireEvent {\n readonly type: string;\n readonly seq: number;\n readonly time: number;\n readonly data: JsonValue;\n readonly ignorable?: true;\n readonly sourceEventSeqs?: number[];\n readonly surfaceOp?: SurfaceOp;\n}',
},
{
name: 'SlotComponent',
declaration: 'export type SlotComponent<P> = (props: P) => ReactNode;',
@@ -773,14 +741,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SnapshotSelectorHook',
declaration: 'export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S;',
},
{
name: 'SteeringMessageNode',
declaration: 'export interface SteeringMessageNode {\n kind: \'steering\';\n messageId: MessageId;\n seq: number;\n time: number;\n content: readonly ContentBlock[];\n source: unknown;\n}',
},
{
name: 'StepLocation',
declaration: 'export interface StepLocation {\n readonly turn: number;\n readonly step: number;\n readonly start: SessionEvent<\'step/start\'> | undefined;\n readonly end: SessionEvent<\'step/end\'> | undefined;\n readonly status: \'open\' | \'closed\' | \'unknown\';\n readonly data: ConversationLocationDataStore<ConversationStepDataMap>;\n}',
},
{
name: 'StoreDecl',
declaration: 'export type StoreDecl = StoreHandle<any, any> | StoreFactory;',
@@ -829,14 +789,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'ThemeTokens',
declaration: 'export type ThemeTokens = Record<string, string>;',
},
{
name: 'ToolCallBlock',
declaration: 'export type ToolCallBlock = RunningToolCall | ToolResultNode;',
},
{
name: 'ToolResultNode',
declaration: 'export interface ToolResultNode {\n kind: \'tool-result\';\n seq: number;\n time: number;\n callId: string;\n call: {\n name: string;\n argsRaw: string;\n } | null;\n callTime: number | null;\n content: readonly ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: unknown;\n callView: ToolCallView | null;\n resultView: ToolResultView | null;\n subCalls: readonly ToolCallBlock[];\n}',
},
{
name: 'Translate',
declaration: 'export type Translate<K extends string = string> = (key: K, params?: Record<string, unknown>) => string;',
@@ -846,24 +798,8 @@ export const TYPE_API: readonly TypeApiEntry[] = [
declaration: 'export type TranslateNS<N extends keyof LocaleNamespaceMap & string> = Translate<LocaleKeysOf<N>>;',
},
{
name: 'TurnErrorNode',
declaration: 'export interface TurnErrorNode {\n kind: \'turn-error\';\n seq: number;\n time: number;\n turn: number;\n step: number;\n message: string;\n code?: string;\n}',
},
{
name: 'TurnLocation',
declaration: 'export interface TurnLocation {\n readonly turn: number;\n readonly start: SessionEvent<\'turn/start\'> | undefined;\n readonly end: SessionEvent<\'turn/end\'> | undefined;\n readonly status: \'open\' | \'closed\' | \'unknown\';\n readonly steps: readonly StepLocation[];\n readonly data: ConversationLocationDataStore<ConversationTurnDataMap>;\n}',
},
{
name: 'TurnMaxTokensNode',
declaration: 'export interface TurnMaxTokensNode {\n kind: \'turn-max-tokens\';\n seq: number;\n time: number;\n turn: number;\n step: number;\n}',
},
{
name: 'UnknownSurfaceNode',
declaration: 'export interface UnknownSurfaceNode {\n kind: \'unknown\';\n seq: number;\n time: number;\n type: string;\n data: unknown;\n}',
},
{
name: 'UserMessageNode',
declaration: 'export interface UserMessageNode {\n kind: \'user\';\n seq: number;\n time: number;\n content: readonly ContentBlock[];\n source: unknown;\n}',
name: 'WorkspaceView',
declaration: 'export interface WorkspaceView {\n readonly workspaceId: WorkspaceId;\n readonly path: string;\n readonly title: string;\n readonly sessionIds: readonly SessionId[];\n readonly createdAt: string;\n readonly updatedAt: string;\n}',
},
]
@@ -888,7 +824,7 @@ function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {
const next: string[] = []
for (const entry of TYPE_API) {
if (included.has(entry.name)) continue
const pattern = new RegExp(`\b${entry.name}\b`)
const pattern = new RegExp(`\\b${entry.name}\\b`)
if (!frontier.some(text => pattern.test(text))) continue
included.add(entry.name)
next.push(entry.declaration)
@@ -147,7 +147,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.approval.detail\', () => ctx.slots.register(\n { name: \'conversation.approval.detail\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-approval/src/client/contract/slots.ts:25',
source: 'packages/client/ui-approval/src/client/contract/slots.ts:37',
},
{
key: 'conversation.chat.assistant-actions',
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest'
import { EVENT_API, queryServiceApi, SERVICE_API } from '../src/client/api-catalog.ts'
describe('Client Cordis inspect catalog', () => {
it('publishes the split Workspace Controller and UI navigation services', () => {
expect(SERVICE_API.find(service => service.key === 'workspaces')?.methods.map(method => method.signature))
.toEqual([
'create(input: { path: string }): Promise<WorkspaceView>',
'rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>',
'delete(workspaceId: WorkspaceId): Promise<void>',
'archiveSession(sessionId: SessionId): Promise<void>',
'insertSessionBefore( workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId, ): Promise<WorkspaceView>',
])
expect(SERVICE_API.find(service => service.key === 'uiWorkspace')?.methods.map(method => method.signature))
.toEqual([
'connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId>',
'startSession(workspaceId?: WorkspaceId): void',
'archiveSession(sessionId: SessionId): Promise<void>',
'pickDirectory(): Promise<string | null>',
'listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing>',
'createDirectory(path: string, name: string): Promise<string>',
'openPath(path: string): Promise<void>',
])
})
it('contains one entry per visible Client event', () => {
const names = EVENT_API.map(event => event.name)
expect(new Set(names).size).toBe(names.length)
})
it('includes the current referenced type closure for the Sessions service', () => {
const result = queryServiceApi('sessions') as {
referencedTypes: readonly { name: string }[]
}
expect(result.referencedTypes.length).toBeGreaterThan(0)
expect(result.referencedTypes.map(type => type.name)).not.toEqual(expect.arrayContaining([
'ConversationSnapshot',
'PendingInteraction',
'PendingPayloads',
'PendingWait',
]))
})
})
@@ -5660,7 +5660,7 @@ function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {
const next: string[] = []
for (const entry of TYPE_API) {
if (included.has(entry.name)) continue
const pattern = new RegExp(`\b${entry.name}\b`)
const pattern = new RegExp(`\\b${entry.name}\\b`)
if (!frontier.some(text => pattern.test(text))) continue
included.add(entry.name)
next.push(entry.declaration)
@@ -826,7 +826,7 @@ function renderRuntimeApi(
' const next: string[] = []',
' for (const entry of TYPE_API) {',
' if (included.has(entry.name)) continue',
' const pattern = new RegExp(`\\b${entry.name}\\b`)',
' const pattern = new RegExp(`\\\\b${entry.name}\\\\b`)',
' if (!frontier.some(text => pattern.test(text))) continue',
' included.add(entry.name)',
' next.push(entry.declaration)',
+42 -11
View File
@@ -1,6 +1,6 @@
/** Generate model-visible Host/Client Service and Event inspect catalogs. */
import { mkdirSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { projectCordisCatalog } from '@deepseek-ai/dsh-typert-generator'
import type { CordisCatalogModel, ServiceMethodEntry } from '@deepseek-ai/dsh-typert-generator'
@@ -15,10 +15,11 @@ const CLIENT_SERVICES: Readonly<Record<string, readonly string[]>> = {
sessions: ['open', 'openSubagent', 'setSubagentCatalogOpen', 'refreshSubagents', 'search', 'fork', 'scope', 'binding'],
slots: ['register', 'inject'],
theme: ['getTheme', 'setTheme', 'register', 'overrideTokens'],
workspaces: [
'connectWorkspace', 'startSession', 'create', 'pickDirectory', 'listDirectory', 'createDirectory',
'openPath', 'rename', 'delete', 'insertSessionBefore', 'archiveSession',
uiWorkspace: [
'connectWorkspace', 'startSession', 'archiveSession', 'pickDirectory', 'listDirectory',
'createDirectory', 'openPath',
],
workspaces: ['create', 'rename', 'delete', 'insertSessionBefore', 'archiveSession'],
}
const CLIENT_EVENTS = new Set([
@@ -33,14 +34,34 @@ function methodName(method: ServiceMethodEntry): string | undefined {
}
function clientModel(model: CordisCatalogModel): CordisCatalogModel {
const services = Object.entries(CLIENT_SERVICES).map(([key, allowed]) => {
const matches = model.services.filter(service => service.key === key)
if (matches.length !== 1) {
throw new Error(`gen-cordis-inspect-catalog: expected one Client Service ${JSON.stringify(key)}, found ${matches.length}`)
}
const service = matches[0] as (typeof matches)[number]
const names = new Set(allowed)
const methods = service.methods.filter(method => names.has(methodName(method) ?? ''))
const found = new Set(methods.map(method => methodName(method)))
const missing = allowed.filter(name => !found.has(name))
if (missing.length > 0) {
throw new Error(
`gen-cordis-inspect-catalog: Client Service ${JSON.stringify(key)}`
+ ` is missing allowlisted method(s): ${missing.join(', ')}`,
)
}
return { ...service, methods }
})
const events = [...CLIENT_EVENTS].map((name) => {
const matches = model.events.filter(event => event.name === name)
if (matches.length !== 1) {
throw new Error(`gen-cordis-inspect-catalog: expected one Client Event ${JSON.stringify(name)}, found ${matches.length}`)
}
return matches[0] as (typeof matches)[number]
})
return {
services: model.services.flatMap((service) => {
const allowed = CLIENT_SERVICES[service.key]
if (allowed === undefined) return []
const names = new Set(allowed)
return [{ ...service, methods: service.methods.filter(method => names.has(methodName(method) ?? '')) }]
}),
events: model.events.filter(event => CLIENT_EVENTS.has(event.name)),
services,
events,
}
}
@@ -48,7 +69,17 @@ function main(): void {
const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY, 'client')
const destination = resolve(root, CLIENT_OUT)
const source = projector.renderRuntimeApi(clientModel(model))
.replace('Generated by scripts/gen-cordis-api.ts', 'Generated by scripts/gen-cordis-inspect-catalog.ts')
.replaceAll('pnpm run gen-cordis-api', 'pnpm run gen-cordis-inspect-catalog')
.replaceAll('pnpm run verify-cordis-api', 'pnpm run verify-cordis-inspect-catalog')
.replaceAll('@deepseek-ai/dsh-tool-cordis/api-catalog', '@deepseek-ai/dsh-cordis-client-runner/client/api-catalog')
if (process.argv.includes('--check')) {
if (!existsSync(destination) || readFileSync(destination, 'utf8') !== source) {
throw new Error(`gen-cordis-inspect-catalog: ${CLIENT_OUT} is stale; run pnpm run gen-cordis-inspect-catalog`)
}
console.log(`gen-cordis-inspect-catalog: ${CLIENT_OUT} is up to date`)
return
}
mkdirSync(dirname(destination), { recursive: true })
writeFileSync(destination, source)
console.log(`gen-cordis-inspect-catalog: wrote ${CLIENT_OUT}`)
+1
View File
@@ -665,6 +665,7 @@ function docSyncLeafGates(options: {
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
pnpmScript('cordis-inspect-catalog', 'verify-cordis-inspect-catalog', { label: 'Cordis inspect catalog' }),
pnpmScript('mermaid', 'verify-mermaid'),
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),