dsh-remote-bridge 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -105,1019 +105,1133 @@ declare class WorkspaceDatabase {
105
105
  private migrate;
106
106
  }
107
107
  //#endregion
108
- //#region src/host/chat-adapter.d.ts
109
- interface RpcResponse<T> {
110
- result: {
111
- ok: true;
112
- value: T;
113
- } | {
114
- ok: false;
115
- error: {
116
- code: string;
117
- message: string;
118
- details?: unknown;
119
- };
120
- };
108
+ //#region src/host/settings-adapter.d.ts
109
+ interface SettingsNamespaceView {
110
+ ns: string;
111
+ schema?: unknown;
112
+ value: unknown;
113
+ base?: unknown;
114
+ user?: unknown;
115
+ revision: number;
121
116
  }
122
- /**
123
- * 一条 prompt 的内容片段 —— 与 DSH 0.2.x 的 `PromptContentPart` 一一对应。
124
- *
125
- * 三种形态的代价差别很大,值得写清楚:
126
- * - `text` 直接发送;
127
- * - `image` 字节**内联**在 payload 里(base64),一次调用完成,但请求体膨胀约 33%,
128
- * 所以客户端必须先限制原图大小,别等 413;
129
- * - `file` 的 `receiptId` 必须来自**先前的**上传调用,并在同一次 prompt 里绑定,
130
- * 否则凭证会被回收。
131
- */
132
- type PromptContentPart = {
133
- type: 'text';
134
- text: string;
135
- } | {
136
- type: 'image';
137
- mediaType: string;
138
- data: string;
139
- name?: string;
140
- } | {
141
- type: 'file';
142
- receiptId: string;
143
- };
144
- interface DshApiProxy {
145
- respond(message: {
146
- type: 'client-response';
147
- rpcId: string;
148
- result: {
149
- ok: true;
150
- value: unknown;
151
- };
152
- }): Promise<{
153
- accepted: true;
154
- } | {
155
- accepted: false;
156
- reason: 'not-pending' | 'bad-response';
157
- }>;
158
- sessions: {
159
- list(request: {
160
- rpcId: string;
161
- payload: {
162
- cursor?: string;
163
- };
164
- }): Promise<RpcResponse<{
165
- items: DshSessionSummary[];
166
- }>>;
167
- create(request: {
168
- rpcId: string;
169
- payload: {
170
- cwd?: string;
171
- workspaceId?: string;
172
- sessionId?: string;
173
- agentPreset?: string;
174
- };
175
- }): Promise<RpcResponse<{
176
- sessionId: string;
177
- agentPreset?: string;
178
- }>>;
179
- history(request: {
180
- rpcId: string;
181
- payload: {
182
- sessionId: string;
183
- beforeSeq?: number;
184
- maxMessages?: number;
185
- };
186
- }): Promise<RpcResponse<{
187
- events: unknown[];
188
- hasMore: boolean;
189
- projections?: unknown;
190
- }>>;
191
- prompt(request: {
192
- rpcId: string;
193
- payload: {
194
- sessionId: string;
195
- mode: 'queue' | 'steer';
196
- content: PromptContentPart[];
197
- clientTimeZone?: string;
198
- };
199
- }): Promise<RpcResponse<{
200
- accepted: true;
201
- command?: unknown;
202
- }>>;
203
- /**
204
- * 上传一个文件,换取 prompt 可引用的凭证(`receiptId`)。
205
- * `data` 是文件字节的**标准 base64**。
206
- */
207
- uploadAttachment(request: {
208
- rpcId: string;
209
- payload: {
210
- sessionId: string;
211
- data: string;
212
- name?: string;
213
- };
214
- }): Promise<RpcResponse<{
215
- receiptId: string;
216
- name: string | null;
217
- }>>;
218
- /**
219
- * 按 id 取回附件字节。
220
- *
221
- * `attachmentId` 形如 `sha256:<hex>`(**含冒号**,客户端必须 URL 编码)。
222
- * 返回 `Uint8Array` 而不是 base64:图片动辄上百 KB,多一次编解码纯属浪费。
223
- */
224
- readAttachment(request: {
225
- rpcId: string;
226
- payload: {
227
- attachmentId: string;
228
- };
229
- }): Promise<RpcResponse<{
230
- bytes: Uint8Array;
231
- mediaType: string;
232
- }>>;
233
- cancel(request: {
117
+ interface ConfigurableProvider {
118
+ provider: string;
119
+ displayName: string;
120
+ settingsNs: string;
121
+ settingsPath: string[];
122
+ active: boolean;
123
+ declared?: boolean;
124
+ }
125
+ interface SettingsApiProxy extends DshApiProxy {
126
+ settings: {
127
+ describe(request: {
234
128
  rpcId: string;
235
- payload: {
236
- sessionId: string;
237
- };
129
+ payload: Record<string, never>;
238
130
  }): Promise<RpcResponse<{
239
- accepted: true;
131
+ writable: boolean;
132
+ hasDocument: boolean;
133
+ namespaces: SettingsNamespaceView[];
240
134
  }>>;
241
- rename(request: {
135
+ mutate(request: {
242
136
  rpcId: string;
243
137
  payload: {
244
- sessionId: string;
245
- title: string;
138
+ ns: string;
139
+ ops: Array<{
140
+ op: 'set';
141
+ path: string[];
142
+ value: unknown;
143
+ } | {
144
+ op: 'unset';
145
+ path: string[];
146
+ }>;
147
+ expectedRevision?: number;
246
148
  };
247
- }): Promise<RpcResponse<{
248
- title: string;
249
- seq: number;
250
- }>>;
251
- fork(request: {
149
+ }): Promise<RpcResponse<SettingsNamespaceView>>;
150
+ };
151
+ credentials: {
152
+ describe(request: {
252
153
  rpcId: string;
253
154
  payload: {
254
- sessionId: string;
255
- atSeq?: number;
155
+ refs: string[];
256
156
  };
257
157
  }): Promise<RpcResponse<{
258
- sessionId: string;
158
+ credentials: Record<string, {
159
+ configured: boolean;
160
+ source?: string;
161
+ writable: boolean;
162
+ }>;
259
163
  }>>;
260
- models(request: {
164
+ set(request: {
261
165
  rpcId: string;
262
166
  payload: {
263
- sessionId: string;
167
+ ref: string;
168
+ value: string;
264
169
  };
265
- }): Promise<RpcResponse<SessionModelsView>>;
266
- selectModel(request: {
170
+ }): Promise<RpcResponse<Record<string, never>>>;
171
+ unset(request: {
267
172
  rpcId: string;
268
173
  payload: {
269
- sessionId: string;
270
- provider: string;
271
- model: string;
272
- reasoningEffort?: string;
174
+ ref: string;
273
175
  };
274
- }): Promise<RpcResponse<{
275
- selected: ModelSelectionView;
276
- }>>;
176
+ }): Promise<RpcResponse<Record<string, never>>>;
277
177
  };
278
- workspace: {
279
- list(request: {
178
+ llm: {
179
+ providers(request: {
280
180
  rpcId: string;
281
181
  payload: Record<string, never>;
282
182
  }): Promise<RpcResponse<{
283
- items: DshWorkspaceSummary[];
284
- archivedSessionIds: string[];
285
- }>>;
286
- create(request: {
287
- rpcId: string;
288
- payload: {
289
- path: string;
290
- };
291
- }): Promise<RpcResponse<{
292
- workspace: DshWorkspaceSummary;
293
- created: boolean;
294
- }>>;
295
- rename(request: {
296
- rpcId: string;
297
- payload: {
298
- workspaceId: string;
299
- title: string;
300
- };
301
- }): Promise<RpcResponse<{
302
- workspace: DshWorkspaceSummary;
183
+ providers: ConfigurableProvider[];
303
184
  }>>;
304
- delete(request: {
185
+ models(request: {
305
186
  rpcId: string;
306
- payload: {
307
- workspaceId: string;
308
- };
187
+ payload: Record<string, never>;
309
188
  }): Promise<RpcResponse<{
310
- deleted: true;
189
+ groups: ModelProviderGroupView[];
190
+ failures: {
191
+ provider: string;
192
+ message: string;
193
+ }[];
311
194
  }>>;
312
- archiveSession(request: {
195
+ discoverModels(request: {
313
196
  rpcId: string;
314
197
  payload: {
315
- sessionId: string;
198
+ settingsNs: string;
199
+ provider?: string;
200
+ baseURL?: string;
201
+ api?: string;
202
+ apiKey?: string;
316
203
  };
317
204
  }): Promise<RpcResponse<{
318
- archivedSessionIds: string[];
205
+ models: ProviderModelView[];
319
206
  }>>;
320
207
  };
321
- agentPresets: {
322
- list(request: {
323
- rpcId: string;
324
- payload: Record<string, never>;
325
- }): Promise<RpcResponse<{
326
- presets: DshAgentPresetSummary[];
327
- authorable: boolean;
328
- hasDocument: boolean;
329
- }>>;
330
- select(request: {
331
- rpcId: string;
332
- payload: {
333
- sessionId: string;
334
- agentPreset: string;
335
- };
336
- }): Promise<RpcResponse<{
337
- agentPreset: string;
338
- }>>;
339
- };
340
- /**
341
- * 用户问题(内核 `ctx.userQuestions`,即 ask_user_question 工具)。
342
- *
343
- * 为什么单开一个命名空间而不是塞进 `sessions`:它由**另一个内核服务**提供
344
- * (`@deepseek-ai/dsh-user-questions`),在一条没装它的 profile 上是**可选能力** ——
345
- * 分开之后,「这个能力不在」和「这个会话有问题」在类型上就分得开。
346
- */
347
- userQuestions: {
348
- /**
349
- * 列出还能回答的问题(内核 `continued(agent)`)。
350
- * 没有 live agent、或没有该服务时返回空列表 —— 冷会话是常态,不该报错。
351
- */
352
- list(request: {
353
- rpcId: string;
354
- payload: {
355
- sessionId: string;
356
- };
357
- }): Promise<RpcResponse<{
358
- items: UserQuestionView[];
359
- }>>;
360
- /** 提交答案;失败原因见 [UserQuestionAnswerOutcome]。 */
361
- answer(request: {
362
- rpcId: string;
363
- payload: {
364
- sessionId: string;
365
- callId: string;
366
- answer: UserQuestionAnswer;
367
- };
368
- }): Promise<RpcResponse<UserQuestionAnswerOutcome>>;
369
- };
370
- events: {
371
- mux(request: {
372
- rpcId: string;
373
- payload: Record<string, never>;
374
- }, signal: AbortSignal): AsyncIterable<{
375
- rpcId: string;
376
- payload: Record<string, unknown>;
377
- }>;
378
- host(request: {
379
- rpcId: string;
380
- payload: Record<string, never>;
381
- }, signal: AbortSignal): AsyncIterable<{
382
- rpcId: string;
383
- payload: Record<string, unknown>;
384
- }>;
385
- };
386
- }
387
- interface ModelSelectionView {
388
- provider: string;
389
- model: string;
390
- reasoningEffort?: string;
391
208
  }
392
- interface ModelReasoningView {
393
- efforts: {
394
- id: string;
395
- name: string;
396
- description?: string;
397
- }[];
398
- defaultEffort?: string;
399
- }
400
- interface ModelProviderGroupView {
209
+ interface ProviderModelView {
401
210
  id: string;
402
- name: string;
403
- models: {
404
- id: string;
405
- name: string;
406
- description?: string;
407
- contextWindow?: number;
408
- maxTokens?: number;
409
- reasoning?: ModelReasoningView;
410
- }[];
411
- }
412
- interface SessionModelsView {
413
- current: ModelSelectionView;
414
- routable: boolean;
415
- groups: ModelProviderGroupView[];
416
- failures: {
417
- provider: string;
418
- message: string;
419
- }[];
420
- }
421
- interface CommandDescriptorView {
422
- name: string;
423
- description: string;
424
- input?: {
425
- hint: string;
426
- };
427
- }
428
- interface CommandExecutionView {
429
- commandId: string;
430
- result: {
431
- kind: 'success' | 'error';
432
- text?: string;
433
- sourceEventSeq?: number;
434
- };
435
- }
436
- interface DshCommandRuntime {
437
- list(agent: unknown): readonly CommandDescriptorView[];
438
- execute(agent: unknown, line: string, signal: AbortSignal): Promise<CommandExecutionView | undefined>;
439
- }
440
- interface DshAgentRegistry {
441
- get(sessionId: string): unknown | undefined;
211
+ name?: string;
212
+ contextWindow?: number;
213
+ maxTokens?: number;
442
214
  }
443
- interface DshCommandServices {
444
- commands: DshCommandRuntime;
445
- agents: DshAgentRegistry;
215
+ interface ProviderConfigView {
216
+ baseURL?: string;
217
+ api?: string;
218
+ displayName?: string;
219
+ thinking?: string;
220
+ reasoningEffort?: string;
221
+ models: ProviderModelView[];
222
+ modelsInherited?: boolean;
446
223
  }
447
- interface DshSessionSummary {
448
- sessionId: string;
449
- updatedAt: number;
450
- running: boolean;
451
- blank: boolean;
452
- cwd?: string;
453
- agentPreset?: string;
454
- parentSessionId?: string;
455
- origin?: 'subagent';
456
- projections?: {
457
- asOfSeq: number;
458
- values: Record<string, unknown>;
224
+ interface ProviderView {
225
+ id: string;
226
+ displayName: string;
227
+ active: boolean;
228
+ declared?: boolean;
229
+ configurable: boolean;
230
+ configured: boolean;
231
+ removable: boolean;
232
+ credential: {
233
+ ref: string;
234
+ configured: boolean;
235
+ source?: string;
236
+ writable: boolean;
459
237
  };
238
+ config: ProviderConfigView;
460
239
  }
461
- interface DshWorkspaceSummary {
462
- workspaceId: string;
463
- path: string;
464
- title: string;
465
- sessionIds: string[];
466
- createdAt: string;
467
- updatedAt: string;
240
+ interface ProviderPatch {
241
+ displayName?: string | null;
242
+ baseURL?: string | null;
243
+ api?: string | null;
244
+ apiKey?: string | null;
245
+ thinking?: string | null;
246
+ reasoningEffort?: string | null;
247
+ models?: ProviderModelView[];
248
+ expectedRevision?: number;
468
249
  }
469
- interface DshAgentPresetSummary {
250
+ interface CustomProviderCreate {
470
251
  id: string;
471
- trust: 'system' | 'user';
472
- isDefault: boolean;
473
- name?: string;
474
- description?: string;
475
- broken?: string;
252
+ displayName?: string;
253
+ baseURL: string;
254
+ api: string;
255
+ apiKey?: string;
256
+ models: ProviderModelView[];
257
+ expectedRevision?: number;
476
258
  }
477
- interface ChatSessionView {
478
- id: string;
479
- rootId: string;
480
- cwd: string;
481
- updatedAt: number;
482
- running: boolean;
483
- blank: boolean;
484
- title?: string;
485
- workspaceId?: string;
486
- workspaceTitle?: string;
487
- agentPreset?: string;
488
- parentSessionId?: string;
489
- origin?: 'subagent';
490
- pendingInteraction?: 'approval';
259
+ interface CustomProviderCapability {
260
+ available: boolean;
261
+ protocols: string[];
262
+ revision?: number;
491
263
  }
492
- interface PendingApprovalView {
493
- id: string;
494
- sessionId: string;
495
- toolName: string;
496
- reason?: string;
497
- detail?: string;
498
- risk: 'standard' | 'full-access';
499
- requestedAt: number;
264
+ declare class DshSettingsAdapter {
265
+ private readonly api;
266
+ constructor(api: SettingsApiProxy);
267
+ listProviders(): Promise<{
268
+ writable: boolean;
269
+ revisionByNamespace: Record<string, number>;
270
+ customProvider: CustomProviderCapability;
271
+ providers: ProviderView[];
272
+ }>;
273
+ catalog(): Promise<{
274
+ groups: ModelProviderGroupView[];
275
+ failures: {
276
+ provider: string;
277
+ message: string;
278
+ }[];
279
+ }>;
280
+ updateProvider(providerId: string, patch: ProviderPatch): Promise<void>;
281
+ createProvider(input: CustomProviderCreate): Promise<void>;
282
+ removeProvider(providerId: string, expectedRevision?: number): Promise<void>;
283
+ discover(providerId: string, draft: {
284
+ baseURL?: string;
285
+ api?: string;
286
+ apiKey?: string;
287
+ }): Promise<ProviderModelView[]>;
288
+ private providerContext;
500
289
  }
290
+ //#endregion
291
+ //#region src/host/dsh-0.2-bridge.d.ts
501
292
  /**
502
- * 一条「还能回答」的用户问题 —— 内核 `userQuestions.continued()` 的一行。
293
+ * Prompt content a 0.2.x Host accepts — mirrors its `PromptContentPart` union.
503
294
  *
504
- * 为什么类型是宽松的、而不是照抄内核的 `AskUserQuestionItem`:内核往 item 或
505
- * question 上加过字段(`intent` 就是一例),手机端该自己决定要不要用,
506
- * 不该等插件跟着发版。所以这里只钉住三个**契约字段**,其余原样透传。
295
+ * 权威定义在 `chat-adapter.ts`(协议层),这里只是复用它,避免两处各写一遍
296
+ * 然后悄悄漂移。
507
297
  */
508
- interface UserQuestionView {
509
- callId: string;
510
- /** 内核 `UserQuestionState`:'open' 或 'continued';本端点只返回 continued。 */
511
- state: string;
512
- /** 问题项**原样**透传(id / question / detail? / header? / options? / multiSelect? / intent?)。 */
513
- questions: unknown[];
514
- /** 内核以后加的字段照样透传,不在这里裁剪。 */
515
- [key: string]: unknown;
516
- }
517
- /** 一条问题的答案(内核 `AskUserQuestionAnswerItem`)。 */
518
- interface UserQuestionAnswerItem {
519
- /** 被回答的问题 id,必须与请求里的问题 id 对上。 */
520
- id: string;
521
- /** 选中的选项 label;多选时可能多项。 */
522
- selected: string[];
523
- /** 可选的「其他」自由文本。 */
524
- custom?: string;
525
- }
526
- /** 一次提交的完整答案批次(内核 `AskUserQuestionAnswer`):一次调用里的每个问题各占一项。 */
527
- interface UserQuestionAnswer {
528
- answers: UserQuestionAnswerItem[];
529
- }
298
+ type PromptContentPartLike = PromptContentPart;
530
299
  /**
531
- * 「把答案送进内核」的结果。
300
+ * `ctx.fileUploads`:内核的文件上传服务。
532
301
  *
533
- * 刻意**不走 RpcResponse 的 error 通道**:`unwrapDsh` 只认 `not-found` /
534
- * `busy` / `locked` 三种 code 去映射 HTTP 状态,其余一律 400 —— 而这里需要区分
535
- * 409 / 400 / 503。做成**值**之后由 adapter 决定 HTTP 状态,
536
- * 和 approvals 的 `respond()` 返回 `{ accepted: false, reason }` 是同一个套路。
302
+ * 由内核的 `@deepseek-ai/dsh-client-file-upload` 以 cordis 服务的形式暴露,
303
+ * 因此插件**不需要**把字节转发到内核的 HTTP 路由(那条路由只绑 loopback,手机也到不了)。
537
304
  */
538
- type UserQuestionAnswerFailure =
539
- /** 内核里没有这个 callId 的悬空问题(`answer()` 返回了 false)。 */
540
- 'not-pending' |
541
- /** 已经有一条回复在排队(内核 `REPLY_QUEUED`)。重复提交不是服务器故障。 */
542
- 'reply-queued' |
543
- /** 答案批次没有「一问一答」地对齐(内核 `BAD_ANSWER`)。 */
544
- 'answer-invalid' |
545
- /** 这个 agent 现在不接受人工回答(非 live root / 被别的 agent 拥有),或会话没有 live agent。 */
546
- 'not-answerable' |
547
- /** 这套 DSH 构建里压根没有 `ctx.userQuestions` 服务。 */
548
- 'unavailable';
549
- type UserQuestionAnswerOutcome = {
550
- accepted: true;
551
- } | {
552
- accepted: false;
553
- reason: UserQuestionAnswerFailure;
554
- };
555
- interface ChatWorkspaceView {
556
- id: string;
557
- title: string;
558
- rootId: string;
559
- path: string;
560
- createdAt: string;
561
- updatedAt: string;
305
+ interface FileUploadsLike {
306
+ upload(agent: unknown, request: {
307
+ data: string;
308
+ name?: string;
309
+ }, signal: AbortSignal): Promise<{
310
+ receiptId: string;
311
+ }>;
312
+ /**
313
+ * 把凭证绑定到某次 prompt。返回值不 commit 的话凭证会被回收 ——
314
+ * 所以调用方必须在 prompt **成功之后** commit()。
315
+ */
316
+ bindPrompt(agent: unknown, receiptIds: readonly string[], requestId: string): {
317
+ commit(): void;
318
+ };
562
319
  }
563
- interface AgentPresetView {
564
- id: string;
565
- name: string;
566
- description?: string;
567
- trust: 'system' | 'user';
568
- isDefault: boolean;
569
- available: boolean;
320
+ /**
321
+ * `ctx.attachments`:内核的附件存储。
322
+ *
323
+ * 为什么会话里的图片必须经由它:消息**发送时**图片是 base64 内联的,但**存进历史后**
324
+ * 会变成 `{type:'image', attachment:{attachmentId:'sha256:…'}}` 这样的引用 ——
325
+ * 字节在附件存储里,不在事件里。客户端只拿到 id,所以必须有一步「按 id 取字节」,
326
+ * 否则历史里的图片一律显示不出来。
327
+ */
328
+ interface AttachmentStoreLike {
329
+ readImage(
330
+ /**
331
+ * 附件引用。
332
+ *
333
+ * `attachmentId` 是唯一的**定位**字段(内核用 `ID_PATTERN.exec()` 取出 sha256
334
+ * 再拼 `objects/<前两位>/<sha>`);而 `mediaType` / `bytes` / `width` / `height`
335
+ * 参与读后**完整性校验** —— 缺任何一项都会抛 `ATTACHMENT_CORRUPT`,
336
+ * 哪怕字节已经正确读出来了。
337
+ */
338
+ ref: {
339
+ attachmentId: string;
340
+ mediaType?: string;
341
+ bytes?: number;
342
+ width?: number;
343
+ height?: number;
344
+ }, signal?: AbortSignal): Promise<{
345
+ ref: {
346
+ mediaType: string;
347
+ };
348
+ data: Uint8Array;
349
+ }>;
570
350
  }
571
- declare class DshChatAdapter {
572
- private readonly api;
573
- private readonly database;
574
- private readonly commandServices?;
575
- private readonly sessionRootIds;
576
- private readonly sessionRootLookups;
577
- private readonly pendingApprovals;
578
- constructor(api: DshApiProxy, database: WorkspaceDatabase, commandServices?: DshCommandServices | undefined);
579
- listSessions(principal: Principal): Promise<ChatSessionView[]>;
580
- listWorkspaces(principal: Principal): Promise<ChatWorkspaceView[]>;
581
- createWorkspace(principal: Principal, rootId: string, relativePath: string): Promise<ChatWorkspaceView>;
582
- renameWorkspace(principal: Principal, workspaceId: string, title: string): Promise<ChatWorkspaceView>;
583
- deleteWorkspace(principal: Principal, workspaceId: string): Promise<void>;
584
- listAgentPresets(): Promise<AgentPresetView[]>;
585
- selectAgentPreset(principal: Principal, sessionId: string, agentPreset: string): Promise<string>;
586
- createSessionInWorkspace(principal: Principal, workspaceId: string, options: {
351
+ interface SessionControllerLike {
352
+ list(request: {
353
+ cursor?: string;
354
+ }, signal: AbortSignal): Promise<{
355
+ items: readonly unknown[];
356
+ }>;
357
+ create(request: {
358
+ workspaceId?: string;
359
+ cwd?: string;
587
360
  sessionId?: string;
588
361
  agentPreset?: string;
589
362
  }): Promise<{
590
- id: string;
363
+ sessionId: string;
591
364
  agentPreset?: string;
592
365
  }>;
593
- createSession(principal: Principal, rootId: string, relativePath: string, options: {
594
- sessionId?: string;
595
- agentPreset?: string;
366
+ /**
367
+ * Live follow stream. Its first frame is always a `snapshot` carrying the
368
+ * opening window (`cursor`, `records`, `hasMore`), and that is the only
369
+ * reliable way to read history: `page()` demands a `throughSeq` "obtained from
370
+ * the corresponding follow opening frame", so guessing one (e.g. `-1`) returns
371
+ * a well-formed but *empty* page — a silent failure, not an error.
372
+ */
373
+ follow(request: {
374
+ address: {
375
+ kind: 'session';
376
+ sessionId: string;
377
+ };
378
+ maxMessages?: number;
379
+ /**
380
+ * Assistant presentation frames are opt-in: without `assistantStream: true`
381
+ * the host emits durable events only, so a live stream would carry no
382
+ * incremental text at all (the transcript then appears only after commit).
383
+ */
384
+ assistantStream?: true;
385
+ }, signal: AbortSignal): AsyncIterable<{
386
+ type?: string;
387
+ cursor?: number;
388
+ records?: readonly unknown[];
389
+ hasMore?: boolean;
390
+ }>;
391
+ prompt(request: {
392
+ requestId: string;
393
+ sessionId: string;
394
+ mode: 'queue' | 'steer';
395
+ content: readonly PromptContentPartLike[];
396
+ clientTimeZone?: string;
397
+ }, signal: AbortSignal): Promise<{
398
+ accepted: true;
399
+ }>;
400
+ cancel(request: {
401
+ sessionId: string;
402
+ }): {
403
+ accepted: true;
404
+ };
405
+ rename(request: {
406
+ sessionId: string;
407
+ title: string;
408
+ }): Promise<{
409
+ title: string;
410
+ seq: number;
411
+ }>;
412
+ fork(request: {
413
+ sessionId: string;
414
+ atSeq?: number;
415
+ }): Promise<{
416
+ sessionId: string;
417
+ }>;
418
+ /**
419
+ * 取一张**被这个会话引用过**的图片字节(内核主机端方法,名字就叫 `attachment`)。
420
+ *
421
+ * 这是取历史图片的**正路**,它内部一次做完三件事:校验会话确实引用了这张图、
422
+ * 调 `ctx.attachments.readImage`、用**会话事件里那条真实 ref**做完整性校验
423
+ *(`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
424
+ *
425
+ * 为什么必须走它、不能自己调 `ctx.attachments`:实测本机这条 profile 里
426
+ * `ctx.attachments` 是 **MISSING**(诊断日志
427
+ * `ctx.attachments MISSING(所以改走内核 sessionController.readAttachment)`),
428
+ * 自己拼 ref 那条路根本走不通;而且绕开内核就得把它内部三件事全复刻一遍,
429
+ * 早先正是只传了 id、缺四项元数据,导致 `ATTACHMENT_CORRUPT`。
430
+ *
431
+ * 注意方法名:**主机端叫 `attachment`**,客户端契约里才叫 `readAttachment`
432
+ *(`dsh-api-session-controller/lib/types/client/contract/session.d.ts`)。
433
+ * `data` 是 **base64 字符串**(主机端实现里 `Buffer.from(stored.data).toString("base64")`),
434
+ * 不是 Uint8Array —— 调用方要自己解码。
435
+ */
436
+ attachment(request: {
437
+ sessionId: string;
438
+ attachmentId: string;
439
+ }): Promise<{
440
+ attachment: {
441
+ mediaType: string;
442
+ };
443
+ data: string;
444
+ }>;
445
+ selectModel(request: {
446
+ sessionId: string;
447
+ provider: string;
448
+ model: string;
449
+ reasoningEffort?: string;
450
+ }): Promise<{
451
+ selected: unknown;
452
+ }>;
453
+ /**
454
+ * Host model catalog. 0.2.x reports `default`, the routable route list, and
455
+ * per-route failures; the plugin's `SessionModelsView` instead wants a boolean
456
+ * `routable` plus a per-session `current`, so the bridge folds them (see the
457
+ * `models` method) rather than passing this through.
458
+ */
459
+ modelCatalog(): Promise<{
460
+ default?: unknown;
461
+ routableProviders?: readonly string[];
462
+ groups?: readonly unknown[];
463
+ failures?: readonly {
464
+ id?: unknown;
465
+ name?: unknown;
466
+ message?: unknown;
467
+ }[];
468
+ }>;
469
+ /**
470
+ * Projection baseline for one session. Unlike the catalog this is per-session,
471
+ * which is what makes it the source of the session's *current* model selection
472
+ * (`values.modelSelection`).
473
+ */
474
+ projections(request: {
475
+ sessionId: string;
476
+ }, signal: AbortSignal): Promise<{
477
+ asOfSeq?: number;
478
+ values?: Record<string, unknown>;
479
+ } | null>;
480
+ resolveAgent(sessionId: string): Promise<unknown>;
481
+ }
482
+ interface WorkspaceControllerLike {
483
+ create(request: {
484
+ path: string;
596
485
  }): Promise<{
597
- id: string;
598
- agentPreset?: string;
486
+ workspace: unknown;
487
+ created: boolean;
599
488
  }>;
600
- history(principal: Principal, sessionId: string, options: {
601
- beforeSeq?: number;
602
- maxMessages?: number;
489
+ rename(request: {
490
+ workspaceId: string;
491
+ title: string;
492
+ }): Promise<unknown>;
493
+ delete(request: {
494
+ workspaceId: string;
603
495
  }): Promise<{
604
- events: unknown[];
605
- hasMore: boolean;
606
- projections?: unknown;
496
+ deleted: boolean;
607
497
  }>;
608
- prompt(principal: Principal, sessionId: string, parts: PromptContentPart[], mode: 'queue' | 'steer', clientTimeZone?: string): Promise<{
609
- accepted: true;
610
- command?: unknown;
498
+ archiveSession(request: {
499
+ sessionId: string;
500
+ }): Promise<{
501
+ archivedSessionIds: readonly string[];
611
502
  }>;
503
+ }
504
+ interface WorkspaceRegistryLike {
505
+ list(): readonly {
506
+ id: string;
507
+ path: string;
508
+ title: string;
509
+ createdAt: string;
510
+ updatedAt: string;
511
+ sessionIds?: readonly string[];
512
+ }[];
513
+ }
514
+ interface SettingsControllerLike {
515
+ describe(): {
516
+ writable: boolean;
517
+ hasDocument: boolean;
518
+ namespaces: readonly unknown[];
519
+ };
520
+ mutate(ns: string, ops: readonly unknown[], expectedRevision: number | undefined): Promise<unknown>;
521
+ }
522
+ interface CredentialsControllerLike {
523
+ describe(refs: readonly string[]): Promise<Record<string, unknown>>;
524
+ set(ref: string, value: string): Promise<void>;
525
+ unset(ref: string): Promise<void>;
526
+ }
527
+ interface AgentPresetsLike {
528
+ list(): Promise<readonly unknown[]>;
529
+ remoteExportList(): Promise<unknown>;
530
+ resolveAgent?: never;
531
+ select(agent: unknown, agentPreset: string): Promise<string>;
532
+ }
533
+ interface LlmLike {
534
+ /** Live routes, as `{ id, name }` rows. */
535
+ listProviders(): readonly {
536
+ id: string;
537
+ }[];
612
538
  /**
613
- * 上传一个附件,返回 prompt 可以引用的凭证。
614
- *
615
- * 为什么需要这一步而不是让客户端直接把字节塞进消息:内核对**文件**类型要求的是
616
- * `receiptId`(由上传换取),只有**图片**才允许内联 base64。
539
+ * Every route an adapter plugin could activate through configuration, live or
540
+ * dormant, carrying the settings namespace and path that configures it.
541
+ * Configuration surfaces merge this with `listProviders()` for liveness.
617
542
  */
618
- uploadAttachment(principal: Principal, sessionId: string, data: string, name?: string): Promise<{
619
- receiptId: string;
620
- name: string | null;
621
- }>;
543
+ listConfigurableProviders(): readonly {
544
+ provider: string;
545
+ displayName: string;
546
+ settingsNs: string;
547
+ settingsPath: readonly string[];
548
+ declared?: boolean;
549
+ error?: string;
550
+ }[];
551
+ discoverModels(settingsNs: string, request: unknown, signal?: AbortSignal): Promise<readonly unknown[]>;
622
552
  /**
623
- * 取回会话历史里某个图片附件的字节。
553
+ * 一个路由 + 模型 id 的**真实元数据**。
624
554
  *
625
- * 这里不做会话归属校验:附件 id 是内容哈希,拿到 id 就等于拿到内容,
626
- * 而且调用方必须已经通过 `files.read` 鉴权并主动持有该 id(它只出现在其有权读取的
627
- * 会话事件里)。多一层会话校验并不能提升安全性,却会让「在别的会话里引用同一张图」
628
- * 这种正常情况莫名失败。
555
+ * 为什么必须用它、而不是自己猜模型能不能看图:内核拒绝「给纯文本模型发图片」时
556
+ * 用的就是**这个方法**(`SessionController` 的 prompt 准入里
557
+ * `this.ctx.llm.resolveModelInfo(...)` 再查 `inputModalities`)。入口要与内核
558
+ * 得出同一个结论,只能查同一份元数据 —— 名字、前缀、家族表都是启发式,会漂。
629
559
  */
630
- readAttachment(principal: Principal, attachmentId: string): Promise<{
631
- bytes: Uint8Array;
632
- mediaType: string;
633
- }>;
634
- cancel(principal: Principal, sessionId: string): Promise<void>;
635
- renameSession(principal: Principal, sessionId: string, title: string): Promise<{
636
- title: string;
637
- seq: number;
638
- }>;
639
- forkSession(principal: Principal, sessionId: string, atSeq?: number): Promise<string>;
640
- archiveSession(principal: Principal, sessionId: string): Promise<string[]>;
641
- listPendingApprovals(principal: Principal, sessionId: string): Promise<PendingApprovalView[]>;
642
- decideApproval(principal: Principal, sessionId: string, approvalId: string, outcome: 'allowed-once' | 'rejected'): Promise<PendingApprovalView>;
560
+ resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<{
561
+ inputModalities?: readonly string[];
562
+ } | undefined>;
563
+ }
564
+ /** Host services the bridge consumes; assembled by `src/index.ts` from `ctx`. */
565
+ interface Dsh02HostServices {
566
+ sessionController: SessionControllerLike;
567
+ workspaceController: WorkspaceControllerLike;
568
+ workspaceRegistry: WorkspaceRegistryLike;
569
+ settingsController: SettingsControllerLike;
570
+ credentialsController: CredentialsControllerLike;
571
+ agentPresets: AgentPresetsLike;
572
+ llm: LlmLike;
643
573
  /**
644
- * 列出「agent 正在等、且还能回答」的用户问题。
574
+ * 内核附件存储(`ctx.attachments`);会话历史里的图片要经它取字节。
645
575
  *
646
- * 口径与 [listPendingApprovals] 一致:先 requireSession(会话对本设备不可见 → 404),
647
- * 再问内核;字段一个都不裁 —— 客户端照着渲染,内核加字段不用改插件。
576
+ * 必须是**已经取到的服务对象**,不能是 `ctx` 本身:cordis 的注入属性是有生命周期的
577
+ * 惰性访问器,只在 `apply` 执行期间可读。把它存进 `host` 里、等到 HTTP 请求回调
578
+ * (`readAttachment`)里再读,就会撞上
579
+ * `cannot get property "attachments" without inject` —— 请求链路本身没问题,
580
+ * 挂在服务端、而且只在真的去取图时才炸,所以很难在本地测出来。
648
581
  */
649
- listUserQuestions(principal: Principal, sessionId: string): Promise<UserQuestionView[]>;
582
+ attachments: AttachmentStoreLike;
650
583
  /**
651
- * 把一次答案提交给内核。
584
+ * Kernel file-upload service (`ctx.fileUploads`).
652
585
  *
653
- * 失败原因全部由 `api.userQuestions.answer` 以**值**给出(见 [UserQuestionAnswerOutcome]),
654
- * 这里翻成 HTTP 语义:503 能力缺失 / 409 状态冲突 / 400 批次不合法。
655
- * 为什么不在 bridge 里直接抛:`unwrapDsh` 只把 `not-found` / `busy` / `locked`
656
- * 映成 404/409,其余一律 400 —— 表达不了这几种区别。
586
+ * 为什么用它而不是内核那条 `/api/session/uploadFileBinary` HTTP 路由:那条路由
587
+ * 只绑 loopback(手机根本到不了),而这个 cordis 服务就在同一个进程里,插件可直接调。
657
588
  */
658
- answerUserQuestion(principal: Principal, sessionId: string, callId: string, answer: UserQuestionAnswer): Promise<void>;
659
- models(principal: Principal, sessionId: string): Promise<SessionModelsView>;
660
- selectModel(principal: Principal, sessionId: string, selection: ModelSelectionView): Promise<ModelSelectionView>;
661
- listCommands(principal: Principal, sessionId: string): Promise<CommandDescriptorView[]>;
662
- executeCommand(principal: Principal, sessionId: string, line: string): Promise<CommandExecutionView>;
663
- isSessionAuthorized(principal: Principal, sessionId: string): Promise<boolean>;
664
- startEvents(signal: AbortSignal, emit: (event: WorkspaceEvent) => void): void;
665
- private captureApproval;
666
- private hasPendingApproval;
589
+ fileUploads: FileUploadsLike;
667
590
  /**
668
- * Resolve the Agent backing a session for command work.
669
- *
670
- * Returns `undefined` when the session exists but cannot be activated. 0.2.x
671
- * rejects activation for a Session owned by a subagent, so that outcome is a
672
- * property of the session rather than a failure of this call: the caller
673
- * decides whether it means "no commands" (listing) or an error (execution).
591
+ * Cordis event bus of the host context. Used for the two facts the old
592
+ * `apiProxy` event streams carried that no controller returns: session
593
+ * registry changes and the approval answerer waterfall.
674
594
  */
675
- private commandAgent;
676
- private requireSession;
677
- private requireWorkspace;
678
- private visibleWorkspaces;
679
- private requireAuthorizedRoot;
680
- private authorizedRoots;
681
- private sessionRootId;
595
+ on(event: string, listener: (...args: any[]) => unknown): () => void;
682
596
  }
597
+ /**
598
+ * 需要交接时,把每个 `image` 部件换成**两行文本**;否则原样返回。
599
+ *
600
+ * 同一行路径要同时伺候两类读者:
601
+ * - **给人看**(第一行):图片序号、原始文件名、工作目录里的短路径;
602
+ * - **给机器看**(第二行 `[Image: source: <绝对路径>]`):这是读图技能明确
603
+ * 识别的占位形状之一(带图片扩展名的绝对路径同样触发),任何按路径读图的
604
+ * 工具都能接手,不出现任何具体插件名。
605
+ *
606
+ * 任何一步抛错都退回原内容:这个功能的失败**不该**把「发消息」本身弄坏。
607
+ */
608
+ /**
609
+ * 交接文案用哪种语言。
610
+ *
611
+ * 判据是**发消息那个客户端的界面语言**,由 HTTP `Accept-Language` 头带进来
612
+ * (router → chat-adapter → 这里)。拿不到就退回中文 —— 那是这个功能既有的默认口径,
613
+ * 判断不出来的时候不该突然换语言。
614
+ */
615
+ type HandoffLocale = 'zh' | 'en';
683
616
  //#endregion
684
- //#region src/host/settings-adapter.d.ts
685
- interface SettingsNamespaceView {
686
- ns: string;
687
- schema?: unknown;
688
- value: unknown;
689
- base?: unknown;
690
- user?: unknown;
691
- revision: number;
692
- }
693
- interface ConfigurableProvider {
694
- provider: string;
695
- displayName: string;
696
- settingsNs: string;
697
- settingsPath: string[];
698
- active: boolean;
699
- declared?: boolean;
617
+ //#region src/host/chat-adapter.d.ts
618
+ interface RpcResponse<T> {
619
+ result: {
620
+ ok: true;
621
+ value: T;
622
+ } | {
623
+ ok: false;
624
+ error: {
625
+ code: string;
626
+ message: string;
627
+ details?: unknown;
628
+ };
629
+ };
700
630
  }
701
- interface SettingsApiProxy extends DshApiProxy {
702
- settings: {
703
- describe(request: {
631
+ /**
632
+ * 一条 prompt 的内容片段 —— 与 DSH 0.2.x 的 `PromptContentPart` 一一对应。
633
+ *
634
+ * 三种形态的代价差别很大,值得写清楚:
635
+ * - `text` 直接发送;
636
+ * - `image` 字节**内联**在 payload 里(base64),一次调用完成,但请求体膨胀约 33%,
637
+ * 所以客户端必须先限制原图大小,别等 413;
638
+ * - `file` 的 `receiptId` 必须来自**先前的**上传调用,并在同一次 prompt 里绑定,
639
+ * 否则凭证会被回收。
640
+ */
641
+ type PromptContentPart = {
642
+ type: 'text';
643
+ text: string;
644
+ } | {
645
+ type: 'image';
646
+ mediaType: string;
647
+ data: string;
648
+ name?: string;
649
+ } | {
650
+ type: 'file';
651
+ receiptId: string;
652
+ };
653
+ interface DshApiProxy {
654
+ respond(message: {
655
+ type: 'client-response';
656
+ rpcId: string;
657
+ result: {
658
+ ok: true;
659
+ value: unknown;
660
+ };
661
+ }): Promise<{
662
+ accepted: true;
663
+ } | {
664
+ accepted: false;
665
+ reason: 'not-pending' | 'bad-response';
666
+ }>;
667
+ sessions: {
668
+ list(request: {
669
+ rpcId: string;
670
+ payload: {
671
+ cursor?: string;
672
+ };
673
+ }): Promise<RpcResponse<{
674
+ items: DshSessionSummary[];
675
+ }>>;
676
+ create(request: {
677
+ rpcId: string;
678
+ payload: {
679
+ cwd?: string;
680
+ workspaceId?: string;
681
+ sessionId?: string;
682
+ agentPreset?: string;
683
+ };
684
+ }): Promise<RpcResponse<{
685
+ sessionId: string;
686
+ agentPreset?: string;
687
+ }>>;
688
+ history(request: {
689
+ rpcId: string;
690
+ payload: {
691
+ sessionId: string;
692
+ beforeSeq?: number;
693
+ maxMessages?: number;
694
+ };
695
+ }): Promise<RpcResponse<{
696
+ events: unknown[];
697
+ hasMore: boolean;
698
+ projections?: unknown;
699
+ }>>;
700
+ /**
701
+ * 提交一条 prompt。
702
+ *
703
+ * `locale` 不是内核字段,是**我们**加的:告诉图片交接「发消息的客户端界面是什么语言」,
704
+ * 好让交接说明说用户看得懂的话。它会一直传到 `dsh-0.2-bridge` 并在调内核前被摘掉。
705
+ */
706
+ prompt(request: {
704
707
  rpcId: string;
705
- payload: Record<string, never>;
708
+ payload: {
709
+ sessionId: string;
710
+ mode: 'queue' | 'steer';
711
+ content: PromptContentPart[];
712
+ clientTimeZone?: string;
713
+ locale?: HandoffLocale;
714
+ };
706
715
  }): Promise<RpcResponse<{
707
- writable: boolean;
708
- hasDocument: boolean;
709
- namespaces: SettingsNamespaceView[];
716
+ accepted: true;
717
+ command?: unknown;
710
718
  }>>;
711
- mutate(request: {
719
+ /**
720
+ * 上传一个文件,换取 prompt 可引用的凭证(`receiptId`)。
721
+ * `data` 是文件字节的**标准 base64**。
722
+ */
723
+ uploadAttachment(request: {
712
724
  rpcId: string;
713
725
  payload: {
714
- ns: string;
715
- ops: Array<{
716
- op: 'set';
717
- path: string[];
718
- value: unknown;
719
- } | {
720
- op: 'unset';
721
- path: string[];
722
- }>;
723
- expectedRevision?: number;
726
+ sessionId: string;
727
+ data: string;
728
+ name?: string;
724
729
  };
725
- }): Promise<RpcResponse<SettingsNamespaceView>>;
726
- };
727
- credentials: {
728
- describe(request: {
730
+ }): Promise<RpcResponse<{
731
+ receiptId: string;
732
+ name: string | null;
733
+ }>>;
734
+ /**
735
+ * 按 id 取回附件字节。
736
+ *
737
+ * `attachmentId` 形如 `sha256:<hex>`(**含冒号**,客户端必须 URL 编码)。
738
+ * 返回 `Uint8Array` 而不是 base64:图片动辄上百 KB,多一次编解码纯属浪费。
739
+ *
740
+ * **必须带 `sessionId`**:内核要靠它读会话事件、取出这张图的真实 ref,
741
+ * 再据此做完整性校验(详见 chat-adapter 的 readAttachment 注释)。
742
+ */
743
+ readAttachment(request: {
729
744
  rpcId: string;
730
745
  payload: {
731
- refs: string[];
746
+ sessionId: string;
747
+ attachmentId: string;
732
748
  };
733
749
  }): Promise<RpcResponse<{
734
- credentials: Record<string, {
735
- configured: boolean;
736
- source?: string;
737
- writable: boolean;
738
- }>;
750
+ bytes: Uint8Array;
751
+ mediaType: string;
739
752
  }>>;
740
- set(request: {
753
+ /**
754
+ * 取回一张**交接图片**的字节(纯文本模型发图时,图片被落成工作区文件)。
755
+ *
756
+ * 与 [readAttachment] 的区别:那一个取内核附件存储里的图(走
757
+ * `sessionController.attachment`),这一个取插件自己写进 `.dsh-images/`
758
+ * 的交接件。手机端用它把图片**显示**给人看;消息里的路径文本照旧留给模型。
759
+ */
760
+ readHandoffImage(request: {
741
761
  rpcId: string;
742
762
  payload: {
743
- ref: string;
744
- value: string;
763
+ sessionId: string;
764
+ name: string;
745
765
  };
746
- }): Promise<RpcResponse<Record<string, never>>>;
747
- unset(request: {
766
+ }): Promise<RpcResponse<{
767
+ bytes: Uint8Array;
768
+ mediaType: string;
769
+ }>>;
770
+ cancel(request: {
748
771
  rpcId: string;
749
772
  payload: {
750
- ref: string;
773
+ sessionId: string;
751
774
  };
752
- }): Promise<RpcResponse<Record<string, never>>>;
775
+ }): Promise<RpcResponse<{
776
+ accepted: true;
777
+ }>>;
778
+ rename(request: {
779
+ rpcId: string;
780
+ payload: {
781
+ sessionId: string;
782
+ title: string;
783
+ };
784
+ }): Promise<RpcResponse<{
785
+ title: string;
786
+ seq: number;
787
+ }>>;
788
+ fork(request: {
789
+ rpcId: string;
790
+ payload: {
791
+ sessionId: string;
792
+ atSeq?: number;
793
+ };
794
+ }): Promise<RpcResponse<{
795
+ sessionId: string;
796
+ }>>;
797
+ models(request: {
798
+ rpcId: string;
799
+ payload: {
800
+ sessionId: string;
801
+ };
802
+ }): Promise<RpcResponse<SessionModelsView>>;
803
+ selectModel(request: {
804
+ rpcId: string;
805
+ payload: {
806
+ sessionId: string;
807
+ provider: string;
808
+ model: string;
809
+ reasoningEffort?: string;
810
+ };
811
+ }): Promise<RpcResponse<{
812
+ selected: ModelSelectionView;
813
+ }>>;
753
814
  };
754
- llm: {
755
- providers(request: {
815
+ workspace: {
816
+ list(request: {
756
817
  rpcId: string;
757
818
  payload: Record<string, never>;
758
819
  }): Promise<RpcResponse<{
759
- providers: ConfigurableProvider[];
820
+ items: DshWorkspaceSummary[];
821
+ archivedSessionIds: string[];
760
822
  }>>;
761
- models(request: {
823
+ create(request: {
824
+ rpcId: string;
825
+ payload: {
826
+ path: string;
827
+ };
828
+ }): Promise<RpcResponse<{
829
+ workspace: DshWorkspaceSummary;
830
+ created: boolean;
831
+ }>>;
832
+ rename(request: {
833
+ rpcId: string;
834
+ payload: {
835
+ workspaceId: string;
836
+ title: string;
837
+ };
838
+ }): Promise<RpcResponse<{
839
+ workspace: DshWorkspaceSummary;
840
+ }>>;
841
+ delete(request: {
842
+ rpcId: string;
843
+ payload: {
844
+ workspaceId: string;
845
+ };
846
+ }): Promise<RpcResponse<{
847
+ deleted: true;
848
+ }>>;
849
+ archiveSession(request: {
850
+ rpcId: string;
851
+ payload: {
852
+ sessionId: string;
853
+ };
854
+ }): Promise<RpcResponse<{
855
+ archivedSessionIds: string[];
856
+ }>>;
857
+ };
858
+ agentPresets: {
859
+ list(request: {
762
860
  rpcId: string;
763
861
  payload: Record<string, never>;
764
862
  }): Promise<RpcResponse<{
765
- groups: ModelProviderGroupView[];
766
- failures: {
767
- provider: string;
768
- message: string;
769
- }[];
863
+ presets: DshAgentPresetSummary[];
864
+ authorable: boolean;
865
+ hasDocument: boolean;
770
866
  }>>;
771
- discoverModels(request: {
867
+ select(request: {
772
868
  rpcId: string;
773
869
  payload: {
774
- settingsNs: string;
775
- provider?: string;
776
- baseURL?: string;
777
- api?: string;
778
- apiKey?: string;
870
+ sessionId: string;
871
+ agentPreset: string;
779
872
  };
780
873
  }): Promise<RpcResponse<{
781
- models: ProviderModelView[];
874
+ agentPreset: string;
875
+ }>>;
876
+ };
877
+ /**
878
+ * 用户问题(内核 `ctx.userQuestions`,即 ask_user_question 工具)。
879
+ *
880
+ * 为什么单开一个命名空间而不是塞进 `sessions`:它由**另一个内核服务**提供
881
+ * (`@deepseek-ai/dsh-user-questions`),在一条没装它的 profile 上是**可选能力** ——
882
+ * 分开之后,「这个能力不在」和「这个会话有问题」在类型上就分得开。
883
+ */
884
+ userQuestions: {
885
+ /**
886
+ * 列出还能回答的问题(内核 `continued(agent)`)。
887
+ * 没有 live agent、或没有该服务时返回空列表 —— 冷会话是常态,不该报错。
888
+ */
889
+ list(request: {
890
+ rpcId: string;
891
+ payload: {
892
+ sessionId: string;
893
+ };
894
+ }): Promise<RpcResponse<{
895
+ items: UserQuestionView[];
782
896
  }>>;
897
+ /** 提交答案;失败原因见 [UserQuestionAnswerOutcome]。 */
898
+ answer(request: {
899
+ rpcId: string;
900
+ payload: {
901
+ sessionId: string;
902
+ callId: string;
903
+ answer: UserQuestionAnswer;
904
+ };
905
+ }): Promise<RpcResponse<UserQuestionAnswerOutcome>>;
906
+ };
907
+ events: {
908
+ mux(request: {
909
+ rpcId: string;
910
+ payload: Record<string, never>;
911
+ }, signal: AbortSignal): AsyncIterable<{
912
+ rpcId: string;
913
+ payload: Record<string, unknown>;
914
+ }>;
915
+ host(request: {
916
+ rpcId: string;
917
+ payload: Record<string, never>;
918
+ }, signal: AbortSignal): AsyncIterable<{
919
+ rpcId: string;
920
+ payload: Record<string, unknown>;
921
+ }>;
922
+ };
923
+ }
924
+ interface ModelSelectionView {
925
+ provider: string;
926
+ model: string;
927
+ reasoningEffort?: string;
928
+ }
929
+ interface ModelReasoningView {
930
+ efforts: {
931
+ id: string;
932
+ name: string;
933
+ description?: string;
934
+ }[];
935
+ defaultEffort?: string;
936
+ }
937
+ interface ModelProviderGroupView {
938
+ id: string;
939
+ name: string;
940
+ models: {
941
+ id: string;
942
+ name: string;
943
+ description?: string;
944
+ contextWindow?: number;
945
+ maxTokens?: number;
946
+ reasoning?: ModelReasoningView;
947
+ }[];
948
+ }
949
+ interface SessionModelsView {
950
+ current: ModelSelectionView;
951
+ routable: boolean;
952
+ groups: ModelProviderGroupView[];
953
+ failures: {
954
+ provider: string;
955
+ message: string;
956
+ }[];
957
+ }
958
+ interface CommandDescriptorView {
959
+ name: string;
960
+ description: string;
961
+ input?: {
962
+ hint: string;
783
963
  };
784
964
  }
785
- interface ProviderModelView {
786
- id: string;
787
- name?: string;
788
- contextWindow?: number;
789
- maxTokens?: number;
965
+ interface CommandExecutionView {
966
+ commandId: string;
967
+ result: {
968
+ kind: 'success' | 'error';
969
+ text?: string;
970
+ sourceEventSeq?: number;
971
+ };
790
972
  }
791
- interface ProviderConfigView {
792
- baseURL?: string;
793
- api?: string;
794
- displayName?: string;
795
- thinking?: string;
796
- reasoningEffort?: string;
797
- models: ProviderModelView[];
798
- modelsInherited?: boolean;
973
+ interface DshCommandRuntime {
974
+ list(agent: unknown): readonly CommandDescriptorView[];
975
+ execute(agent: unknown, line: string, signal: AbortSignal): Promise<CommandExecutionView | undefined>;
799
976
  }
800
- interface ProviderView {
801
- id: string;
802
- displayName: string;
803
- active: boolean;
804
- declared?: boolean;
805
- configurable: boolean;
806
- configured: boolean;
807
- removable: boolean;
808
- credential: {
809
- ref: string;
810
- configured: boolean;
811
- source?: string;
812
- writable: boolean;
977
+ interface DshAgentRegistry {
978
+ get(sessionId: string): unknown | undefined;
979
+ }
980
+ interface DshCommandServices {
981
+ commands: DshCommandRuntime;
982
+ agents: DshAgentRegistry;
983
+ }
984
+ interface DshSessionSummary {
985
+ sessionId: string;
986
+ updatedAt: number;
987
+ running: boolean;
988
+ blank: boolean;
989
+ cwd?: string;
990
+ agentPreset?: string;
991
+ parentSessionId?: string;
992
+ origin?: 'subagent';
993
+ projections?: {
994
+ asOfSeq: number;
995
+ values: Record<string, unknown>;
813
996
  };
814
- config: ProviderConfigView;
815
997
  }
816
- interface ProviderPatch {
817
- displayName?: string | null;
818
- baseURL?: string | null;
819
- api?: string | null;
820
- apiKey?: string | null;
821
- thinking?: string | null;
822
- reasoningEffort?: string | null;
823
- models?: ProviderModelView[];
824
- expectedRevision?: number;
998
+ interface DshWorkspaceSummary {
999
+ workspaceId: string;
1000
+ path: string;
1001
+ title: string;
1002
+ sessionIds: string[];
1003
+ createdAt: string;
1004
+ updatedAt: string;
825
1005
  }
826
- interface CustomProviderCreate {
1006
+ interface DshAgentPresetSummary {
827
1007
  id: string;
828
- displayName?: string;
829
- baseURL: string;
830
- api: string;
831
- apiKey?: string;
832
- models: ProviderModelView[];
833
- expectedRevision?: number;
1008
+ trust: 'system' | 'user';
1009
+ isDefault: boolean;
1010
+ name?: string;
1011
+ description?: string;
1012
+ broken?: string;
834
1013
  }
835
- interface CustomProviderCapability {
836
- available: boolean;
837
- protocols: string[];
838
- revision?: number;
1014
+ interface ChatSessionView {
1015
+ id: string;
1016
+ rootId: string;
1017
+ cwd: string;
1018
+ updatedAt: number;
1019
+ running: boolean;
1020
+ blank: boolean;
1021
+ title?: string;
1022
+ workspaceId?: string;
1023
+ workspaceTitle?: string;
1024
+ agentPreset?: string;
1025
+ parentSessionId?: string;
1026
+ origin?: 'subagent';
1027
+ pendingInteraction?: 'approval';
839
1028
  }
840
- declare class DshSettingsAdapter {
841
- private readonly api;
842
- constructor(api: SettingsApiProxy);
843
- listProviders(): Promise<{
844
- writable: boolean;
845
- revisionByNamespace: Record<string, number>;
846
- customProvider: CustomProviderCapability;
847
- providers: ProviderView[];
848
- }>;
849
- catalog(): Promise<{
850
- groups: ModelProviderGroupView[];
851
- failures: {
852
- provider: string;
853
- message: string;
854
- }[];
855
- }>;
856
- updateProvider(providerId: string, patch: ProviderPatch): Promise<void>;
857
- createProvider(input: CustomProviderCreate): Promise<void>;
858
- removeProvider(providerId: string, expectedRevision?: number): Promise<void>;
859
- discover(providerId: string, draft: {
860
- baseURL?: string;
861
- api?: string;
862
- apiKey?: string;
863
- }): Promise<ProviderModelView[]>;
864
- private providerContext;
1029
+ interface PendingApprovalView {
1030
+ id: string;
1031
+ sessionId: string;
1032
+ toolName: string;
1033
+ reason?: string;
1034
+ detail?: string;
1035
+ risk: 'standard' | 'full-access';
1036
+ requestedAt: number;
865
1037
  }
866
- //#endregion
867
- //#region src/host/dsh-0.2-bridge.d.ts
868
- /**
869
- * Prompt content a 0.2.x Host accepts — mirrors its `PromptContentPart` union.
870
- *
871
- * 权威定义在 `chat-adapter.ts`(协议层),这里只是复用它,避免两处各写一遍
872
- * 然后悄悄漂移。
873
- */
874
- type PromptContentPartLike = PromptContentPart;
875
1038
  /**
876
- * `ctx.fileUploads`:内核的文件上传服务。
1039
+ * 一条「还能回答」的用户问题 —— 内核 `userQuestions.continued()` 的一行。
877
1040
  *
878
- * 由内核的 `@deepseek-ai/dsh-client-file-upload` 以 cordis 服务的形式暴露,
879
- * 因此插件**不需要**把字节转发到内核的 HTTP 路由(那条路由只绑 loopback,手机也到不了)。
1041
+ * 为什么类型是宽松的、而不是照抄内核的 `AskUserQuestionItem`:内核往 item 或
1042
+ * question 上加过字段(`intent` 就是一例),手机端该自己决定要不要用,
1043
+ * 不该等插件跟着发版。所以这里只钉住三个**契约字段**,其余原样透传。
880
1044
  */
881
- interface FileUploadsLike {
882
- upload(agent: unknown, request: {
883
- data: string;
884
- name?: string;
885
- }, signal: AbortSignal): Promise<{
886
- receiptId: string;
887
- }>;
888
- /**
889
- * 把凭证绑定到某次 prompt。返回值不 commit 的话凭证会被回收 ——
890
- * 所以调用方必须在 prompt **成功之后** commit()。
891
- */
892
- bindPrompt(agent: unknown, receiptIds: readonly string[], requestId: string): {
893
- commit(): void;
894
- };
1045
+ interface UserQuestionView {
1046
+ callId: string;
1047
+ /** 内核 `UserQuestionState`:'open' 或 'continued';本端点只返回 continued。 */
1048
+ state: string;
1049
+ /** 问题项**原样**透传(id / question / detail? / header? / options? / multiSelect? / intent?)。 */
1050
+ questions: unknown[];
1051
+ /** 内核以后加的字段照样透传,不在这里裁剪。 */
1052
+ [key: string]: unknown;
1053
+ }
1054
+ /** 一条问题的答案(内核 `AskUserQuestionAnswerItem`)。 */
1055
+ interface UserQuestionAnswerItem {
1056
+ /** 被回答的问题 id,必须与请求里的问题 id 对上。 */
1057
+ id: string;
1058
+ /** 选中的选项 label;多选时可能多项。 */
1059
+ selected: string[];
1060
+ /** 可选的「其他」自由文本。 */
1061
+ custom?: string;
1062
+ }
1063
+ /** 一次提交的完整答案批次(内核 `AskUserQuestionAnswer`):一次调用里的每个问题各占一项。 */
1064
+ interface UserQuestionAnswer {
1065
+ answers: UserQuestionAnswerItem[];
895
1066
  }
896
1067
  /**
897
- * `ctx.attachments`:内核的附件存储。
1068
+ * 「把答案送进内核」的结果。
898
1069
  *
899
- * 为什么会话里的图片必须经由它:消息**发送时**图片是 base64 内联的,但**存进历史后**
900
- * 会变成 `{type:'image', attachment:{attachmentId:'sha256:…'}}` 这样的引用 ——
901
- * 字节在附件存储里,不在事件里。客户端只拿到 id,所以必须有一步「按 id 取字节」,
902
- * 否则历史里的图片一律显示不出来。
1070
+ * 刻意**不走 RpcResponse 的 error 通道**:`unwrapDsh` 只认 `not-found` /
1071
+ * `busy` / `locked` 三种 code 去映射 HTTP 状态,其余一律 400 —— 而这里需要区分
1072
+ * 409 / 400 / 503。做成**值**之后由 adapter 决定 HTTP 状态,
1073
+ * 和 approvals 的 `respond()` 返回 `{ accepted: false, reason }` 是同一个套路。
903
1074
  */
904
- interface AttachmentStoreLike {
905
- readImage(ref: {
906
- attachmentId: string;
907
- }, signal?: AbortSignal): Promise<{
908
- ref: {
909
- mediaType: string;
910
- };
911
- data: Uint8Array;
912
- }>;
1075
+ type UserQuestionAnswerFailure =
1076
+ /** 内核里没有这个 callId 的悬空问题(`answer()` 返回了 false)。 */
1077
+ 'not-pending' |
1078
+ /** 已经有一条回复在排队(内核 `REPLY_QUEUED`)。重复提交不是服务器故障。 */
1079
+ 'reply-queued' |
1080
+ /** 答案批次没有「一问一答」地对齐(内核 `BAD_ANSWER`)。 */
1081
+ 'answer-invalid' |
1082
+ /** 这个 agent 现在不接受人工回答(非 live root / 被别的 agent 拥有),或会话没有 live agent。 */
1083
+ 'not-answerable' |
1084
+ /** 这套 DSH 构建里压根没有 `ctx.userQuestions` 服务。 */
1085
+ 'unavailable';
1086
+ type UserQuestionAnswerOutcome = {
1087
+ accepted: true;
1088
+ } | {
1089
+ accepted: false;
1090
+ reason: UserQuestionAnswerFailure;
1091
+ };
1092
+ interface ChatWorkspaceView {
1093
+ id: string;
1094
+ title: string;
1095
+ rootId: string;
1096
+ path: string;
1097
+ createdAt: string;
1098
+ updatedAt: string;
913
1099
  }
914
- interface SessionControllerLike {
915
- list(request: {
916
- cursor?: string;
917
- }, signal: AbortSignal): Promise<{
918
- items: readonly unknown[];
1100
+ interface AgentPresetView {
1101
+ id: string;
1102
+ name: string;
1103
+ description?: string;
1104
+ trust: 'system' | 'user';
1105
+ isDefault: boolean;
1106
+ available: boolean;
1107
+ }
1108
+ declare class DshChatAdapter {
1109
+ private readonly api;
1110
+ private readonly database;
1111
+ private readonly commandServices?;
1112
+ private readonly sessionRootIds;
1113
+ private readonly sessionRootLookups;
1114
+ private readonly pendingApprovals;
1115
+ constructor(api: DshApiProxy, database: WorkspaceDatabase, commandServices?: DshCommandServices | undefined);
1116
+ listSessions(principal: Principal): Promise<ChatSessionView[]>;
1117
+ listWorkspaces(principal: Principal): Promise<ChatWorkspaceView[]>;
1118
+ createWorkspace(principal: Principal, rootId: string, relativePath: string): Promise<ChatWorkspaceView>;
1119
+ renameWorkspace(principal: Principal, workspaceId: string, title: string): Promise<ChatWorkspaceView>;
1120
+ deleteWorkspace(principal: Principal, workspaceId: string): Promise<void>;
1121
+ listAgentPresets(): Promise<AgentPresetView[]>;
1122
+ selectAgentPreset(principal: Principal, sessionId: string, agentPreset: string): Promise<string>;
1123
+ createSessionInWorkspace(principal: Principal, workspaceId: string, options: {
1124
+ sessionId?: string;
1125
+ agentPreset?: string;
1126
+ }): Promise<{
1127
+ id: string;
1128
+ agentPreset?: string;
919
1129
  }>;
920
- create(request: {
921
- workspaceId?: string;
922
- cwd?: string;
1130
+ createSession(principal: Principal, rootId: string, relativePath: string, options: {
923
1131
  sessionId?: string;
924
1132
  agentPreset?: string;
925
1133
  }): Promise<{
926
- sessionId: string;
1134
+ id: string;
927
1135
  agentPreset?: string;
928
1136
  }>;
929
- /**
930
- * Live follow stream. Its first frame is always a `snapshot` carrying the
931
- * opening window (`cursor`, `records`, `hasMore`), and that is the only
932
- * reliable way to read history: `page()` demands a `throughSeq` "obtained from
933
- * the corresponding follow opening frame", so guessing one (e.g. `-1`) returns
934
- * a well-formed but *empty* page — a silent failure, not an error.
935
- */
936
- follow(request: {
937
- address: {
938
- kind: 'session';
939
- sessionId: string;
940
- };
1137
+ history(principal: Principal, sessionId: string, options: {
1138
+ beforeSeq?: number;
941
1139
  maxMessages?: number;
942
- /**
943
- * Assistant presentation frames are opt-in: without `assistantStream: true`
944
- * the host emits durable events only, so a live stream would carry no
945
- * incremental text at all (the transcript then appears only after commit).
946
- */
947
- assistantStream?: true;
948
- }, signal: AbortSignal): AsyncIterable<{
949
- type?: string;
950
- cursor?: number;
951
- records?: readonly unknown[];
952
- hasMore?: boolean;
953
- }>;
954
- prompt(request: {
955
- requestId: string;
956
- sessionId: string;
957
- mode: 'queue' | 'steer';
958
- content: readonly PromptContentPartLike[];
959
- clientTimeZone?: string;
960
- }, signal: AbortSignal): Promise<{
961
- accepted: true;
962
- }>;
963
- cancel(request: {
964
- sessionId: string;
965
- }): {
966
- accepted: true;
967
- };
968
- rename(request: {
969
- sessionId: string;
970
- title: string;
971
1140
  }): Promise<{
972
- title: string;
973
- seq: number;
1141
+ events: unknown[];
1142
+ hasMore: boolean;
1143
+ projections?: unknown;
974
1144
  }>;
975
- fork(request: {
976
- sessionId: string;
977
- atSeq?: number;
978
- }): Promise<{
979
- sessionId: string;
1145
+ prompt(principal: Principal, sessionId: string, parts: PromptContentPart[], mode: 'queue' | 'steer', clientTimeZone?: string, locale?: HandoffLocale): Promise<{
1146
+ accepted: true;
1147
+ command?: unknown;
980
1148
  }>;
981
- selectModel(request: {
982
- sessionId: string;
983
- provider: string;
984
- model: string;
985
- reasoningEffort?: string;
986
- }): Promise<{
987
- selected: unknown;
1149
+ /**
1150
+ * 上传一个附件,返回 prompt 可以引用的凭证。
1151
+ *
1152
+ * 为什么需要这一步而不是让客户端直接把字节塞进消息:内核对**文件**类型要求的是
1153
+ * `receiptId`(由上传换取),只有**图片**才允许内联 base64。
1154
+ */
1155
+ uploadAttachment(principal: Principal, sessionId: string, data: string, name?: string): Promise<{
1156
+ receiptId: string;
1157
+ name: string | null;
988
1158
  }>;
989
1159
  /**
990
- * Host model catalog. 0.2.x reports `default`, the routable route list, and
991
- * per-route failures; the plugin's `SessionModelsView` instead wants a boolean
992
- * `routable` plus a per-session `current`, so the bridge folds them (see the
993
- * `models` method) rather than passing this through.
1160
+ * 取回会话历史里某个图片附件的字节。
1161
+ *
1162
+ * **为什么会走内核的 `sessionController.attachment()`,而不自己调
1163
+ * `ctx.attachments.readImage()`**:
1164
+ * 1. 实测本机 profile 里 `ctx.attachments` 是 **MISSING**(拿不到注入属性)——
1165
+ * 自己拼 ref 那条路根本走不通;
1166
+ * 2. 内核那个方法内部一次做完三件事:校验该会话确实引用了这张图、
1167
+ * 读出字节、用**会话事件里那条真实 ref** 做完整性校验
1168
+ * (`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
1169
+ * 绕开它就得把这三件事全复刻 —— 先前正是漏了第三件,于是每张图
1170
+ * `ATTACHMENT_CORRUPT`:「字节读到了,却被校验拒之门外」。
1171
+ *
1172
+ * 因此 **sessionId 是必需参数**(内核要拿它读会话事件、找到那张图的真实 ref);
1173
+ * 早先设计的「客户端把四项元数据带上来」是多余的 —— 内核手里本来就有。
994
1174
  */
995
- modelCatalog(): Promise<{
996
- default?: unknown;
997
- routableProviders?: readonly string[];
998
- groups?: readonly unknown[];
999
- failures?: readonly {
1000
- id?: unknown;
1001
- name?: unknown;
1002
- message?: unknown;
1003
- }[];
1175
+ readAttachment(principal: Principal, sessionId: string, attachmentId: string): Promise<{
1176
+ bytes: Uint8Array;
1177
+ mediaType: string;
1004
1178
  }>;
1005
1179
  /**
1006
- * Projection baseline for one session. Unlike the catalog this is per-session,
1007
- * which is what makes it the source of the session's *current* model selection
1008
- * (`values.modelSelection`).
1180
+ * 取回一张**交接图片**(纯文本模型发图时落在工作区 `.dsh-images/` 的那份)。
1181
+ * 与 [readAttachment] 同样要求会话归属;文件名在桥里还有一道白名单校验。
1009
1182
  */
1010
- projections(request: {
1011
- sessionId: string;
1012
- }, signal: AbortSignal): Promise<{
1013
- asOfSeq?: number;
1014
- values?: Record<string, unknown>;
1015
- } | null>;
1016
- resolveAgent(sessionId: string): Promise<unknown>;
1017
- }
1018
- interface WorkspaceControllerLike {
1019
- create(request: {
1020
- path: string;
1021
- }): Promise<{
1022
- workspace: unknown;
1023
- created: boolean;
1183
+ readHandoffImage(principal: Principal, sessionId: string, name: string): Promise<{
1184
+ bytes: Uint8Array;
1185
+ mediaType: string;
1024
1186
  }>;
1025
- rename(request: {
1026
- workspaceId: string;
1187
+ cancel(principal: Principal, sessionId: string): Promise<void>;
1188
+ renameSession(principal: Principal, sessionId: string, title: string): Promise<{
1027
1189
  title: string;
1028
- }): Promise<unknown>;
1029
- delete(request: {
1030
- workspaceId: string;
1031
- }): Promise<{
1032
- deleted: boolean;
1033
- }>;
1034
- archiveSession(request: {
1035
- sessionId: string;
1036
- }): Promise<{
1037
- archivedSessionIds: readonly string[];
1190
+ seq: number;
1038
1191
  }>;
1039
- }
1040
- interface WorkspaceRegistryLike {
1041
- list(): readonly {
1042
- id: string;
1043
- path: string;
1044
- title: string;
1045
- createdAt: string;
1046
- updatedAt: string;
1047
- sessionIds?: readonly string[];
1048
- }[];
1049
- }
1050
- interface SettingsControllerLike {
1051
- describe(): {
1052
- writable: boolean;
1053
- hasDocument: boolean;
1054
- namespaces: readonly unknown[];
1055
- };
1056
- mutate(ns: string, ops: readonly unknown[], expectedRevision: number | undefined): Promise<unknown>;
1057
- }
1058
- interface CredentialsControllerLike {
1059
- describe(refs: readonly string[]): Promise<Record<string, unknown>>;
1060
- set(ref: string, value: string): Promise<void>;
1061
- unset(ref: string): Promise<void>;
1062
- }
1063
- interface AgentPresetsLike {
1064
- list(): Promise<readonly unknown[]>;
1065
- remoteExportList(): Promise<unknown>;
1066
- resolveAgent?: never;
1067
- select(agent: unknown, agentPreset: string): Promise<string>;
1068
- }
1069
- interface LlmLike {
1070
- /** Live routes, as `{ id, name }` rows. */
1071
- listProviders(): readonly {
1072
- id: string;
1073
- }[];
1074
- /**
1075
- * Every route an adapter plugin could activate through configuration, live or
1076
- * dormant, carrying the settings namespace and path that configures it.
1077
- * Configuration surfaces merge this with `listProviders()` for liveness.
1078
- */
1079
- listConfigurableProviders(): readonly {
1080
- provider: string;
1081
- displayName: string;
1082
- settingsNs: string;
1083
- settingsPath: readonly string[];
1084
- declared?: boolean;
1085
- error?: string;
1086
- }[];
1087
- discoverModels(settingsNs: string, request: unknown, signal?: AbortSignal): Promise<readonly unknown[]>;
1088
- }
1089
- /** Host services the bridge consumes; assembled by `src/index.ts` from `ctx`. */
1090
- interface Dsh02HostServices {
1091
- sessionController: SessionControllerLike;
1092
- workspaceController: WorkspaceControllerLike;
1093
- workspaceRegistry: WorkspaceRegistryLike;
1094
- settingsController: SettingsControllerLike;
1095
- credentialsController: CredentialsControllerLike;
1096
- agentPresets: AgentPresetsLike;
1097
- llm: LlmLike;
1192
+ forkSession(principal: Principal, sessionId: string, atSeq?: number): Promise<string>;
1193
+ archiveSession(principal: Principal, sessionId: string): Promise<string[]>;
1194
+ listPendingApprovals(principal: Principal, sessionId: string): Promise<PendingApprovalView[]>;
1195
+ decideApproval(principal: Principal, sessionId: string, approvalId: string, outcome: 'allowed-once' | 'rejected'): Promise<PendingApprovalView>;
1098
1196
  /**
1099
- * 内核附件存储(`ctx.attachments`);会话历史里的图片要经它取字节。
1197
+ * 列出「agent 正在等、且还能回答」的用户问题。
1100
1198
  *
1101
- * 必须是**已经取到的服务对象**,不能是 `ctx` 本身:cordis 的注入属性是有生命周期的
1102
- * 惰性访问器,只在 `apply` 执行期间可读。把它存进 `host` 里、等到 HTTP 请求回调
1103
- * (`readAttachment`)里再读,就会撞上
1104
- * `cannot get property "attachments" without inject` —— 请求链路本身没问题,
1105
- * 挂在服务端、而且只在真的去取图时才炸,所以很难在本地测出来。
1199
+ * 口径与 [listPendingApprovals] 一致:先 requireSession(会话对本设备不可见 → 404),
1200
+ * 再问内核;字段一个都不裁 —— 客户端照着渲染,内核加字段不用改插件。
1106
1201
  */
1107
- attachments: AttachmentStoreLike;
1202
+ listUserQuestions(principal: Principal, sessionId: string): Promise<UserQuestionView[]>;
1108
1203
  /**
1109
- * Kernel file-upload service (`ctx.fileUploads`).
1204
+ * 把一次答案提交给内核。
1110
1205
  *
1111
- * 为什么用它而不是内核那条 `/api/session/uploadFileBinary` HTTP 路由:那条路由
1112
- * 只绑 loopback(手机根本到不了),而这个 cordis 服务就在同一个进程里,插件可直接调。
1206
+ * 失败原因全部由 `api.userQuestions.answer` 以**值**给出(见 [UserQuestionAnswerOutcome]),
1207
+ * 这里翻成 HTTP 语义:503 能力缺失 / 409 状态冲突 / 400 批次不合法。
1208
+ * 为什么不在 bridge 里直接抛:`unwrapDsh` 只把 `not-found` / `busy` / `locked`
1209
+ * 映成 404/409,其余一律 400 —— 表达不了这几种区别。
1113
1210
  */
1114
- fileUploads: FileUploadsLike;
1211
+ answerUserQuestion(principal: Principal, sessionId: string, callId: string, answer: UserQuestionAnswer): Promise<void>;
1212
+ models(principal: Principal, sessionId: string): Promise<SessionModelsView>;
1213
+ selectModel(principal: Principal, sessionId: string, selection: ModelSelectionView): Promise<ModelSelectionView>;
1214
+ listCommands(principal: Principal, sessionId: string): Promise<CommandDescriptorView[]>;
1215
+ executeCommand(principal: Principal, sessionId: string, line: string): Promise<CommandExecutionView>;
1216
+ isSessionAuthorized(principal: Principal, sessionId: string): Promise<boolean>;
1217
+ startEvents(signal: AbortSignal, emit: (event: WorkspaceEvent) => void): void;
1218
+ private captureApproval;
1219
+ private hasPendingApproval;
1115
1220
  /**
1116
- * Cordis event bus of the host context. Used for the two facts the old
1117
- * `apiProxy` event streams carried that no controller returns: session
1118
- * registry changes and the approval answerer waterfall.
1221
+ * Resolve the Agent backing a session for command work.
1222
+ *
1223
+ * Returns `undefined` when the session exists but cannot be activated. 0.2.x
1224
+ * rejects activation for a Session owned by a subagent, so that outcome is a
1225
+ * property of the session rather than a failure of this call: the caller
1226
+ * decides whether it means "no commands" (listing) or an error (execution).
1119
1227
  */
1120
- on(event: string, listener: (...args: any[]) => unknown): () => void;
1228
+ private commandAgent;
1229
+ private requireSession;
1230
+ private requireWorkspace;
1231
+ private visibleWorkspaces;
1232
+ private requireAuthorizedRoot;
1233
+ private authorizedRoots;
1234
+ private sessionRootId;
1121
1235
  }
1122
1236
  //#endregion
1123
1237
  //#region src/host/rate-limit.d.ts