@openma/deepseek-harness-acp 0.4.6 → 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
@@ -444,7 +444,7 @@ function extractMessageTexts(message) {
444
444
  }
445
445
  function planEntries(data) {
446
446
  const todos = data["todos"];
447
- if (!Array.isArray(todos)) return [];
447
+ if (!Array.isArray(todos)) return void 0;
448
448
  const entries = [];
449
449
  for (const todo of todos) {
450
450
  if (todo === null || typeof todo !== "object") continue;
@@ -487,6 +487,27 @@ function emptyWindow() {
487
487
  error: void 0
488
488
  };
489
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
+ }
490
511
  var SessionProjection = class {
491
512
  /** Context capacity from the latest `request/context` event, when known. */
492
513
  contextWindow;
@@ -501,10 +522,14 @@ var SessionProjection = class {
501
522
  terminalOutput;
502
523
  /** Session working directory, advertised on display terminals. */
503
524
  cwd;
525
+ presentCall;
526
+ subagent;
504
527
  constructor(contextWindow, options) {
505
528
  this.contextWindow = contextWindow;
506
529
  this.terminalOutput = options?.terminalOutput ?? false;
507
530
  this.cwd = options?.cwd;
531
+ this.presentCall = options?.presentCall;
532
+ this.subagent = options?.subagent;
508
533
  }
509
534
  /** Reset per-prompt accumulators; call when a new `session/prompt` starts. */
510
535
  beginPrompt() {
@@ -539,6 +564,11 @@ var SessionProjection = class {
539
564
  * Project one session event onto zero or more ACP updates.
540
565
  */
541
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) {
542
572
  const data = event.data ?? {};
543
573
  switch (event.type) {
544
574
  case "assistant/chunk":
@@ -549,9 +579,34 @@ var SessionProjection = class {
549
579
  return this.onToolCall(data);
550
580
  case "tool/result":
551
581
  return this.onToolResult(data);
582
+ case "subagent/start":
583
+ return this.onSubagentStart(data);
584
+ case "subagent/end":
585
+ return this.onSubagentEnd(data);
552
586
  case "todo/write": {
553
587
  const entries = planEntries(data);
554
- 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
+ ];
555
610
  }
556
611
  case "turn/end":
557
612
  return this.onTurnEnd(data);
@@ -573,31 +628,117 @@ var SessionProjection = class {
573
628
  stepKey(data) {
574
629
  return `${String(data["turn"])}:${String(data["step"])}`;
575
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
+ }
576
690
  onChunk(data) {
577
691
  const chunk = data["chunk"];
578
692
  if (chunk === null || typeof chunk === "undefined" || typeof chunk !== "object") return [];
579
693
  const c = chunk;
580
694
  const text = c["text"];
581
695
  if (typeof text !== "string" || text.length === 0) return [];
696
+ const messageId = this.stepKey(data);
582
697
  if (c["type"] === "text-delta") {
583
- this.streamedText.add(this.stepKey(data));
584
- 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 }];
585
700
  }
586
701
  if (c["type"] === "reasoning-delta") {
587
- this.streamedReasoning.add(this.stepKey(data));
588
- 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 }];
589
704
  }
590
705
  return [];
591
706
  }
592
707
  onAssistantMessage(data) {
593
708
  const updates = [];
594
709
  const key = this.stepKey(data);
595
- 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
+ };
596
721
  if (reasoning.length > 0 && !this.streamedReasoning.has(key)) {
597
- 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
+ });
598
727
  }
599
728
  if (text.length > 0 && !this.streamedText.has(key)) {
600
- 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
+ });
601
742
  }
602
743
  this.streamedText.delete(key);
603
744
  this.streamedReasoning.delete(key);
@@ -631,7 +772,14 @@ var SessionProjection = class {
631
772
  const name3 = asString(data["name"]) ?? "tool";
632
773
  if (callId === void 0) return [];
633
774
  const rawArguments = typeof data["arguments"] === "string" ? data["arguments"] : "{}";
634
- 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);
635
783
  this.toolCalls.set(callId, facts);
