lody 0.102.0 → 0.104.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,38 +1,82 @@
1
+ import { R as RequestError, 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, p as ACP_EXTENSION_DSH_VERSION, P as PROTOCOL_VERSION, q as DEEPSEEK_HARNESS_DEFAULT_MODEL, r as DEEPSEEK_HARNESS_PROVIDER } from "./chunks/acp-C0LiROfB.js";
1
2
  import { resolve, isAbsolute } from "node:path";
2
- import { o as object, s as string, b as boolean, c as array, l as literal, u as unknown, n as number, a as _enum, r as record } from "./chunks/schemas-WuyisFi9.js";
3
+ import { o as object$1, s as string, c as boolean, b as array, l as literal, u as unknown, n as number, a as _enum, r as record } from "./chunks/schemas-D_XLKCXC.js";
3
4
  import { S as SessionUsageAccumulator, L as LODY_EXTENSION_METHODS, e as supportsLodySubagentEvents, d as LODY_SUBAGENT_EVENT_METHOD } from "./chunks/subagent-events-BJ9JwZva.js";
4
5
  import { randomUUID, createHash } from "node:crypto";
5
6
  import { Readable, Writable } from "node:stream";
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/acp-Bsd2Lmd6.js";
7
7
  import { c as createPlanModeConfigOption, L as LODY_PLAN_MODE_CONFIG_ID } from "./chunks/plan-mode-D0D5cnMs.js";
8
8
  import { L as LodySubagentEmitter } from "./chunks/subagent-emitter-BsTuVPDw.js";
9
9
  import "node:url";
