@openma/deepseek-harness-acp 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bridge.js +257 -78
  2. package/dist/index.js +257 -78
  3. package/package.json +1 -1
package/dist/bridge.js CHANGED
@@ -863,6 +863,87 @@ async function apply(ctx, config = {}) {
863
863
  if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
864
864
  return record;
865
865
  };
866
+ const requireOrRestoreSession = async (sessionId) => {
867
+ const record = sessions.get(sessionId);
868
+ if (record !== void 0) return record;
869
+ if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
870
+ logWarn(`restoring session ${sessionId} from the persisted log`);
871
+ try {
872
+ return await restoreSession(sessionId, { replay: false });
873
+ } catch (error) {
874
+ throw invalidParams(`unknown session: ${sessionId} (${errorChain2(error)})`);
875
+ }
876
+ };
877
+ const restoreSession = async (sessionId, options) => {
878
+ const persistence = requirePersistence();
879
+ const existing = sessions.get(sessionId);
880
+ if (existing !== void 0) {
881
+ sessions.delete(sessionId);
882
+ existing.agent.cancel({ kind: "user" });
883
+ settlePrompt(existing, "cancelled");
884
+ await existing.dispose().catch((error) => {
885
+ logWarn(`dispose before reload failed: ${String(error)}`);
886
+ });
887
+ }
888
+ let events;
889
+ let storedHeader;
890
+ try {
891
+ const inspected = await persistence.inspect(SessionId2(sessionId));
892
+ events = inspected.events;
893
+ storedHeader = inspected.header;
894
+ } catch (error) {
895
+ throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
896
+ }
897
+ const replay = buildReplay(events);
898
+ if (options.replay) {
899
+ for (const update of replay.updates) notify(sessionId, update);
900
+ if (replay.title !== void 0) {
901
+ notify(sessionId, {
902
+ sessionUpdate: "session_info_update",
903
+ title: replay.title
904
+ });
905
+ }
906
+ }
907
+ const presets = presetsService();
908
+ const storedPreset = presetFromLog(storedHeader, events);
909
+ const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
910
+ const handle = await agents.resume({
911
+ resumeSessionId: SessionId2(sessionId),
912
+ agentOptions: agentOptionsFor(config.model),
913
+ ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
914
+ });
915
+ const cwd = options.cwd ?? storedHeader?.cwd;
916
+ const projection = new SessionProjection(replay.contextWindow, {
917
+ terminalOutput: clientTerminalOutput,
918
+ ...cwd !== void 0 ? { cwd } : {}
919
+ });
920
+ projection.title = replay.title;
921
+ const record = registerRecord(
922
+ sessionId,
923
+ handle.agent,
924
+ () => handle.dispose(),
925
+ config.model,
926
+ config.permissionMode ?? "workspace-write",
927
+ projection
928
+ );
929
+ if (presetId !== void 0) record.preset = presetId;
930
+ {
931
+ const permission = permissionService();
932
+ const storedMode = permission?.current(events);
933
+ if (storedMode !== void 0) record.modeId = storedMode;
934
+ for (let index = events.length - 1; index >= 0; index -= 1) {
935
+ const event = events[index];
936
+ if (event?.type === "approval/policy") {
937
+ const policy = event.data?.policy;
938
+ if (policy === "ask" || policy === "never") record.approvals = policy;
939
+ break;
940
+ }
941
+ }
942
+ }
943
+ ensureSelection(record);
944
+ queueMicrotask(() => publishCommands(sessionId));
945
+ return record;
946
+ };
866
947
  const assertOpen = () => {
867
948
  if (closed) throw internalError("the ACP bridge has been disposed");
868
949
  };
@@ -954,6 +1035,7 @@ async function apply(ctx, config = {}) {
954
1035
  delete record.selection;
955
1036
  ensureSelection(record);
956
1037
  setApprovalPolicy(record, record.approvals);
1038
+ publishCommands(acpSessionId, record);
957
1039
  };