636
784
  const displayTerminal = this.terminalOutput && facts.kind === "execute";
637
785
  return [
@@ -736,7 +884,7 @@ ${text.replace(/\n+$/, "")}
736
884
  };
737
885
 
738
886
  // src/bridge/history.ts
739
- function userText(data) {
887
+ function userMessage(data) {
740
888
  if (data === void 0) return void 0;
741
889
  const source = data["source"];
742
890
  const kind = source !== null && typeof source === "object" ? source["kind"] : void 0;
@@ -751,20 +899,28 @@ function userText(data) {
751
899
  }
752
900
  }
753
901
  const text = parts.join("");
754
- return text.length > 0 ? text : void 0;
902
+ return text.length > 0 ? { text } : {};
755
903
  }
756
904
  function buildReplay(events) {
757
905
  const projection = new SessionProjection();
758
906
  const updates = [];
759
907
  let plan;
760
908
  let title;
909
+ let sawUserInteraction = false;
761
910
  for (const event of events) {
762
911
  if (event.type === "user/message") {
763
- const text = userText(event.data);
764
- if (text !== void 0) {
765
- 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;
766
923
  }
767
- continue;
768
924
  }
769
925
  if (event.type === "session/title") {
770
926
  const t = event.data?.["title"];
@@ -781,25 +937,132 @@ function buildReplay(events) {
781
937
  }
782
938
  }
783
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
+ }
784
947
  return { updates, title, contextWindow: projection.contextWindow };
785
948
  }
786
949
 
787
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
+ }
788
959
  var UnsupportedPromptContentError = class extends Error {
789
960
  constructor(contentType) {
790
961
  super(`unsupported prompt content type: ${contentType}`);
791
962
  this.contentType = contentType;
963
+ this.name = "UnsupportedPromptContentError";
792
964
  }
793
965
  contentType;
794
966
  };
967
+ var PromptImageError = class extends Error {
968
+ constructor(message, options) {
969
+ super(message, options);
970
+ this.name = "PromptImageError";
971
+ }
972
+ };
795
973
  function contextBlock(uri, text) {
796
974
  return `<context ref=${JSON.stringify(uri)}>
797
975
  ${text}
798
976
  </context>`;
799
977
  }
800
- 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
+ }
801
1062
  const parts = [];
802
1063
  const display = [];
1064
+ const blocks = [];
1065
+ let imageIndex = 0;
803
1066
  for (const block of prompt) {
804
1067
  switch (block.type) {
805
1068
  case "text":
@@ -823,20 +1086,535 @@ ${contextBlock(resource.uri, resource.text)}
823
1086
  }
824
1087
  throw new UnsupportedPromptContentError("resource (binary)");
825
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
+ }
826
1105
  default:
827
1106
  throw new UnsupportedPromptContentError(block.type);
828
1107
  }
829
1108
  }
830
- const text = parts.join("");
1109
+ flushText(parts, blocks);
831
1110
  return {
832
- blocks: text.length > 0 ? [{ type: "text", text }] : [],
1111
+ blocks,
833
1112
  displayText: display.join(" ").trim()
834
1113
  };
835
1114
  }
836
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
+
837
1615
  // src/bridge/index.ts
838
1616
  var name = "acp-bridge";
839
- var inject = ["agents", "credentials", "llm", "agentDefaultModel", "sessionPersistence", "approval", "permissionPresets", "commands", "agentPresets", "skills", "subagents"];
1617
+ var inject = ["agents", "credentials", "llm", "agentDefaultModel", "sessionPersistence", "approval", "permissionPresets", "commands", "agentPresets", "skills", "subagents", "userQuestions"];
840
1618
  function invalidParams(detail) {
841
1619
  return RequestError.invalidParams(void 0, detail);
842
1620
  }
