@openma/deepseek-harness-acp 0.4.5 → 0.4.7

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/dist/bridge.js CHANGED
@@ -22,7 +22,6 @@ import { createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
22
22
  import { SessionId } from "@deepseek-ai/dsh-session";
23
23
  import { foldSessionTitle } from "@deepseek-ai/dsh-session-title";
24
24
  import { SANDBOX_MODES, setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
25
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
26
25
  import * as agentModule from "@deepseek-ai/dsh-agent";
27
26
  import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
28
27
  function selfHarness() {
@@ -35,7 +34,6 @@ function selfHarness() {
35
34
  foldSessionTitle,
36
35
  setSandboxMode,
37
36
  sandboxModes: SANDBOX_MODES,
38
- credentialRef,
39
37
  ...installModelSelection !== void 0 ? { installModelSelection } : {},
40
38
  mcpClient
41
39
  };
@@ -56,11 +54,245 @@ import {
56
54
  PROTOCOL_VERSION,
57
55
  RequestError
58
56
  } from "@agentclientprotocol/sdk";
57
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
59
58
 
60
59
  // src/version.ts
61
60
  import { createRequire } from "node:module";
62
61
  var VERSION = createRequire(import.meta.url)("../package.json").version;
63
62
 
63
+ // src/auth.ts
64
+ var LOGIN_DESCRIPTION = "Save an API key to the harness credential store shared with the dsh Web UI";
65
+ function isDeepseekRoute(provider) {
66
+ return provider === void 0 || provider === "deepseek-official" || provider === "deepseek";
67
+ }
68
+ function credentialEnvNames(provider) {
69
+ if (provider === void 0 || isDeepseekRoute(provider)) return ["DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL"];
70
+ if (provider === "anthropic") return ["ANTHROPIC_API_KEY"];
71
+ if (provider === "openai" || provider.startsWith("openai")) return ["OPENAI_API_KEY"];
72
+ return [`${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_API_KEY`];
73
+ }
74
+ function primaryCredentialName(provider) {
75
+ return credentialEnvNames(provider)[0] ?? "DEEPSEEK_API_KEY";
76
+ }
77
+ function providerFromAuthMethodId(methodId) {
78
+ if (!methodId) return void 0;
79
+ for (const prefix of ["api-key:", "terminal-login:"]) {
80
+ if (methodId.startsWith(prefix)) return methodId.slice(prefix.length);
81
+ }
82
+ return void 0;
83
+ }
84
+ function apiKeyFromAuthenticate(params) {
85
+ const meta = params._meta;
86
+ const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta["api-key"] : void 0;
87
+ const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
88
+ const key = typeof record?.apiKey === "string" ? record.apiKey.trim() : void 0;
89
+ const providerFromMeta = typeof record?.provider === "string" ? record.provider : void 0;
90
+ const provider = providerFromMeta || providerFromAuthMethodId(params.methodId);
91
+ return {
92
+ ...provider && provider.length > 0 ? { provider } : {},
93
+ ...key && key.length > 0 ? { key } : {}
94
+ };
95
+ }
96
+ function gatewayFromAuthenticate(params) {
97
+ const meta = params._meta;
98
+ const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.gateway : void 0;
99
+ const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
100
+ const baseUrl = typeof record?.baseUrl === "string" ? record.baseUrl.trim() : void 0;
101
+ const headers = stringRecord(record?.headers);
102
+ const providerName = typeof record?.providerName === "string" ? record.providerName.trim() : void 0;
103
+ const key = apiKeyFromHeaders(headers);
104
+ return {
105
+ ...baseUrl && baseUrl.length > 0 ? { baseUrl } : {},
106
+ ...headers ? { headers } : {},
107
+ ...providerName && providerName.length > 0 ? { providerName } : {},
108
+ ...key && key.length > 0 ? { key } : {}
109
+ };
110
+ }
111
+ function isBrowserAuthMethod(methodId) {
112
+ return methodId === "browser";
113
+ }
114
+ function isGatewayAuthMethod(methodId) {
115
+ return methodId === "gateway";
116
+ }
117
+ function credentialBaseUrlName(provider) {
118
+ if (isDeepseekRoute(provider) || provider === void 0) return "DEEPSEEK_BASE_URL";
119
+ if (provider === "anthropic") return "ANTHROPIC_BASE_URL";
120
+ if (provider === "openai" || provider.startsWith("openai")) return "OPENAI_BASE_URL";
121
+ return `${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_BASE_URL`;
122
+ }
123
+ function advertisedAuthMethods(providers, clientCapabilities, env = process.env) {
124
+ const methods = [...apiKeyAuthMethods(providers)];
125
+ if (shouldOfferLocalAuthPage(env)) methods.push(browserAuthMethod());
126
+ if (clientCapabilities?.auth?._meta?.["gateway"] === true) methods.push(gatewayAuthMethod());
127
+ return methods;
128
+ }
129
+ function apiKeyAuthMethods(providers) {
130
+ if (providers.length <= 1) {
131
+ const provider = providers[0];
132
+ return [apiKeyAuthMethod(provider?.id, provider?.name, providers.length === 0)];
133
+ }
134
+ return providers.map((provider) => apiKeyAuthMethod(provider.id, provider.name, false));
135
+ }
136
+ function apiKeyAuthMethod(provider, name3, unscoped) {
137
+ const scoped = !unscoped && provider !== void 0;
138
+ return {
139
+ id: scoped ? `api-key:${provider}` : "api-key",
140
+ name: scoped ? `${name3 ?? provider} API key` : "API Key",
141
+ description: LOGIN_DESCRIPTION,
142
+ _meta: { "api-key": scoped ? { provider } : {} }
143
+ };
144
+ }
145
+ function shouldOfferLocalAuthPage(env = process.env) {
146
+ if (env["NO_BROWSER"]) return false;
147
+ if (env["NODE_ENV"] === "test" || env["CI"] === "true" || env["CI"] === "1") return false;
148
+ if (env["DSH_ACP_AUTH_PAGE"] === "0") return false;
149
+ return true;
150
+ }
151
+ function browserAuthMethod() {
152
+ return {
153
+ id: "browser",
154
+ name: "Browser",
155
+ description: "Open a local sign-in page to save an API key. The secret never travels over ACP."
156
+ };
157
+ }
158
+ function gatewayAuthMethod() {
159
+ return {
160
+ id: "gateway",
161
+ name: "Custom model gateway",
162
+ description: "Use a custom OpenAI-compatible gateway",
163
+ _meta: { gateway: { protocol: "openai", restartRequired: "false" } }
164
+ };
165
+ }
166
+ function stringRecord(value) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
168
+ const entries = Object.entries(value).filter(
169
+ (entry) => typeof entry[1] === "string" && entry[1].trim().length > 0
170
+ );
171
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
172
+ }
173
+ function apiKeyFromHeaders(headers) {
174
+ if (!headers) return void 0;
175
+ const authorization = headers["Authorization"] ?? headers["authorization"];
176
+ if (authorization) {
177
+ const bearer = authorization.match(/^Bearer\s+(.+)$/i);
178
+ const token = (bearer?.[1] ?? authorization).trim();
179
+ if (token.length > 0) return token;
180
+ }
181
+ const first = Object.values(headers).find((value) => value.trim().length > 0);
182
+ return first?.trim();
183
+ }
184
+
185
+ // src/auth-page.ts
186
+ import { createServer } from "node:http";
187
+ import { execFile } from "node:child_process";
188
+ function startLocalAuthPage(options) {
189
+ return new Promise((resolve, reject) => {
190
+ const server = createServer((request, response) => {
191
+ void handle(request, response);
192
+ });
193
+ let settle;
194
+ let fail;
195
+ const completed = new Promise((res, rej) => {
196
+ settle = res;
197
+ fail = rej;
198
+ });
199
+ const timeoutMs = options.timeoutMs ?? 10 * 6e4;
200
+ const timer = setTimeout(() => {
201
+ server.close();
202
+ fail?.(new Error("local auth page timed out"));
203
+ }, timeoutMs);
204
+ timer.unref?.();
205
+ const handle = async (request, response) => {
206
+ if (request.method === "GET" || request.method === void 0) {
207
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
208
+ response.end(authPageHtml(options.credentialName));
209
+ return;
210
+ }
211
+ if (request.method === "POST") {
212
+ const body = await readBody(request);
213
+ const key = new URLSearchParams(body).get("api-key")?.trim() ?? "";
214
+ if (key.length === 0) {
215
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
216
+ response.end(authPageHtml(options.credentialName, "Enter an API key."));
217
+ return;
218
+ }
219
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
220
+ response.end(
221
+ "<!doctype html><html><body><p>Saved. You can return to the client.</p></body></html>"
222
+ );
223
+ clearTimeout(timer);
224
+ server.close();
225
+ settle?.(key);
226
+ return;
227
+ }
228
+ response.writeHead(405);
229
+ response.end();
230
+ };
231
+ server.once("error", (error) => {
232
+ clearTimeout(timer);
233
+ reject(error);
234
+ });
235
+ server.listen(0, "127.0.0.1", () => {
236
+ const address = server.address();
237
+ if (!address || typeof address === "string") {
238
+ server.close();
239
+ reject(new Error("local auth page failed to bind"));
240
+ return;
241
+ }
242
+ const url = `http://127.0.0.1:${address.port}/`;
243
+ resolve({
244
+ url,
245
+ close: () => {
246
+ clearTimeout(timer);
247
+ server.close();
248
+ },
249
+ completed
250
+ });
251
+ });
252
+ });
253
+ }
254
+ function openLocalAuthPage(url) {
255
+ const platform = process.platform;
256
+ if (platform === "darwin") {
257
+ execFile("open", [url], () => void 0);
258
+ return;
259
+ }
260
+ if (platform === "win32") {
261
+ execFile("cmd", ["/c", "start", "", url], () => void 0);
262
+ return;
263
+ }
264
+ execFile("xdg-open", [url], () => void 0);
265
+ }
266
+ function authPageHtml(credentialName, error) {
267
+ const message = error ? `<p>${escapeHtml(error)}</p>` : "";
268
+ return `<!doctype html>
269
+ <html lang="en">
270
+ <head><meta charset="utf-8"><title>DeepSeek Harness sign in</title></head>
271
+ <body>
272
+ <h1>Save API key</h1>
273
+ <p>Stored as ${escapeHtml(credentialName)} in the harness credential store.</p>
274
+ ${message}
275
+ <form method="post">
276
+ <label>API key <input type="password" name="api-key" autocomplete="off" required></label>
277
+ <button type="submit">Save</button>
278
+ </form>
279
+ </body>
280
+ </html>`;
281
+ }
282
+ function escapeHtml(value) {
283
+ return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char] ?? char);
284
+ }
285
+ function readBody(request) {
286
+ return new Promise((resolve, reject) => {
287
+ const chunks = [];
288
+ request.on("data", (chunk) => {
289
+ chunks.push(Buffer.from(chunk));
290
+ });
291
+ request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
292
+ request.on("error", reject);
293
+ });
294
+ }
295
+
64
296
  // src/log.ts
65
297
  var PREFIX = "[dsh-acp]";
