@pwrdrvr/agent-acp 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -384,6 +384,11 @@ declare class AcpAgentClient implements AgentBackend {
384
384
  private unsubscribeRequest;
385
385
  private initialized;
386
386
  private runtimeCapabilities?;
387
+ /** What `initialize` reported: agent-level, and never any one session's. The
388
+ * base a session's own menus are built on — the client-wide snapshot above is
389
+ * whichever session wrote last, and a fresh session must not inherit its
390
+ * menus or their current values. */
391
+ private initializeCapabilities?;
387
392
  constructor(options: AcpAgentClientOptions);
388
393
  private readonly clientName;
389
394
  private readonly clientTitle;
@@ -510,7 +515,7 @@ declare class AcpAgentClient implements AgentBackend {
510
515
  * `session/set_model` answers `{}` and refreshes only via an async
511
516
  * notification. On Kimi the model DETERMINES which thought levels exist —
512
517
  * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
513
- * `applyReasoning` picks from `runtimeCapabilities` on the following
518
+ * `applyReasoning` picks from THIS session's menus on the following
514
519
  * `startTurn`, so the refresh this write leaves behind is what it reads. */
515
520
  setModel(threadId: string, modelId: string): Promise<void>;
516
521
  setConfigOption(threadId: string, optionId: string, value: string): Promise<void>;
@@ -537,7 +542,17 @@ declare class AcpAgentClient implements AgentBackend {
537
542
  /** Read an image file and build an ACP `image` content block (base64 + inferred
538
543
  * mimeType). Throws on read failure; the caller catches + skips. */
539
544
  private imageBlockFromPath;
545
+ /** Normalize `result` into a capability snapshot, filling what it omits from
546
+ * `base`. Required, not defaulted: an explicit `undefined` would trigger a
547
+ * default parameter, and the only default on hand is the client-wide
548
+ * snapshot — which may be another session's. */
540
549
  private captureRuntimeCapabilities;
550
+ /** The option menus to consult when choosing a value for `threadId`. A
551
+ * session with none of its own gets the agent-level `initialize` menus, never
552
+ * another session's. */
553
+ private sessionCapabilities;
554
+ /** What a session's menus are built on: its own, or `initialize`'s. */
555
+ private sessionBase;
541
556
  private notifyRuntimeCapabilities;
542
557
  private emitThreadSettings;
543
558
  private requireSession;
@@ -550,9 +565,10 @@ declare class AcpAgentClient implements AgentBackend {
550
565
  supportsSseMcp(): boolean;
551
566
  }
552
567
  /** Pick the value to set on a "thinking" config option for a neutral reasoning
553
- * token: low-effort → the OFF-like value, high-effort → the ON-like value.
554
- * Returns undefined when the token isn't an effort signal we recognize, or the
555
- * option has no value of the needed polarity (caller leaves the option alone).
568
+ * token: high-effort → the ON-like value; low-effort → the OFF-like value, or,
569
+ * when the agent offers only graded levels, the LOW one — the closest thing to
570
+ * off it has. Returns undefined when the token isn't an effort signal we
571
+ * recognize, or the option has no value to match (caller leaves it alone).
556
572
  * Exported for testing. */
557
573
  declare function reasoningValueForThoughtLevel(reasoning: string, values: ReadonlyArray<AcpRuntimeConfigOptionValue>): string | undefined;
558
574
 
package/dist/index.js CHANGED
@@ -702,9 +702,10 @@ function acpRuntimeSupportsSseMcp(capabilities) {
702
702
  return capabilities?.agentCapabilities?.mcp?.sse === true;
703
703
  }
704
704
  function modelConfigOption(capabilities) {
705
- return capabilities?.configOptions?.find(
706
- (option) => option.category === "model" || option.id === "model"
707
- );
705
+ return capabilities?.configOptions?.find(isModelConfigOption);
706
+ }
707
+ function isModelConfigOption(option) {
708
+ return option.category === "model" || option.id === "model";
708
709
  }
709
710
  function modelIdFromCapabilities(capabilities) {
710
711
  return capabilities?.models?.currentModelId ?? modelConfigOption(capabilities)?.currentValue;
@@ -751,6 +752,8 @@ function acpSessionRuntimeStateFromUpdate(update, now) {
751
752
  return currentModeId ? { currentModeId, updatedAt: now } : void 0;
752
753
  }
753
754
  if (kind === "config_option_update") {
755
+ const fullSet = configOptionUpdateSet(update);
756
+ if (fullSet !== void 0) return runtimeStateFromConfigOptionSet(fullSet, now);
754
757
  const configOption = asRecord(update.configOption ?? update.config_option) ?? update;
755
758
  const id = readString(configOption, "id") ?? readString(configOption, "configOptionId") ?? readString(configOption, "configId");
756
759
  const value = readString(configOption, "currentValue") ?? readString(configOption, "value");
@@ -768,6 +771,24 @@ function mergeAcpRuntimeState(existing, update) {
768
771
  }
769
772
  };
770
773
  }
774
+ function configOptionUpdateSet(update) {
775
+ if (readKind(update) !== "config_option_update") return void 0;
776
+ const set = update.configOptions ?? update.config_options;
777
+ return Array.isArray(set) ? set : void 0;
778
+ }
779
+ function runtimeStateFromConfigOptionSet(set, now) {
780
+ const options = readConfigOptions(set);
781
+ const configValues = Object.fromEntries(
782
+ options.flatMap(
783
+ (option) => option.currentValue !== void 0 ? [[option.id, option.currentValue]] : []
784
+ )
785
+ );
786
+ if (Object.keys(configValues).length === 0) return void 0;
787
+ const state = { configValues, updatedAt: now };
788
+ const model = options.find(isModelConfigOption)?.currentValue;
789
+ if (model !== void 0) state.currentModelId = model;
790
+ return state;
791
+ }
771
792
  function modeLabelFor(capabilities, modeId) {
772
793
  return capabilities?.modes?.availableModes.find((mode) => mode.id === modeId)?.label;
773
794
  }
@@ -1006,6 +1027,11 @@ var AcpAgentClient = class {
1006
1027
  unsubscribeRequest = void 0;
1007
1028
  initialized = false;
1008
1029
  runtimeCapabilities;
1030
+ /** What `initialize` reported: agent-level, and never any one session's. The
1031
+ * base a session's own menus are built on — the client-wide snapshot above is
1032
+ * whichever session wrote last, and a fresh session must not inherit its
1033
+ * menus or their current values. */
1034
+ initializeCapabilities;
1009
1035
  constructor(options) {
1010
1036
  this.transport = options.transport;
1011
1037
  this.strategy = options.strategy;
@@ -1219,6 +1245,7 @@ var AcpAgentClient = class {
1219
1245
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1220
1246
  turnId: void 0,
1221
1247
  runtimeState: void 0,
1248
+ runtimeCapabilities: void 0,
1222
1249
  pendingTurn: void 0,
1223
1250
  pendingInstructions: options.instructions,
1224
1251
  mcpServerNames: configuredMcpServers.map((server) => server.name)
@@ -1260,6 +1287,7 @@ var AcpAgentClient = class {
1260
1287
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1261
1288
  turnId: void 0,
1262
1289
  runtimeState: void 0,
1290
+ runtimeCapabilities: void 0,
1263
1291
  pendingTurn: void 0,
1264
1292
  pendingInstructions: options.instructions,
1265
1293
  // Remember ONLY the MCP servers attached to THIS session so the host's
@@ -1272,9 +1300,11 @@ var AcpAgentClient = class {
1272
1300
  }
1273
1301
  const runtimeCapabilities = this.captureRuntimeCapabilities(
1274
1302
  options.lifecycle === "load" ? "session-load" : "session-new",
1275
- result
1303
+ result,
1304
+ this.sessionBase(session)
1276
1305
  );
1277
1306
  if (runtimeCapabilities) {
1307
+ session.runtimeCapabilities = runtimeCapabilities;
1278
1308
  const runtimeState = acpSessionRuntimeStateFromCapabilities(
1279
1309
  runtimeCapabilities,
1280
1310
  this.now()
@@ -1459,11 +1489,12 @@ var AcpAgentClient = class {
1459
1489
  * `session/set_model` answers `{}` and refreshes only via an async
1460
1490
  * notification. On Kimi the model DETERMINES which thought levels exist —
1461
1491
  * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
1462
- * `applyReasoning` picks from `runtimeCapabilities` on the following
1492
+ * `applyReasoning` picks from THIS session's menus on the following
1463
1493
  * `startTurn`, so the refresh this write leaves behind is what it reads. */
1464
1494
  async setModel(threadId, modelId) {
1465
- const advertisesModelsCapability = (this.runtimeCapabilities?.models?.availableModels?.length ?? 0) > 0;
1466
- const option = modelConfigOption(this.runtimeCapabilities);
1495
+ const capabilities = this.sessionCapabilities(threadId);
1496
+ const advertisesModelsCapability = (capabilities?.models?.availableModels?.length ?? 0) > 0;
1497
+ const option = modelConfigOption(capabilities);
1467
1498
  if (!advertisesModelsCapability && option !== void 0) {
1468
1499
  await this.setRuntimeOption(threadId, "modelConfigOption", option.id, modelId);
1469
1500
  return;
@@ -1516,8 +1547,13 @@ var AcpAgentClient = class {
1516
1547
  },
1517
1548
  ACP_REQUEST_TIMEOUT_MS
1518
1549
  );
1519
- const runtimeCapabilities = this.captureRuntimeCapabilities("initialize", result);
1550
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1551
+ "initialize",
1552
+ result,
1553
+ this.initializeCapabilities
1554
+ );
1520
1555
  if (runtimeCapabilities) {
1556
+ this.initializeCapabilities = runtimeCapabilities;
1521
1557
  this.notifyRuntimeCapabilities({ runtimeCapabilities });
1522
1558
  }
1523
1559
  this.initialized = true;
@@ -1543,17 +1579,28 @@ var AcpAgentClient = class {
1543
1579
  this.logger.debug("acp session/update for unknown session", { protocolSessionId });
1544
1580
  return;
1545
1581
  }
1582
+ if (configOptionUpdateSet(update) !== void 0) {
1583
+ const refreshed = this.captureRuntimeCapabilities(
1584
+ // A live refresh of this session's menus, not a load: keep the source
1585
+ // they were opened with.
1586
+ session.runtimeCapabilities?.source ?? "session-new",
1587
+ update,
1588
+ this.sessionBase(session)
1589
+ );
1590
+ if (refreshed) session.runtimeCapabilities = refreshed;
1591
+ }
1546
1592
  const runtimeState = acpSessionRuntimeStateFromUpdate(update, this.now());
1547
1593
  if (runtimeState) {
1548
1594
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, runtimeState);
1549
- if (this.runtimeCapabilities) {
1595
+ const capabilities = this.sessionCapabilities(session.threadId);
1596
+ if (capabilities) {
1550
1597
  this.notifyRuntimeCapabilities({
1551
1598
  threadId: session.threadId,
1552
- runtimeCapabilities: this.runtimeCapabilities,
1599
+ runtimeCapabilities: capabilities,
1553
1600
  runtimeState: session.runtimeState
1554
1601
  });
1555
1602
  }
1556
- this.emitThreadSettings(session, this.runtimeCapabilities, session.runtimeState);
1603
+ this.emitThreadSettings(session, capabilities, session.runtimeState);
1557
1604
  return;
1558
1605
  }
1559
1606
  const updateKind = readString2(update, "sessionUpdate");
@@ -1623,7 +1670,12 @@ var AcpAgentClient = class {
1623
1670
  optionId,
1624
1671
  value
1625
1672
  );
1626
- const runtimeCapabilities = this.captureRuntimeCapabilities("session-load", result);
1673
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1674
+ "session-load",
1675
+ result,
1676
+ this.sessionBase(session)
1677
+ );
1678
+ if (runtimeCapabilities) session.runtimeCapabilities = runtimeCapabilities;
1627
1679
  const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : source === "modelConfigOption" ? (
1628
1680
  // Record BOTH: it IS the config option's value, and it IS the
1629
1681
  // session's model. A consumer reading `currentModelId` must not
@@ -1635,7 +1687,13 @@ var AcpAgentClient = class {
1635
1687
  }
1636
1688
  ) : { currentModelId: value, updatedAt: this.now() };
1637
1689
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, requested);
1638
- const effectiveCapabilities = runtimeCapabilities ?? this.runtimeCapabilities;
1690
+ const reply = asRecord2(result);
1691
+ const reported = runtimeStateFromConfigOptionSet(
1692
+ reply?.configOptions ?? reply?.config_options,
1693
+ this.now()
1694
+ );
1695
+ if (reported) session.runtimeState = mergeAcpRuntimeState(session.runtimeState, reported);
1696
+ const effectiveCapabilities = this.sessionCapabilities(threadId);
1639
1697
  if (effectiveCapabilities) {
1640
1698
  this.notifyRuntimeCapabilities({
1641
1699
  threadId,
@@ -1676,7 +1734,8 @@ var AcpAgentClient = class {
1676
1734
  * If nothing matches we leave it alone — the caller's `.catch` debug-logs. */
1677
1735
  async applyReasoning(threadId, reasoning) {
1678
1736
  const target = reasoning.toLowerCase();
1679
- const modes = this.runtimeCapabilities?.modes?.availableModes ?? [];
1737
+ const capabilities = this.sessionCapabilities(threadId);
1738
+ const modes = capabilities?.modes?.availableModes ?? [];
1680
1739
  const modeMatch = modes.find(
1681
1740
  (mode) => mode.id.toLowerCase() === target || typeof mode.label === "string" && mode.label.toLowerCase() === target
1682
1741
  );
@@ -1684,7 +1743,7 @@ var AcpAgentClient = class {
1684
1743
  await this.setMode(threadId, modeMatch.id);
1685
1744
  return;
1686
1745
  }
1687
- const thinking = this.runtimeCapabilities?.configOptions?.find(
1746
+ const thinking = capabilities?.configOptions?.find(
1688
1747
  (option) => option.category === "thought_level" || option.id === "thinking"
1689
1748
  );
1690
1749
  if (thinking !== void 0) {
@@ -1714,18 +1773,32 @@ var AcpAgentClient = class {
1714
1773
  data: data.toString("base64")
1715
1774
  };
1716
1775
  }
1717
- captureRuntimeCapabilities(source, result) {
1776
+ /** Normalize `result` into a capability snapshot, filling what it omits from
1777
+ * `base`. Required, not defaulted: an explicit `undefined` would trigger a
1778
+ * default parameter, and the only default on hand is the client-wide
1779
+ * snapshot — which may be another session's. */
1780
+ captureRuntimeCapabilities(source, result, base) {
1718
1781
  const runtimeCapabilities = normalizeAcpRuntimeCapabilities({
1719
1782
  value: result,
1720
1783
  now: this.now(),
1721
1784
  source,
1722
- ...this.runtimeCapabilities !== void 0 ? { initialize: this.runtimeCapabilities } : {}
1785
+ ...base !== void 0 ? { initialize: base } : {}
1723
1786
  });
1724
1787
  if (runtimeCapabilities) {
1725
1788
  this.runtimeCapabilities = runtimeCapabilities;
1726
1789
  }
1727
1790
  return runtimeCapabilities;
1728
1791
  }
1792
+ /** The option menus to consult when choosing a value for `threadId`. A
1793
+ * session with none of its own gets the agent-level `initialize` menus, never
1794
+ * another session's. */
1795
+ sessionCapabilities(threadId) {
1796
+ return this.sessionBase(this.sessions.get(threadId));
1797
+ }
1798
+ /** What a session's menus are built on: its own, or `initialize`'s. */
1799
+ sessionBase(session) {
1800
+ return session?.runtimeCapabilities ?? this.initializeCapabilities;
1801
+ }
1729
1802
  notifyRuntimeCapabilities(event) {
1730
1803
  this.runtimeCapabilitiesHandler?.({
1731
1804
  ...event.threadId !== void 0 ? { threadId: event.threadId } : {},
@@ -1803,6 +1876,7 @@ function thoughtLevelPolarity(value) {
1803
1876
  const haystack = `${value.value} ${value.label ?? ""}`.toLowerCase();
1804
1877
  if (/\b(off|none|disabled?|no)\b/.test(haystack)) return "off";
1805
1878
  if (/\b(on|enabled?|high|full|yes)\b/.test(haystack)) return "on";
1879
+ if (/\b(low|minimal|min|minimum)\b/.test(haystack)) return "low";
1806
1880
  return "other";
1807
1881
  }
1808
1882
  function reasoningValueForThoughtLevel(reasoning, values) {
@@ -1810,8 +1884,9 @@ function reasoningValueForThoughtLevel(reasoning, values) {
1810
1884
  const wantOff = LOW_EFFORT_TOKENS.has(token);
1811
1885
  const wantOn = HIGH_EFFORT_TOKENS.has(token);
1812
1886
  if (!wantOff && !wantOn) return void 0;
1813
- const desired = wantOff ? "off" : "on";
1814
- return values.find((value) => thoughtLevelPolarity(value) === desired)?.value;
1887
+ const withPolarity = (polarity) => values.find((value) => thoughtLevelPolarity(value) === polarity);
1888
+ if (wantOn) return withPolarity("on")?.value;
1889
+ return (withPolarity("off") ?? withPolarity("low"))?.value;
1815
1890
  }
1816
1891
  function buildApprovalRequest(args) {
1817
1892
  const toolCall = asRecord2(args.params.toolCall) ?? {};