958
1040
  const setApprovalPolicy = (record, policy) => {
959
1041
  try {
@@ -963,7 +1045,7 @@ async function apply(ctx, config = {}) {
963
1045
  }
964
1046
  record.approvals = policy;
965
1047
  };
966
- const permissionService = () => ctx.get("permission");
1048
+ const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
967
1049
  const applyMode = (record, sessionId, modeId) => {
968
1050
  const permission = permissionService();
969
1051
  if (permission !== void 0 && permission.names.includes(modeId)) {
@@ -1085,20 +1167,102 @@ async function apply(ctx, config = {}) {
1085
1167
  }
1086
1168
  return result;
1087
1169
  };
1088
- const publishCommands = (sessionId) => {
1089
- notify(sessionId, {
1090
- sessionUpdate: "available_commands_update",
1091
- availableCommands: [
1092
- { name: "status", description: "Show adapter, model, mode, and token status" },
1093
- {
1094
- name: "login",
1095
- description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1096
- input: { hint: "<api-key>" }
1097
- },
1098
- { name: "logout", description: "Remove the API key stored in the harness credential store" }
1099
- ]
1170
+ const BUILTIN_COMMANDS = [
1171
+ { name: "status", description: "Show adapter, model, mode, and token status" },
1172
+ {
1173
+ name: "login",
1174
+ description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1175
+ input: { hint: "<api-key>" }
1176
+ },
1177
+ { name: "logout", description: "Remove the API key stored in the harness credential store" },
1178
+ {
1179
+ name: "model",
1180
+ description: "Select the model for this conversation",
1181
+ input: { hint: "[model] \u2014 blank lists available models" }
1182
+ }
1183
+ ];
1184
+ const availableCommandsFor = async (record) => {
1185
+ const list = [...BUILTIN_COMMANDS];
1186
+ const seen = new Set(list.map((command) => command.name));
1187
+ const commandsSvc = ctx.get("commands");
1188
+ if (commandsSvc !== void 0) {
1189
+ try {
1190
+ for (const descriptor of commandsSvc.list(record.agent)) {
1191
+ if (seen.has(descriptor.name)) continue;
1192
+ seen.add(descriptor.name);
1193
+ list.push({
1194
+ name: descriptor.name,
1195
+ description: descriptor.description,
1196
+ ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
1197
+ });
1198
+ }
1199
+ } catch (error) {
1200
+ logWarn(`command listing failed: ${String(error)}`);
1201
+ }
1202
+ }
1203
+ const skillsSvc = ctx.get("skills");
1204
+ if (skillsSvc !== void 0) {
1205
+ try {
1206
+ const skills = await skillsSvc.list({
1207
+ ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
1208
+ scope: record.agent
1209
+ });
1210
+ for (const skill of skills) {
1211
+ if (skill.invocation.userInvocable !== true) continue;
1212
+ if (seen.has(skill.name)) continue;
1213
+ seen.add(skill.name);
1214
+ list.push({
1215
+ name: skill.name,
1216
+ description: skill.description,
1217
+ input: { hint: "instructions for the skill" }
1218
+ });
1219
+ }
1220
+ } catch (error) {
1221
+ logDebug(`skill listing failed: ${String(error)}`);
1222
+ }
1223
+ }
1224
+ return list;
1225
+ };
1226
+ const publishCommands = (sessionId, record) => {
1227
+ const target = record ?? sessions.get(sessionId);
1228
+ if (target === void 0) return;
1229
+ void availableCommandsFor(target).then((availableCommands) => {
1230
+ notify(sessionId, { sessionUpdate: "available_commands_update", availableCommands });
1231
+ }).catch((error) => {
1232
+ logWarn(`publishing commands failed: ${String(error)}`);
1100
1233
  });
1101
1234
  };
1235
+ const publishConfigOptions = (sessionId, record) => {
1236
+ void configOptions(record).then((options) => {
1237
+ notify(sessionId, {
1238
+ sessionUpdate: "config_option_update",
1239
+ configOptions: options
1240
+ });
1241
+ }).catch((error) => {
1242
+ logWarn(`publishing config options failed: ${String(error)}`);
1243
+ });
1244
+ };
1245
+ const syncPermissionState = (record, sessionId) => {
1246
+ const events = record.agent.session.events;
1247
+ if (events === void 0) return;
1248
+ const permission = permissionService();
1249
+ const storedMode = permission?.current(events);
1250
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1251
+ const event = events[index];
1252
+ if (event?.type === "approval/policy") {
1253
+ const policy = event.data?.policy;
1254
+ if (policy === "ask" || policy === "never") record.approvals = policy;
1255
+ break;
1256
+ }
1257
+ }
1258
+ if (storedMode !== void 0 && storedMode !== record.modeId) {
1259
+ record.modeId = storedMode;
1260
+ notify(sessionId, {
1261
+ sessionUpdate: "current_mode_update",
1262
+ currentModeId: record.modeId
1263
+ });
1264
+ }
1265
+ };
1102
1266
  const credentialStore = () => {
1103
1267
  const brand = harness.credentialRef;
1104
1268
  const store = ctx.get("credentials");
@@ -1163,6 +1327,49 @@ async function apply(ctx, config = {}) {
1163
1327
  const remaining = await describeCredential();
1164
1328
  return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1165
1329
  };
1330
+ const modelCommandText = async (record, acpSessionId, query) => {
1331
+ const discovered = await discoverModels();
1332
+ const catalog = [
1333
+ ...discovered.map((entry) => ({
1334
+ value: encodeChoice(entry.provider, entry.model),
1335
+ label: entry.label
1336
+ })),
1337
+ ...modelCandidates().filter((model) => !discovered.some((entry) => entry.model === model)).map((model) => ({ value: encodeChoice(void 0, model), label: model }))
1338
+ ];
1339
+ const route = routeOf(record);
1340
+ const currentValue = encodeChoice(record.provider, record.model ?? route.model ?? "");
1341
+ if (query === "") {
1342
+ const lines = catalog.map(
1343
+ (entry) => `${entry.value === currentValue ? "\u2192" : " "} ${entry.label} \u2014 ${entry.value}`
1344
+ );
1345
+ return [
1346
+ `model: ${route.model ?? "(product default)"}${record.effort !== void 0 ? ` (effort ${record.effort})` : ""}`,
1347
+ "",
1348
+ ...lines.length > 0 ? lines : ["no models discovered \u2014 check credentials with /status"],
1349
+ "",
1350
+ "switch with /model <name>"
1351
+ ].join("\n");
1352
+ }
1353
+ const lowered = query.toLowerCase();
1354
+ const exact = catalog.filter(
1355
+ (entry) => entry.value.toLowerCase() === lowered || decodeChoice(entry.value).model.toLowerCase() === lowered
1356
+ );
1357
+ const matches = exact.length > 0 ? exact : catalog.filter(
1358
+ (entry) => entry.value.toLowerCase().includes(lowered) || entry.label.toLowerCase().includes(lowered)
1359
+ );
1360
+ if (matches.length === 0) return `no model matches "${query}" \u2014 see /model`;
1361
+ if (matches.length > 1) {
1362
+ return [`"${query}" is ambiguous:`, ...matches.map((entry) => ` ${entry.label} \u2014 ${entry.value}`)].join(
1363
+ "\n"
1364
+ );
1365
+ }
1366
+ const target = matches[0];
1367
+ if (target.value === currentValue) return `already on ${target.label}`;
1368
+ await switchModel(record, acpSessionId, target.value);
1369
+ publishCommands(acpSessionId, record);
1370
+ publishConfigOptions(acpSessionId, record);
1371
+ return `model \u2192 ${target.label}`;
1372
+ };
1166
1373
  const statusText = async (record) => {
1167
1374
  const used = record.projection.contextWindow;
1168
1375
  const route = routeOf(record);
@@ -1323,68 +1530,8 @@ async function apply(ctx, config = {}) {
1323
1530
  assertOpen();
1324
1531
  validateCwd(params.cwd);
1325
1532
  syncMcpServers(params.mcpServers, params.cwd);
1326
- const persistence = requirePersistence();
1327
- const sessionId = params.sessionId;
1328
- const existing = sessions.get(sessionId);
1329
- if (existing !== void 0) {
1330
- sessions.delete(sessionId);
1331
- existing.agent.cancel({ kind: "user" });
1332
- settlePrompt(existing, "cancelled");
1333
- await existing.dispose().catch((error) => {
1334
- logWarn(`dispose before reload failed: ${String(error)}`);
1335
- });
1336
- }
1337
- let events;
1338
- let storedHeader;
1339
- try {
1340
- const inspected = await persistence.inspect(SessionId2(sessionId));
1341
- events = inspected.events;
1342
- storedHeader = inspected.header;
1343
- } catch (error) {
1344
- throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
1345
- }
1346
- const replay = buildReplay(events);
1347
- for (const update of replay.updates) notify(sessionId, update);
1348
- if (replay.title !== void 0) {
1349
- notify(sessionId, {
1350
- sessionUpdate: "session_info_update",
1351
- title: replay.title
1352
- });
1353
- }
1354
- const presets = presetsService();
1355
- const storedPreset = presetFromLog(storedHeader, events);
1356
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1357
- const handle = await agents.resume({
1358
- resumeSessionId: SessionId2(sessionId),
1359
- agentOptions: agentOptionsFor(config.model),
1360
- ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1361
- });
1362
- const projection = new SessionProjection(replay.contextWindow);
1363
- projection.title = replay.title;
1364
- const record = registerRecord(
1365
- sessionId,
1366
- handle.agent,
1367
- () => handle.dispose(),
1368
- config.model,
1369
- config.permissionMode ?? "workspace-write",
1370
- projection
1371
- );
1372
- if (presetId !== void 0) record.preset = presetId;
1373
- {
1374
- const permission = permissionService();
1375
- const storedMode = permission?.current(events);
1376
- if (storedMode !== void 0) record.modeId = storedMode;
1377
- for (let index = events.length - 1; index >= 0; index -= 1) {
1378
- const event = events[index];
1379
- if (event?.type === "approval/policy") {
1380
- const policy = event.data?.policy;
1381
- if (policy === "ask" || policy === "never") record.approvals = policy;
1382
- break;
1383
- }
1384
- }
1385
- }
1386
- ensureSelection(record);
1387
- queueMicrotask(() => publishCommands(sessionId));
1533
+ requirePersistence();
1534
+ const record = await restoreSession(params.sessionId, { replay: true, cwd: params.cwd });
1388
1535
  const options = await configOptions(record);
1389
1536
  return {
1390
1537
  modes: modeState(record),
@@ -1418,7 +1565,7 @@ async function apply(ctx, config = {}) {
1418
1565
  },
1419
1566
  async prompt(params) {
1420
1567
  assertOpen();
1421
- const record = requireSession(params.sessionId);
1568
+ const record = await requireOrRestoreSession(params.sessionId);
1422
1569
  if (record.inflight !== void 0) {
1423
1570
  throw invalidParams("a prompt is already in flight for this session");
1424
1571
  }
@@ -1444,6 +1591,38 @@ async function apply(ctx, config = {}) {
1444
1591
  return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1445
1592
  }
1446
1593
  if (commandMatch?.[1] === "logout") return respond(await logoutText());
1594
+ if (commandMatch?.[1] === "model") {
1595
+ return respond(
1596
+ await modelCommandText(
1597
+ record,
1598
+ params.sessionId,
1599
+ trimmed.slice(commandMatch[0].length).trim()
1600
+ )
1601
+ );
1602
+ }
1603
+ if (commandMatch !== null && commandMatch[1] !== void 0) {
1604
+ const commandsSvc = ctx.get("commands");
1605
+ if (commandsSvc !== void 0) {
1606
+ let execution;
1607
+ try {
1608
+ execution = await commandsSvc.execute(
1609
+ record.agent,
1610
+ trimmed,
1611
+ new AbortController().signal
1612
+ );
1613
+ } catch (error) {
1614
+ return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
1615
+ }
1616
+ if (execution !== void 0) {
1617
+ const { result } = execution;
1618
+ const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1619
+ syncPermissionState(record, params.sessionId);
1620
+ publishCommands(params.sessionId, record);
1621
+ publishConfigOptions(params.sessionId, record);
1622
+ return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1623
+ }
1624
+ }
1625
+ }
1447
1626
  if (!await credentialPresent()) {
1448
1627
  return respond(
1449
1628
  [
@@ -1490,7 +1669,7 @@ async function apply(ctx, config = {}) {
1490
1669
  return { stopReason, ...usage !== void 0 ? { usage } : {} };
1491
1670
  },
1492
1671
  async setSessionMode(params) {
1493
- const record = requireSession(params.sessionId);
1672
+ const record = await requireOrRestoreSession(params.sessionId);
1494
1673
  applyMode(record, params.sessionId, params.modeId);
1495
1674
  notify(params.sessionId, {
1496
1675
  sessionUpdate: "config_option_update",
@@ -1499,7 +1678,7 @@ async function apply(ctx, config = {}) {
1499
1678
  return {};
1500
1679
  },
1501
1680
  async setSessionConfigOption(params) {
1502
- const record = requireSession(params.sessionId);
1681
+ const record = await requireOrRestoreSession(params.sessionId);
1503
1682
  const value = typeof params.value === "string" ? params.value : void 0;
1504
1683
  if (value === void 0) throw invalidParams(`invalid value: ${String(params.value)}`);
1505
1684
  switch (params.configId) {
package/dist/index.js CHANGED
@@ -888,6 +888,87 @@ async function apply(ctx, config = {}) {
888
888
  if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
889
889
  return record;
890
890
  };
891
+ const requireOrRestoreSession = async (sessionId) => {
892
+ const record = sessions.get(sessionId);
893
+ if (record !== void 0) return record;
894
+ if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
895
+ logWarn(`restoring session ${sessionId} from the persisted log`);
896
+ try {
897
+ return await restoreSession(sessionId, { replay: false });
898
+ } catch (error) {
899
+ throw invalidParams(`unknown session: ${sessionId} (${errorChain2(error)})`);
900
+ }
901
+ };
902
+ const restoreSession = async (sessionId, options) => {
903
+ const persistence = requirePersistence();
904
+ const existing = sessions.get(sessionId);
905
+ if (existing !== void 0) {
906
+ sessions.delete(sessionId);
907
+ existing.agent.cancel({ kind: "user" });
908
+ settlePrompt(existing, "cancelled");
909
+ await existing.dispose().catch((error) => {
910
+ logWarn(`dispose before reload failed: ${String(error)}`);
911
+ });
912
+ }
913
+ let events;
914
+ let storedHeader;
915
+ try {
916
+ const inspected = await persistence.inspect(SessionId2(sessionId));
917
+ events = inspected.events;
918
+ storedHeader = inspected.header;
919
+ } catch (error) {
920
+ throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
921
+ }
922
+ const replay = buildReplay(events);
923
+ if (options.replay) {
924
+ for (const update of replay.updates) notify(sessionId, update);
925
+ if (replay.title !== void 0) {
926
+ notify(sessionId, {
927
+ sessionUpdate: "session_info_update",
928
+ title: replay.title
929
+ });
930
+ }
931
+ }
932
+ const presets = presetsService();
933
+ const storedPreset = presetFromLog(storedHeader, events);
934
+ const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
935
+ const handle = await agents.resume({
936
+ resumeSessionId: SessionId2(sessionId),
937
+ agentOptions: agentOptionsFor(config.model),
938
+ ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
939
+ });
940
+ const cwd = options.cwd ?? storedHeader?.cwd;
941
+ const projection = new SessionProjection(replay.contextWindow, {
942
+ terminalOutput: clientTerminalOutput,
943
+ ...cwd !== void 0 ? { cwd } : {}
944
+ });
945
+ projection.title = replay.title;
946
+ const record = registerRecord(
947
+ sessionId,
948
+ handle.agent,
949
+ () => handle.dispose(),
950
+ config.model,
951
+ config.permissionMode ?? "workspace-write",
952
+ projection
953
+ );
954
+ if (presetId !== void 0) record.preset = presetId;
955
+ {
956
+ const permission = permissionService();
957
+ const storedMode = permission?.current(events);
958
+ if (storedMode !== void 0) record.modeId = storedMode;
959
+ for (let index = events.length - 1; index >= 0; index -= 1) {
960
+ const event = events[index];
961
+ if (event?.type === "approval/policy") {
962
+ const policy = event.data?.policy;
963
+ if (policy === "ask" || policy === "never") record.approvals = policy;
964
+ break;
965
+ }
966
+ }
967
+ }
968
+ ensureSelection(record);
969
+ queueMicrotask(() => publishCommands(sessionId));
970
+ return record;
971
+ };
891
972
  const assertOpen = () => {
892
973
  if (closed) throw internalError("the ACP bridge has been disposed");
893
974
  };
@@ -979,6 +1060,7 @@ async function apply(ctx, config = {}) {
979
1060
  delete record.selection;
980
1061
  ensureSelection(record);
981
1062
  setApprovalPolicy(record, record.approvals);
1063
+ publishCommands(acpSessionId, record);
982
1064
  };
983
1065
  const setApprovalPolicy = (record, policy) => {
984
1066
  try {
@@ -988,7 +1070,7 @@ async function apply(ctx, config = {}) {
988
1070
  }
989
1071
  record.approvals = policy;
990
1072
  };
991
- const permissionService = () => ctx.get("permission");
1073
+ const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
992
1074
  const applyMode = (record, sessionId, modeId) => {
993
1075
  const permission = permissionService();
994
1076
  if (permission !== void 0 && permission.names.includes(modeId)) {
@@ -1110,20 +1192,102 @@ async function apply(ctx, config = {}) {
1110
1192
  }
1111
1193
  return result;
1112
1194
  };
1113
- const publishCommands = (sessionId) => {
1114
- notify(sessionId, {
1115
- sessionUpdate: "available_commands_update",
1116
- availableCommands: [
1117
- { name: "status", description: "Show adapter, model, mode, and token status" },
1118
- {
1119
- name: "login",
1120
- description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1121
- input: { hint: "<api-key>" }
1122
- },
1123
- { name: "logout", description: "Remove the API key stored in the harness credential store" }
1124
- ]
1195
+ const BUILTIN_COMMANDS = [
1196
+ { name: "status", description: "Show adapter, model, mode, and token status" },
1197
+ {
1198
+ name: "login",
1199
+ description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1200
+ input: { hint: "<api-key>" }
1201
+ },
1202
+ { name: "logout", description: "Remove the API key stored in the harness credential store" },
1203
+ {
1204
+ name: "model",
1205
+ description: "Select the model for this conversation",
1206
+ input: { hint: "[model] \u2014 blank lists available models" }
1207
+ }
1208
+ ];
1209
+ const availableCommandsFor = async (record) => {
1210
+ const list = [...BUILTIN_COMMANDS];
1211
+ const seen = new Set(list.map((command) => command.name));
1212
+ const commandsSvc = ctx.get("commands");
1213
+ if (commandsSvc !== void 0) {
1214
+ try {
1215
+ for (const descriptor of commandsSvc.list(record.agent)) {
1216
+ if (seen.has(descriptor.name)) continue;
1217
+ seen.add(descriptor.name);
1218
+ list.push({
1219
+ name: descriptor.name,
1220
+ description: descriptor.description,
1221
+ ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
1222
+ });
1223
+ }
1224
+ } catch (error) {
1225
+ logWarn(`command listing failed: ${String(error)}`);
1226
+ }
1227
+ }
1228
+ const skillsSvc = ctx.get("skills");
1229
+ if (skillsSvc !== void 0) {
1230
+ try {
1231
+ const skills = await skillsSvc.list({
1232
+ ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
1233
+ scope: record.agent
1234
+ });
1235
+ for (const skill of skills) {
1236
+ if (skill.invocation.userInvocable !== true) continue;
1237
+ if (seen.has(skill.name)) continue;
1238
+ seen.add(skill.name);
1239
+ list.push({
1240
+ name: skill.name,
1241
+ description: skill.description,
1242
+ input: { hint: "instructions for the skill" }
1243
+ });
1244
+ }
1245
+ } catch (error) {
1246
+ logDebug(`skill listing failed: ${String(error)}`);
1247
+ }
1248
+ }
1249
+ return list;
1250
+ };
1251
+ const publishCommands = (sessionId, record) => {
1252
+ const target = record ?? sessions.get(sessionId);
1253
+ if (target === void 0) return;
1254
+ void availableCommandsFor(target).then((availableCommands) => {
1255
+ notify(sessionId, { sessionUpdate: "available_commands_update", availableCommands });
1256
+ }).catch((error) => {
1257
+ logWarn(`publishing commands failed: ${String(error)}`);
1258
+ });
1259
+ };
1260
+ const publishConfigOptions = (sessionId, record) => {
1261
+ void configOptions(record).then((options) => {
1262
+ notify(sessionId, {
1263
+ sessionUpdate: "config_option_update",
1264
+ configOptions: options
1265
+ });
1266
+ }).catch((error) => {
1267
+ logWarn(`publishing config options failed: ${String(error)}`);
1125
1268
  });
1126
1269
  };
1270
+ const syncPermissionState = (record, sessionId) => {
1271
+ const events = record.agent.session.events;
1272
+ if (events === void 0) return;
1273
+ const permission = permissionService();
1274
+ const storedMode = permission?.current(events);
1275
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1276
+ const event = events[index];
1277
+ if (event?.type === "approval/policy") {
1278
+ const policy = event.data?.policy;
1279
+ if (policy === "ask" || policy === "never") record.approvals = policy;
1280
+ break;
1281
+ }
1282
+ }
1283
+ if (storedMode !== void 0 && storedMode !== record.modeId) {
1284
+ record.modeId = storedMode;
1285
+ notify(sessionId, {
1286
+ sessionUpdate: "current_mode_update",
1287
+ currentModeId: record.modeId
1288
+ });
1289
+ }
1290
+ };
1127
1291
  const credentialStore = () => {
1128
1292
  const brand = harness.credentialRef;
1129
1293
  const store = ctx.get("credentials");
@@ -1188,6 +1352,49 @@ async function apply(ctx, config = {}) {
1188
1352
  const remaining = await describeCredential();
1189
1353
  return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1190
1354
  };
1355
+ const modelCommandText = async (record, acpSessionId, query) => {
1356
+ const discovered = await discoverModels();
1357
+ const catalog = [
1358
+ ...discovered.map((entry) => ({
1359
+ value: encodeChoice(entry.provider, entry.model),
1360
+ label: entry.label
1361
+ })),
1362
+ ...modelCandidates().filter((model) => !discovered.some((entry) => entry.model === model)).map((model) => ({ value: encodeChoice(void 0, model), label: model }))
1363
+ ];
1364
+ const route = routeOf(record);
1365
+ const currentValue = encodeChoice(record.provider, record.model ?? route.model ?? "");
1366
+ if (query === "") {
1367
+ const lines = catalog.map(
1368
+ (entry) => `${entry.value === currentValue ? "\u2192" : " "} ${entry.label} \u2014 ${entry.value}`
1369
+ );
1370
+ return [
1371
+ `model: ${route.model ?? "(product default)"}${record.effort !== void 0 ? ` (effort ${record.effort})` : ""}`,
1372
+ "",
1373
+ ...lines.length > 0 ? lines : ["no models discovered \u2014 check credentials with /status"],
1374
+ "",
1375
+ "switch with /model <name>"
1376
+ ].join("\n");
1377
+ }
1378
+ const lowered = query.toLowerCase();
1379
+ const exact = catalog.filter(
1380
+ (entry) => entry.value.toLowerCase() === lowered || decodeChoice(entry.value).model.toLowerCase() === lowered
1381
+ );
1382
+ const matches = exact.length > 0 ? exact : catalog.filter(
1383
+ (entry) => entry.value.toLowerCase().includes(lowered) || entry.label.toLowerCase().includes(lowered)
1384
+ );
1385
+ if (matches.length === 0) return `no model matches "${query}" \u2014 see /model`;
1386
+ if (matches.length > 1) {
1387
+ return [`"${query}" is ambiguous:`, ...matches.map((entry) => ` ${entry.label} \u2014 ${entry.value}`)].join(
1388
+ "\n"
1389
+ );
1390
+ }
1391
+ const target = matches[0];
1392
+ if (target.value === currentValue) return `already on ${target.label}`;
1393
+ await switchModel(record, acpSessionId, target.value);
1394
+ publishCommands(acpSessionId, record);
1395
+ publishConfigOptions(acpSessionId, record);
1396
+ return `model \u2192 ${target.label}`;
1397
+ };
1191
1398
  const statusText = async (record) => {
1192
1399
  const used = record.projection.contextWindow;
1193
1400
  const route = routeOf(record);
@@ -1348,68 +1555,8 @@ async function apply(ctx, config = {}) {
1348
1555
  assertOpen();
1349
1556
  validateCwd(params.cwd);
1350
1557
  syncMcpServers(params.mcpServers, params.cwd);
1351
- const persistence = requirePersistence();
1352
- const sessionId = params.sessionId;
1353
- const existing = sessions.get(sessionId);
1354
- if (existing !== void 0) {
1355
- sessions.delete(sessionId);
1356
- existing.agent.cancel({ kind: "user" });
1357
- settlePrompt(existing, "cancelled");
1358
- await existing.dispose().catch((error) => {
1359
- logWarn(`dispose before reload failed: ${String(error)}`);
1360
- });
1361
- }
1362
- let events;
1363
- let storedHeader;
1364
- try {
1365
- const inspected = await persistence.inspect(SessionId2(sessionId));
1366
- events = inspected.events;
1367
- storedHeader = inspected.header;
1368
- } catch (error) {
1369
- throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
1370
- }
1371
- const replay = buildReplay(events);
1372
- for (const update of replay.updates) notify(sessionId, update);
1373
- if (replay.title !== void 0) {
1374
- notify(sessionId, {
1375
- sessionUpdate: "session_info_update",
1376
- title: replay.title
1377
- });
1378
- }
1379
- const presets = presetsService();
1380
- const storedPreset = presetFromLog(storedHeader, events);
1381
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1382
- const handle = await agents.resume({
1383
- resumeSessionId: SessionId2(sessionId),
1384
- agentOptions: agentOptionsFor(config.model),
1385
- ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1386
- });
1387
- const projection = new SessionProjection(replay.contextWindow);
1388
- projection.title = replay.title;
1389
- const record = registerRecord(
1390
- sessionId,
1391
- handle.agent,
1392
- () => handle.dispose(),
1393
- config.model,
1394
- config.permissionMode ?? "workspace-write",
1395
- projection
1396
- );
1397
- if (presetId !== void 0) record.preset = presetId;
1398
- {
1399
- const permission = permissionService();
1400
- const storedMode = permission?.current(events);
1401
- if (storedMode !== void 0) record.modeId = storedMode;
1402
- for (let index = events.length - 1; index >= 0; index -= 1) {
1403
- const event = events[index];
1404
- if (event?.type === "approval/policy") {
1405
- const policy = event.data?.policy;
1406
- if (policy === "ask" || policy === "never") record.approvals = policy;
1407
- break;
1408
- }
1409
- }
1410
- }
1411
- ensureSelection(record);
1412
- queueMicrotask(() => publishCommands(sessionId));
1558
+ requirePersistence();
1559
+ const record = await restoreSession(params.sessionId, { replay: true, cwd: params.cwd });
1413
1560
  const options = await configOptions(record);
1414
1561
  return {
1415
1562
  modes: modeState(record),
@@ -1443,7 +1590,7 @@ async function apply(ctx, config = {}) {
1443
1590
  },
1444
1591
  async prompt(params) {
1445
1592
  assertOpen();
1446
- const record = requireSession(params.sessionId);
1593
+ const record = await requireOrRestoreSession(params.sessionId);
1447
1594
  if (record.inflight !== void 0) {
1448
1595
  throw invalidParams("a prompt is already in flight for this session");
1449
1596
  }
@@ -1469,6 +1616,38 @@ async function apply(ctx, config = {}) {
1469
1616
  return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1470
1617
  }
1471
1618
  if (commandMatch?.[1] === "logout") return respond(await logoutText());
1619
+ if (commandMatch?.[1] === "model") {
1620
+ return respond(
1621
+ await modelCommandText(
1622
+ record,
1623
+ params.sessionId,
1624
+ trimmed.slice(commandMatch[0].length).trim()
1625
+ )
1626
+ );
1627
+ }
1628
+ if (commandMatch !== null && commandMatch[1] !== void 0) {
1629
+ const commandsSvc = ctx.get("commands");
1630
+ if (commandsSvc !== void 0) {
1631
+ let execution;
1632
+ try {
1633
+ execution = await commandsSvc.execute(
1634
+ record.agent,
1635
+ trimmed,
1636
+ new AbortController().signal
1637
+ );
1638
+ } catch (error) {
1639
+ return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
1640
+ }
1641
+ if (execution !== void 0) {
1642
+ const { result } = execution;
1643
+ const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1644
+ syncPermissionState(record, params.sessionId);
1645
+ publishCommands(params.sessionId, record);
1646
+ publishConfigOptions(params.sessionId, record);
1647
+ return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1648
+ }
1649
+ }
1650
+ }
1472
1651
  if (!await credentialPresent()) {
1473
1652
  return respond(
1474
1653
  [
@@ -1515,7 +1694,7 @@ async function apply(ctx, config = {}) {
1515
1694
  return { stopReason, ...usage !== void 0 ? { usage } : {} };
1516
1695
  },
1517
1696
  async setSessionMode(params) {
1518
- const record = requireSession(params.sessionId);
1697
+ const record = await requireOrRestoreSession(params.sessionId);
1519
1698
  applyMode(record, params.sessionId, params.modeId);
1520
1699
  notify(params.sessionId, {
1521
1700
  sessionUpdate: "config_option_update",
@@ -1524,7 +1703,7 @@ async function apply(ctx, config = {}) {
1524
1703
  return {};
1525
1704
  },
1526
1705
  async setSessionConfigOption(params) {
1527
- const record = requireSession(params.sessionId);
1706
+ const record = await requireOrRestoreSession(params.sessionId);
1528
1707
  const value = typeof params.value === "string" ? params.value : void 0;
1529
1708
  if (value === void 0) throw invalidParams(`invalid value: ${String(params.value)}`);
1530
1709
  switch (params.configId) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.2",
6
+ "version": "0.4.4",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/openma-ai/deepseek-harness-acp.git"