66
298
  function logWarn(message) {
@@ -212,7 +444,7 @@ function extractMessageTexts(message) {
212
444
  }
213
445
  function planEntries(data) {
214
446
  const todos = data["todos"];
215
- if (!Array.isArray(todos)) return [];
447
+ if (!Array.isArray(todos)) return void 0;
216
448
  const entries = [];
217
449
  for (const todo of todos) {
218
450
  if (todo === null || typeof todo !== "object") continue;
@@ -255,6 +487,27 @@ function emptyWindow() {
255
487
  error: void 0
256
488
  };
257
489
  }
490
+ var TOOL_KINDS = /* @__PURE__ */ new Set([
491
+ "read",
492
+ "edit",
493
+ "delete",
494
+ "move",
495
+ "search",
496
+ "execute",
497
+ "think",
498
+ "fetch",
499
+ "switch_mode",
500
+ "other"
501
+ ]);
502
+ function factsFromPresentCall(name3, rawArguments, presented) {
503
+ const fallback = classifyToolCall(name3, rawArguments);
504
+ const kind = presented.kind !== void 0 && TOOL_KINDS.has(presented.kind) ? presented.kind : fallback.kind;
505
+ const title = presented.title !== void 0 && presented.title.length > 0 ? presented.title : fallback.title;
506
+ const locations = presented.locations !== void 0 && presented.locations.length > 0 ? presented.locations.map(
507
+ (location) => location.line === void 0 ? { path: location.path } : { path: location.path, line: location.line }
508
+ ) : fallback.locations;
509
+ return { kind, title, locations, rawInput: fallback.rawInput };
510
+ }
258
511
  var SessionProjection = class {
259
512
  /** Context capacity from the latest `request/context` event, when known. */
260
513
  contextWindow;
@@ -269,10 +522,14 @@ var SessionProjection = class {
269
522
  terminalOutput;
270
523
  /** Session working directory, advertised on display terminals. */
271
524
  cwd;
525
+ presentCall;
526
+ subagent;
272
527
  constructor(contextWindow, options) {
273
528
  this.contextWindow = contextWindow;
274
529
  this.terminalOutput = options?.terminalOutput ?? false;
275
530
  this.cwd = options?.cwd;
531
+ this.presentCall = options?.presentCall;
532
+ this.subagent = options?.subagent;
276
533
  }
277
534
  /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
278
535
  beginPrompt() {
@@ -307,6 +564,11 @@ var SessionProjection = class {
307
564
  * Project one session event onto zero or more ACP updates.
308
565
  */
309
566
  onEvent(event) {
567
+ const updates = this.projectEvent(event);
568
+ return this.subagent === void 0 ? updates : updates.map((update) => this.withSubagentAttribution(update));
569
+ }
570
+ /** Project one event before optional subagent attribution is attached. */
571
+ projectEvent(event) {
310
572
  const data = event.data ?? {};
311
573
  switch (event.type) {
312
574
  case "assistant/chunk":
@@ -317,9 +579,34 @@ var SessionProjection = class {
317
579
  return this.onToolCall(data);
318
580
  case "tool/result":
319
581
  return this.onToolResult(data);
582
+ case "subagent/start":
583
+ return this.onSubagentStart(data);
584
+ case "subagent/end":
585
+ return this.onSubagentEnd(data);
320
586
  case "todo/write": {
321
587
  const entries = planEntries(data);
322
- return entries.length > 0 ? [{ sessionUpdate: "plan", entries }] : [];
588
+ return entries !== void 0 ? [{ sessionUpdate: "plan", entries }] : [];
589
+ }
590
+ case "user/message": {
591
+ const source = data["source"];
592
+ const kind = source !== null && typeof source === "object" ? asString(source["kind"]) : void 0;
593
+ if (kind === void 0 || kind === "user") return [];
594
+ const content = data["content"];
595
+ const text = Array.isArray(content) ? content.filter(
596
+ (block) => block !== null && typeof block === "object" && block["type"] === "text" && typeof block["text"] === "string"
597
+ ).map((block) => block["text"]).join("") : "";
598
+ return [
599
+ {
600
+ sessionUpdate: "session_info_update",
601
+ _meta: {
602
+ dsh: {
603
+ event: "user/message",
604
+ source: kind,
605
+ preview: Array.from(text).slice(0, 160).join("")
606
+ }
607
+ }
608
+ }
609
+ ];
323
610
  }
324
611
  case "turn/end":
325
612
  return this.onTurnEnd(data);
@@ -341,31 +628,117 @@ var SessionProjection = class {
341
628
  stepKey(data) {
342
629
  return `${String(data["turn"])}:${String(data["step"])}`;
343
630
  }
631
+ withSubagentAttribution(update) {
632
+ const value = update;
633
+ const meta = value["_meta"] !== null && typeof value["_meta"] === "object" ? value["_meta"] : {};
634
+ const dsh = meta["dsh"] !== null && typeof meta["dsh"] === "object" ? meta["dsh"] : {};
635
+ return {
636
+ ...value,
637
+ _meta: { ...meta, dsh: { ...dsh, subagent: this.subagent } }
638
+ };
639
+ }
640
+ subagentMeta(data, state) {
641
+ const runId = asString(data["runId"]);
642
+ const childSessionId = asString(data["id"]);
643
+ const provider = asString(data["provider"]);
644
+ if (runId === void 0 || childSessionId === void 0 || provider === void 0) return void 0;
645
+ const parentToolCallId = asString(data["parentToolCallId"]);
646
+ return {
647
+ state,
648
+ runId,
649
+ childSessionId,
650
+ provider,
651
+ local: data["local"] === true,
652
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {}
653
+ };
654
+ }
655
+ onSubagentStart(data) {
656
+ const subagent = this.subagentMeta(data, "started");
657
+ if (subagent === void 0) return [];
658
+ const runId = subagent["runId"];
659
+ const childSessionId = subagent["childSessionId"];
660
+ return [{
661
+ sessionUpdate: "tool_call",
662
+ toolCallId: `subagent:${runId}`,
663
+ title: `Start subagent ${childSessionId}`,
664
+ kind: "other",
665
+ status: "in_progress",
666
+ rawInput: {
667
+ childSessionId,
668
+ provider: subagent["provider"],
669
+ local: subagent["local"]
670
+ },
671
+ _meta: { dsh: { subagent } }
672
+ }];
673
+ }
674
+ onSubagentEnd(data) {
675
+ const subagent = this.subagentMeta(data, "finished");
676
+ if (subagent === void 0) return [];
677
+ const stopReason = asString(data["stopReason"]) ?? "error";
678
+ const lastAssistantMessage = data["lastAssistantMessage"];
679
+ return [{
680
+ sessionUpdate: "tool_call_update",
681
+ toolCallId: `subagent:${String(subagent["runId"])}`,
682
+ status: stopReason === "completed" ? "completed" : "failed",
683
+ rawOutput: {
684
+ stopReason,
685
+ ...Array.isArray(lastAssistantMessage) ? { lastAssistantMessage } : {}
686
+ },
687
+ _meta: { dsh: { subagent } }
688
+ }];
689
+ }
344
690
  onChunk(data) {
345
691
  const chunk = data["chunk"];
346
692
  if (chunk === null || typeof chunk === "undefined" || typeof chunk !== "object") return [];
347
693
  const c = chunk;
348
694
  const text = c["text"];
349
695
  if (typeof text !== "string" || text.length === 0) return [];
696
+ const messageId = this.stepKey(data);
350
697
  if (c["type"] === "text-delta") {
351
- this.streamedText.add(this.stepKey(data));
352
- return [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }];
698
+ this.streamedText.add(messageId);
699
+ return [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text }, messageId }];
353
700
  }
354
701
  if (c["type"] === "reasoning-delta") {
355
- this.streamedReasoning.add(this.stepKey(data));
356
- return [{ sessionUpdate: "agent_thought_chunk", content: { type: "text", text } }];
702
+ this.streamedReasoning.add(messageId);
703
+ return [{ sessionUpdate: "agent_thought_chunk", content: { type: "text", text }, messageId }];
357
704
  }
358
705
  return [];
359
706
  }
360
707
  onAssistantMessage(data) {
361
708
  const updates = [];
362
709
  const key = this.stepKey(data);
363
- const { text, reasoning } = extractMessageTexts(data["message"]);
710
+ const message = data["message"];
711
+ const { text, reasoning } = extractMessageTexts(message);
712
+ const model = message !== null && typeof message === "object" ? asString(
713
+ message["source"] !== null && typeof message["source"] === "object" ? message["source"]["model"] : void 0
714
+ ) : void 0;
715
+ const completionMeta = {
716
+ dsh: {
717
+ event: "assistant_message",
718
+ ...model !== void 0 ? { model } : {}
719
+ }
720
+ };
364
721
  if (reasoning.length > 0 && !this.streamedReasoning.has(key)) {
365
- updates.push({ sessionUpdate: "agent_thought_chunk", content: { type: "text", text: reasoning } });
722
+ updates.push({
723
+ sessionUpdate: "agent_thought_chunk",
724
+ content: { type: "text", text: reasoning },
725
+ messageId: key
726
+ });
366
727
  }
367
728
  if (text.length > 0 && !this.streamedText.has(key)) {
368
- updates.push({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
729
+ updates.push({
730
+ sessionUpdate: "agent_message_chunk",
731
+ content: { type: "text", text },
732
+ messageId: key,
733
+ _meta: completionMeta
734
+ });
735
+ } else {
736
+ updates.push({
737
+ sessionUpdate: "agent_message_chunk",
738
+ content: { type: "text", text: "" },
739
+ messageId: key,
740
+ _meta: completionMeta
741
+ });
369
742
  }
370
743
  this.streamedText.delete(key);
371
744
  this.streamedReasoning.delete(key);
@@ -399,7 +772,14 @@ var SessionProjection = class {
399
772
  const name3 = asString(data["name"]) ?? "tool";
400
773
  if (callId === void 0) return [];
401
774
  const rawArguments = typeof data["arguments"] === "string" ? data["arguments"] : "{}";
402
- const facts = classifyToolCall(name3, rawArguments);
775
+ let parsedArgs = {};
776
+ try {
777
+ parsedArgs = JSON.parse(rawArguments);
778
+ } catch {
779
+ parsedArgs = { arguments: rawArguments };
780
+ }
781
+ const presented = this.presentCall?.(name3, parsedArgs);
782
+ const facts = presented !== void 0 ? factsFromPresentCall(name3, rawArguments, presented) : classifyToolCall(name3, rawArguments);
403
783
  this.toolCalls.set(callId, facts);
404
784
  const displayTerminal = this.terminalOutput && facts.kind === "execute";
405
785
  return [
@@ -504,7 +884,7 @@ ${text.replace(/\n+$/, "")}
504
884
  };
505
885
 
506
886
  // src/bridge/history.ts
507
- function userText(data) {
887
+ function userMessage(data) {
508
888
  if (data === void 0) return void 0;
509
889
  const source = data["source"];
510
890
  const kind = source !== null && typeof source === "object" ? source["kind"] : void 0;
@@ -519,20 +899,28 @@ function userText(data) {
519
899
  }
520
900
  }
521
901
  const text = parts.join("");
522
- return text.length > 0 ? text : void 0;
902
+ return text.length > 0 ? { text } : {};
523
903
  }
524
904
  function buildReplay(events) {
525
905
  const projection = new SessionProjection();
526
906
  const updates = [];
527
907
  let plan;
528
908
  let title;
909
+ let sawUserInteraction = false;
529
910
  for (const event of events) {
530
911
  if (event.type === "user/message") {
531
- const text = userText(event.data);
532
- if (text !== void 0) {
533
- updates.push({ sessionUpdate: "user_message_chunk", content: { type: "text", text } });
912
+ const user = userMessage(event.data);
913
+ if (user !== void 0) {
914
+ projection.beginPrompt();
915
+ sawUserInteraction = true;
916
+ if (user.text !== void 0) {
917
+ updates.push({
918
+ sessionUpdate: "user_message_chunk",
919
+ content: { type: "text", text: user.text }
920
+ });
921
+ }
922
+ continue;
534
923
  }
535
- continue;
536
924
  }
537
925
  if (event.type === "session/title") {
538
926
  const t = event.data?.["title"];
@@ -549,25 +937,132 @@ function buildReplay(events) {
549
937
  }
550
938
  }
551
939
  if (plan !== void 0) updates.push(plan);
940
+ const usage = sawUserInteraction ? projection.promptUsage() : void 0;
941
+ if (usage !== void 0) {
942
+ updates.push({
943
+ sessionUpdate: "session_info_update",
944
+ _meta: { dsh: { event: "prompt/usage", usage } }
945
+ });
946
+ }
552
947
  return { updates, title, contextWindow: projection.contextWindow };
553
948
  }
554
949
 
555
950
  // src/bridge/prompt.ts
951
+ function deliverPrompt(target, message, running) {
952
+ if (running) {
953
+ target.steer(message);
954
+ return "steer";
955
+ }
956
+ target.followup(message);
957
+ return "followup";
958
+ }
556
959
  var UnsupportedPromptContentError = class extends Error {
557
960
  constructor(contentType) {
558
961
  super(`unsupported prompt content type: ${contentType}`);
559
962
  this.contentType = contentType;
963
+ this.name = "UnsupportedPromptContentError";
560
964
  }
561
965
  contentType;
562
966
  };
967
+ var PromptImageError = class extends Error {
968
+ constructor(message, options) {
969
+ super(message, options);
970
+ this.name = "PromptImageError";
971
+ }
972
+ };
563
973
  function contextBlock(uri, text) {
564
974
  return `<context ref=${JSON.stringify(uri)}>
565
975
  ${text}
566
976
  </context>`;
567
977
  }
568
- function convertPrompt(prompt) {
978
+ var IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
979
+ function canonicalImageMediaType(mimeType) {
980
+ const lower = mimeType.trim().toLowerCase();
981
+ const mapped = lower === "image/jpg" ? "image/jpeg" : lower;
982
+ return IMAGE_MEDIA_TYPES.has(mapped) ? mapped : void 0;
983
+ }
984
+ function attachmentIngestOf(value) {
985
+ if (value === null || typeof value !== "object") return void 0;
986
+ const candidate = value;
987
+ if (typeof candidate.validateImage !== "function" || typeof candidate.saveImage !== "function") {
988
+ return void 0;
989
+ }
990
+ const limits = candidate.imageLimits;
991
+ if (limits === void 0 || typeof limits.maxImagesPerMessage !== "number" || typeof limits.maxMessageImageBytes !== "number" || typeof limits.maxImageBytes !== "number") {
992
+ return void 0;
993
+ }
994
+ return candidate;
995
+ }
996
+ function decodeImageData(data) {
997
+ if (data.length === 0) throw new PromptImageError("image data is empty");
998
+ const decoded = Buffer.from(data, "base64");
999
+ if (decoded.byteLength === 0) throw new PromptImageError("image data is empty");
1000
+ return new Uint8Array(decoded);
1001
+ }
1002
+ function imageName(uri) {
1003
+ if (typeof uri !== "string" || uri.length === 0) return void 0;
1004
+ let leaf;
1005
+ try {
1006
+ leaf = new URL(uri).pathname.split("/").filter(Boolean).at(-1);
1007
+ } catch {
1008
+ leaf = uri.split(/[/\\]/).filter(Boolean).at(-1);
1009
+ }
1010
+ if (leaf === void 0 || leaf.length === 0) return void 0;
1011
+ try {
1012
+ return decodeURIComponent(leaf);
1013
+ } catch {
1014
+ return leaf;
1015
+ }
1016
+ }
1017
+ function prepareImage(block) {
1018
+ const mediaType = canonicalImageMediaType(block.mimeType);
1019
+ if (mediaType === void 0) {
1020
+ throw new PromptImageError(`unsupported image media type: ${block.mimeType}`);
1021
+ }
1022
+ const data = decodeImageData(block.data);
1023
+ const name3 = imageName(block.uri);
1024
+ return name3 === void 0 ? { data, mediaType } : { data, mediaType, name: name3 };
1025
+ }
1026
+ function saveInput(image) {
1027
+ return image.name === void 0 ? { data: image.data, mediaType: image.mediaType } : { data: image.data, mediaType: image.mediaType, name: image.name };
1028
+ }
1029
+ function flushText(parts, blocks) {
1030
+ const text = parts.join("");
1031
+ parts.length = 0;
1032
+ if (text.length > 0) blocks.push({ type: "text", text });
1033
+ }
1034
+ async function convertPrompt(prompt, attachments) {
1035
+ const preparedImages = [];
1036
+ for (const block of prompt) {
1037
+ if (block.type !== "image") continue;
1038
+ if (attachments === void 0) throw new UnsupportedPromptContentError("image");
1039
+ preparedImages.push(prepareImage(block));
1040
+ }
1041
+ if (preparedImages.length > 0 && attachments !== void 0) {
1042
+ const { maxImagesPerMessage, maxMessageImageBytes, maxImageBytes } = attachments.imageLimits;
1043
+ if (preparedImages.length > maxImagesPerMessage) {
1044
+ throw new PromptImageError("prompt exceeds the configured image-count limit");
1045
+ }
1046
+ const totalBytes = preparedImages.reduce((sum, image) => sum + image.data.byteLength, 0);
1047
+ if (totalBytes > maxMessageImageBytes) {
1048
+ throw new PromptImageError("prompt exceeds the configured aggregate image-byte limit");
1049
+ }
1050
+ for (const image of preparedImages) {
1051
+ if (image.data.byteLength > maxImageBytes) {
1052
+ throw new PromptImageError("image exceeds the configured encoded-byte limit");
1053
+ }
1054
+ try {
1055
+ await attachments.validateImage(saveInput(image));
1056
+ } catch (error) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ throw new PromptImageError(message, { cause: error });
1059
+ }
1060
+ }
1061
+ }
569
1062
  const parts = [];
570
1063
  const display = [];
1064
+ const blocks = [];
1065
+ let imageIndex = 0;
571
1066
  for (const block of prompt) {
572
1067
  switch (block.type) {
573
1068
  case "text":
@@ -591,20 +1086,535 @@ ${contextBlock(resource.uri, resource.text)}
591
1086
  }
592
1087
  throw new UnsupportedPromptContentError("resource (binary)");
593
1088
  }
1089
+ case "image": {
1090
+ if (attachments === void 0) throw new UnsupportedPromptContentError("image");
1091
+ const prepared = preparedImages[imageIndex];
1092
+ imageIndex += 1;
1093
+ if (prepared === void 0) throw new PromptImageError("image block was not prepared");
1094
+ flushText(parts, blocks);
1095
+ let attachment;
1096
+ try {
1097
+ attachment = await attachments.saveImage(saveInput(prepared));
1098
+ } catch (error) {
1099
+ const message = error instanceof Error ? error.message : String(error);
1100
+ throw new PromptImageError(message, { cause: error });
1101
+ }
1102
+ blocks.push({ type: "image", attachment });
1103
+ break;
1104
+ }
594
1105
  default:
595
1106
  throw new UnsupportedPromptContentError(block.type);
596
1107
  }
597
1108
  }
598
- const text = parts.join("");
1109
+ flushText(parts, blocks);
599
1110
  return {
600
- blocks: text.length > 0 ? [{ type: "text", text }] : [],
1111
+ blocks,
601
1112
  displayText: display.join(" ").trim()
602
1113
  };
603
1114
  }
604
1115
 
1116
+ // src/bridge/rpc.ts
1117
+ var AcpRpc = class {
1118
+ handlers = /* @__PURE__ */ new Map();
1119
+ writeJson;
1120
+ /**
1121
+ * Bind the mux writer. Called once from {@link muxAcpStream}.
1122
+ * @param writeJson - write one JSON-RPC message onto the ACP stream.
1123
+ */
1124
+ attachWriter(writeJson) {
1125
+ this.writeJson = writeJson;
1126
+ }
1127
+ /**
1128
+ * Register a bridge-owned request method intercepted before the ACP SDK.
1129
+ * @param method - JSON-RPC method name (e.g. `tui/refresh`).
1130
+ * @param handler - implementation.
1131
+ * @returns disposer.
1132
+ */
1133
+ registerMethod(method, handler) {
1134
+ if (this.handlers.has(method)) {
1135
+ throw new Error(`acpRpc: method ${JSON.stringify(method)} is already registered`);
1136
+ }
1137
+ this.handlers.set(method, handler);
1138
+ return () => {
1139
+ this.handlers.delete(method);
1140
+ };
1141
+ }
1142
+ /**
1143
+ * @param method - JSON-RPC method name.
1144
+ * @returns whether a TUI/extension handler owns this request.
1145
+ */
1146
+ has(method) {
1147
+ return this.handlers.has(method);
1148
+ }
1149
+ /**
1150
+ * Dispatch one intercepted request and write the JSON-RPC response.
1151
+ * @param id - JSON-RPC id.
1152
+ * @param method - method name.
1153
+ * @param params - params object.
1154
+ */
1155
+ async dispatch(id, method, params) {
1156
+ const handler = this.handlers.get(method);
1157
+ const write = this.writeJson;
1158
+ if (handler === void 0 || write === void 0) return;
1159
+ try {
1160
+ const result = await handler(params);
1161
+ write({ jsonrpc: "2.0", id, result: result ?? null });
1162
+ } catch (error) {
1163
+ const message = error instanceof Error ? error.message : String(error);
1164
+ write({ jsonrpc: "2.0", id, error: { code: -32e3, message } });
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Send a JSON-RPC notification on the ACP stream (e.g. `tui/snapshot`).
1169
+ * @param method - notification method.
1170
+ * @param params - payload.
1171
+ */
1172
+ notify(method, params) {
1173
+ const write = this.writeJson;
1174
+ if (write === void 0) return;
1175
+ write(params === void 0 ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params });
1176
+ }
1177
+ };
1178
+ function isInterceptedRequest(message, rpc) {
1179
+ return "method" in message && typeof message.method === "string" && "id" in message && message.id !== void 0 && rpc.has(message.method);
1180
+ }
1181
+ function muxAcpStream(stream, rpc) {
1182
+ const dest = stream.writable.getWriter();
1183
+ const writeMessage = (value) => {
1184
+ void dest.write(value).catch(() => {
1185
+ });
1186
+ };
1187
+ rpc.attachWriter(writeMessage);
1188
+ const acpWritable = new WritableStream({
1189
+ write(chunk) {
1190
+ return dest.write(chunk);
1191
+ },
1192
+ close() {
1193
+ return dest.close();
1194
+ },
1195
+ abort(reason) {
1196
+ return dest.abort(reason);
1197
+ }
1198
+ });
1199
+ const { readable, writable } = new TransformStream({
1200
+ transform(message, controller) {
1201
+ if (isInterceptedRequest(message, rpc)) {
1202
+ void rpc.dispatch(message.id, message.method, message.params);
1203
+ return;
1204
+ }
1205
+ controller.enqueue(message);
1206
+ }
1207
+ });
1208
+ void stream.readable.pipeTo(writable).catch(() => {
1209
+ });
1210
+ return { readable, writable: acpWritable };
1211
+ }
1212
+
1213
+ // src/bridge/tui-client.ts
1214
+ function installTuiClientPlane(ctx, rpc, opts) {
1215
+ const { findAgent, advertisement } = opts;
1216
+ ctx.effect(
1217
+ () => rpc.registerMethod("tui/inspect-sync", (params) => {
1218
+ const inspect = optionalInspect(ctx);
1219
+ if (inspect === void 0) {
1220
+ throw new Error("this agent has no Cordis inspect registry");
1221
+ }
1222
+ const providers = readProviders(params);
1223
+ inspect.syncClientManifest(providers);
1224
+ advertisement.advertised = true;
1225
+ return { ok: true };
1226
+ }),
1227
+ "acp-bridge: tui/inspect-sync"
1228
+ );
1229
+ ctx.effect(
1230
+ () => rpc.registerMethod("tui/inspect-resolve", (params) => {
1231
+ const inspect = optionalInspect(ctx);
1232
+ if (inspect === void 0) {
1233
+ throw new Error("this agent has no Cordis inspect registry");
1234
+ }
1235
+ const body = asObject(params);
1236
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1237
+ const requestId = readString(body, "requestId");
1238
+ return inspect.resolveClientQuery(agent, requestId, body["resolution"]);
1239
+ }),
1240
+ "acp-bridge: tui/inspect-resolve"
1241
+ );
1242
+ ctx.effect(
1243
+ () => rpc.registerMethod("tui/run-host-half", async (params) => {
1244
+ const runner = optionalRunner(ctx);
1245
+ if (runner === void 0) {
1246
+ throw new Error("this agent has no dynamic Cordis runner");
1247
+ }
1248
+ const body = asObject(params);
1249
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1250
+ const requestId = body["requestId"];
1251
+ return await runner.runHostHalf(
1252
+ agent,
1253
+ readString(body, "pluginId"),
1254
+ readString(body, "packageId"),
1255
+ readString(body, "mode"),
1256
+ typeof requestId === "string" ? requestId : null,
1257
+ body["approveFutureVersions"] === true
1258
+ );
1259
+ }),
1260
+ "acp-bridge: tui/run-host-half"
1261
+ );
1262
+ ctx.effect(
1263
+ () => rpc.registerMethod("tui/get-client-code", (params) => {
1264
+ const runner = optionalRunner(ctx);
1265
+ if (runner === void 0) {
1266
+ throw new Error("this agent has no dynamic Cordis runner");
1267
+ }
1268
+ const body = asObject(params);
1269
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1270
+ return runner.getClientCode(
1271
+ agent,
1272
+ readString(body, "pluginId"),
1273
+ readString(body, "pluginRunId")
1274
+ );
1275
+ }),
1276
+ "acp-bridge: tui/get-client-code"
1277
+ );
1278
+ ctx.effect(
1279
+ () => rpc.registerMethod("tui/resolve-request-run", async (params) => {
1280
+ const runner = optionalRunner(ctx);
1281
+ if (runner === void 0) {
1282
+ throw new Error("this agent has no dynamic Cordis runner");
1283
+ }
1284
+ const body = asObject(params);
1285
+ return await runner.resolveRequestRun(
1286
+ readString(body, "requestId"),
1287
+ body["resolution"]
1288
+ );
1289
+ }),
1290
+ "acp-bridge: tui/resolve-request-run"
1291
+ );
1292
+ ctx.effect(
1293
+ () => rpc.registerMethod("tui/invoke", async (params) => {
1294
+ const runner = optionalRunner(ctx);
1295
+ if (runner === void 0) {
1296
+ throw new Error("this agent has no dynamic Cordis runner");
1297
+ }
1298
+ const body = asObject(params);
1299
+ requireAgent(findAgent, readString(body, "agentId"));
1300
+ return await runner.invoke(
1301
+ readString(body, "pluginId"),
1302
+ readString(body, "pluginRunId"),
1303
+ readString(body, "method"),
1304
+ body["args"] ?? null
1305
+ );
1306
+ }),
1307
+ "acp-bridge: tui/invoke"
1308
+ );
1309
+ ctx.effect(
1310
+ () => rpc.registerMethod("tui/list-plugins", (params) => {
1311
+ const runner = requireRunnerMethod(ctx, "listPlugins");
1312
+ const body = asObject(params);
1313
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1314
+ return runner.listPlugins(agent);
1315
+ }),
1316
+ "acp-bridge: tui/list-plugins"
1317
+ );
1318
+ ctx.effect(
1319
+ () => rpc.registerMethod("tui/stop-plugin", async (params) => {
1320
+ const runner = requireRunnerMethod(ctx, "stopFromPanel");
1321
+ const body = asObject(params);
1322
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1323
+ return await runner.stopFromPanel(agent, readString(body, "pluginId"));
1324
+ }),
1325
+ "acp-bridge: tui/stop-plugin"
1326
+ );
1327
+ ctx.effect(
1328
+ () => rpc.registerMethod("tui/start-plugin", async (params) => {
1329
+ const runner = requireRunnerMethod(ctx, "listPlugins");
1330
+ const body = asObject(params);
1331
+ const agentId = readString(body, "agentId");
1332
+ const agent = requireAgent(findAgent, agentId);
1333
+ const pluginId = readString(body, "pluginId");
1334
+ const plugin = runner.listPlugins(agent).find((row) => row.pluginId === pluginId);
1335
+ if (plugin === void 0) throw new Error(`dynamic plugin "${pluginId}" does not exist`);
1336
+ const packageId = plugin.currentPackageId;
1337
+ if (packageId === void 0) {
1338
+ throw new Error(`dynamic plugin "${pluginId}" has no current package to restore`);
1339
+ }
1340
+ const pkg = plugin.packages.find((row) => row.packageId === packageId);
1341
+ if (pkg === void 0) {
1342
+ throw new Error(`dynamic plugin "${pluginId}" current package "${packageId}" does not exist`);
1343
+ }
1344
+ const started = asObject(await runner.runHostHalf(
1345
+ agent,
1346
+ pluginId,
1347
+ packageId,
1348
+ "run",
1349
+ null,
1350
+ false
1351
+ ));
1352
+ if (started["ok"] !== true) return started;
1353
+ const pluginRunId = readString(started, "pluginRunId");
1354
+ const startedHere = started["startedHere"] !== false;
1355
+ if (pkg.hasClientHalf) {
1356
+ rpc.notify("tui/user-run", {
1357
+ agentId,
1358
+ pluginId,
1359
+ packageId,
1360
+ pluginRunId,
1361
+ startedHere,
1362
+ mode: "run",
1363
+ hasClientHalf: true
1364
+ });
1365
+ }
1366
+ return { ok: true, status: "starting", pluginId, packageId, pluginRunId };
1367
+ }),
1368
+ "acp-bridge: tui/start-plugin"
1369
+ );
1370
+ ctx.effect(
1371
+ () => rpc.registerMethod("tui/settle-user-run", async (params) => {
1372
+ const runner = requireRunnerMethod(ctx, "settleUserRun");
1373
+ const body = asObject(params);
1374
+ const agent = requireAgent(findAgent, readString(body, "agentId"));
1375
+ return await runner.settleUserRun(
1376
+ agent,
1377
+ readString(body, "pluginId"),
1378
+ body["resolution"]
1379
+ );
1380
+ }),
1381
+ "acp-bridge: tui/settle-user-run"
1382
+ );
1383
+ ctx.on("cordis/inspect-query", (request) => {
1384
+ if (!advertisement.advertised) return;
1385
+ rpc.notify("tui/inspect-query", request);
1386
+ });
1387
+ ctx.on("cordis/inspect-query-resolved", (resolved) => {
1388
+ if (!advertisement.advertised) return;
1389
+ rpc.notify("tui/inspect-query-resolved", resolved);
1390
+ });
1391
+ ctx.on("cordis/request-run", (request) => {
1392
+ if (!advertisement.advertised) return;
1393
+ rpc.notify("tui/request-run", request);
1394
+ });
1395
+ ctx.on("cordis/request-run-resolved", (resolved) => {
1396
+ if (!advertisement.advertised) return;
1397
+ rpc.notify("tui/request-run-resolved", resolved);
1398
+ });
1399
+ ctx.on("cordis/dynamic-retract", (retracted) => {
1400
+ if (!advertisement.advertised) return;
1401
+ rpc.notify("tui/dynamic-retract", retracted);
1402
+ });
1403
+ }
1404
+ function optionalInspect(ctx) {
1405
+ try {
1406
+ const value = ctx.get("cordisInspect");
1407
+ return value !== void 0 && typeof value.syncClientManifest === "function" ? value : void 0;
1408
+ } catch {
1409
+ return void 0;
1410
+ }
1411
+ }
1412
+ function optionalRunner(ctx) {
1413
+ try {
1414
+ const value = ctx.get("dynamicCordisRunner");
1415
+ return value !== void 0 && typeof value.runHostHalf === "function" ? value : void 0;
1416
+ } catch {
1417
+ return void 0;
1418
+ }
1419
+ }
1420
+ function requireRunnerMethod(ctx, method) {
1421
+ const runner = optionalRunner(ctx);
1422
+ if (runner === void 0 || typeof runner[method] !== "function") {
1423
+ throw new Error(`this agent has no dynamic Cordis runner method ${String(method)}`);
1424
+ }
1425
+ return runner;
1426
+ }
1427
+ function requireAgent(findAgent, agentId) {
1428
+ const agent = findAgent(agentId);
1429
+ if (agent === void 0) throw new Error(`unknown agent: ${agentId}`);
1430
+ return agent;
1431
+ }
1432
+ function asObject(value) {
1433
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1434
+ throw new Error("params must be an object");
1435
+ }
1436
+ return value;
1437
+ }
1438
+ function readString(body, field) {
1439
+ const value = body[field];
1440
+ if (typeof value !== "string" || value.length === 0) {
1441
+ throw new Error(`${field} must be a non-empty string`);
1442
+ }
1443
+ return value;
1444
+ }
1445
+ function readProviders(params) {
1446
+ const body = asObject(params);
1447
+ const providers = body["providers"];
1448
+ if (!Array.isArray(providers)) throw new Error("providers must be an array");
1449
+ return providers.map((provider, index) => {
1450
+ if (provider === null || typeof provider !== "object" || Array.isArray(provider)) {
1451
+ throw new Error(`providers[${index}] must be an object`);
1452
+ }
1453
+ const row = provider;
1454
+ if (typeof row["id"] !== "string" || row["id"].trim() === "") {
1455
+ throw new Error(`providers[${index}].id must be a non-empty string`);
1456
+ }
1457
+ if (typeof row["description"] !== "string" || row["description"].trim() === "") {
1458
+ throw new Error(`providers[${index}].description must be a non-empty string`);
1459
+ }
1460
+ if (!Array.isArray(row["methods"])) {
1461
+ throw new Error(`providers[${index}].methods must be an array`);
1462
+ }
1463
+ return provider;
1464
+ });
1465
+ }
1466
+
1467
+ // src/bridge/user-questions.ts
1468
+ import {
1469
+ UserQuestionError
1470
+ } from "@deepseek-ai/dsh-user-questions";
1471
+ var META_KEY = "dsh.userQuestions";
1472
+ function fieldName(index) {
1473
+ return `question_${index}`;
1474
+ }
1475
+ function customFieldName(index) {
1476
+ return `${fieldName(index)}_custom`;
1477
+ }
1478
+ function optionValue(index) {
1479
+ return `option_${index}`;
1480
+ }
1481
+ function customValue(index) {
1482
+ return `custom_${index}`;
1483
+ }
1484
+ function descriptionOf(question) {
1485
+ const parts = [question.header === void 0 ? void 0 : question.question, question.detail].filter((part) => part !== void 0 && part.length > 0);
1486
+ return parts.length === 0 ? void 0 : parts.join("\n\n");
1487
+ }
1488
+ function propertyOf(question, index) {
1489
+ const title = question.header ?? question.question;
1490
+ const description = descriptionOf(question);
1491
+ const options = question.options ?? [];
1492
+ if (options.length === 0) {
1493
+ return {
1494
+ type: "string",
1495
+ title,
1496
+ ...description === void 0 ? {} : { description }
1497
+ };
1498
+ }
1499
+ const choices = [
1500
+ ...options.map((option, optionIndex) => ({
1501
+ const: optionValue(optionIndex),
1502
+ title: option.label,
1503
+ ...option.description === void 0 ? {} : { description: option.description }
1504
+ })),
1505
+ { const: customValue(index), title: "Other" }
1506
+ ];
1507
+ if (question.multiSelect === true) {
1508
+ return {
1509
+ type: "array",
1510
+ title,
1511
+ ...description === void 0 ? {} : { description },
1512
+ minItems: 1,
1513
+ items: { anyOf: choices }
1514
+ };
1515
+ }
1516
+ return {
1517
+ type: "string",
1518
+ title,
1519
+ ...description === void 0 ? {} : { description },
1520
+ oneOf: choices
1521
+ };
1522
+ }
1523
+ function questionsToElicitation(questions, sessionId) {
1524
+ const properties = {};
1525
+ const required = [];
1526
+ questions.forEach((question, index) => {
1527
+ const field = fieldName(index);
1528
+ properties[field] = propertyOf(question, index);
1529
+ if ((question.options?.length ?? 0) > 0) {
1530
+ properties[customFieldName(index)] = {
1531
+ type: "string",
1532
+ title: "Other",
1533
+ description: "Type a custom answer."
1534
+ };
1535
+ }
1536
+ });
1537
+ return {
1538
+ mode: "form",
1539
+ sessionId,
1540
+ message: "The agent needs your input.",
1541
+ requestedSchema: {
1542
+ type: "object",
1543
+ properties,
1544
+ required
1545
+ },
1546
+ _meta: {
1547
+ [META_KEY]: {
1548
+ version: 1,
1549
+ questions
1550
+ }
1551
+ }
1552
+ };
1553
+ }
1554
+ function answerFromElicitation(questions, response) {
1555
+ if (response.action !== "accept") return void 0;
1556
+ const content = response.content ?? {};
1557
+ return {
1558
+ answers: questions.map((question, index) => {
1559
+ const options = question.options ?? [];
1560
+ const raw = content[fieldName(index)];
1561
+ if (options.length === 0) {
1562
+ const custom2 = typeof raw === "string" ? raw.trim() : "";
1563
+ return {
1564
+ id: question.id,
1565
+ selected: [],
1566
+ ...custom2.length === 0 ? {} : { custom: custom2 }
1567
+ };
1568
+ }
1569
+ const values = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
1570
+ const selected = values.flatMap((value) => {
1571
+ const match = /^option_(\d+)$/.exec(value);
1572
+ if (match === null) return [];
1573
+ const option = options[Number(match[1])];
1574
+ return option === void 0 ? [] : [option.label];
1575
+ });
1576
+ const customRaw = content[customFieldName(index)];
1577
+ const custom = typeof customRaw === "string" ? customRaw.trim() : "";
1578
+ return {
1579
+ id: question.id,
1580
+ selected: custom.length > 0 && question.multiSelect !== true ? [] : selected,
1581
+ ...custom.length === 0 ? {} : { custom }
1582
+ };
1583
+ })
1584
+ };
1585
+ }
1586
+ async function askUserQuestionsOverAcp(request, sessionId, create) {
1587
+ const response = await create(questionsToElicitation(request.questions, sessionId));
1588
+ const answer = answerFromElicitation(request.questions, response);
1589
+ if (answer === void 0) {
1590
+ throw new UserQuestionError("the user cancelled ask_user_question", "ASK_CANCELLED");
1591
+ }
1592
+ return answer;
1593
+ }
1594
+ function installAcpUserQuestionProvider(service, options) {
1595
+ return service.registerProvider({
1596
+ async ask(request) {
1597
+ if (!options.formSupported()) {
1598
+ throw new UserQuestionError(
1599
+ "the ACP client does not support form elicitation",
1600
+ "CLIENT_UNSUPPORTED"
1601
+ );
1602
+ }
1603
+ const sessionId = options.sessionIdForRequest(request);
1604
+ if (sessionId === void 0) {
1605
+ throw new UserQuestionError(
1606
+ "ACP user interaction requires a live root session",
1607
+ "ASK_MISSING_AGENT"
1608
+ );
1609
+ }
1610
+ return askUserQuestionsOverAcp(request, sessionId, options.create);
1611
+ }
1612
+ });
1613
+ }
1614
+
605
1615
  // src/bridge/index.ts
606
1616
  var name = "acp-bridge";
607
- var inject = ["agents"];
1617
+ var inject = ["agents", "credentials", "llm", "agentDefaultModel", "sessionPersistence", "approval", "permissionPresets", "commands", "agentPresets", "skills", "subagents", "userQuestions"];
608
1618
  function invalidParams(detail) {
609
1619
  return RequestError.invalidParams(void 0, detail);
610
1620
  }
@@ -614,9 +1624,6 @@ function internalError(detail) {
614
1624
  function authRequired(detail) {
615
1625
  return RequestError.authRequired(void 0, detail);
616
1626
  }
617
- function envCredentialPresent() {
618
- return Boolean(process.env["DEEPSEEK_API_KEY"] || process.env["DEEPSEEK_BASE_URL"]);
619
- }
620
1627
  var MODE_LABELS = {
621
1628
  "read-only": { name: "Read-only", description: "Bash and file mutations are denied by the sandbox" },
622
1629
  "workspace-write": {
@@ -630,13 +1637,40 @@ var MODE_LABELS = {
630
1637
  };
631
1638
  async function apply(ctx, config = {}) {
632
1639
  const agents = ctx.agents;
1640
+ const credentials = ctx.credentials;
1641
+ const llm = ctx.llm;
1642
+ const agentDefaultModel = ctx.agentDefaultModel;
1643
+ const sessionPersistence = ctx.sessionPersistence;
1644
+ const approval = ctx.approval;
1645
+ const permissionPresets = ctx.permissionPresets;
1646
+ const commandRuntime = ctx.commands;
1647
+ const agentPresets = ctx.agentPresets;
1648
+ const skillRegistry = ctx.skills;
1649
+ const subagents = ctx.subagents;
1650
+ const userQuestions = ctx.userQuestions;
633
1651
  const harness = config.harness ?? (await Promise.resolve().then(() => (init_self_harness(), self_harness_exports))).selfHarness();
634
1652
  const { createUserMessage: createUserMessage2, errorChain: errorChain2, sessionId: SessionId2, foldSessionTitle: foldSessionTitle2, setSandboxMode: setSandboxMode2 } = harness;
635
1653
  const SANDBOX_MODES2 = harness.sandboxModes;
636
1654
  const sessions = /* @__PURE__ */ new Map();
1655
+ const subagentByChild = /* @__PURE__ */ new Map();
1656
+ const subagentByRun = /* @__PURE__ */ new Map();
1657
+ const watchedSubagentParents = /* @__PURE__ */ new WeakSet();
637
1658
  let closed = false;
638
1659
  let conn;
639
1660
  let clientTerminalOutput = false;
1661
+ let clientSubagentTranscript = false;
1662
+ let clientElicitationForm = false;
1663
+ const tuiClient = { advertised: false };
1664
+ const rpc = new AcpRpc();
1665
+ const findAgent = (agentId) => {
1666
+ const fromRegistry = agents.get(agentId);
1667
+ if (fromRegistry !== void 0) return fromRegistry;
1668
+ for (const record of sessions.values()) {
1669
+ if (record.agent.id === agentId) return record.agent;
1670
+ }
1671
+ return void 0;
1672
+ };
1673
+ installTuiClientPlane(ctx, rpc, { findAgent, advertisement: tuiClient });
640
1674
  const modelCandidates = () => {
641
1675
  const seen = /* @__PURE__ */ new Set();
642
1676
  if (config.model !== void 0) seen.add(config.model);
@@ -644,10 +1678,8 @@ async function apply(ctx, config = {}) {
644
1678
  return [...seen];
645
1679
  };
646
1680
  const defaultSelection = () => {
647
- const service = ctx.get("agentDefaultModel");
648
- if (service === void 0) return {};
649
1681
  try {
650
- return service.currentSelection();
1682
+ return agentDefaultModel.currentSelection();
651
1683
  } catch (error) {
652
1684
  logDebug(`agentDefaultModel.currentSelection failed: ${String(error)}`);
653
1685
  return {};
@@ -664,28 +1696,25 @@ async function apply(ctx, config = {}) {
664
1696
  const effortCache = /* @__PURE__ */ new Map();
665
1697
  const discoverModels = async () => {
666
1698
  if (liveCatalog !== void 0) return liveCatalog;
667
- const llm = ctx.get("llm");
668
1699
  const found = [];
669
- if (llm !== void 0) {
670
- try {
671
- const providers = llm.listProviders();
672
- const multi = providers.length > 1;
673
- for (const provider of providers) {
674
- const models = await llm.listModels(provider.id).catch((error) => {
675
- logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
676
- return [];
1700
+ try {
1701
+ const providers = llm.listProviders();
1702
+ const multi = providers.length > 1;
1703
+ for (const provider of providers) {
1704
+ const models = await llm.listModels(provider.id).catch((error) => {
1705
+ logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
1706
+ return [];
1707
+ });
1708
+ for (const model of models) {
1709
+ found.push({
1710
+ provider: provider.id,
1711
+ model: model.id,
1712
+ label: multi ? `${model.name} (${provider.name})` : model.name
677
1713
  });
678
- for (const model of models) {
679
- found.push({
680
- provider: provider.id,
681
- model: model.id,
682
- label: multi ? `${model.name} (${provider.name})` : model.name
683
- });
684
- }
685
1714
  }
686
- } catch (error) {
687
- logDebug(`model discovery failed: ${String(error)}`);
688
1715
  }
1716
+ } catch (error) {
1717
+ logDebug(`model discovery failed: ${String(error)}`);
689
1718
  }
690
1719
  liveCatalog = found;
691
1720
  return found;
@@ -721,10 +1750,9 @@ async function apply(ctx, config = {}) {
721
1750
  if (provider === void 0 || model === void 0) return void 0;
722
1751
  const key = `${provider}::${model}`;
723
1752
  if (effortCache.has(key)) return effortCache.get(key);
724
- const llm = ctx.get("llm");
725
1753
  let catalog;
726
1754
  try {
727
- const reasoning = (await llm?.resolveModelInfo?.(provider, model))?.reasoning;
1755
+ const reasoning = (await llm.resolveModelInfo(provider, model)).reasoning;
728
1756
  if (reasoning !== void 0 && reasoning.efforts.length > 0) {
729
1757
  catalog = {
730
1758
  efforts: reasoning.efforts.map((effort) => ({
@@ -827,27 +1855,38 @@ async function apply(ctx, config = {}) {
827
1855
  }
828
1856
  setApprovalPolicy(record, record.approvals);
829
1857
  };
830
- const credentialPresent = async () => {
831
- if (envCredentialPresent()) return true;
832
- const brand = harness.credentialRef;
833
- if (brand === void 0) return false;
834
- const deadline = Date.now() + 5e3;
835
- for (; ; ) {
836
- const seam = ctx.get("credentials");
837
- if (seam !== void 0) {
838
- try {
839
- return await seam.resolve(brand("DEEPSEEK_API_KEY")) !== void 0;
840
- } catch (error) {
841
- logWarn(`credential lookup failed: ${String(error)}`);
842
- return false;
843
- }
844
- }
845
- if (Date.now() >= deadline) {
846
- logDebug("credential gate: no credentials service registered within 5s");
847
- return false;
1858
+ const listProviderRoutes = async () => {
1859
+ try {
1860
+ const raw2 = llm.listProviders();
1861
+ if (!Array.isArray(raw2)) return [];
1862
+ return raw2.flatMap((entry) => {
1863
+ if (!entry || typeof entry !== "object") return [];
1864
+ const id = entry.id;
1865
+ if (typeof id !== "string" || id.length === 0) return [];
1866
+ const name3 = entry.name;
1867
+ return [{ id, ...typeof name3 === "string" && name3.length > 0 ? { name: name3 } : {} }];
1868
+ });
1869
+ } catch (error) {
1870
+ logDebug(`listProviders failed: ${String(error)}`);
1871
+ return [];
1872
+ }
1873
+ };
1874
+ const credentialPresent = async (provider) => {
1875
+ const names = credentialEnvNames(provider).filter((name3) => !name3.endsWith("_BASE_URL"));
1876
+ for (const name3 of names) {
1877
+ try {
1878
+ if (await credentials.resolve(credentialRef(name3)) !== void 0) return true;
1879
+ } catch (error) {
1880
+ logWarn(`credential lookup failed: ${String(error)}`);
848
1881
  }
849
- await new Promise((resolve) => setTimeout(resolve, 100));
850
1882
  }
1883
+ return false;
1884
+ };
1885
+ const requireCredential = async (provider) => {
1886
+ if (await credentialPresent(provider)) return;
1887
+ throw authRequired(
1888
+ 'no credential found for this provider: call authenticate with `_meta["api-key"].apiKey` or `_meta.gateway`, use the browser method, run `dsh-acp login`, or save a key in the dsh Web UI (Settings \u2192 Models)'
1889
+ );
851
1890
  };
852
1891
  const agentOptionsFor = (model, provider) => ({
853
1892
  ...provider !== void 0 ? { provider } : config.provider !== void 0 ? { provider: config.provider } : {},
@@ -866,7 +1905,6 @@ async function apply(ctx, config = {}) {
866
1905
  const requireOrRestoreSession = async (sessionId) => {
867
1906
  const record = sessions.get(sessionId);
868
1907
  if (record !== void 0) return record;
869
- if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
870
1908
  logWarn(`restoring session ${sessionId} from the persisted log`);
871
1909
  try {
872
1910
  return await restoreSession(sessionId, { replay: false });
@@ -906,7 +1944,7 @@ async function apply(ctx, config = {}) {
906
1944
  }
907
1945
  const presets = presetsService();
908
1946
  const storedPreset = presetFromLog(storedHeader, events);
909
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1947
+ const presetId = (await presets.resolve(storedPreset)).id;
910
1948
  const handle = await agents.resume({
911
1949
  resumeSessionId: SessionId2(sessionId),
912
1950
  agentOptions: agentOptionsFor(config.model),
@@ -929,7 +1967,7 @@ async function apply(ctx, config = {}) {
929
1967
  if (presetId !== void 0) record.preset = presetId;
930
1968
  {
931
1969
  const permission = permissionService();
932
- const storedMode = permission?.current(events);
1970
+ const storedMode = permission.current(events);
933
1971
  if (storedMode !== void 0) record.modeId = storedMode;
934
1972
  for (let index = events.length - 1; index >= 0; index -= 1) {
935
1973
  const event = events[index];
@@ -958,13 +1996,78 @@ async function apply(ctx, config = {}) {
958
1996
  record.inflight = void 0;
959
1997
  inflight.resolve(reason);
960
1998
  };
961
- const presetsService = () => ctx.get("agentPresets");
1999
+ const watchSubagentParent = (parent) => {
2000
+ if (watchedSubagentParents.has(parent)) return;
2001
+ watchedSubagentParents.add(parent);
2002
+ parent.ctx.on("subagent/start", (info) => {
2003
+ const parentLink = subagentByChild.get(String(parent.id));
2004
+ const rootSessionId = parentLink?.rootSessionId ?? String(parent.session.id);
2005
+ const record = sessions.get(rootSessionId);
2006
+ if (record === void 0) return;
2007
+ const runId = String(info.runId);
2008
+ const childSessionId = String(info.id);
2009
+ const toolCallId = `subagent:${runId}`;
2010
+ const payload = {
2011
+ runId,
2012
+ provider: info.provider,
2013
+ id: childSessionId,
2014
+ local: info.local,
2015
+ ...parentLink !== void 0 ? { parentToolCallId: parentLink.toolCallId } : {}
2016
+ };
2017
+ for (const update of record.projection.onEvent({ type: "subagent/start", data: payload })) {
2018
+ notify(rootSessionId, update);
2019
+ }
2020
+ const child = agents.get(info.id);
2021
+ const link = {
2022
+ rootSessionId,
2023
+ childSessionId,
2024
+ runId,
2025
+ provider: info.provider,
2026
+ toolCallId,
2027
+ projection: new SessionProjection(void 0, {
2028
+ terminalOutput: clientTerminalOutput,
2029
+ ...child?.session.header.cwd !== void 0 ? { cwd: child.session.header.cwd } : {},
2030
+ subagent: { childSessionId, parentToolCallId: toolCallId, provider: info.provider }
2031
+ })
2032
+ };
2033
+ subagentByChild.set(childSessionId, link);
2034
+ subagentByRun.set(runId, link);
2035
+ if (child !== void 0) watchSubagentParent(child);
2036
+ });
2037
+ parent.ctx.on("subagent/end", (info) => {
2038
+ const runId = String(info.runId);
2039
+ const link = subagentByRun.get(runId);
2040
+ if (link === void 0) return;
2041
+ const record = sessions.get(link.rootSessionId);
2042
+ if (record !== void 0) {
2043
+ const parentLink = subagentByChild.get(String(parent.id));
2044
+ const payload = {
2045
+ runId,
2046
+ provider: info.provider,
2047
+ id: String(info.id),
2048
+ local: info.local,
2049
+ stopReason: info.stopReason,
2050
+ ...info.lastAssistantMessage !== void 0 ? { lastAssistantMessage: info.lastAssistantMessage } : {},
2051
+ ...parentLink !== void 0 ? { parentToolCallId: parentLink.toolCallId } : {}
2052
+ };
2053
+ for (const update of record.projection.onEvent({ type: "subagent/end", data: payload })) {
2054
+ notify(link.rootSessionId, update);
2055
+ }
2056
+ }
2057
+ subagentByRun.delete(runId);
2058
+ if (subagentByChild.get(link.childSessionId)?.runId === runId) {
2059
+ subagentByChild.delete(link.childSessionId);
2060
+ }
2061
+ });
2062
+ };
2063
+ const presetsService = () => agentPresets;
962
2064
  const presetSetup = (presets, presetId) => {
963
- if (presets === void 0 || presetId === void 0) return void 0;
2065
+ if (presetId === void 0) return void 0;
964
2066
  return async (agentCtx) => {
2067
+ const started = performance.now();
965
2068
  logDebug(`preset setup: mounting "${presetId}"`);
966
2069
  await presets.mount(agentCtx, presetId);
967
- logDebug(`preset setup: mounted "${presetId}"`);
2070
+ logDebug(`preset setup: mounted "${presetId}" in ${(performance.now() - started).toFixed(1)}ms`);
968
2071
  };
969
2072
  };
970
2073
  const presetFromLog = (header, events) => {
@@ -979,7 +2082,7 @@ async function apply(ctx, config = {}) {
979
2082
  const presetLabel = (id) => id.charAt(0).toUpperCase() + id.slice(1);
980
2083
  const modeState = (record) => {
981
2084
  const permission = permissionService();
982
- if (permission !== void 0 && permission.names.length > 0) {
2085
+ if (permission.names.length > 0) {
983
2086
  return {
984
2087
  currentModeId: record.modeId,
985
2088
  availableModes: permission.names.map((name3) => {
@@ -1001,7 +2104,6 @@ async function apply(ctx, config = {}) {
1001
2104
  throw invalidParams("cannot switch presets while a prompt is running");
1002
2105
  }
1003
2106
  const presets = presetsService();
1004
- if (presets === void 0) throw invalidParams(`unknown mode: ${presetId}`);
1005
2107
  let resolved;
1006
2108
  try {
1007
2109
  resolved = (await presets.resolve(presetId)).id;
@@ -1009,29 +2111,44 @@ async function apply(ctx, config = {}) {
1009
2111
  throw invalidParams(`unknown preset: ${presetId} (${errorChain2(error)})`);
1010
2112
  }
1011
2113
  if (resolved === record.preset) return;
1012
- try {
1013
- record.agent.session.append("agent-preset/selected", { agentPreset: resolved });
1014
- } catch (error) {
1015
- logWarn(`recording preset selection failed: ${String(error)}`);
1016
- }
2114
+ const previous = record.preset;
1017
2115
  const sessionId = record.agent.session.id;
2116
+ const resumeWith = (id) => agents.resume({
2117
+ resumeSessionId: sessionId,
2118
+ agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
2119
+ ...presetSetup(presets, id) !== void 0 ? { setup: presetSetup(presets, id) } : {}
2120
+ });
1018
2121
  await record.dispose().catch((error) => {
1019
2122
  logWarn(`dispose during preset switch failed: ${String(error)}`);
1020
2123
  });
1021
2124
  let handle;
1022
2125
  try {
1023
- handle = await agents.resume({
1024
- resumeSessionId: sessionId,
1025
- agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
1026
- setup: presetSetup(presets, resolved)
1027
- });
2126
+ handle = await resumeWith(resolved);
1028
2127
  } catch (error) {
1029
- sessions.delete(acpSessionId);
1030
- throw internalError(`preset switch failed: ${errorChain2(error)}`);
2128
+ try {
2129
+ handle = await resumeWith(previous);
2130
+ } catch (restoreError) {
2131
+ sessions.delete(acpSessionId);
2132
+ throw internalError(
2133
+ `preset switch failed: ${errorChain2(error)}; restore also failed: ${errorChain2(restoreError)}`
2134
+ );
2135
+ }
2136
+ record.agent = handle.agent;
2137
+ record.dispose = () => handle.dispose();
2138
+ delete record.selection;
2139
+ ensureSelection(record);
2140
+ setApprovalPolicy(record, record.approvals);
2141
+ publishCommands(acpSessionId, record);
2142
+ throw invalidParams(`preset "${resolved}" failed to mount: ${errorChain2(error)}`);
1031
2143
  }
1032
2144
  record.agent = handle.agent;
1033
2145
  record.dispose = () => handle.dispose();
1034
2146
  record.preset = resolved;
2147
+ try {
2148
+ handle.agent.session.append("agent-preset/selected", { agentPreset: resolved });
2149
+ } catch (error) {
2150
+ logWarn(`recording preset selection failed: ${String(error)}`);
2151
+ }
1035
2152
  delete record.selection;
1036
2153
  ensureSelection(record);
1037
2154
  setApprovalPolicy(record, record.approvals);
@@ -1039,16 +2156,16 @@ async function apply(ctx, config = {}) {
1039
2156
  };
1040
2157
  const setApprovalPolicy = (record, policy) => {
1041
2158
  try {
1042
- ctx.get("approval")?.setPolicy?.(record.agent, policy);
2159
+ approval.setPolicy(record.agent, policy);
1043
2160
  } catch (error) {
1044
2161
  logWarn(`approval policy switch failed: ${String(error)}`);
1045
2162
  }
1046
2163
  record.approvals = policy;
1047
2164
  };
1048
- const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
2165
+ const permissionService = () => permissionPresets;
1049
2166
  const applyMode = (record, sessionId, modeId) => {
1050
2167
  const permission = permissionService();
1051
- if (permission !== void 0 && permission.names.includes(modeId)) {
2168
+ if (permission.names.includes(modeId)) {
1052
2169
  let bundledApproval = record.approvals;
1053
2170
  permission.apply(record.agent.session, modeId, (policy) => {
1054
2171
  bundledApproval = policy;
@@ -1090,7 +2207,7 @@ async function apply(ctx, config = {}) {
1090
2207
  const result = [];
1091
2208
  {
1092
2209
  const permission = permissionService();
1093
- const levels = permission !== void 0 && permission.names.length > 0 ? permission.names.map((name3) => {
2210
+ const levels = permission.names.length > 0 ? permission.names.map((name3) => {
1094
2211
  const spec = permission.resolve(name3);
1095
2212
  return { value: name3, name: spec.name, description: spec.description };
1096
2213
  }) : SANDBOX_MODES2.map((mode) => {
@@ -1106,6 +2223,19 @@ async function apply(ctx, config = {}) {
1106
2223
  options: levels
1107
2224
  });
1108
2225
  }
2226
+ if (commandRuntime.list(record.agent).some((command) => command.name === "plan")) {
2227
+ const active = [...record.agent.session.events].reverse().map((event) => event).find((event) => event.type === "plan/mode")?.data?.["active"] === true;
2228
+ result.push({
2229
+ type: "select",
2230
+ id: "collaboration_mode",
2231
+ name: "Collaboration mode",
2232
+ currentValue: active ? "plan" : "default",
2233
+ options: [
2234
+ { value: "default", name: "Default" },
2235
+ { value: "plan", name: "Plan" }
2236
+ ]
2237
+ });
2238
+ }
1109
2239
  const models = await modelChoices(record);
1110
2240
  if (models.length >= 2) {
1111
2241
  result.push({
@@ -1145,14 +2275,14 @@ async function apply(ctx, config = {}) {
1145
2275
  });
1146
2276
  }
1147
2277
  const presets = presetsService();
1148
- if (presets !== void 0 && record.preset !== void 0) {
2278
+ if (record.preset !== void 0) {
1149
2279
  try {
1150
2280
  const roster = await presets.list();
1151
2281
  if (roster.length >= 2) {
1152
2282
  result.push({
1153
2283
  type: "select",
1154
- id: "preset",
1155
- name: "Preset",
2284
+ id: "agent",
2285
+ name: "Agent",
1156
2286
  currentValue: record.preset,
1157
2287
  options: roster.map((preset) => ({
1158
2288
  value: preset.id,
@@ -1169,12 +2299,6 @@ async function apply(ctx, config = {}) {
1169
2299
  };
1170
2300
  const BUILTIN_COMMANDS = [
1171
2301
  { name: "status", description: "Show adapter, model, mode, and token status" },
1172
- {
1173
- name: "login",
1174
- description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1175
- input: { hint: "<api-key>" }
1176
- },
1177
- { name: "logout", description: "Remove the API key stored in the harness credential store" },
1178
2302
  {
1179
2303
  name: "model",
1180
2304
  description: "Select the model for this conversation",
@@ -1184,42 +2308,47 @@ async function apply(ctx, config = {}) {
1184
2308
  const availableCommandsFor = async (record) => {
1185
2309
  const list = [...BUILTIN_COMMANDS];
1186
2310
  const seen = new Set(list.map((command) => command.name));
1187
- const commandsSvc = ctx.get("commands");
1188
- if (commandsSvc !== void 0) {
1189
- try {
1190
- for (const descriptor of commandsSvc.list(record.agent)) {
1191
- if (seen.has(descriptor.name)) continue;
1192
- seen.add(descriptor.name);
1193
- list.push({
1194
- name: descriptor.name,
1195
- description: descriptor.description,
1196
- ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
1197
- });
1198
- }
1199
- } catch (error) {
1200
- logWarn(`command listing failed: ${String(error)}`);
2311
+ try {
2312
+ for (const descriptor of commandRuntime.list(record.agent)) {
2313
+ if (seen.has(descriptor.name)) continue;
2314
+ seen.add(descriptor.name);
2315
+ list.push({
2316
+ name: descriptor.name,
2317
+ description: descriptor.description,
2318
+ ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {},
2319
+ ...descriptor.name === "plan" ? {
2320
+ _meta: {
2321
+ commandAction: {
2322
+ kind: "setConfigOption",
2323
+ configId: "collaboration_mode",
2324
+ value: "plan",
2325
+ resetValue: "default",
2326
+ presentation: "state"
2327
+ }
2328
+ }
2329
+ } : {}
2330
+ });
1201
2331
  }
2332
+ } catch (error) {
2333
+ logWarn(`command listing failed: ${String(error)}`);
1202
2334
  }
1203
- const skillsSvc = ctx.get("skills");
1204
- if (skillsSvc !== void 0) {
1205
- try {
1206
- const skills = await skillsSvc.list({
1207
- ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
1208
- scope: record.agent
2335
+ try {
2336
+ const skills = await skillRegistry.list({
2337
+ ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
2338
+ scope: record.agent
2339
+ });
2340
+ for (const skill of skills) {
2341
+ if (skill.invocation.userInvocable !== true) continue;
2342
+ if (seen.has(skill.name)) continue;
2343
+ seen.add(skill.name);
2344
+ list.push({
2345
+ name: skill.name,
2346
+ description: skill.description,
2347
+ input: { hint: "instructions for the skill" }
1209
2348
  });
1210
- for (const skill of skills) {
1211
- if (skill.invocation.userInvocable !== true) continue;
1212
- if (seen.has(skill.name)) continue;
1213
- seen.add(skill.name);
1214
- list.push({
1215
- name: skill.name,
1216
- description: skill.description,
1217
- input: { hint: "instructions for the skill" }
1218
- });
1219
- }
1220
- } catch (error) {
1221
- logDebug(`skill listing failed: ${String(error)}`);
1222
2349
  }
2350
+ } catch (error) {
2351
+ logDebug(`skill listing failed: ${String(error)}`);
1223
2352
  }
1224
2353
  return list;
1225
2354
  };
@@ -1246,7 +2375,7 @@ async function apply(ctx, config = {}) {
1246
2375
  const events = record.agent.session.events;
1247
2376
  if (events === void 0) return;
1248
2377
  const permission = permissionService();
1249
- const storedMode = permission?.current(events);
2378
+ const storedMode = permission.current(events);
1250
2379
  for (let index = events.length - 1; index >= 0; index -= 1) {
1251
2380
  const event = events[index];
1252
2381
  if (event?.type === "approval/policy") {
@@ -1263,69 +2392,35 @@ async function apply(ctx, config = {}) {
1263
2392
  });
1264
2393
  }
1265
2394
  };
1266
- const credentialStore = () => {
1267
- const brand = harness.credentialRef;
1268
- const store = ctx.get("credentials");
1269
- if (brand === void 0 || store === void 0) return void 0;
1270
- return { store, ref: brand("DEEPSEEK_API_KEY") };
1271
- };
1272
- const maskKey = (value) => value.length <= 8 ? "\u2026" : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
1273
- const describeCredential = async () => {
1274
- if (envCredentialPresent()) {
1275
- const viaEnv = process.env["DEEPSEEK_API_KEY"] !== void 0 ? "DEEPSEEK_API_KEY" : "DEEPSEEK_BASE_URL";
1276
- return `process environment (${viaEnv})`;
1277
- }
1278
- const cs = credentialStore();
1279
- if (cs === void 0) return "not configured";
1280
- try {
1281
- const info = await cs.store.describe(cs.ref);
1282
- if (!info.configured) return "not configured";
1283
- return info.source ?? "credential store";
1284
- } catch (error) {
1285
- logWarn(`credential describe failed: ${String(error)}`);
1286
- return "unknown";
2395
+ const describeCredential = async (provider) => {
2396
+ const names = credentialEnvNames(provider).filter((candidate) => !candidate.endsWith("_BASE_URL"));
2397
+ for (const name3 of names) {
2398
+ try {
2399
+ const info = await credentials.describe(credentialRef(name3));
2400
+ if (info.configured) return info.source ?? "credential store";
2401
+ } catch (error) {
2402
+ logWarn(`credential describe failed: ${String(error)}`);
2403
+ }
1287
2404
  }
2405
+ return "not configured";
1288
2406
  };
1289
- const loginText = async (rawArgument) => {
1290
- const key = rawArgument.trim();
1291
- const cs = credentialStore();
1292
- if (cs === void 0) {
1293
- return "This composition has no writable credential store; export DEEPSEEK_API_KEY in the environment that launches the agent instead.";
1294
- }
1295
- if (key.length === 0) {
1296
- return [
1297
- "Usage: `/login <api-key>` \u2014 stores the key in the harness credential store",
1298
- "(`~/.dsh/.credentials.yaml`, the same file the dsh Web UI writes; mode 600).",
1299
- "",
1300
- `Current credential source: ${await describeCredential()}.`,
1301
- "",
1302
- "Note: text entered here also lands in your client's chat history. If you",
1303
- "prefer to keep the key out of it, save the key in the dsh Web UI instead."
1304
- ].join("\n");
1305
- }
1306
- try {
1307
- await cs.store.set(cs.ref, key);
1308
- } catch (error) {
1309
- return [
1310
- `Could not store the key: ${error instanceof Error ? error.message : String(error)}`,
1311
- "",
1312
- "A read-only source (usually the process environment) is currently supplying",
1313
- "this credential, so a stored value would be shadowed. Unset DEEPSEEK_API_KEY",
1314
- "in the launching environment, or keep using it as the credential."
1315
- ].join("\n");
1316
- }
1317
- return `Saved DEEPSEEK_API_KEY (${maskKey(key)}) to the harness credential store \u2014 source now: ${await describeCredential()}. The dsh Web UI sees the same key.`;
2407
+ const saveCredential = async (provider, key) => {
2408
+ await credentials.set(credentialRef(primaryCredentialName(provider)), key);
1318
2409
  };
1319
- const logoutText = async () => {
1320
- const cs = credentialStore();
1321
- if (cs === void 0) return "This composition has no writable credential store; nothing to remove.";
1322
- try {
1323
- await cs.store.unset(cs.ref);
1324
- } catch (error) {
1325
- return `Could not remove the stored key: ${error instanceof Error ? error.message : String(error)}`;
2410
+ const saveGateway = async (provider, key, baseUrl) => {
2411
+ await saveCredential(provider, key);
2412
+ await credentials.set(credentialRef(credentialBaseUrlName(provider)), baseUrl);
2413
+ };
2414
+ const logoutStored = async () => {
2415
+ const routes = await listProviderRoutes();
2416
+ const providers = routes.length > 0 ? routes.map((route) => route.id) : [config.provider];
2417
+ for (const provider of providers) {
2418
+ try {
2419
+ await credentials.unset(credentialRef(primaryCredentialName(provider)));
2420
+ } catch (error) {
2421
+ logWarn(`credential unset failed: ${String(error)}`);
2422
+ }
1326
2423
  }
1327
- const remaining = await describeCredential();
1328
- return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1329
2424
  };
1330
2425
  const modelCommandText = async (record, acpSessionId, query) => {
1331
2426
  const discovered = await discoverModels();
@@ -1382,7 +2477,7 @@ async function apply(ctx, config = {}) {
1382
2477
  `| Provider | ${route.provider ?? "(composition default)"} |`,
1383
2478
  `| Model | ${route.model ?? "(composition default)"} |`,
1384
2479
  `| Reasoning | ${effort ?? "(adapter default)"} |`,
1385
- `| Credential | ${await describeCredential()} |`,
2480
+ `| Credential | ${await describeCredential(route.provider)} |`,
1386
2481
  ...record.preset !== void 0 ? [`| Preset | ${record.preset} |`] : [],
1387
2482
  `| Permission mode | ${record.modeId} |`,
1388
2483
  `| Approvals | ${record.approvals} |`,
@@ -1407,16 +2502,27 @@ async function apply(ctx, config = {}) {
1407
2502
  inflight: void 0
1408
2503
  };
1409
2504
  sessions.set(sessionId, record);
2505
+ watchSubagentParent(agent);
1410
2506
  return record;
1411
2507
  };
1412
2508
  ctx.on("session/event", (session, event) => {
1413
2509
  const sessionId = String(session.header.id);
1414
2510
  const record = sessions.get(sessionId);
1415
- if (record === void 0 || record.agent.session !== session) return;
2511
+ if (record === void 0 || record.agent.session !== session) {
2512
+ const child = subagentByChild.get(sessionId);
2513
+ if (child !== void 0 && clientSubagentTranscript) {
2514
+ for (const update of child.projection.onEvent(event)) {
2515
+ notify(child.rootSessionId, update);
2516
+ }
2517
+ }
2518
+ return;
2519
+ }
1416
2520
  try {
1417
- for (const update of record.projection.onEvent(event)) {
2521
+ const harnessEvent = event;
2522
+ for (const update of record.projection.onEvent(harnessEvent)) {
1418
2523
  notify(sessionId, update);
1419
2524
  }
2525
+ if (harnessEvent.type === "plan/mode") publishConfigOptions(sessionId, record);
1420
2526
  } finally {
1421
2527
  const inflight = record.inflight;
1422
2528
  if (inflight !== void 0 && event.type === "turn/end" && inflight.turn === event.data.turn) {
@@ -1453,11 +2559,7 @@ async function apply(ctx, config = {}) {
1453
2559
  }).then(({ outcome }) => {
1454
2560
  if (outcome.outcome === "cancelled") return "cancelled";
1455
2561
  if (outcome.optionId === "allow-always") {
1456
- try {
1457
- ctx.get("approval")?.setPolicy?.(record.agent, "never");
1458
- } catch (error) {
1459
- logWarn(`approval policy switch failed: ${String(error)}`);
1460
- }
2562
+ setApprovalPolicy(record, "never");
1461
2563
  return "allowed-once";
1462
2564
  }
1463
2565
  return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
@@ -1466,56 +2568,103 @@ async function apply(ctx, config = {}) {
1466
2568
  const makeAgent = (connection) => {
1467
2569
  conn = connection;
1468
2570
  return {
1469
- initialize(params) {
1470
- const hasPersistence = ctx.get("sessionPersistence") !== void 0;
2571
+ async initialize(params) {
1471
2572
  const requested = params.protocolVersion;
1472
2573
  const capsMeta = params.clientCapabilities?.["_meta"];
1473
2574
  clientTerminalOutput = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["terminal_output"] === true : false;
1474
- return Promise.resolve({
2575
+ clientSubagentTranscript = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["subagent-transcript"] === true : false;
2576
+ clientElicitationForm = params.clientCapabilities?.elicitation?.form !== void 0 && params.clientCapabilities.elicitation.form !== null;
2577
+ if (capsMeta !== null && typeof capsMeta === "object" && capsMeta["tuiInspect"] === true) {
2578
+ tuiClient.advertised = true;
2579
+ }
2580
+ const providers = await listProviderRoutes();
2581
+ return {
1475
2582
  protocolVersion: typeof requested === "number" && requested >= 1 && requested < PROTOCOL_VERSION ? requested : PROTOCOL_VERSION,
1476
2583
  agentInfo: { name: "dsh-acp", title: "DeepSeek Harness", version: VERSION },
1477
2584
  agentCapabilities: {
1478
- loadSession: hasPersistence,
1479
- promptCapabilities: { image: false, audio: false, embeddedContext: true },
2585
+ loadSession: true,
2586
+ promptCapabilities: {
2587
+ image: attachmentIngestOf(ctx.get("attachments")) !== void 0,
2588
+ audio: false,
2589
+ embeddedContext: true
2590
+ },
1480
2591
  // Stdio servers always work; streamable HTTP maps onto
1481
2592
  // mcp-client's second transport. Legacy SSE does not.
1482
2593
  mcpCapabilities: { http: true, sse: false },
1483
- ...hasPersistence ? { sessionCapabilities: { list: {} } } : {}
2594
+ auth: { logout: {} },
2595
+ sessionCapabilities: { list: {} }
1484
2596
  },
1485
- authMethods: [
1486
- // ACP Terminal Auth (see the registry's AUTHENTICATION.md):
1487
- // the client runs `dsh-acp login` in an interactive
1488
- // terminal; the key lands in the shared harness
1489
- // credential store (~/.dsh/.credentials.yaml).
1490
- {
1491
- id: "terminal-login",
1492
- name: "Log in with a DeepSeek API key",
1493
- description: "Interactive terminal setup \u2014 saves the key to the harness credential store shared with the dsh Web UI",
1494
- _meta: { "terminal-auth": { args: ["login"], env: {} } }
1495
- }
1496
- ]
1497
- });
2597
+ authMethods: advertisedAuthMethods(
2598
+ providers,
2599
+ params.clientCapabilities
2600
+ )
2601
+ };
1498
2602
  },
1499
- async authenticate(_params) {
1500
- if (!await credentialPresent()) {
1501
- throw authRequired(
1502
- "no DeepSeek credential found: run `dsh-acp login`, save one in the dsh Web UI (Settings \u2192 Models), or set DEEPSEEK_API_KEY"
2603
+ async authenticate(params) {
2604
+ const gateway = gatewayFromAuthenticate(params);
2605
+ if (isGatewayAuthMethod(params.methodId) || gateway.baseUrl) {
2606
+ if (!gateway.baseUrl || !gateway.key) {
2607
+ throw authRequired(
2608
+ "authenticate gateway requires `_meta.gateway.baseUrl` and an Authorization header"
2609
+ );
2610
+ }
2611
+ await saveGateway(
2612
+ gateway.providerName ?? config.provider,
2613
+ gateway.key,
2614
+ gateway.baseUrl
1503
2615
  );
2616
+ return;
1504
2617
  }
2618
+ const submitted = apiKeyFromAuthenticate(params);
2619
+ const provider = submitted.provider ?? providerFromAuthMethodId(params.methodId) ?? config.provider;
2620
+ if (submitted.key) {
2621
+ await saveCredential(provider, submitted.key);
2622
+ return;
2623
+ }
2624
+ if (await credentialPresent(provider)) return;
2625
+ if (isBrowserAuthMethod(params.methodId) && shouldOfferLocalAuthPage()) {
2626
+ const page = await startLocalAuthPage({
2627
+ credentialName: primaryCredentialName(provider)
2628
+ });
2629
+ openLocalAuthPage(page.url);
2630
+ try {
2631
+ const key = await page.completed;
2632
+ await saveCredential(provider, key);
2633
+ return;
2634
+ } finally {
2635
+ page.close();
2636
+ }
2637
+ }
2638
+ await requireCredential(provider);
2639
+ },
2640
+ async logout(_params) {
2641
+ await logoutStored();
1505
2642
  },
1506
2643
  async newSession(params) {
1507
2644
  assertOpen();
2645
+ const started = performance.now();
2646
+ let checkpoint = started;
2647
+ const mark = (name3) => {
2648
+ const now = performance.now();
2649
+ logDebug(`session/new ${name3}: ${(now - checkpoint).toFixed(1)}ms (+${(now - started).toFixed(1)}ms)`);
2650
+ checkpoint = now;
2651
+ };
2652
+ await requireCredential(config.provider);
2653
+ mark("credential");
1508
2654
  validateCwd(params.cwd);
1509
2655
  syncMcpServers(params.mcpServers, params.cwd);
2656
+ mark("request setup");
1510
2657
  const sessionId = SessionId2(randomUUID());
1511
2658
  const presets = presetsService();
1512
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(void 0)).id;
2659
+ const presetId = (await presets.resolve(void 0)).id;
2660
+ mark("preset resolve");
1513
2661
  const handle = await agents.create({
1514
2662
  sessionId,
1515
2663
  meta: { cwd: params.cwd, ...presetId !== void 0 ? { agentPreset: presetId } : {} },
1516
2664
  agentOptions: agentOptionsFor(config.model),
1517
2665
  ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1518
2666
  });
2667
+ mark("agent create");
1519
2668
  if (closed) {
1520
2669
  await handle.dispose();
1521
2670
  throw internalError("connection closed during session/new");
@@ -1531,6 +2680,7 @@ async function apply(ctx, config = {}) {
1531
2680
  ensureSelection(record);
1532
2681
  queueMicrotask(() => publishCommands(String(sessionId)));
1533
2682
  const options = await configOptions(record);
2683
+ mark("response surface");
1534
2684
  return {
1535
2685
  sessionId: String(sessionId),
1536
2686
  modes: modeState(record),
@@ -1539,6 +2689,7 @@ async function apply(ctx, config = {}) {
1539
2689
  },
1540
2690
  async loadSession(params) {
1541
2691
  assertOpen();
2692
+ await requireCredential(config.provider);
1542
2693
  validateCwd(params.cwd);
1543
2694
  syncMcpServers(params.mcpServers, params.cwd);
1544
2695
  requirePersistence();
@@ -1577,17 +2728,36 @@ async function apply(ctx, config = {}) {
1577
2728
  async prompt(params) {
1578
2729
  assertOpen();
1579
2730
  const record = await requireOrRestoreSession(params.sessionId);
1580
- if (record.inflight !== void 0) {
1581
- throw invalidParams("a prompt is already in flight for this session");
1582
- }
1583
2731
  let converted;
1584
2732
  try {
1585
- converted = convertPrompt(params.prompt);
2733
+ converted = await convertPrompt(
2734
+ params.prompt,
2735
+ attachmentIngestOf(ctx.get("attachments"))
2736
+ );
1586
2737
  } catch (error) {
1587
- if (error instanceof UnsupportedPromptContentError) throw invalidParams(error.message);
2738
+ if (error instanceof UnsupportedPromptContentError || error instanceof PromptImageError) {
2739
+ throw invalidParams(error.message);
2740
+ }
1588
2741
  throw error;
1589
2742
  }
1590
2743
  if (converted.blocks.length === 0) throw invalidParams("empty prompt");
2744
+ if (record.inflight !== void 0) {
2745
+ await requireCredential(record.provider ?? config.provider);
2746
+ if (agents.get(record.agent.id) !== record.agent) {
2747
+ throw internalError("prompt was not steered: the agent was disposed outside the bridge");
2748
+ }
2749
+ const message2 = createUserMessage2({
2750
+ content: converted.blocks,
2751
+ source: { kind: "user" }
2752
+ });
2753
+ try {
2754
+ deliverPrompt(record.agent, message2, true);
2755
+ } catch (error) {
2756
+ const detail = error instanceof Error ? error.message : String(error);
2757
+ throw internalError(`prompt was not steered: ${detail}`);
2758
+ }
2759
+ return { stopReason: "end_turn" };
2760
+ }
1591
2761
  const trimmed = converted.displayText.trim();
1592
2762
  const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/);
1593
2763
  const respond = (text) => {
@@ -1598,10 +2768,6 @@ async function apply(ctx, config = {}) {
1598
2768
  return { stopReason: "end_turn" };
1599
2769
  };
1600
2770
  if (commandMatch?.[1] === "status") return respond(await statusText(record));
1601
- if (commandMatch?.[1] === "login") {
1602
- return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1603
- }
1604
- if (commandMatch?.[1] === "logout") return respond(await logoutText());
1605
2771
  if (commandMatch?.[1] === "model") {
1606
2772
  return respond(
1607
2773
  await modelCommandText(
@@ -1612,54 +2778,49 @@ async function apply(ctx, config = {}) {
1612
2778
  );
1613
2779
  }
1614
2780
  if (commandMatch !== null && commandMatch[1] !== void 0) {
1615
- const commandsSvc = ctx.get("commands");
1616
- if (commandsSvc !== void 0) {
1617
- let execution;
1618
- try {
1619
- execution = await commandsSvc.execute(
1620
- record.agent,
1621
- trimmed,
1622
- new AbortController().signal
1623
- );
1624
- } catch (error) {
1625
- return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
1626
- }
1627
- if (execution !== void 0) {
1628
- const { result } = execution;
1629
- const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1630
- syncPermissionState(record, params.sessionId);
1631
- publishCommands(params.sessionId, record);
1632
- publishConfigOptions(params.sessionId, record);
1633
- return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1634
- }
2781
+ let execution;
2782
+ try {
2783
+ execution = await commandRuntime.execute(
2784
+ record.agent,
2785
+ trimmed,
2786
+ new AbortController().signal
2787
+ );
2788
+ } catch (error) {
2789
+ return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
2790
+ }
2791
+ if (execution !== void 0) {
2792
+ const { result } = execution;
2793
+ const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
2794
+ syncPermissionState(record, params.sessionId);
2795
+ publishCommands(params.sessionId, record);
2796
+ publishConfigOptions(params.sessionId, record);
2797
+ return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1635
2798
  }
1636
2799
  }
1637
- if (!await credentialPresent()) {
1638
- return respond(
1639
- [
1640
- "No DeepSeek credential is configured, so this prompt was not sent to the model.",
1641
- "",
1642
- "Configure one of:",
1643
- "- `/login <api-key>` \u2014 store a key in the harness credential store right here,",
1644
- "- the dsh Web UI (`dsh web`, Settings \u2192 Models),",
1645
- "- `DEEPSEEK_API_KEY` in the environment that launches this agent."
1646
- ].join("\n")
1647
- );
1648
- }
2800
+ await requireCredential(record.provider ?? config.provider);
1649
2801
  if (agents.get(record.agent.id) !== record.agent) {
1650
2802
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1651
2803
  }
1652
2804
  record.cancelled = false;
1653
- record.projection.beginPrompt();
1654
2805
  const message = createUserMessage2({
1655
2806
  content: converted.blocks,
1656
2807
  source: { kind: "user" }
1657
2808
  });
2809
+ if (record.inflight !== void 0) {
2810
+ try {
2811
+ deliverPrompt(record.agent, message, true);
2812
+ } catch (error) {
2813
+ const detail = error instanceof Error ? error.message : String(error);
2814
+ throw internalError(`prompt was not steered: ${detail}`);
2815
+ }
2816
+ return { stopReason: "end_turn" };
2817
+ }
2818
+ record.projection.beginPrompt();
1658
2819
  const stopReason = await new Promise((resolve, reject) => {
1659
2820
  const inflight = { resolve, reject, messageId: message.id, turn: void 0 };
1660
2821
  record.inflight = inflight;
1661
2822
  try {
1662
- record.agent.followup(message);
2823
+ deliverPrompt(record.agent, message, false);
1663
2824
  } catch (error) {
1664
2825
  record.inflight = void 0;
1665
2826
  const detail = error instanceof Error ? error.message : String(error);
@@ -1697,6 +2858,7 @@ async function apply(ctx, config = {}) {
1697
2858
  applyMode(record, params.sessionId, value);
1698
2859
  break;
1699
2860
  }
2861
+ case "agent":
1700
2862
  case "preset": {
1701
2863
  await switchPreset(record, params.sessionId, value);
1702
2864
  break;
@@ -1719,6 +2881,23 @@ async function apply(ctx, config = {}) {
1719
2881
  record.effort = value;
1720
2882
  break;
1721
2883
  }
2884
+ case "collaboration_mode": {
2885
+ if (value !== "default" && value !== "plan") {
2886
+ throw invalidParams(`unknown collaboration mode: ${value}`);
2887
+ }
2888
+ const execution = await commandRuntime.execute(
2889
+ record.agent,
2890
+ value === "plan" ? "/plan" : "/plan off",
2891
+ new AbortController().signal
2892
+ );
2893
+ if (execution === void 0) {
2894
+ throw invalidParams("plan mode is unavailable on this session");
2895
+ }
2896
+ if (execution.result.kind === "error") {
2897
+ throw invalidParams(execution.result.text);
2898
+ }
2899
+ break;
2900
+ }
1722
2901
  case "model": {
1723
2902
  await switchModel(record, params.sessionId, value);
1724
2903
  break;
@@ -1749,22 +2928,15 @@ async function apply(ctx, config = {}) {
1749
2928
  };
1750
2929
  };
1751
2930
  function requirePersistence() {
1752
- const persistence = getPersistence();
1753
- if (persistence === void 0) {
1754
- throw internalError("session persistence is not composed; session history is unavailable");
1755
- }
1756
- return persistence;
1757
- }
1758
- function getPersistence() {
1759
- return ctx.get("sessionPersistence");
2931
+ return sessionPersistence;
1760
2932
  }
1761
2933
  function validateCwd(cwd) {
1762
2934
  if (!isAbsolute(cwd)) throw invalidParams(`cwd must be an absolute path: ${cwd}`);
1763
2935
  }
1764
2936
  const mcpMounts = /* @__PURE__ */ new Map();
1765
2937
  let warnedNoMcpClient = false;
1766
- const sanitizeServerName = (raw) => {
1767
- const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 32);
2938
+ const sanitizeServerName = (raw2) => {
2939
+ const cleaned = raw2.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 32);
1768
2940
  return cleaned.length > 0 ? cleaned : "server";
1769
2941
  };
1770
2942
  const mcpConfigFor = (server, cwd, serverName) => {
@@ -1845,29 +3017,38 @@ async function apply(ctx, config = {}) {
1845
3017
  }
1846
3018
  }
1847
3019
  };
1848
- const stream = config.stream ?? ndJsonStream(
3020
+ const raw = config.stream ?? ndJsonStream(
1849
3021
  Writable.toWeb(process.stdout),
1850
3022
  Readable.toWeb(process.stdin)
1851
3023
  );
3024
+ const stream = muxAcpStream(raw, rpc);
1852
3025
  conn = new AgentSideConnection(makeAgent, stream);
3026
+ const disposeUserQuestions = installAcpUserQuestionProvider(userQuestions, {
3027
+ formSupported: () => clientElicitationForm,
3028
+ sessionIdForRequest: (request) => {
3029
+ if (request.agent === void 0) return void 0;
3030
+ const record = ownedRecord(request.agent);
3031
+ return record === void 0 ? void 0 : String(record.agent.session.id);
3032
+ },
3033
+ create: (request) => conn.unstable_createElicitation(request)
3034
+ });
1853
3035
  let quiescing;
1854
3036
  const quiesce = () => {
1855
3037
  if (quiescing !== void 0) return quiescing;
1856
3038
  closed = true;
1857
3039
  const records = [...sessions.values()];
1858
3040
  sessions.clear();
3041
+ subagentByChild.clear();
3042
+ subagentByRun.clear();
1859
3043
  for (const record of records) {
1860
3044
  record.agent.cancel({ kind: "user" });
1861
3045
  settlePrompt(record, "cancelled");
1862
3046
  }
1863
3047
  quiescing = (async () => {
1864
- const subagents = ctx.get("subagents");
1865
- if (subagents !== void 0) {
1866
- try {
1867
- await subagents.drainContinuableDescendants(records.map((record) => record.agent));
1868
- } catch (error) {
1869
- logWarn(`continuable subagent teardown failed: ${String(error)}`);
1870
- }
3048
+ try {
3049
+ await subagents.drainContinuableDescendants(records.map((record) => record.agent));
3050
+ } catch (error) {
3051
+ logWarn(`continuable subagent teardown failed: ${String(error)}`);
1871
3052
  }
1872
3053
  const disposals = await Promise.allSettled(records.map((record) => record.dispose()));
1873
3054
  const failures = disposals.filter(
@@ -1889,6 +3070,7 @@ async function apply(ctx, config = {}) {
1889
3070
  logWarn(`connection-close teardown failed: ${String(error)}`);
1890
3071
  });
1891
3072
  ctx.effect(() => quiesce, "acp-bridge.connection");
3073
+ ctx.effect(() => disposeUserQuestions, "acp-bridge.user-questions");
1892
3074
  }
1893
3075
 
1894
3076
  // src/bundle.ts