@pwrdrvr/agent-acp 0.15.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
@@ -14,7 +14,16 @@ interface AcpJsonRpcTransport {
14
14
  * drives. Injectable (via `createConnection`) so the adapter can be unit-tested
15
15
  * without spawning a real agent. The library's `ClientSideConnection` satisfies
16
16
  * this structurally (its methods take typed schema params; we pass records that
17
- * match at runtime). */
17
+ * match at runtime).
18
+ *
19
+ * Every entry here MUST exist on the real `ClientSideConnection`. It is
20
+ * attached with an `as unknown as` cast (see `createConnection`), so a name
21
+ * that is merely plausible type-checks and then throws
22
+ * "conn.<name> is not a function" at runtime, on the wire, against a user's
23
+ * agent. `setSessionModel` was such an entry: ACP 1.3 has no `session/set_model`
24
+ * at all, so every model selection died before it was sent. Anything the SDK
25
+ * does not expose as a typed method belongs in `dispatch`'s default arm, which
26
+ * sends it verbatim through `request()`. */
18
27
  interface AcpAgentConnection {
19
28
  initialize(params: Record<string, unknown>): Promise<unknown>;
20
29
  newSession(params: Record<string, unknown>): Promise<unknown>;
@@ -22,7 +31,6 @@ interface AcpAgentConnection {
22
31
  prompt(params: Record<string, unknown>): Promise<unknown>;
23
32
  cancel(params: Record<string, unknown>): Promise<void>;
24
33
  setSessionMode(params: Record<string, unknown>): Promise<unknown>;
25
- setSessionModel(params: Record<string, unknown>): Promise<unknown>;
26
34
  authenticate(params: Record<string, unknown>): Promise<unknown>;
27
35
  request(method: string, params: Record<string, unknown>): Promise<unknown>;
28
36
  notify(method: string, params: Record<string, unknown>): Promise<void>;
@@ -301,7 +309,11 @@ type AcpPromptContentBlock = {
301
309
  mimeType: string;
302
310
  data: string;
303
311
  };
304
- type AcpRuntimeOptionSource = "mode" | "model" | "configOption";
312
+ type AcpRuntimeOptionSource = "mode" | "model" | "configOption"
313
+ /** A model write that travels as `session/set_config_option` because the
314
+ * agent exposes model choice as a config option rather than via the legacy
315
+ * top-level `models` capability. See `setModel`. */
316
+ | "modelConfigOption";
305
317
  type AcpAgentClientOptions = {
306
318
  /** The transport (real stdio or a fake). */
307
319
  transport: AcpJsonRpcTransport;
@@ -372,6 +384,11 @@ declare class AcpAgentClient implements AgentBackend {
372
384
  private unsubscribeRequest;
373
385
  private initialized;
374
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?;
375
392
  constructor(options: AcpAgentClientOptions);
376
393
  private readonly clientName;
377
394
  private readonly clientTitle;
@@ -389,8 +406,9 @@ declare class AcpAgentClient implements AgentBackend {
389
406
  * Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
390
407
  * and maps them onto an ACP `session/new`. ACP supports:
391
408
  * • `cwd` → `session/new.cwd`.
392
- * • `model` → applied via `session/set_model` after the session opens, when
393
- * the agent advertises model selection (best-effort; debug-logged if not).
409
+ * • `model` → applied after the session opens, by whichever route the agent
410
+ * advertises: the `model` config option, or legacy `session/set_model`
411
+ * (best-effort; debug-logged if not). See `setModel`.
394
412
  * • `instructions` is NOT injected here — ACP `session/new` has no base-
395
413
  * instructions slot, matching the adapter's existing behavior. A host that
396
414
  * wants system framing sends it as leading turn text.
@@ -480,6 +498,25 @@ declare class AcpAgentClient implements AgentBackend {
480
498
  * releasing here never blocks resume. */
481
499
  archiveThread(threadId: string): Promise<void>;
482
500
  setMode(threadId: string, modeId: string): Promise<void>;
501
+ /** Apply a model selection by whichever route the agent actually advertises.
502
+ *
503
+ * ACP 1.3 has NO `session/set_model` — it is absent from `AGENT_METHODS`.
504
+ * An agent that offers model choice does it through a config option with
505
+ * `category: "model"` (Kimi's `session/new` returns exactly that, and nothing
506
+ * under `models`). Some agents still accept `session/set_model` as a vendor
507
+ * extension, and the `models.availableModels` shape is still honoured for
508
+ * them, but it is not what the protocol defines any more.
509
+ *
510
+ * Two reasons to prefer the config option when the agent offers it. The READ
511
+ * path already draws this distinction — `modelIdFromCapabilities` and
512
+ * `modelsFromCapabilities` both fall back to the config option — and the two
513
+ * disagreeing lets a host list models it cannot then select. And the config
514
+ * option's REPLY carries the agent's full refreshed config set, where
515
+ * `session/set_model` answers `{}` and refreshes only via an async
516
+ * notification. On Kimi the model DETERMINES which thought levels exist —
517
+ * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
518
+ * `applyReasoning` picks from THIS session's menus on the following
519
+ * `startTurn`, so the refresh this write leaves behind is what it reads. */
483
520
  setModel(threadId: string, modelId: string): Promise<void>;
484
521
  setConfigOption(threadId: string, optionId: string, value: string): Promise<void>;
485
522
  close(): Promise<void>;
@@ -505,7 +542,17 @@ declare class AcpAgentClient implements AgentBackend {
505
542
  /** Read an image file and build an ACP `image` content block (base64 + inferred
506
543
  * mimeType). Throws on read failure; the caller catches + skips. */
507
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. */
508
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;
509
556
  private notifyRuntimeCapabilities;
510
557
  private emitThreadSettings;
511
558
  private requireSession;
@@ -518,9 +565,10 @@ declare class AcpAgentClient implements AgentBackend {
518
565
  supportsSseMcp(): boolean;
519
566
  }
520
567
  /** Pick the value to set on a "thinking" config option for a neutral reasoning
521
- * token: low-effort → the OFF-like value, high-effort → the ON-like value.
522
- * Returns undefined when the token isn't an effort signal we recognize, or the
523
- * 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).
524
572
  * Exported for testing. */
525
573
  declare function reasoningValueForThoughtLevel(reasoning: string, values: ReadonlyArray<AcpRuntimeConfigOptionValue>): string | undefined;
526
574
 
package/dist/index.js CHANGED
@@ -144,8 +144,9 @@ var AcpConnection = class {
144
144
  return await conn.prompt(params);
145
145
  case "session/set_mode":
146
146
  return await conn.setSessionMode(params);
147
- case "session/set_model":
148
- return await conn.setSessionModel(params);
147
+ // `session/set_model` is deliberately NOT cased here: SDK 1.3 has no
148
+ // typed `setSessionModel`, so it goes out through the default arm's
149
+ // generic `request()` for agents still speaking the older method.
149
150
  case "session/cancel":
150
151
  await conn.cancel(params);
151
152
  return {};
@@ -701,9 +702,10 @@ function acpRuntimeSupportsSseMcp(capabilities) {
701
702
  return capabilities?.agentCapabilities?.mcp?.sse === true;
702
703
  }
703
704
  function modelConfigOption(capabilities) {
704
- return capabilities?.configOptions?.find(
705
- (option) => option.category === "model" || option.id === "model"
706
- );
705
+ return capabilities?.configOptions?.find(isModelConfigOption);
706
+ }
707
+ function isModelConfigOption(option) {
708
+ return option.category === "model" || option.id === "model";
707
709
  }
708
710
  function modelIdFromCapabilities(capabilities) {
709
711
  return capabilities?.models?.currentModelId ?? modelConfigOption(capabilities)?.currentValue;
@@ -750,6 +752,8 @@ function acpSessionRuntimeStateFromUpdate(update, now) {
750
752
  return currentModeId ? { currentModeId, updatedAt: now } : void 0;
751
753
  }
752
754
  if (kind === "config_option_update") {
755
+ const fullSet = configOptionUpdateSet(update);
756
+ if (fullSet !== void 0) return runtimeStateFromConfigOptionSet(fullSet, now);
753
757
  const configOption = asRecord(update.configOption ?? update.config_option) ?? update;
754
758
  const id = readString(configOption, "id") ?? readString(configOption, "configOptionId") ?? readString(configOption, "configId");
755
759
  const value = readString(configOption, "currentValue") ?? readString(configOption, "value");
@@ -767,6 +771,24 @@ function mergeAcpRuntimeState(existing, update) {
767
771
  }
768
772
  };
769
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
+ }
770
792
  function modeLabelFor(capabilities, modeId) {
771
793
  return capabilities?.modes?.availableModes.find((mode) => mode.id === modeId)?.label;
772
794
  }
@@ -1005,6 +1027,11 @@ var AcpAgentClient = class {
1005
1027
  unsubscribeRequest = void 0;
1006
1028
  initialized = false;
1007
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;
1008
1035
  constructor(options) {
1009
1036
  this.transport = options.transport;
1010
1037
  this.strategy = options.strategy;
@@ -1061,8 +1088,9 @@ var AcpAgentClient = class {
1061
1088
  * Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
1062
1089
  * and maps them onto an ACP `session/new`. ACP supports:
1063
1090
  * • `cwd` → `session/new.cwd`.
1064
- * • `model` → applied via `session/set_model` after the session opens, when
1065
- * the agent advertises model selection (best-effort; debug-logged if not).
1091
+ * • `model` → applied after the session opens, by whichever route the agent
1092
+ * advertises: the `model` config option, or legacy `session/set_model`
1093
+ * (best-effort; debug-logged if not). See `setModel`.
1066
1094
  * • `instructions` is NOT injected here — ACP `session/new` has no base-
1067
1095
  * instructions slot, matching the adapter's existing behavior. A host that
1068
1096
  * wants system framing sends it as leading turn text.
@@ -1217,6 +1245,7 @@ var AcpAgentClient = class {
1217
1245
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1218
1246
  turnId: void 0,
1219
1247
  runtimeState: void 0,
1248
+ runtimeCapabilities: void 0,
1220
1249
  pendingTurn: void 0,
1221
1250
  pendingInstructions: options.instructions,
1222
1251
  mcpServerNames: configuredMcpServers.map((server) => server.name)
@@ -1258,6 +1287,7 @@ var AcpAgentClient = class {
1258
1287
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1259
1288
  turnId: void 0,
1260
1289
  runtimeState: void 0,
1290
+ runtimeCapabilities: void 0,
1261
1291
  pendingTurn: void 0,
1262
1292
  pendingInstructions: options.instructions,
1263
1293
  // Remember ONLY the MCP servers attached to THIS session so the host's
@@ -1270,9 +1300,11 @@ var AcpAgentClient = class {
1270
1300
  }
1271
1301
  const runtimeCapabilities = this.captureRuntimeCapabilities(
1272
1302
  options.lifecycle === "load" ? "session-load" : "session-new",
1273
- result
1303
+ result,
1304
+ this.sessionBase(session)
1274
1305
  );
1275
1306
  if (runtimeCapabilities) {
1307
+ session.runtimeCapabilities = runtimeCapabilities;
1276
1308
  const runtimeState = acpSessionRuntimeStateFromCapabilities(
1277
1309
  runtimeCapabilities,
1278
1310
  this.now()
@@ -1440,7 +1472,33 @@ var AcpAgentClient = class {
1440
1472
  async setMode(threadId, modeId) {
1441
1473
  await this.setRuntimeOption(threadId, "mode", modeId, modeId);
1442
1474
  }
1475
+ /** Apply a model selection by whichever route the agent actually advertises.
1476
+ *
1477
+ * ACP 1.3 has NO `session/set_model` — it is absent from `AGENT_METHODS`.
1478
+ * An agent that offers model choice does it through a config option with
1479
+ * `category: "model"` (Kimi's `session/new` returns exactly that, and nothing
1480
+ * under `models`). Some agents still accept `session/set_model` as a vendor
1481
+ * extension, and the `models.availableModels` shape is still honoured for
1482
+ * them, but it is not what the protocol defines any more.
1483
+ *
1484
+ * Two reasons to prefer the config option when the agent offers it. The READ
1485
+ * path already draws this distinction — `modelIdFromCapabilities` and
1486
+ * `modelsFromCapabilities` both fall back to the config option — and the two
1487
+ * disagreeing lets a host list models it cannot then select. And the config
1488
+ * option's REPLY carries the agent's full refreshed config set, where
1489
+ * `session/set_model` answers `{}` and refreshes only via an async
1490
+ * notification. On Kimi the model DETERMINES which thought levels exist —
1491
+ * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
1492
+ * `applyReasoning` picks from THIS session's menus on the following
1493
+ * `startTurn`, so the refresh this write leaves behind is what it reads. */
1443
1494
  async setModel(threadId, modelId) {
1495
+ const capabilities = this.sessionCapabilities(threadId);
1496
+ const advertisesModelsCapability = (capabilities?.models?.availableModels?.length ?? 0) > 0;
1497
+ const option = modelConfigOption(capabilities);
1498
+ if (!advertisesModelsCapability && option !== void 0) {
1499
+ await this.setRuntimeOption(threadId, "modelConfigOption", option.id, modelId);
1500
+ return;
1501
+ }
1444
1502
  await this.setRuntimeOption(threadId, "model", modelId, modelId);
1445
1503
  }
1446
1504
  async setConfigOption(threadId, optionId, value) {
@@ -1489,8 +1547,13 @@ var AcpAgentClient = class {
1489
1547
  },
1490
1548
  ACP_REQUEST_TIMEOUT_MS
1491
1549
  );
1492
- const runtimeCapabilities = this.captureRuntimeCapabilities("initialize", result);
1550
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1551
+ "initialize",
1552
+ result,
1553
+ this.initializeCapabilities
1554
+ );
1493
1555
  if (runtimeCapabilities) {
1556
+ this.initializeCapabilities = runtimeCapabilities;
1494
1557
  this.notifyRuntimeCapabilities({ runtimeCapabilities });
1495
1558
  }
1496
1559
  this.initialized = true;
@@ -1516,17 +1579,28 @@ var AcpAgentClient = class {
1516
1579
  this.logger.debug("acp session/update for unknown session", { protocolSessionId });
1517
1580
  return;
1518
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
+ }
1519
1592
  const runtimeState = acpSessionRuntimeStateFromUpdate(update, this.now());
1520
1593
  if (runtimeState) {
1521
1594
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, runtimeState);
1522
- if (this.runtimeCapabilities) {
1595
+ const capabilities = this.sessionCapabilities(session.threadId);
1596
+ if (capabilities) {
1523
1597
  this.notifyRuntimeCapabilities({
1524
1598
  threadId: session.threadId,
1525
- runtimeCapabilities: this.runtimeCapabilities,
1599
+ runtimeCapabilities: capabilities,
1526
1600
  runtimeState: session.runtimeState
1527
1601
  });
1528
1602
  }
1529
- this.emitThreadSettings(session, this.runtimeCapabilities, session.runtimeState);
1603
+ this.emitThreadSettings(session, capabilities, session.runtimeState);
1530
1604
  return;
1531
1605
  }
1532
1606
  const updateKind = readString2(update, "sessionUpdate");
@@ -1596,10 +1670,30 @@ var AcpAgentClient = class {
1596
1670
  optionId,
1597
1671
  value
1598
1672
  );
1599
- const runtimeCapabilities = this.captureRuntimeCapabilities("session-load", result);
1600
- const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : { currentModelId: value, updatedAt: this.now() };
1673
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1674
+ "session-load",
1675
+ result,
1676
+ this.sessionBase(session)
1677
+ );
1678
+ if (runtimeCapabilities) session.runtimeCapabilities = runtimeCapabilities;
1679
+ const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : source === "modelConfigOption" ? (
1680
+ // Record BOTH: it IS the config option's value, and it IS the
1681
+ // session's model. A consumer reading `currentModelId` must not
1682
+ // have to know which route the agent happened to expose.
1683
+ {
1684
+ currentModelId: value,
1685
+ configValues: { [optionId]: value },
1686
+ updatedAt: this.now()
1687
+ }
1688
+ ) : { currentModelId: value, updatedAt: this.now() };
1601
1689
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, requested);
1602
- 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);
1603
1697
  if (effectiveCapabilities) {
1604
1698
  this.notifyRuntimeCapabilities({
1605
1699
  threadId,
@@ -1610,7 +1704,7 @@ var AcpAgentClient = class {
1610
1704
  this.emitThreadSettings(session, effectiveCapabilities, session.runtimeState);
1611
1705
  }
1612
1706
  async setRuntimeOptionOnTransport(protocolSessionId, source, optionId, value) {
1613
- if (source === "configOption") {
1707
+ if (source === "configOption" || source === "modelConfigOption") {
1614
1708
  return await this.transport.request("session/set_config_option", {
1615
1709
  sessionId: protocolSessionId,
1616
1710
  configId: optionId,
@@ -1640,7 +1734,8 @@ var AcpAgentClient = class {
1640
1734
  * If nothing matches we leave it alone — the caller's `.catch` debug-logs. */
1641
1735
  async applyReasoning(threadId, reasoning) {
1642
1736
  const target = reasoning.toLowerCase();
1643
- const modes = this.runtimeCapabilities?.modes?.availableModes ?? [];
1737
+ const capabilities = this.sessionCapabilities(threadId);
1738
+ const modes = capabilities?.modes?.availableModes ?? [];
1644
1739
  const modeMatch = modes.find(
1645
1740
  (mode) => mode.id.toLowerCase() === target || typeof mode.label === "string" && mode.label.toLowerCase() === target
1646
1741
  );
@@ -1648,7 +1743,7 @@ var AcpAgentClient = class {
1648
1743
  await this.setMode(threadId, modeMatch.id);
1649
1744
  return;
1650
1745
  }
1651
- const thinking = this.runtimeCapabilities?.configOptions?.find(
1746
+ const thinking = capabilities?.configOptions?.find(
1652
1747
  (option) => option.category === "thought_level" || option.id === "thinking"
1653
1748
  );
1654
1749
  if (thinking !== void 0) {
@@ -1678,18 +1773,32 @@ var AcpAgentClient = class {
1678
1773
  data: data.toString("base64")
1679
1774
  };
1680
1775
  }
1681
- 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) {
1682
1781
  const runtimeCapabilities = normalizeAcpRuntimeCapabilities({
1683
1782
  value: result,
1684
1783
  now: this.now(),
1685
1784
  source,
1686
- ...this.runtimeCapabilities !== void 0 ? { initialize: this.runtimeCapabilities } : {}
1785
+ ...base !== void 0 ? { initialize: base } : {}
1687
1786
  });
1688
1787
  if (runtimeCapabilities) {
1689
1788
  this.runtimeCapabilities = runtimeCapabilities;
1690
1789
  }
1691
1790
  return runtimeCapabilities;
1692
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
+ }
1693
1802
  notifyRuntimeCapabilities(event) {
1694
1803
  this.runtimeCapabilitiesHandler?.({
1695
1804
  ...event.threadId !== void 0 ? { threadId: event.threadId } : {},
@@ -1767,6 +1876,7 @@ function thoughtLevelPolarity(value) {
1767
1876
  const haystack = `${value.value} ${value.label ?? ""}`.toLowerCase();
1768
1877
  if (/\b(off|none|disabled?|no)\b/.test(haystack)) return "off";
1769
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";
1770
1880
  return "other";
1771
1881
  }
1772
1882
  function reasoningValueForThoughtLevel(reasoning, values) {
@@ -1774,8 +1884,9 @@ function reasoningValueForThoughtLevel(reasoning, values) {
1774
1884
  const wantOff = LOW_EFFORT_TOKENS.has(token);
1775
1885
  const wantOn = HIGH_EFFORT_TOKENS.has(token);
1776
1886
  if (!wantOff && !wantOn) return void 0;
1777
- const desired = wantOff ? "off" : "on";
1778
- 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;
1779
1890
  }
1780
1891
  function buildApprovalRequest(args) {
1781
1892
  const toolCall = asRecord2(args.params.toolCall) ?? {};