lody 0.97.1 → 0.98.0

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.
@@ -1,13 +1,12 @@
1
1
  import { S as SessionUsageAccumulator } from "./chunks/usage-DY4Eg1Mf.js";
2
+ import { o as object, s as string, l as literal, b as boolean, c as array, r as record, u as unknown } from "./chunks/schemas-C-NzK2uQ.js";
2
3
  import { randomUUID, createHash } from "node:crypto";
3
4
  import { isAbsolute } from "node:path";
4
5
  import { Readable, Writable } from "node:stream";
5
- import { n as ndJsonStream, A as AgentSideConnection, R as RequestError, P as PROTOCOL_VERSION } from "./chunks/acp-DCoAUZc7.js";
6
- import { j as DEEPSEEK_HARNESS_BASE_URL_ENV, k as DEEPSEEK_HARNESS_API_KEY_ENV, a as DEEPSEEK_HARNESS_AGENT_PRESETS, l as ACP_EXTENSION_DSH_VERSION, m as DEEPSEEK_HARNESS_DEFAULT_MODEL, n as DEEPSEEK_HARNESS_PROVIDER } from "./chunks/profile-DQaC7SJZ.js";
6
+ import { j as DEEPSEEK_HARNESS_BASE_URL_ENV, l as DEEPSEEK_HARNESS_API_KEY_ENV, n as ndJsonStream, o as AgentSideConnection, a as DEEPSEEK_HARNESS_AGENT_PRESETS, R as RequestError, p as ACP_EXTENSION_DSH_VERSION, P as PROTOCOL_VERSION, q as DEEPSEEK_HARNESS_DEFAULT_MODEL, r as DEEPSEEK_HARNESS_PROVIDER } from "./chunks/profile-BmD8b-zH.js";
7
7
  import { L as LODY_EXTENSION_METHODS } from "./chunks/methods-CvEDMahD.js";
8
8
  import { c as createPlanModeConfigOption, L as LODY_PLAN_MODE_CONFIG_ID } from "./chunks/plan-mode-BLOER-fv.js";
9
9
  import "node:url";
10
- import "./chunks/schemas-C-NzK2uQ.js";
11
10
  const prices = {
12
11
  "deepseek-flash": [0.15, 3e-3, 0.6],
13
12
  // Official aliases now served and billed as DeepSeek-V4.1-Flash.
@@ -60,6 +59,172 @@ class HarnessUsageTracker {
60
59
  return this.accumulator.update(sessionId, String(seq), { [this.route.model]: usage });
61
60
  }
62
61
  }