10
- const blockSchema = object({ type: string() }).passthrough();
11
- const resultSchema = object({
10
+ function nativeTurnId(turn) {
11
+ return `dsh-turn:${turn}`;
12
+ }
13
+ function object(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
15
+ }
16
+ function forkTarget(meta2) {
17
+ const lody = object(object(meta2)?.lody);
18
+ if (!lody || !Object.hasOwn(lody, "forkAtTurn"))
19
+ return void 0;
20
+ const target = object(lody.forkAtTurn);
21
+ if (!target || target.version !== 1 || target.turnId !== void 0 && (typeof target.turnId !== "string" || !/^dsh-turn:(0|[1-9]\d*)$/.test(target.turnId) || !Number.isSafeInteger(Number(target.turnId.slice("dsh-turn:".length)))))
22
+ throw RequestError.invalidParams(void 0, "invalid _meta.lody.forkAtTurn");
23
+ return target?.turnId;
24
+ }
25
+ function forkSnapshot(source, target) {
26
+ const events = source.events;
27
+ const end = target === void 0 ? events.length - 1 : events.findIndex((event) => event.type === "turn/end" && typeof event.data.turn === "number" && nativeTurnId(event.data.turn) === target);
28
+ if (target !== void 0 && end === -1)
29
+ throw RequestError.invalidParams(void 0, "fork turn is unknown or has not ended");
30
+ const prefix = events.slice(0, end + 1);
31
+ const lastTurn = [...prefix].reverse().find((event) => event.type === "turn/start" || event.type === "turn/end");
32
+ if (lastTurn?.type === "turn/start")
33
+ throw RequestError.invalidParams(void 0, "cannot fork an unfinished turn; select an earlier completed turn");
34
+ let agentPreset = source.header.agentPreset;
35
+ let selection;
36
+ for (const [index, event] of prefix.entries()) {
37
+ if (event.seq !== index)
38
+ throw RequestError.invalidParams(void 0, "source session has a non-contiguous log");
39
+ if (event.type === "agent-preset/selected" && typeof event.data.agentPreset === "string")
40
+ agentPreset = event.data.agentPreset;
41
+ if (event.type === "request/header") {
42
+ const config = object(object(event.data.header)?.config);
43
+ if (!config || typeof config.provider !== "string" || typeof config.model !== "string")
44
+ throw RequestError.invalidParams(void 0, "source request header has no model route");
45
+ selection = {
46
+ provider: config.provider,
47
+ model: config.model,
48
+ ...typeof config.reasoningEffort === "string" ? { reasoningEffort: config.reasoningEffort } : {}
49
+ };
50
+ }
51
+ }
52
+ return { sourceId: source.header.id, seed: structuredClone(prefix), agentPreset, selection };
53
+ }
54
+ const blockSchema = object$1({ type: string() }).passthrough();
55
+ const resultSchema = object$1({
12
56
  type: literal("tool-result"),
13
57
  toolCallId: string().min(1),
14
58
  content: array(blockSchema),
15
59
  isError: boolean().optional()
16
60
  });
17
- const callSchema = object({
61
+ const callSchema = object$1({
18
62
  callId: string().min(1),
19
63
  name: string().min(1),
20
64
  arguments: string()
21
65
  });
22
- const ptcSchema = object({
66
+ const ptcSchema = object$1({
23
67
  subCallId: string().min(1),
24
68
  name: string().min(1),
25
69
  arguments: unknown(),
26
70
  parentCallId: string(),
27
71
  rootCallId: string()
28
72
  });
29
- const locationSchema = object({ path: string(), line: number().int().positive().optional() });
30
- const viewSchema = object({
73
+ const locationSchema = object$1({ path: string(), line: number().int().positive().optional() });
74
+ const viewSchema = object$1({
31
75
  card: _enum(["generic", "terminal", "diff", "read", "search", "web"]),
32
76
  title: string().optional(),
33
77
  kind: _enum(["read", "edit", "delete", "move", "search", "execute", "fetch", "other"]).optional(),
34
78
  locations: array(locationSchema).optional(),
35
- diffs: array(object({ path: string(), oldText: string().nullable(), newText: string() })).optional(),
79
+ diffs: array(object$1({ path: string(), oldText: string().nullable(), newText: string() })).optional(),
36
80
  output: string().optional(),
37
81
  content: array(blockSchema).optional()
38
82
  }).passthrough();
@@ -163,8 +207,8 @@ class ToolCallBridge {
163
207
  }
164
208
  await this.start(call.callId, call.name, args);
165
209
  } else if (type === "tool/result") {
166
- const event = object({
167
- message: object({ content: array(resultSchema) }),
210
+ const event = object$1({
211
+ message: object$1({ content: array(resultSchema) }),
168
212
  meta: unknown().optional()
169
213
  }).passthrough().parse(data);
170
214
  for (const block of event.message.content) {
@@ -174,7 +218,7 @@ class ToolCallBridge {
174
218
  const call = ptcSchema.parse(data);
175
219
  await this.start(call.subCallId, call.name, call.arguments);
176
220
  if (type === "tool/ptc-dispatch") {
177
- const result = object({ content: array(blockSchema), isError: boolean() }).passthrough().parse(data);
221
+ const result = object$1({ content: array(blockSchema), isError: boolean() }).passthrough().parse(data);
178
222
  await this.finish(call.subCallId, result, result);
179
223
  }
180
224
  } else if (type === "turn/end") {
@@ -234,17 +278,17 @@ class HarnessUsageTracker {
234
278
  return this.accumulator.update(sessionId, String(seq), { [this.route.model]: usage });
235
279
  }
236
280
  }
237
- const questionSchema = object({
281
+ const questionSchema = object$1({
238
282
  id: string().refine((value) => value.trim().length > 0),
239
283
  question: string().min(1),
240
284
  header: string().optional(),
241
285
  detail: string().optional(),
242
- options: array(object({
286
+ options: array(object$1({
243
287
  label: string().min(1),
244
288
  description: string().optional()
245
289
  })).optional(),
246
290
  multiSelect: boolean().optional(),
247
- intent: object({ kind: literal("plan-review"), approve: string() }).optional()
291
+ intent: object$1({ kind: literal("plan-review"), approve: string() }).optional()
248
292
  });
249
293
  function failure(code, message) {
250
294
  return Object.assign(new Error(message), { name: "UserQuestionError", code });
@@ -364,7 +408,7 @@ class UserQuestionBridge {
364
408
  throw failure("ASK_ABORTED", "The user cancelled the question");
365
409
  if (response.action === "decline")
366
410
  throw failure("ASK_DECLINED", "The user declined the question");
367
- const accepted = object({
411
+ const accepted = object$1({
368
412
  action: literal("accept"),
369
413
  content: record(string(), unknown())
370
414
  }).safeParse(response);
@@ -409,11 +453,13 @@ const inject = [
409
453
  "llm",
410
454
  "permissionPresets",
411
455
  "sessionPersistence",
456
+ "sessions",
412
457
  "sessionQuery",
413
458
  "sessionTitle",
414
459
  "userQuestions"
415
460
  ];
416
461
  const LODY_CAPABILITIES = {
462
+ forkAtTurn: { version: 1 },
417
463
  subagentEvents: { version: 1 },
418
464
  sessionTitle: { version: 1 },
419
465
  compaction: { version: 1 },
@@ -432,6 +478,7 @@ const MODEL_DISCOVERY_TIMEOUT_MS = 15e3;
432
478
  const MODEL_DISCOVERY_MAX_BYTES = 1024 * 1024;
433
479
  const MODEL_DISCOVERY_MAX_MODELS = 1e3;
434
480
  const MODEL_ID_MAX_LENGTH = 512;
481
+ const ACP_MODEL_ID_PREFIX = "dsh-route:";
435
482
  const INVALID_MCP_SERVER_NAME_CHARS = /[^A-Za-z0-9_-]/gu;
436
483
  const IMAGE_MEDIA_TYPES = [
437
484
  "image/png",
@@ -470,33 +517,60 @@ function resolveAdapterConfig(config) {
470
517
  ...config?.stream ? { stream: config.stream } : {}
471
518
  };
472
519
  }
473
- async function loadHarnessModels(llm, provider, configuredModel, endpoint) {
474
- const listed = endpoint ? (await discoverDeepSeekModelIds(endpoint.baseUrl, endpoint.apiKey)).map((id) => ({
475
- provider,
476
- id
477
- })) : await llm.listModels(provider);
478
- const modelIds = [];
479
- const ids = /* @__PURE__ */ new Set();
480
- for (const model of listed) {
481
- if (model.provider !== provider || !model.id.trim())
482
- continue;
483
- if (ids.has(model.id)) {
484
- throw new Error(`acp-extension-dsh: duplicate model ${JSON.stringify(model.id)} for provider ${JSON.stringify(provider)}`);
520
+ async function loadHarnessModels(llm, defaultProvider, configuredModel, endpoint) {
521
+ const providers = llm.listProviders?.() ?? [{ id: defaultProvider, name: defaultProvider }];
522
+ if (!providers.some(({ id }) => id === defaultProvider)) {
523
+ throw new Error(`configured provider ${JSON.stringify(defaultProvider)} is unavailable`);
524
+ }
525
+ const identities = [];
526
+ for (const { id: provider } of providers) {
527
+ const listed = endpoint && provider === defaultProvider ? (await discoverDeepSeekModelIds(endpoint.baseUrl, endpoint.apiKey)).map((id) => ({
528
+ provider,
529
+ id
530
+ })) : await llm.listModels(provider);
531
+ const ids = /* @__PURE__ */ new Set();
532
+ for (const model of listed) {
533
+ if (model.provider !== provider || !model.id.trim())
534
+ continue;
535
+ if (ids.has(model.id)) {
536
+ throw new Error(`acp-extension-dsh: duplicate model ${JSON.stringify(model.id)} for provider ${JSON.stringify(provider)}`);
537
+ }
538
+ ids.add(model.id);
539
+ identities.push({ provider, model: model.id });
540
+ }
541
+ if (endpoint && provider === defaultProvider && ids.size === 0) {
542
+ throw new Error("the endpoint returned no usable models");
543
+ }
544
+ if (provider === defaultProvider && !endpoint && !ids.has(configuredModel)) {
545
+ identities.unshift({ provider, model: configuredModel });
485
546
  }
486
- ids.add(model.id);
487
- modelIds.push(model.id);
488
547
  }
489
- if (endpoint && modelIds.length === 0) {
548
+ const initialSelection = endpoint ? identities.find(({ provider }) => provider === defaultProvider) : { provider: defaultProvider, model: configuredModel };
549
+ if (!initialSelection)
490
550
  throw new Error("the endpoint returned no usable models");
491
- }
492
- if (!endpoint && !ids.has(configuredModel))
493
- modelIds.unshift(configuredModel);
494
- const initialModel = endpoint ? modelIds[0] : configuredModel;
495
551
  return {
496
- models: await Promise.all(modelIds.map((modelId) => resolveHarnessModel(llm, provider, modelId))),
497
- initialModel
552
+ models: await Promise.all(identities.map(({ provider, model }) => resolveHarnessModel(llm, provider, model))),
553
+ initialSelection
498
554
  };
499
555
  }
556
+ function encodeAcpModelId(provider, model) {
557
+ return `${ACP_MODEL_ID_PREFIX}${Buffer.from(JSON.stringify([provider, model])).toString("base64url")}`;
558
+ }
559
+ function decodeAcpModelId(value, legacyProvider) {
560
+ if (!value.startsWith(ACP_MODEL_ID_PREFIX))
561
+ return { provider: legacyProvider, model: value };
562
+ try {
563
+ const decoded = JSON.parse(Buffer.from(value.slice(ACP_MODEL_ID_PREFIX.length), "base64url").toString("utf8"));
564
+ if (Array.isArray(decoded) && decoded.length === 2 && decoded.every((part) => typeof part === "string" && part.trim())) {
565
+ return { provider: decoded[0], model: decoded[1] };
566
+ }
567
+ } catch {
568
+ }
569
+ throw invalidParams("model route id is invalid");
570
+ }
571
+ function selectedModel(record2) {
572
+ return record2.models.find((candidate) => candidate.provider === record2.selection.current.provider && candidate.id === record2.selection.current.model);
573
+ }
500
574
  function modelDiscoveryUrl(baseUrl) {
501
575
  const url = new URL(baseUrl);
502
576
  url.pathname = `${url.pathname.replace(/\/+$/u, "")}/models`;
@@ -727,13 +801,13 @@ function configOptions(record2) {
727
801
  {
728
802
  id: MODEL_CONFIG_ID,
729
803
  name: "Model",
730
- description: "DeepSeek model used for the session",
804
+ description: "Provider route and model used for the session",
731
805
  category: "model",
732
806
  type: "select",
733
- currentValue: record2.selection.current.model,
807
+ currentValue: encodeAcpModelId(record2.selection.current.provider, record2.selection.current.model),
734
808
  options: record2.models.map((model2) => ({
735
- value: model2.id,
736
- name: model2.name,
809
+ value: encodeAcpModelId(model2.provider, model2.id),
810
+ name: `${model2.name} (${model2.provider})`,
737
811
  description: model2.description ?? null
738
812
  }))
739
813
  }
@@ -743,7 +817,7 @@ function configOptions(record2) {
743
817
  const state = plan.get(record2.agent);
744
818
  options.push(createPlanModeConfigOption(state.pending ?? state.active));
745
819
  }
746
- const model = record2.models.find((candidate) => candidate.id === record2.selection.current.model);
820
+ const model = selectedModel(record2);
747
821
  if (model?.reasoning && record2.selection.current.reasoningEffort) {
748
822
  options.push({
749
823
  id: REASONING_EFFORT_CONFIG_ID,
@@ -763,16 +837,16 @@ function configOptions(record2) {
763
837
  }
764
838
  function legacyModels(record2) {
765
839
  return {
766
- currentModelId: record2.selection.current.model,
840
+ currentModelId: encodeAcpModelId(record2.selection.current.provider, record2.selection.current.model),
767
841
  availableModels: record2.models.flatMap((model) => [
768
842
  {
769
- modelId: model.id,
770
- name: model.name,
843
+ modelId: encodeAcpModelId(model.provider, model.id),
844
+ name: `${model.name} (${model.provider})`,
771
845
  description: model.description ?? null
772
846
  },
773
847
  ...model.reasoning?.efforts.map((effort) => ({
774
- modelId: `${model.id}[${effort.id}]`,
775
- name: `${model.name} (${effort.name})`,
848
+ modelId: `${encodeAcpModelId(model.provider, model.id)}[${effort.id}]`,
849
+ name: `${model.name} (${model.provider}, ${effort.name})`,
776
850
  description: model.description ?? null
777
851
  })) ?? []
778
852
  ])
@@ -1112,6 +1186,11 @@ function apply(ctx, rawConfig) {
1112
1186
  });
1113
1187
  });
1114
1188
  });
1189
+ const flushSession = async (session) => {
1190
+ const store = ctx.get("sessions");
1191
+ if (!store || !await store.flush(session))
1192
+ throw new Error("no session durability listener is available");
1193
+ };
1115
1194
  const settleAfterQuiescence = (record2, inflight) => {
1116
1195
  if (inflight.settlementStarted)
1117
1196
  return;
@@ -1122,6 +1201,7 @@ function apply(ctx, rawConfig) {
1122
1201
  await record2.agent.whenIdle();
1123
1202
  await record2.outputTail;
1124
1203
  await record2.tools.interrupt();
1204
+ await flushSession(record2.agent.session);
1125
1205
  }
1126
1206
  if (record2.inflight !== inflight)
1127
1207
  return;
@@ -1300,7 +1380,11 @@ function apply(ctx, rawConfig) {
1300
1380
  continue;
1301
1381
  await notify({
1302
1382
  sessionId: record2.agent.session.id,
1303
- update: { sessionUpdate: "agent_message_chunk", content }
1383
+ update: {
1384
+ sessionUpdate: "agent_message_chunk",
1385
+ content,
1386
+ ...typeof event.data.turn === "number" ? { _meta: { lody: { turnId: nativeTurnId(event.data.turn) } } } : {}
1387
+ }
1304
1388
  });
1305
1389
  }
1306
1390
  }, inflight);
@@ -1347,9 +1431,19 @@ function apply(ctx, rawConfig) {
1347
1431
  }
1348
1432
  });
1349
1433
  ctx.on("agent/inbox/claimed", ({ agent, message, turn }) => {
1350
- const inflight = ownedRecord(agent)?.inflight;
1351
- if (inflight && inflight.messageId === message.id)
1434
+ const record2 = ownedRecord(agent);
1435
+ const inflight = record2?.inflight;
1436
+ if (record2 && inflight && inflight.messageId === message.id) {
1352
1437
  inflight.turn = turn;
1438
+ enqueueNotification(record2, {
1439
+ sessionId: agent.session.id,
1440
+ update: {
1441
+ sessionUpdate: "agent_message_chunk",
1442
+ content: { type: "text", text: "" },
1443
+ _meta: { lody: { turnId: nativeTurnId(turn) } }
1444
+ }
1445
+ }, inflight);
1446
+ }
1353
1447
  });
1354
1448
  ctx.on("agent/error", ({ agent, turn, error }) => {
1355
1449
  const record2 = ownedRecord(agent);
@@ -1440,26 +1534,27 @@ function apply(ctx, rawConfig) {
1440
1534
  throw invalidParams(`failed to select agent preset ${JSON.stringify(value)}: ${errorChain(error)}`);
1441
1535
  });
1442
1536
  } else if (params.configId === MODEL_CONFIG_ID) {
1443
- return resolveHarnessModel(llm, record2.selection.current.provider, value).then((model) => {
1444
- const index = record2.models.findIndex((candidate) => candidate.id === value);
1537
+ const selected = decodeAcpModelId(value, record2.selection.current.provider);
1538
+ return resolveHarnessModel(llm, selected.provider, selected.model).then((model) => {
1539
+ const index = record2.models.findIndex((candidate) => candidate.provider === selected.provider && candidate.id === selected.model);
1445
1540
  if (index === -1)
1446
1541
  record2.models.push(model);
1447
1542
  else
1448
1543
  record2.models[index] = model;
1449
1544
  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);
1450
1545
  record2.selection.current = {
1451
- provider: record2.selection.current.provider,
1452
- model: value,
1546
+ provider: selected.provider,
1547
+ model: selected.model,
1453
1548
  ...reasoningEffort ? { reasoningEffort } : {}
1454
1549
  };
1455
1550
  return { configOptions: configOptions(record2) };
1456
1551
  }).catch((error) => {
1457
1552
  if (error instanceof RequestError)
1458
1553
  throw error;
1459
- throw invalidParams(`failed to resolve model ${JSON.stringify(value)}: ${errorChain(error)}`);
1554
+ throw invalidParams(`failed to resolve model route ${JSON.stringify(selected.provider)} / ${JSON.stringify(selected.model)}: ${errorChain(error)}`);
1460
1555
  });
1461
1556
  } else if (params.configId === REASONING_EFFORT_CONFIG_ID) {
1462
- const model = record2.models.find((candidate) => candidate.id === record2.selection.current.model);
1557
+ const model = selectedModel(record2);
1463
1558
  if (!model)
1464
1559
  throw internalError("selected model metadata is unavailable");
1465
1560
  const reasoningEffort = resolveReasoningEffort(model, value);
@@ -1475,6 +1570,150 @@ function apply(ctx, rawConfig) {
1475
1570
  }
1476
1571
  return { configOptions: configOptions(record2) };
1477
1572
  };
1573
+ const createSession = async (params, fork) => {
1574
+ assertOpen();
1575
+ validateSessionParams(params);
1576
+ let catalog;
1577
+ try {
1578
+ catalog = await loadModelCatalog();
1579
+ } catch (error) {
1580
+ throw internalError(`failed to discover models: ${errorChain(error)}`);
1581
+ }
1582
+ const models = [...catalog.models];
1583
+ const initialSelection = fork?.selection ?? catalog.initialSelection;
1584
+ if (fork?.selection) {
1585
+ const model = await resolveHarnessModel(llm, initialSelection.provider, initialSelection.model);
1586
+ const index = models.findIndex((candidate) => candidate.provider === model.provider && candidate.id === model.id);
1587
+ if (index < 0)
1588
+ models.push(model);
1589
+ else
1590
+ models[index] = model;
1591
+ }
1592
+ const sessionId = randomUUID();
1593
+ const questions = new UserQuestionBridge(sessionId, (request) => conn.unstable_createElicitation(request), () => questionCapabilities);
1594
+ const initialModel = models.find((model) => model.provider === initialSelection.provider && model.id === initialSelection.model);
1595
+ if (!initialModel)
1596
+ throw internalError("initial model metadata is unavailable");
1597
+ const reasoningEffort = resolveReasoningEffort(initialModel, fork?.selection ? fork.selection.reasoningEffort : config.reasoningEffort);
1598
+ const selection = {
1599
+ current: {
1600
+ provider: initialSelection.provider,
1601
+ model: initialSelection.model,
1602
+ ...reasoningEffort ? { reasoningEffort } : {}
1603
+ }
1604
+ };
1605
+ const agentPresetOptions = (await ctx.agentPresets.list()).filter((preset) => preset.broken === void 0);
1606
+ const requestedPreset = fork?.agentPreset ?? ctx.agentPresets.defaultId;
1607
+ if (!agentPresetOptions.some((preset) => preset.id === requestedPreset)) {
1608
+ throw internalError(`default agent preset ${JSON.stringify(requestedPreset)} is unavailable`);
1609
+ }
1610
+ const mcpServerNames = reserveMcpServerNames(params.mcpServers, sessionId, activeMcpServerNames);
1611
+ let mountedPreset = requestedPreset;
1612
+ let handle;
1613
+ try {
1614
+ handle = await ctx.agents.create({
1615
+ sessionId,
1616
+ meta: {
1617
+ cwd: params.cwd,
1618
+ agentPreset: requestedPreset,
1619
+ ...fork ? { parentSession: fork.sourceId, isSeeded: true } : {}
1620
+ },
1621
+ ...fork ? { seed: fork.seed, inheritedEventCount: fork.seed.length } : {},
1622
+ agentOptions: {
1623
+ provider: initialSelection.provider,
1624
+ model: initialSelection.model
1625
+ },
1626
+ setup: async (agentContext) => {
1627
+ if (subagentEvents && !carrierKeyOf) {
1628
+ const scope = await agentContext.loader.import("@deepseek-ai/dsh-scope");
1629
+ if (typeof scope.carrierKeyOf !== "function")
1630
+ throw new Error("Harness scope identity API is unavailable");
1631
+ carrierKeyOf = scope.carrierKeyOf;
1632
+ }
1633
+ installModelSelection(agentContext, selection);
1634
+ agentContext.on("user-questions/request", (request, next) => {
1635
+ if (!request.agent || ownedRecord(request.agent)?.questions !== questions)
1636
+ return next();
1637
+ return questions.ask(request);
1638
+ });
1639
+ mountedPreset = (await ctx.agentPresets.mount(agentContext, requestedPreset)).id;
1640
+ await mountMcpServers(agentContext, params.mcpServers, mcpServerNames.names, params.cwd);
1641
+ }
1642
+ });
1643
+ } catch (error) {
1644
+ mcpServerNames.release();
1645
+ if (error instanceof RequestError)
1646
+ throw error;
1647
+ throw internalError(`failed to create session: ${errorChain(error)}`);
1648
+ }
1649
+ const dispose = async () => {
1650
+ questions.cancel();
1651
+ try {
1652
+ await handle.dispose();
1653
+ } finally {
1654
+ mcpServerNames.release();
1655
+ }
1656
+ };
1657
+ if (closed) {
1658
+ await dispose();
1659
+ throw internalError("connection closed during session/new");
1660
+ }
1661
+ let permissionMode;
1662
+ let permissionOptions;
1663
+ try {
1664
+ permissionMode = ctx.permissionPresets.current(handle.agent.session);
1665
+ permissionOptions = permissionState(ctx, permissionMode);
1666
+ } catch (error) {
1667
+ await dispose();
1668
+ throw error;
1669
+ }
1670
+ const record2 = {
1671
+ cwd: params.cwd,
1672
+ tools: new ToolCallBridge({
1673
+ cwd: params.cwd,
1674
+ lookup: (name2) => {
1675
+ const tools = handle.agent.ctx.get("tools");
1676
+ return tools?.get(name2, handle.agent);
1677
+ },
1678
+ content: (block) => assistantBlockToAcp(block, attachments),
1679
+ emit: (update) => notify({ sessionId, update }),
1680
+ warn: (message) => ctx.logger.warn(`acp-extension-dsh: ${message}`)
1681
+ }),
1682
+ questions,
1683
+ usage: new HarnessUsageTracker(!baseUrl || /^https:\/\/api\.deepseek\.com(?:\/v1)?\/?$/.test(baseUrl)),
1684
+ agent: handle.agent,
1685
+ dispose,
1686
+ selection,
1687
+ permissionMode,
1688
+ permissionOptions,
1689
+ agentPreset: mountedPreset,
1690
+ agentPresetOptions,
1691
+ models,
1692
+ started: fork?.seed.some((event) => event.type === "turn/start") ?? false,
1693
+ outputTail: Promise.resolve()
1694
+ };
1695
+ if (fork) {
1696
+ try {
1697
+ await flushSession(handle.agent.session);
1698
+ assertOpen();
1699
+ } catch (error) {
1700
+ let cleanupError;
1701
+ try {
1702
+ await dispose();
1703
+ } catch (failure2) {
1704
+ cleanupError = failure2;
1705
+ }
1706
+ throw internalError(`fork child ${sessionId} could not be committed: ${errorChain(error)}${cleanupError ? `; cleanup: ${errorChain(cleanupError)}` : ""}`);
1707
+ }
1708
+ }
1709
+ sessions.set(sessionId, record2);
1710
+ return {
1711
+ sessionId,
1712
+ modes: modeState(record2),
1713
+ configOptions: configOptions(record2),
1714
+ models: legacyModels(record2)
1715
+ };
1716
+ };
1478
1717
  const makeAgent = (connection) => {
1479
1718
  conn = connection;
1480
1719
  return {
@@ -1501,7 +1740,7 @@ function apply(ctx, rawConfig) {
1501
1740
  embeddedContext: false
1502
1741
  },
1503
1742
  mcpCapabilities: { http: true },
1504
- sessionCapabilities: { close: {} },
1743
+ sessionCapabilities: { close: {}, fork: {} },
1505
1744
  _meta: { lody: LODY_CAPABILITIES }
1506
1745
  },
1507
1746
  authMethods: []
@@ -1510,118 +1749,36 @@ function apply(ctx, rawConfig) {
1510
1749
  authenticate(_params) {
1511
1750
  return Promise.resolve();
1512
1751
  },
1513
- async newSession(params) {
1752
+ newSession(params) {
1753
+ return createSession(params);
1754
+ },
1755
+ async unstable_forkSession(params) {
1514
1756
  assertOpen();
1515
- validateSessionParams(params);
1516
- let catalog;
1517
- try {
1518
- catalog = await loadModelCatalog();
1519
- } catch (error) {
1520
- throw internalError(`failed to discover models: ${errorChain(error)}`);
1521
- }
1522
- const { models, initialModel: initialModelId } = catalog;
1523
- const sessionId = randomUUID();
1524
- const questions = new UserQuestionBridge(sessionId, (request) => conn.unstable_createElicitation(request), () => questionCapabilities);
1525
- const initialModel = models.find((model) => model.id === initialModelId);
1526
- if (!initialModel)
1527
- throw internalError("initial model metadata is unavailable");
1528
- const reasoningEffort = resolveReasoningEffort(initialModel, config.reasoningEffort);
1529
- const selection = {
1530
- current: {
1531
- provider: config.provider,
1532
- model: initialModelId,
1533
- ...reasoningEffort ? { reasoningEffort } : {}
1534
- }
1757
+ const target = forkTarget(params._meta);
1758
+ const creation = {
1759
+ cwd: params.cwd,
1760
+ mcpServers: params.mcpServers ?? [],
1761
+ additionalDirectories: params.additionalDirectories
1535
1762
  };
1536
- const agentPresetOptions = (await ctx.agentPresets.list()).filter((preset) => preset.broken === void 0);
1537
- const requestedPreset = ctx.agentPresets.defaultId;
1538
- if (!agentPresetOptions.some((preset) => preset.id === requestedPreset)) {
1539
- throw internalError(`default agent preset ${JSON.stringify(requestedPreset)} is unavailable`);
1540
- }
1541
- const mcpServerNames = reserveMcpServerNames(params.mcpServers, sessionId, activeMcpServerNames);
1542
- let mountedPreset = requestedPreset;
1543
- let handle;
1763
+ validateSessionParams(creation);
1764
+ const query = ctx.get("sessionQuery");
1765
+ if (!query)
1766
+ throw internalError("Harness session query is unavailable");
1767
+ let source;
1544
1768
  try {
1545
- handle = await ctx.agents.create({
1546
- sessionId,
1547
- meta: { cwd: params.cwd, agentPreset: requestedPreset },
1548
- agentOptions: { provider: config.provider, model: initialModelId },
1549
- setup: async (agentContext) => {
1550
- if (subagentEvents && !carrierKeyOf) {
1551
- const scope = await agentContext.loader.import("@deepseek-ai/dsh-scope");
1552
- if (typeof scope.carrierKeyOf !== "function")
1553
- throw new Error("Harness scope identity API is unavailable");
1554
- carrierKeyOf = scope.carrierKeyOf;
1555
- }
1556
- installModelSelection(agentContext, selection);
1557
- agentContext.on("user-questions/request", (request, next) => {
1558
- if (!request.agent || ownedRecord(request.agent)?.questions !== questions)
1559
- return next();
1560
- return questions.ask(request);
1561
- });
1562
- mountedPreset = (await ctx.agentPresets.mount(agentContext, requestedPreset)).id;
1563
- await mountMcpServers(agentContext, params.mcpServers, mcpServerNames.names, params.cwd);
1564
- }
1565
- });
1769
+ source = await query.observeSession(params.sessionId);
1566
1770
  } catch (error) {
1567
- mcpServerNames.release();
1568
- if (error instanceof RequestError)
1569
- throw error;
1570
- throw internalError(`failed to create session: ${errorChain(error)}`);
1771
+ throw invalidParams(`fork source unavailable: ${errorChain(error)}`);
1571
1772
  }
1572
- const dispose = async () => {
1573
- questions.cancel();
1574
- try {
1575
- await handle.dispose();
1576
- } finally {
1577
- mcpServerNames.release();
1578
- }
1579
- };
1580
- if (closed) {
1581
- await dispose();
1582
- throw internalError("connection closed during session/new");
1583
- }
1584
- let permissionMode;
1585
- let permissionOptions;
1773
+ let snapshot;
1586
1774
  try {
1587
- permissionMode = ctx.permissionPresets.current(handle.agent.session);
1588
- permissionOptions = permissionState(ctx, permissionMode);
1589
- } catch (error) {
1590
- await dispose();
1591
- throw error;
1775
+ if (source.header.id !== params.sessionId)
1776
+ throw invalidParams("fork source identity mismatch");
1777
+ snapshot = forkSnapshot(source, target);
1778
+ } finally {
1779
+ source[Symbol.dispose]();
1592
1780
  }
1593
- const record2 = {
1594
- cwd: params.cwd,
1595
- tools: new ToolCallBridge({
1596
- cwd: params.cwd,
1597
- lookup: (name2) => {
1598
- const tools = handle.agent.ctx.get("tools");
1599
- return tools?.get(name2, handle.agent);
1600
- },
1601
- content: (block) => assistantBlockToAcp(block, attachments),
1602
- emit: (update) => notify({ sessionId, update }),
1603
- warn: (message) => ctx.logger.warn(`acp-extension-dsh: ${message}`)
1604
- }),
1605
- questions,
1606
- usage: new HarnessUsageTracker(!baseUrl || /^https:\/\/api\.deepseek\.com(?:\/v1)?\/?$/.test(baseUrl)),
1607
- agent: handle.agent,
1608
- dispose,
1609
- selection,
1610
- permissionMode,
1611
- permissionOptions,
1612
- agentPreset: mountedPreset,
1613
- agentPresetOptions,
1614
- models,
1615
- started: false,
1616
- outputTail: Promise.resolve()
1617
- };
1618
- sessions.set(sessionId, record2);
1619
- return {
1620
- sessionId,
1621
- modes: modeState(record2),
1622
- configOptions: configOptions(record2),
1623
- models: legacyModels(record2)
1624
- };
1781
+ return createSession(creation, snapshot);
1625
1782
  },
1626
1783
  setSessionMode(params) {
1627
1784
  const record2 = requireSession(params.sessionId);