@@ -869,13 +1647,30 @@ async function apply(ctx, config = {}) {
869
1647
  const agentPresets = ctx.agentPresets;
870
1648
  const skillRegistry = ctx.skills;
871
1649
  const subagents = ctx.subagents;
1650
+ const userQuestions = ctx.userQuestions;
872
1651
  const harness = config.harness ?? (await Promise.resolve().then(() => (init_self_harness(), self_harness_exports))).selfHarness();
873
1652
  const { createUserMessage: createUserMessage2, errorChain: errorChain2, sessionId: SessionId2, foldSessionTitle: foldSessionTitle2, setSandboxMode: setSandboxMode2 } = harness;
874
1653
  const SANDBOX_MODES2 = harness.sandboxModes;
875
1654
  const sessions = /* @__PURE__ */ new Map();
1655
+ const subagentByChild = /* @__PURE__ */ new Map();
1656
+ const subagentByRun = /* @__PURE__ */ new Map();
1657
+ const watchedSubagentParents = /* @__PURE__ */ new WeakSet();
876
1658
  let closed = false;
877
1659
  let conn;
878
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 });
879
1674
  const modelCandidates = () => {
880
1675
  const seen = /* @__PURE__ */ new Set();
881
1676
  if (config.model !== void 0) seen.add(config.model);
@@ -1062,9 +1857,9 @@ async function apply(ctx, config = {}) {
1062
1857
  };
1063
1858
  const listProviderRoutes = async () => {
1064
1859
  try {
1065
- const raw = llm.listProviders();
1066
- if (!Array.isArray(raw)) return [];
1067
- return raw.flatMap((entry) => {
1860
+ const raw2 = llm.listProviders();
1861
+ if (!Array.isArray(raw2)) return [];
1862
+ return raw2.flatMap((entry) => {
1068
1863
  if (!entry || typeof entry !== "object") return [];
1069
1864
  const id = entry.id;
1070
1865
  if (typeof id !== "string" || id.length === 0) return [];
@@ -1201,13 +1996,78 @@ async function apply(ctx, config = {}) {
1201
1996
  record.inflight = void 0;
1202
1997
  inflight.resolve(reason);
1203
1998
  };
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
+ };
1204
2063
  const presetsService = () => agentPresets;
1205
2064
  const presetSetup = (presets, presetId) => {
1206
2065
  if (presetId === void 0) return void 0;
1207
2066
  return async (agentCtx) => {
2067
+ const started = performance.now();
1208
2068
  logDebug(`preset setup: mounting "${presetId}"`);
1209
2069
  await presets.mount(agentCtx, presetId);
1210
- logDebug(`preset setup: mounted "${presetId}"`);
2070
+ logDebug(`preset setup: mounted "${presetId}" in ${(performance.now() - started).toFixed(1)}ms`);
1211
2071
  };
1212
2072
  };
1213
2073
  const presetFromLog = (header, events) => {
@@ -1363,6 +2223,19 @@ async function apply(ctx, config = {}) {
1363
2223
  options: levels
1364
2224
  });
1365
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
+ }
1366
2239
  const models = await modelChoices(record);
1367
2240
  if (models.length >= 2) {
1368
2241
  result.push({
@@ -1442,7 +2315,18 @@ async function apply(ctx, config = {}) {
1442
2315
  list.push({
1443
2316
  name: descriptor.name,
1444
2317
  description: descriptor.description,
1445
- ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
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
+ } : {}
1446
2330
  });
1447
2331
  }
1448
2332
  } catch (error) {
@@ -1618,16 +2502,27 @@ async function apply(ctx, config = {}) {
1618
2502
  inflight: void 0
1619
2503
  };
1620
2504
  sessions.set(sessionId, record);
2505
+ watchSubagentParent(agent);
1621
2506
  return record;
1622
2507
  };
1623
2508
  ctx.on("session/event", (session, event) => {
1624
2509
  const sessionId = String(session.header.id);
1625
2510
  const record = sessions.get(sessionId);
1626
- 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
+ }
1627
2520
  try {
1628
- for (const update of record.projection.onEvent(event)) {
2521
+ const harnessEvent = event;
2522
+ for (const update of record.projection.onEvent(harnessEvent)) {
1629
2523
  notify(sessionId, update);
1630
2524
  }
2525
+ if (harnessEvent.type === "plan/mode") publishConfigOptions(sessionId, record);
1631
2526
  } finally {
1632
2527
  const inflight = record.inflight;
1633
2528
  if (inflight !== void 0 && event.type === "turn/end" && inflight.turn === event.data.turn) {
@@ -1677,13 +2572,22 @@ async function apply(ctx, config = {}) {
1677
2572
  const requested = params.protocolVersion;
1678
2573
  const capsMeta = params.clientCapabilities?.["_meta"];
1679
2574
  clientTerminalOutput = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["terminal_output"] === true : false;
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
+ }
1680
2580
  const providers = await listProviderRoutes();
1681
2581
  return {
1682
2582
  protocolVersion: typeof requested === "number" && requested >= 1 && requested < PROTOCOL_VERSION ? requested : PROTOCOL_VERSION,
1683
2583
  agentInfo: { name: "dsh-acp", title: "DeepSeek Harness", version: VERSION },
1684
2584
  agentCapabilities: {
1685
2585
  loadSession: true,
1686
- promptCapabilities: { image: false, audio: false, embeddedContext: true },
2586
+ promptCapabilities: {
2587
+ image: attachmentIngestOf(ctx.get("attachments")) !== void 0,
2588
+ audio: false,
2589
+ embeddedContext: true
2590
+ },
1687
2591
  // Stdio servers always work; streamable HTTP maps onto
1688
2592
  // mcp-client's second transport. Legacy SSE does not.
1689
2593
  mcpCapabilities: { http: true, sse: false },
@@ -1738,18 +2642,29 @@ async function apply(ctx, config = {}) {
1738
2642
  },
1739
2643
  async newSession(params) {
1740
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
+ };
1741
2652
  await requireCredential(config.provider);
2653
+ mark("credential");
1742
2654
  validateCwd(params.cwd);
1743
2655
  syncMcpServers(params.mcpServers, params.cwd);
2656
+ mark("request setup");
1744
2657
  const sessionId = SessionId2(randomUUID());
1745
2658
  const presets = presetsService();
1746
2659
  const presetId = (await presets.resolve(void 0)).id;
2660
+ mark("preset resolve");
1747
2661
  const handle = await agents.create({
1748
2662
  sessionId,
1749
2663
  meta: { cwd: params.cwd, ...presetId !== void 0 ? { agentPreset: presetId } : {} },
1750
2664
  agentOptions: agentOptionsFor(config.model),
1751
2665
  ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1752
2666
  });
2667
+ mark("agent create");
1753
2668
  if (closed) {
1754
2669
  await handle.dispose();
1755
2670
  throw internalError("connection closed during session/new");
@@ -1765,6 +2680,7 @@ async function apply(ctx, config = {}) {
1765
2680
  ensureSelection(record);
1766
2681
  queueMicrotask(() => publishCommands(String(sessionId)));
1767
2682
  const options = await configOptions(record);
2683
+ mark("response surface");
1768
2684
  return {
1769
2685
  sessionId: String(sessionId),
1770
2686
  modes: modeState(record),
@@ -1812,17 +2728,36 @@ async function apply(ctx, config = {}) {
1812
2728
  async prompt(params) {
1813
2729
  assertOpen();
1814
2730
  const record = await requireOrRestoreSession(params.sessionId);
1815
- if (record.inflight !== void 0) {
1816
- throw invalidParams("a prompt is already in flight for this session");
1817
- }
1818
2731
  let converted;
1819
2732
  try {
1820
- converted = convertPrompt(params.prompt);
2733
+ converted = await convertPrompt(
2734
+ params.prompt,
2735
+ attachmentIngestOf(ctx.get("attachments"))
2736
+ );
1821
2737
  } catch (error) {
1822
- if (error instanceof UnsupportedPromptContentError) throw invalidParams(error.message);
2738
+ if (error instanceof UnsupportedPromptContentError || error instanceof PromptImageError) {
2739
+ throw invalidParams(error.message);
2740
+ }
1823
2741
  throw error;
1824
2742
  }
1825
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
+ }
1826
2761
  const trimmed = converted.displayText.trim();
1827
2762
  const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/);
1828
2763
  const respond = (text) => {
@@ -1867,16 +2802,25 @@ async function apply(ctx, config = {}) {
1867
2802
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1868
2803
  }
1869
2804
  record.cancelled = false;
1870
- record.projection.beginPrompt();
1871
2805
  const message = createUserMessage2({
1872
2806
  content: converted.blocks,
1873
2807
  source: { kind: "user" }
1874
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();
1875
2819
  const stopReason = await new Promise((resolve, reject) => {
1876
2820
  const inflight = { resolve, reject, messageId: message.id, turn: void 0 };
1877
2821
  record.inflight = inflight;
1878
2822
  try {
1879
- record.agent.followup(message);
2823
+ deliverPrompt(record.agent, message, false);
1880
2824
  } catch (error) {
1881
2825
  record.inflight = void 0;
1882
2826
  const detail = error instanceof Error ? error.message : String(error);
@@ -1937,6 +2881,23 @@ async function apply(ctx, config = {}) {
1937
2881
  record.effort = value;
1938
2882
  break;
1939
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
+ }
1940
2901
  case "model": {
1941
2902
  await switchModel(record, params.sessionId, value);
1942
2903
  break;
@@ -1974,8 +2935,8 @@ async function apply(ctx, config = {}) {
1974
2935
  }
1975
2936
  const mcpMounts = /* @__PURE__ */ new Map();
1976
2937
  let warnedNoMcpClient = false;
1977
- const sanitizeServerName = (raw) => {
1978
- 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);
1979
2940
  return cleaned.length > 0 ? cleaned : "server";
1980
2941
  };
1981
2942
  const mcpConfigFor = (server, cwd, serverName) => {
@@ -2056,17 +3017,29 @@ async function apply(ctx, config = {}) {
2056
3017
  }
2057
3018
  }
2058
3019
  };
2059
- const stream = config.stream ?? ndJsonStream(
3020
+ const raw = config.stream ?? ndJsonStream(
2060
3021
  Writable.toWeb(process.stdout),
2061
3022
  Readable.toWeb(process.stdin)
2062
3023
  );
3024
+ const stream = muxAcpStream(raw, rpc);
2063
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
+ });
2064
3035
  let quiescing;
2065
3036
  const quiesce = () => {
2066
3037
  if (quiescing !== void 0) return quiescing;
2067
3038
  closed = true;
2068
3039
  const records = [...sessions.values()];
2069
3040
  sessions.clear();
3041
+ subagentByChild.clear();
3042
+ subagentByRun.clear();
2070
3043
  for (const record of records) {
2071
3044
  record.agent.cancel({ kind: "user" });
2072
3045
  settlePrompt(record, "cancelled");
@@ -2097,6 +3070,7 @@ async function apply(ctx, config = {}) {
2097
3070
  logWarn(`connection-close teardown failed: ${String(error)}`);
2098
3071
  });
2099
3072
  ctx.effect(() => quiesce, "acp-bridge.connection");
3073
+ ctx.effect(() => disposeUserQuestions, "acp-bridge.user-questions");
2100
3074
  }
2101
3075
 
2102
3076
  // src/bundle.ts