62
+ const questionSchema = object({
63
+ id: string().refine((value) => value.trim().length > 0),
64
+ question: string().min(1),
65
+ header: string().optional(),
66
+ detail: string().optional(),
67
+ options: array(object({
68
+ label: string().min(1),
69
+ description: string().optional()
70
+ })).optional(),
71
+ multiSelect: boolean().optional(),
72
+ intent: object({ kind: literal("plan-review"), approve: string() }).optional()
73
+ });
74
+ function failure(code, message) {
75
+ return Object.assign(new Error(message), { name: "UserQuestionError", code });
76
+ }
77
+ const meta = (fields = {}) => ({
78
+ lody: { elicitation: { version: 1, ...fields } }
79
+ });
80
+ class UserQuestionBridge {
81
+ sessionId;
82
+ send;
83
+ capabilities;
84
+ tail = Promise.resolve();
85
+ pending = /* @__PURE__ */ new Set();
86
+ constructor(sessionId, send, capabilities) {
87
+ this.sessionId = sessionId;
88
+ this.send = send;
89
+ this.capabilities = capabilities;
90
+ }
91
+ cancel() {
92
+ for (const controller of this.pending)
93
+ controller.abort();
94
+ }
95
+ ask(request) {
96
+ const controller = new AbortController();
97
+ this.pending.add(controller);
98
+ const signal = request.signal ? AbortSignal.any([request.signal, controller.signal]) : controller.signal;
99
+ const aborted = () => failure("ASK_ABORTED", "ask_user_question was cancelled before the user answered");
100
+ let removeListener = () => {
101
+ };
102
+ const cancellation = new Promise((_, reject) => {
103
+ const abort = () => reject(aborted());
104
+ if (signal.aborted)
105
+ abort();
106
+ else {
107
+ signal.addEventListener("abort", abort, { once: true });
108
+ removeListener = () => signal.removeEventListener("abort", abort);
109
+ }
110
+ });
111
+ const previous = this.tail;
112
+ const work = previous.then(async () => {
113
+ if (signal.aborted)
114
+ throw aborted();
115
+ return this.elicit(request);
116
+ });
117
+ const result = Promise.race([work, cancellation]).finally(() => {
118
+ removeListener();
119
+ this.pending.delete(controller);
120
+ });
121
+ this.tail = Promise.all([previous, result.catch(() => {
122
+ })]).then(() => {
123
+ });
124
+ return result;
125
+ }
126
+ async elicit(request) {
127
+ const capabilities = this.capabilities();
128
+ if (!capabilities.form)
129
+ throw failure("NO_PROVIDER", "ACP client does not support form elicitation");
130
+ const parsed = array(questionSchema).min(1).safeParse(request.questions);
131
+ if (!parsed.success)
132
+ throw failure("BAD_QUESTIONS", "Invalid user question request");
133
+ const questions = parsed.data;
134
+ if (new Set(questions.map((q) => q.id)).size !== questions.length)
135
+ throw failure("BAD_QUESTIONS", "Question ids must be unique");
136
+ const properties = /* @__PURE__ */ Object.create(null);
137
+ const fields = questions.map((question, index) => {
138
+ const key = `question_${index}`;
139
+ const customKey = `custom_${index}`;
140
+ const options = question.options ?? [];
141
+ if (new Set(options.map((o) => o.label)).size !== options.length)
142
+ throw failure("BAD_QUESTIONS", "Option labels must be unique");
143
+ if (question.intent && (question.detail === void 0 || !options.some((o) => o.label === question.intent?.approve)))
144
+ throw failure("BAD_INTENT", "Plan review requires detail and an existing approval option");
145
+ const description = [question.question, question.detail].filter(Boolean).join("\n\n");
146
+ const additive = options.length > 0 && question.multiSelect === true && capabilities.answerNotes;
147
+ let other = "Other";
148
+ while (options.some((o) => o.label === other))
149
+ other += " (custom)";
150
+ const choices = options.map((o) => ({
151
+ const: o.label,
152
+ title: o.label,
153
+ ...o.description ? { description: o.description } : {}
154
+ }));
155
+ if (additive)
156
+ choices.push({ const: other, title: other });
157
+ const base = { title: question.header || question.id, description };
158
+ properties[key] = options.length === 0 ? { ...base, type: "string" } : question.multiSelect ? { ...base, type: "array", items: { anyOf: choices } } : { ...base, type: "string", oneOf: choices };
159
+ if (options.length > 0)
160
+ properties[customKey] = {
161
+ type: "string",
162
+ title: "Other",
163
+ description: additive ? "Add a custom answer alongside your selections, or select Other and enter it here." : "Type your own answer instead of selecting an option.",
164
+ _meta: meta(additive ? { noteFor: key } : { customAnswerFor: key })
165
+ };
166
+ return {
167
+ question,
168
+ key,
169
+ customKey,
170
+ options,
171
+ additive,
172
+ other,
173
+ description
174
+ };
175
+ });
176
+ let response;
177
+ try {
178
+ response = await this.send({
179
+ sessionId: this.sessionId,
180
+ mode: "form",
181
+ message: fields.length === 1 ? fields[0]?.description ?? "Input requested" : "Input requested",
182
+ requestedSchema: { type: "object", properties },
183
+ _meta: meta({ autoResolveAfterSeconds: null })
184
+ });
185
+ } catch (cause) {
186
+ throw failure("ANSWER_FAILED", `ACP user question request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
187
+ }
188
+ if (response.action === "cancel")
189
+ throw failure("ASK_ABORTED", "The user cancelled the question");
190
+ if (response.action === "decline")
191
+ throw failure("ASK_DECLINED", "The user declined the question");
192
+ const accepted = object({
193
+ action: literal("accept"),
194
+ content: record(string(), unknown())
195
+ }).safeParse(response);
196
+ if (!accepted.success)
197
+ throw failure("INVALID_ANSWER", "ACP client returned an invalid question response");
198
+ const content = accepted.data.content;
199
+ return {
200
+ answers: fields.map(({ question, key, customKey, options, additive, other }) => {
201
+ const raw = content[key];
202
+ const custom = content[customKey];
203
+ const invalid = () => failure("INVALID_ANSWER", `Invalid answer for question ${question.id}`);
204
+ if (custom !== void 0 && typeof custom !== "string")
205
+ throw invalid();
206
+ if (options.length === 0) {
207
+ if (typeof raw !== "string" || !raw.trim())
208
+ throw invalid();
209
+ return { id: question.id, selected: [], custom: raw };
210
+ }
211
+ const selected = raw === void 0 ? [] : question.multiSelect ? raw : [raw];
212
+ if (!Array.isArray(selected) || selected.some((v) => typeof v !== "string") || new Set(selected).size !== selected.length)
213
+ throw invalid();
214
+ if (selected.some((v) => !options.some((o) => o.label === v) && !(additive && v === other)))
215
+ throw invalid();
216
+ const hasCustom = typeof custom === "string" && custom.trim().length > 0;
217
+ if (!selected.length && !hasCustom || additive && selected.includes(other) && !hasCustom || !question.multiSelect && selected.length > 0 && hasCustom)
218
+ throw invalid();
219
+ return {
220
+ id: question.id,
221
+ selected: selected.filter((v) => !(additive && v === other)),
222
+ ...hasCustom ? { custom } : {}
223
+ };
224
+ })
225
+ };
226
+ }
227
+ }
63
228
  const name = "acp-extension-dsh";
64
229
  const inject = [
65
230
  "agents",
@@ -69,7 +234,8 @@ const inject = [
69
234
  "llm",
70
235
  "permissionPresets",
71
236
  "sessionPersistence",
72
- "sessionQuery"
237
+ "sessionQuery",
238
+ "userQuestions"
73
239
  ];
74
240
  const LODY_CAPABILITIES = {
75
241
  compaction: { version: 1 },
@@ -346,10 +512,10 @@ function installModelSelection(agentContext, selection) {
346
512
  };
347
513
  });
348
514
  }
349
- function planModeService(record) {
350
- return record.agent.ctx.get("planMode");
515
+ function planModeService(record2) {
516
+ return record2.agent.ctx.get("planMode");
351
517
  }
352
- function configOptions(record) {
518
+ function configOptions(record2) {
353
519
  const options = [
354
520
  {
355
521
  id: MODE_CONFIG_ID,
@@ -357,8 +523,8 @@ function configOptions(record) {
357
523
  description: "Sandbox and approval policy for the session",
358
524
  category: "mode",
359
525
  type: "select",
360
- currentValue: record.permissionMode,
361
- options: record.permissionOptions.map((mode) => ({
526
+ currentValue: record2.permissionMode,
527
+ options: record2.permissionOptions.map((mode) => ({
362
528
  value: mode.value,
363
529
  name: mode.name,
364
530
  description: mode.description ?? null
@@ -370,8 +536,8 @@ function configOptions(record) {
370
536
  description: "Tools, prompt, and capabilities composed for the session",
371
537
  category: "agent_preset",
372
538
  type: "select",
373
- currentValue: record.agentPreset,
374
- options: record.agentPresetOptions.map((preset) => {
539
+ currentValue: record2.agentPreset,
540
+ options: record2.agentPresetOptions.map((preset) => {
375
541
  const builtIn = DEEPSEEK_HARNESS_AGENT_PRESETS.find((candidate) => candidate.value === preset.id);
376
542
  return {
377
543
  value: preset.id,
@@ -386,28 +552,28 @@ function configOptions(record) {
386
552
  description: "DeepSeek model used for the session",
387
553
  category: "model",
388
554
  type: "select",
389
- currentValue: record.selection.current.model,
390
- options: record.models.map((model2) => ({
555
+ currentValue: record2.selection.current.model,
556
+ options: record2.models.map((model2) => ({
391
557
  value: model2.id,
392
558
  name: model2.name,
393
559
  description: model2.description ?? null
394
560
  }))
395
561
  }
396
562
  ];
397
- const plan = planModeService(record);
563
+ const plan = planModeService(record2);
398
564
  if (plan) {
399
- const state = plan.get(record.agent);
565
+ const state = plan.get(record2.agent);
400
566
  options.push(createPlanModeConfigOption(state.pending ?? state.active));
401
567
  }
402
- const model = record.models.find((candidate) => candidate.id === record.selection.current.model);
403
- if (model?.reasoning && record.selection.current.reasoningEffort) {
568
+ const model = record2.models.find((candidate) => candidate.id === record2.selection.current.model);
569
+ if (model?.reasoning && record2.selection.current.reasoningEffort) {
404
570
  options.push({
405
571
  id: REASONING_EFFORT_CONFIG_ID,
406
572
  name: "Reasoning effort",
407
573
  description: "How much reasoning effort the model should use",
408
574
  category: "thought_level",
409
575
  type: "select",
410
- currentValue: record.selection.current.reasoningEffort,
576
+ currentValue: record2.selection.current.reasoningEffort,
411
577
  options: model.reasoning.efforts.map((effort) => ({
412
578
  value: effort.id,
413
579
  name: effort.name,
@@ -417,10 +583,10 @@ function configOptions(record) {
417
583
  }
418
584
  return options;
419
585
  }
420
- function legacyModels(record) {
586
+ function legacyModels(record2) {
421
587
  return {
422
- currentModelId: record.selection.current.model,
423
- availableModels: record.models.flatMap((model) => [
588
+ currentModelId: record2.selection.current.model,
589
+ availableModels: record2.models.flatMap((model) => [
424
590
  {
425
591
  modelId: model.id,
426
592
  name: model.name,
@@ -434,10 +600,10 @@ function legacyModels(record) {
434
600
  ])
435
601
  };
436
602
  }
437
- function modeState(record) {
603
+ function modeState(record2) {
438
604
  return {
439
- currentModeId: record.permissionMode,
440
- availableModes: record.permissionOptions.map((mode) => ({
605
+ currentModeId: record2.permissionMode,
606
+ availableModes: record2.permissionOptions.map((mode) => ({
441
607
  id: mode.value,
442
608
  name: mode.name,
443
609
  description: mode.description ?? null
@@ -642,6 +808,7 @@ function apply(ctx, rawConfig) {
642
808
  let closed = false;
643
809
  let conn;
644
810
  let imagePromptEnabled = false;
811
+ let questionCapabilities = { form: false, answerNotes: false };
645
812
  if (ctx.permissionPresets.names.length === 0) {
646
813
  throw new Error("acp-extension-dsh: no permission presets are composed");
647
814
  }
@@ -658,42 +825,42 @@ function apply(ctx, rawConfig) {
658
825
  throw internalError("the ACP bridge has been disposed");
659
826
  };
660
827
  const requireSession = (sessionId) => {
661
- const record = sessions.get(sessionId);
662
- if (!record)
828
+ const record2 = sessions.get(sessionId);
829
+ if (!record2)
663
830
  throw invalidParams(`unknown session: ${sessionId}`);
664
- return record;
831
+ return record2;
665
832
  };
666
833
  const ownedRecord = (agent) => {
667
- const record = sessions.get(agent.session.id);
668
- return record?.agent === agent ? record : void 0;
834
+ const record2 = sessions.get(agent.session.id);
835
+ return record2?.agent === agent ? record2 : void 0;
669
836
  };
670
837
  const notify = async (notification) => {
671
838
  await conn.sessionUpdate(notification).catch((error) => {
672
839
  ctx.logger.warn(`acp-extension-dsh: session/update failed: ${String(error)}`);
673
840
  });
674
841
  };
675
- const enqueueOutput = (record, work, inflight) => {
676
- record.outputTail = record.outputTail.then(work).catch((error) => {
677
- const failure = error instanceof Error ? error : new Error(String(error));
842
+ const enqueueOutput = (record2, work, inflight) => {
843
+ record2.outputTail = record2.outputTail.then(work).catch((error) => {
844
+ const failure2 = error instanceof Error ? error : new Error(String(error));
678
845
  if (inflight)
679
- inflight.outputError ??= failure;
846
+ inflight.outputError ??= failure2;
680
847
  ctx.logger.warn(`acp-extension-dsh: assistant output failed: ${errorChain(error)}`);
681
848
  });
682
849
  };
683
- const enqueueNotification = (record, notification, inflight) => enqueueOutput(record, () => notify(notification), inflight);
684
- const settleAfterQuiescence = (record, inflight) => {
850
+ const enqueueNotification = (record2, notification, inflight) => enqueueOutput(record2, () => notify(notification), inflight);
851
+ const settleAfterQuiescence = (record2, inflight) => {
685
852
  if (inflight.settlementStarted)
686
853
  return;
687
854
  inflight.settlementStarted = true;
688
855
  void (async () => {
689
856
  await inflight.admissionDone;
690
857
  if (inflight.messageQueued) {
691
- await record.agent.whenIdle();
692
- await record.outputTail;
858
+ await record2.agent.whenIdle();
859
+ await record2.outputTail;
693
860
  }
694
- if (record.inflight !== inflight)
861
+ if (record2.inflight !== inflight)
695
862
  return;
696
- record.inflight = void 0;
863
+ record2.inflight = void 0;
697
864
  if (inflight.cancelRequested) {
698
865
  inflight.resolve("cancelled");
699
866
  } else if (inflight.outputError) {
@@ -707,43 +874,43 @@ function apply(ctx, rawConfig) {
707
874
  inflight.resolve(end ? end.kind === "interrupted" ? "cancelled" : "end_turn" : "cancelled");
708
875
  }
709
876
  })().catch((error) => {
710
- if (record.inflight !== inflight)
877
+ if (record2.inflight !== inflight)
711
878
  return;
712
- record.inflight = void 0;
879
+ record2.inflight = void 0;
713
880
  inflight.reject(internalError(`prompt settlement failed: ${errorChain(error)}`));
714
881
  });
715
882
  };
716
- const refreshPermissionState = (record) => {
717
- const permissionMode = ctx.permissionPresets.current(record.agent.session);
718
- if (permissionMode === record.permissionMode)
883
+ const refreshPermissionState = (record2) => {
884
+ const permissionMode = ctx.permissionPresets.current(record2.agent.session);
885
+ if (permissionMode === record2.permissionMode)
719
886
  return false;
720
- record.permissionMode = permissionMode;
721
- record.permissionOptions = permissionState(ctx, permissionMode);
887
+ record2.permissionMode = permissionMode;
888
+ record2.permissionOptions = permissionState(ctx, permissionMode);
722
889
  return true;
723
890
  };
724
- const schedulePermissionSync = (record) => {
725
- if (record.permissionSyncQueued)
891
+ const schedulePermissionSync = (record2) => {
892
+ if (record2.permissionSyncQueued)
726
893
  return;
727
- record.permissionSyncQueued = true;
894
+ record2.permissionSyncQueued = true;
728
895
  queueMicrotask(() => {
729
- record.permissionSyncQueued = false;
730
- if (sessions.get(record.agent.session.id) !== record)
896
+ record2.permissionSyncQueued = false;
897
+ if (sessions.get(record2.agent.session.id) !== record2)
731
898
  return;
732
899
  try {
733
- if (!refreshPermissionState(record))
900
+ if (!refreshPermissionState(record2))
734
901
  return;
735
- enqueueNotification(record, {
736
- sessionId: record.agent.session.id,
902
+ enqueueNotification(record2, {
903
+ sessionId: record2.agent.session.id,
737
904
  update: {
738
905
  sessionUpdate: "current_mode_update",
739
- currentModeId: record.permissionMode
906
+ currentModeId: record2.permissionMode
740
907
  }
741
908
  });
742
- enqueueNotification(record, {
743
- sessionId: record.agent.session.id,
909
+ enqueueNotification(record2, {
910
+ sessionId: record2.agent.session.id,
744
911
  update: {
745
912
  sessionUpdate: "config_option_update",
746
- configOptions: configOptions(record)
913
+ configOptions: configOptions(record2)
747
914
  }
748
915
  });
749
916
  } catch (error) {
@@ -752,43 +919,43 @@ function apply(ctx, rawConfig) {
752
919
  });
753
920
  };
754
921
  const disposeRecords = async (records) => {
755
- await Promise.all(records.map(async (record) => {
756
- await record.inflight?.admissionDone;
757
- await record.agent.whenIdle();
758
- await record.outputTail;
922
+ await Promise.all(records.map(async (record2) => {
923
+ await record2.inflight?.admissionDone;
924
+ await record2.agent.whenIdle();
925
+ await record2.outputTail;
759
926
  }));
760
927
  const subagents = ctx.get("subagents");
761
928
  if (subagents) {
762
929
  try {
763
- await subagents.drainContinuableDescendants(records.map((record) => record.agent));
930
+ await subagents.drainContinuableDescendants(records.map((record2) => record2.agent));
764
931
  } catch (error) {
765
932
  ctx.logger.warn(`acp-extension-dsh: continuable subagent teardown failed: ${String(error)}`);
766
933
  }
767
934
  }
768
- const results = await Promise.allSettled(records.map((record) => record.dispose()));
935
+ const results = await Promise.allSettled(records.map((record2) => record2.dispose()));
769
936
  const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
770
937
  if (failures.length > 0) {
771
938
  throw new AggregateError(failures, `DeepSeek ACP teardown failed for ${failures.length} session(s): ${failures.map(errorChain).join("; ")}`);
772
939
  }
773
940
  };
774
941
  ctx.on("agent/assistant-stream", ({ agent, frame }) => {
775
- const record = ownedRecord(agent);
776
- if (!record || frame.type !== "chunk")
942
+ const record2 = ownedRecord(agent);
943
+ if (!record2 || frame.type !== "chunk")
777
944
  return;
778
945
  const chunk = frame.chunk;
779
946
  if (!chunk)
780
947
  return;
781
948
  if (chunk.type === "reasoning-delta" && typeof chunk.text === "string" && chunk.text.length > 0) {
782
- enqueueNotification(record, {
783
- sessionId: record.agent.session.id,
949
+ enqueueNotification(record2, {
950
+ sessionId: record2.agent.session.id,
784
951
  update: {
785
952
  sessionUpdate: "agent_thought_chunk",
786
953
  content: { type: "text", text: chunk.text }
787
954
  }
788
955
  });
789
956
  } else if (chunk.type === "block-end" && chunk.block?.type === "reasoning") {
790
- enqueueNotification(record, {
791
- sessionId: record.agent.session.id,
957
+ enqueueNotification(record2, {
958
+ sessionId: record2.agent.session.id,
792
959
  update: {
793
960
  sessionUpdate: "agent_thought_chunk",
794
961
  content: { type: "text", text: "\n\n" }
@@ -797,35 +964,38 @@ function apply(ctx, rawConfig) {
797
964
  }
798
965
  });
799
966
  ctx.on("session/event", (session, event) => {
800
- const record = sessions.get(session.header.id);
801
- if (!record || record.agent.session !== session)
967
+ const record2 = sessions.get(session.header.id);
968
+ if (!record2 || record2.agent.session !== session)
802
969
  return;
803
970
  if (event.type === "plan/mode") {
804
- enqueueNotification(record, {
805
- sessionId: record.agent.session.id,
806
- update: { sessionUpdate: "config_option_update", configOptions: configOptions(record) }
971
+ enqueueNotification(record2, {
972
+ sessionId: record2.agent.session.id,
973
+ update: {
974
+ sessionUpdate: "config_option_update",
975
+ configOptions: configOptions(record2)
976
+ }
807
977
  });
808
978
  }
809
979
  if (PERMISSION_EVENT_TYPES.has(event.type))
810
- schedulePermissionSync(record);
980
+ schedulePermissionSync(record2);
811
981
  try {
812
982
  if (event.type === "request/context" && event.data.provider && event.data.model) {
813
- record.usage.setRoute(event.data.provider, event.data.model);
983
+ record2.usage.setRoute(event.data.provider, event.data.model);
814
984
  }
815
985
  if (event.type === "assistant/message" && event.data.usage && event.seq !== void 0) {
816
- const update = record.usage.record(session.id, event.seq, event.time ?? NaN, event.data.usage);
986
+ const update = record2.usage.record(session.id, event.seq, event.time ?? NaN, event.data.usage);
817
987
  if (update)
818
- enqueueOutput(record, () => conn.extNotification(LODY_EXTENSION_METHODS.sessionUsageUpdate, update), record.inflight);
988
+ enqueueOutput(record2, () => conn.extNotification(LODY_EXTENSION_METHODS.sessionUsageUpdate, update), record2.inflight);
819
989
  }
820
990
  if (event.type === "assistant/message") {
821
- const inflight = record.inflight?.turn === event.data.turn ? record.inflight : void 0;
822
- enqueueOutput(record, async () => {
991
+ const inflight = record2.inflight?.turn === event.data.turn ? record2.inflight : void 0;
992
+ enqueueOutput(record2, async () => {
823
993
  for (const block of event.data.message?.content ?? []) {
824
994
  const content = await assistantBlockToAcp(block, attachments);
825
995
  if (!content)
826
996
  continue;
827
997
  await notify({
828
- sessionId: record.agent.session.id,
998
+ sessionId: record2.agent.session.id,
829
999
  update: { sessionUpdate: "agent_message_chunk", content }
830
1000
  });
831
1001
  }
@@ -836,8 +1006,8 @@ function apply(ctx, rawConfig) {
836
1006
  kind: "context_compaction",
837
1007
  automatic: event.data.turn !== null
838
1008
  };
839
- enqueueNotification(record, {
840
- sessionId: record.agent.session.id,
1009
+ enqueueNotification(record2, {
1010
+ sessionId: record2.agent.session.id,
841
1011
  update: {
842
1012
  sessionUpdate: "tool_call",
843
1013
  toolCallId: `context-compaction:${event.data.compactionId}`,
@@ -854,8 +1024,8 @@ function apply(ctx, rawConfig) {
854
1024
  automatic: event.data.turn !== null,
855
1025
  ...event.data.error ? { failureReason: event.data.error } : {}
856
1026
  };
857
- enqueueNotification(record, {
858
- sessionId: record.agent.session.id,
1027
+ enqueueNotification(record2, {
1028
+ sessionId: record2.agent.session.id,
859
1029
  update: {
860
1030
  sessionUpdate: "tool_call_update",
861
1031
  toolCallId: `context-compaction:${event.data.compactionId}`,
@@ -866,7 +1036,7 @@ function apply(ctx, rawConfig) {
866
1036
  });
867
1037
  }
868
1038
  } finally {
869
- const inflight = record.inflight;
1039
+ const inflight = record2.inflight;
870
1040
  if (inflight && event.type === "turn/end" && inflight.turn === event.data.turn && event.data.reason) {
871
1041
  inflight.endReason = event.data.reason;
872
1042
  }
@@ -878,19 +1048,19 @@ function apply(ctx, rawConfig) {
878
1048
  inflight.turn = turn;
879
1049
  });
880
1050
  ctx.on("agent/error", ({ agent, turn, error }) => {
881
- const record = ownedRecord(agent);
882
- const inflight = record?.inflight;
883
- if (!record || !inflight || !inflight.messageQueued || inflight.turn !== turn)
1051
+ const record2 = ownedRecord(agent);
1052
+ const inflight = record2?.inflight;
1053
+ if (!record2 || !inflight || !inflight.messageQueued || inflight.turn !== turn)
884
1054
  return;
885
1055
  inflight.agentError = new Error(errorChain(error));
886
- settleAfterQuiescence(record, inflight);
1056
+ settleAfterQuiescence(record2, inflight);
887
1057
  });
888
1058
  ctx.on("approval/request", (request, next) => {
889
- const record = ownedRecord(request.agent);
890
- if (!record || !request.callId)
1059
+ const record2 = ownedRecord(request.agent);
1060
+ if (!record2 || !request.callId)
891
1061
  return next();
892
1062
  return conn.requestPermission({
893
- sessionId: record.agent.session.id,
1063
+ sessionId: record2.agent.session.id,
894
1064
  toolCall: { toolCallId: request.callId },
895
1065
  options: [
896
1066
  { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
@@ -902,91 +1072,100 @@ function apply(ctx, rawConfig) {
902
1072
  return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
903
1073
  });
904
1074
  });
905
- const setPermissionMode = (record, modeId) => {
906
- if (modeId === record.permissionMode)
1075
+ const setPermissionMode = (record2, modeId) => {
1076
+ if (modeId === record2.permissionMode)
907
1077
  return;
908
1078
  assertAllowed(modeId, new Set(ctx.permissionPresets.names), "permission mode");
909
- ctx.permissionPresets.set(record.agent.session, modeId);
910
- const permissionMode = ctx.permissionPresets.current(record.agent.session);
1079
+ ctx.permissionPresets.set(record2.agent.session, modeId);
1080
+ const permissionMode = ctx.permissionPresets.current(record2.agent.session);
911
1081
  if (permissionMode !== modeId) {
912
1082
  throw internalError(`permission preset ${JSON.stringify(modeId)} did not become the effective mode`);
913
1083
  }
914
- record.permissionMode = permissionMode;
915
- record.permissionOptions = permissionState(ctx, permissionMode);
1084
+ record2.permissionMode = permissionMode;
1085
+ record2.permissionOptions = permissionState(ctx, permissionMode);
916
1086
  };
917
- const setConfigOption = (record, params) => {
1087
+ const setConfigOption = (record2, params) => {
918
1088
  if (params.configId === LODY_PLAN_MODE_CONFIG_ID) {
919
- const plan = planModeService(record);
1089
+ const plan = planModeService(record2);
920
1090
  if (!plan || typeof params.value !== "boolean") {
921
1091
  throw invalidParams("Plan mode requires a supported Agent preset and a boolean value");
922
1092
  }
923
- plan.set(record.agent, params.value);
924
- return { configOptions: configOptions(record) };
1093
+ plan.set(record2.agent, params.value);
1094
+ return { configOptions: configOptions(record2) };
925
1095
  }
926
1096
  const value = requireSelectValue(params);
927
1097
  if (params.configId === MODE_CONFIG_ID) {
928
- setPermissionMode(record, value);
1098
+ setPermissionMode(record2, value);
929
1099
  } else if (params.configId === AGENT_PRESET_CONFIG_ID) {
930
- const available = new Set(record.agentPresetOptions.map((preset) => preset.id));
1100
+ const available = new Set(record2.agentPresetOptions.map((preset) => preset.id));
931
1101
  assertAllowed(value, available, "agent preset");
932
- if (value === record.agentPreset)
933
- return { configOptions: configOptions(record) };
934
- if (record.started) {
1102
+ if (value === record2.agentPreset)
1103
+ return { configOptions: configOptions(record2) };
1104
+ if (record2.started) {
935
1105
  throw invalidParams("agent preset is fixed after the session has started");
936
1106
  }
937
- return ctx.agentPresets.select(record.agent, value).then((selectedPreset) => {
938
- record.agentPreset = selectedPreset;
939
- return { configOptions: configOptions(record) };
1107
+ return ctx.agentPresets.select(record2.agent, value).then((selectedPreset) => {
1108
+ record2.agentPreset = selectedPreset;
1109
+ return { configOptions: configOptions(record2) };
940
1110
  }).catch((error) => {
941
1111
  if (error instanceof RequestError)
942
1112
  throw error;
943
1113
  throw invalidParams(`failed to select agent preset ${JSON.stringify(value)}: ${errorChain(error)}`);
944
1114
  });
945
1115
  } else if (params.configId === MODEL_CONFIG_ID) {
946
- return resolveHarnessModel(llm, record.selection.current.provider, value).then((model) => {
947
- const index = record.models.findIndex((candidate) => candidate.id === value);
1116
+ return resolveHarnessModel(llm, record2.selection.current.provider, value).then((model) => {
1117
+ const index = record2.models.findIndex((candidate) => candidate.id === value);
948
1118
  if (index === -1)
949
- record.models.push(model);
1119
+ record2.models.push(model);
950
1120
  else
951
- record.models[index] = model;
952
- const reasoningEffort = resolveReasoningEffort(model, record.selection.current.reasoningEffort && model.reasoning?.efforts.some((effort) => effort.id === record.selection.current.reasoningEffort) ? record.selection.current.reasoningEffort : void 0);
953
- record.selection.current = {
954
- provider: record.selection.current.provider,
1121
+ record2.models[index] = model;
1122
+ const reasoningEffort = resolveReasoningEffort(model, record2.selection.current.reasoningEffort && model.reasoning?.efforts.some((effort) => effort.id === record2.selection.current.reasoningEffort) ? record2.selection.current.reasoningEffort : void 0);
1123
+ record2.selection.current = {
1124
+ provider: record2.selection.current.provider,
955
1125
  model: value,
956
1126
  ...reasoningEffort ? { reasoningEffort } : {}
957
1127
  };
958
- return { configOptions: configOptions(record) };
1128
+ return { configOptions: configOptions(record2) };
959
1129
  }).catch((error) => {
960
1130
  if (error instanceof RequestError)
961
1131
  throw error;
962
1132
  throw invalidParams(`failed to resolve model ${JSON.stringify(value)}: ${errorChain(error)}`);
963
1133
  });
964
1134
  } else if (params.configId === REASONING_EFFORT_CONFIG_ID) {
965
- const model = record.models.find((candidate) => candidate.id === record.selection.current.model);
1135
+ const model = record2.models.find((candidate) => candidate.id === record2.selection.current.model);
966
1136
  if (!model)
967
1137
  throw internalError("selected model metadata is unavailable");
968
1138
  const reasoningEffort = resolveReasoningEffort(model, value);
969
1139
  if (!reasoningEffort) {
970
1140
  throw invalidParams(`model ${JSON.stringify(model.id)} has no reasoning selector`);
971
1141
  }
972
- record.selection.current = {
973
- ...record.selection.current,
1142
+ record2.selection.current = {
1143
+ ...record2.selection.current,
974
1144
  reasoningEffort
975
1145
  };
976
1146
  } else {
977
1147
  throw invalidParams(`unknown config option: ${params.configId}`);
978
1148
  }
979
- return { configOptions: configOptions(record) };
1149
+ return { configOptions: configOptions(record2) };
980
1150
  };
981
1151
  const makeAgent = (connection) => {
982
1152
  conn = connection;
983
1153
  return {
984
1154
  async initialize(_params) {
1155
+ const elicitation = _params.clientCapabilities?._meta?.lody;
1156
+ const notes = elicitation && typeof elicitation === "object" && !Array.isArray(elicitation) && "elicitation" in elicitation ? elicitation.elicitation : void 0;
1157
+ questionCapabilities = {
1158
+ form: _params.clientCapabilities?.elicitation?.form != null,
1159
+ answerNotes: !!notes && typeof notes === "object" && !Array.isArray(notes) && "version" in notes && notes.version === 1 && "answerNotes" in notes && notes.answerNotes === true
1160
+ };
985
1161
  const { models } = await loadModelCatalog();
986
1162
  imagePromptEnabled = supportsAcpImagePrompts(attachments, models);
987
1163
  return {
988
1164
  protocolVersion: PROTOCOL_VERSION,
989
- agentInfo: { name: "acp-extension-dsh", version: ACP_EXTENSION_DSH_VERSION },
1165
+ agentInfo: {
1166
+ name: "acp-extension-dsh",
1167
+ version: ACP_EXTENSION_DSH_VERSION
1168
+ },
990
1169
  agentCapabilities: {
991
1170
  promptCapabilities: {
992
1171
  image: imagePromptEnabled,
@@ -1014,6 +1193,7 @@ function apply(ctx, rawConfig) {
1014
1193
  }
1015
1194
  const { models, initialModel: initialModelId } = catalog;
1016
1195
  const sessionId = randomUUID();
1196
+ const questions = new UserQuestionBridge(sessionId, (request) => conn.unstable_createElicitation(request), () => questionCapabilities);
1017
1197
  const initialModel = models.find((model) => model.id === initialModelId);
1018
1198
  if (!initialModel)
1019
1199
  throw internalError("initial model metadata is unavailable");
@@ -1026,13 +1206,9 @@ function apply(ctx, rawConfig) {
1026
1206
  }
1027
1207
  };
1028
1208
  const agentPresetOptions = (await ctx.agentPresets.list()).filter((preset) => preset.broken === void 0);
1029
- let requestedPreset = ctx.agentPresets.defaultId;
1209
+ const requestedPreset = ctx.agentPresets.defaultId;
1030
1210
  if (!agentPresetOptions.some((preset) => preset.id === requestedPreset)) {
1031
- if (!agentPresetOptions.some((preset) => preset.id === "standard")) {
1032
- throw internalError(`default agent preset ${JSON.stringify(requestedPreset)} and recovery preset "standard" are unavailable; restore the bundled standard preset or choose an available agent-presets.default in $DSH_HOME/settings.yaml`);
1033
- }
1034
- ctx.logger.warn(`acp-extension-dsh: default agent preset ${JSON.stringify(requestedPreset)} is unavailable; using "standard" for this session without changing settings`);
1035
- requestedPreset = "standard";
1211
+ throw internalError(`default agent preset ${JSON.stringify(requestedPreset)} is unavailable`);
1036
1212
  }
1037
1213
  const mcpServerNames = reserveMcpServerNames(params.mcpServers, sessionId, activeMcpServerNames);
1038
1214
  let mountedPreset = requestedPreset;
@@ -1044,6 +1220,11 @@ function apply(ctx, rawConfig) {
1044
1220
  agentOptions: { provider: config.provider, model: initialModelId },
1045
1221
  setup: async (agentContext) => {
1046
1222
  installModelSelection(agentContext, selection);
1223
+ agentContext.on("user-questions/request", (request, next) => {
1224
+ if (!request.agent || ownedRecord(request.agent)?.questions !== questions)
1225
+ return next();
1226
+ return questions.ask(request);
1227
+ });
1047
1228
  mountedPreset = (await ctx.agentPresets.mount(agentContext, requestedPreset)).id;
1048
1229
  await mountMcpServers(agentContext, params.mcpServers, mcpServerNames.names, params.cwd);
1049
1230
  }
@@ -1055,6 +1236,7 @@ function apply(ctx, rawConfig) {
1055
1236
  throw internalError(`failed to create session: ${errorChain(error)}`);
1056
1237
  }
1057
1238
  const dispose = async () => {
1239
+ questions.cancel();
1058
1240
  try {
1059
1241
  await handle.dispose();
1060
1242
  } finally {
@@ -1074,7 +1256,8 @@ function apply(ctx, rawConfig) {
1074
1256
  await dispose();
1075
1257
  throw error;
1076
1258
  }
1077
- const record = {
1259
+ const record2 = {
1260
+ questions,
1078
1261
  usage: new HarnessUsageTracker(!baseUrl || /^https:\/\/api\.deepseek\.com(?:\/v1)?\/?$/.test(baseUrl)),
1079
1262
  agent: handle.agent,
1080
1263
  dispose,
@@ -1087,27 +1270,27 @@ function apply(ctx, rawConfig) {
1087
1270
  started: false,
1088
1271
  outputTail: Promise.resolve()
1089
1272
  };
1090
- sessions.set(sessionId, record);
1273
+ sessions.set(sessionId, record2);
1091
1274
  return {
1092
1275
  sessionId,
1093
- modes: modeState(record),
1094
- configOptions: configOptions(record),
1095
- models: legacyModels(record)
1276
+ modes: modeState(record2),
1277
+ configOptions: configOptions(record2),
1278
+ models: legacyModels(record2)
1096
1279
  };
1097
1280
  },
1098
1281
  setSessionMode(params) {
1099
- const record = requireSession(params.sessionId);
1100
- setPermissionMode(record, params.modeId);
1282
+ const record2 = requireSession(params.sessionId);
1283
+ setPermissionMode(record2, params.modeId);
1101
1284
  },
1102
1285
  setSessionConfigOption(params) {
1103
1286
  return setConfigOption(requireSession(params.sessionId), params);
1104
1287
  },
1105
1288
  async prompt(params) {
1106
1289
  assertOpen();
1107
- const record = requireSession(params.sessionId);
1108
- if (record.inflight)
1290
+ const record2 = requireSession(params.sessionId);
1291
+ if (record2.inflight)
1109
1292
  throw invalidParams("a prompt is already in flight for this session");
1110
- if (ctx.agents.get(record.agent.id) !== record.agent) {
1293
+ if (ctx.agents.get(record2.agent.id) !== record2.agent) {
1111
1294
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1112
1295
  }
1113
1296
  const messageId = randomUUID();
@@ -1131,24 +1314,24 @@ function apply(ctx, rawConfig) {
1131
1314
  cancelRequested: false,
1132
1315
  settlementStarted: false
1133
1316
  };
1134
- record.inflight = inflight;
1317
+ record2.inflight = inflight;
1135
1318
  let admissionError;
1136
1319
  try {
1137
- const content = await admitAcpPrompt(params.prompt, llm, record.selection.current, attachments, imagePromptEnabled, admissionController.signal);
1320
+ const content = await admitAcpPrompt(params.prompt, llm, record2.selection.current, attachments, imagePromptEnabled, admissionController.signal);
1138
1321
  if (!inflight.cancelRequested) {
1139
- if (ctx.agents.get(record.agent.id) !== record.agent) {
1322
+ if (ctx.agents.get(record2.agent.id) !== record2.agent) {
1140
1323
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1141
1324
  }
1142
1325
  const message = createUserMessage(messageId, content);
1143
1326
  inflight.messageId = messageId;
1144
1327
  inflight.messageQueued = true;
1145
- const wasStarted = record.started;
1328
+ const wasStarted = record2.started;
1146
1329
  try {
1147
- record.agent.followup(message);
1148
- record.started = true;
1330
+ record2.agent.followup(message);
1331
+ record2.started = true;
1149
1332
  } catch (error) {
1150
1333
  inflight.messageQueued = false;
1151
- record.started = wasStarted;
1334
+ record2.started = wasStarted;
1152
1335
  throw new Error(`prompt was not queued: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1153
1336
  }
1154
1337
  }
@@ -1158,45 +1341,47 @@ function apply(ctx, rawConfig) {
1158
1341
  finishAdmission();
1159
1342
  }
1160
1343
  if (inflight.cancelRequested) {
1161
- settleAfterQuiescence(record, inflight);
1344
+ settleAfterQuiescence(record2, inflight);
1162
1345
  return { stopReason: await completion };
1163
1346
  }
1164
1347
  if (admissionError) {
1165
- if (record.inflight === inflight)
1166
- record.inflight = void 0;
1348
+ if (record2.inflight === inflight)
1349
+ record2.inflight = void 0;
1167
1350
  if (admissionError instanceof RequestError)
1168
1351
  throw admissionError;
1169
1352
  throw internalError(errorChain(admissionError));
1170
1353
  }
1171
- settleAfterQuiescence(record, inflight);
1354
+ settleAfterQuiescence(record2, inflight);
1172
1355
  return { stopReason: await completion };
1173
1356
  },
1174
1357
  cancel(params) {
1175
- const record = sessions.get(params.sessionId);
1176
- if (!record)
1358
+ const record2 = sessions.get(params.sessionId);
1359
+ if (!record2)
1177
1360
  return Promise.resolve();
1178
- const inflight = record.inflight;
1361
+ record2.questions.cancel();
1362
+ const inflight = record2.inflight;
1179
1363
  if (inflight) {
1180
1364
  inflight.cancelRequested = true;
1181
1365
  inflight.admissionController.abort(new Error("ACP prompt cancelled"));
1182
- settleAfterQuiescence(record, inflight);
1366
+ settleAfterQuiescence(record2, inflight);
1183
1367
  }
1184
1368
  if (!inflight || inflight.messageQueued)
1185
- record.agent.cancel({ kind: "user" });
1369
+ record2.agent.cancel({ kind: "user" });
1186
1370
  return Promise.resolve();
1187
1371
  },
1188
1372
  async closeSession(params) {
1189
- const record = requireSession(params.sessionId);
1373
+ const record2 = requireSession(params.sessionId);
1374
+ record2.questions.cancel();
1190
1375
  sessions.delete(params.sessionId);
1191
- const inflight = record.inflight;
1376
+ const inflight = record2.inflight;
1192
1377
  if (inflight) {
1193
1378
  inflight.cancelRequested = true;
1194
1379
  inflight.admissionController.abort(new Error("ACP session closed"));
1195
- settleAfterQuiescence(record, inflight);
1380
+ settleAfterQuiescence(record2, inflight);
1196
1381
  }
1197
1382
  if (!inflight || inflight.messageQueued)
1198
- record.agent.cancel({ kind: "user" });
1199
- await disposeRecords([record]);
1383
+ record2.agent.cancel({ kind: "user" });
1384
+ await disposeRecords([record2]);
1200
1385
  }
1201
1386
  };
1202
1387
  };
@@ -1209,15 +1394,16 @@ function apply(ctx, rawConfig) {
1209
1394
  closed = true;
1210
1395
  const records = [...sessions.values()];
1211
1396
  sessions.clear();
1212
- for (const record of records) {
1213
- const inflight = record.inflight;
1397
+ for (const record2 of records) {
1398
+ record2.questions.cancel();
1399
+ const inflight = record2.inflight;
1214
1400
  if (inflight) {
1215
1401
  inflight.cancelRequested = true;
1216
1402
  inflight.admissionController.abort(new Error("ACP bridge disposed"));
1217
- settleAfterQuiescence(record, inflight);
1403
+ settleAfterQuiescence(record2, inflight);
1218
1404
  }
1219
1405
  if (!inflight || inflight.messageQueued)
1220
- record.agent.cancel({ kind: "user" });
1406
+ record2.agent.cancel({ kind: "user" });
1221
1407
  }
1222
1408
  quiescing = (async () => {
1223
1409
  await disposeRecords(records);