@pwrdrvr/agent-acp 0.14.0 → 0.16.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.
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,11 +31,14 @@ 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>;
29
37
  }
38
+ /** Prefix on the rejection when a request outlived its budget. Lets a caller
39
+ * tell "the agent did not answer in time" apart from "the agent answered with
40
+ * an error" — the two want different recovery (retry / restart vs. surface). */
41
+ declare const ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX = "acp request timed out";
30
42
  /** Built connection + a teardown handle (kills the child / closes streams). */
31
43
  type AcpAgentConnectionHandle = {
32
44
  connection: AcpAgentConnection;
@@ -55,7 +67,8 @@ declare class AcpConnection implements AcpJsonRpcTransport {
55
67
  private ensureConnected;
56
68
  private connect;
57
69
  private defaultCreateConnection;
58
- request(method: string, params?: Record<string, unknown>, _timeoutMs?: number): Promise<unknown>;
70
+ request(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
71
+ private dispatch;
59
72
  notify(method: string, params?: Record<string, unknown>): Promise<void>;
60
73
  close(): Promise<void>;
61
74
  onNotification(listener: (method: string, params: Record<string, unknown>) => void): () => void;
@@ -66,7 +79,14 @@ type LocalAcpProbeResult = {
66
79
  stdout?: string | Buffer;
67
80
  stderr?: string | Buffer;
68
81
  };
69
- type LocalAcpAgentProbe = (command: string, args: string[]) => Promise<LocalAcpProbeResult>;
82
+ type LocalAcpProbeOptions = {
83
+ /** Budget for this probe spawn. An injected probe should honor it so a slow
84
+ * CLI is reported as "did not answer in time", not as "not installed". */
85
+ timeoutMs?: number | undefined;
86
+ /** Fires when discovery is abandoned mid-probe. */
87
+ signal?: AbortSignal | undefined;
88
+ };
89
+ type LocalAcpAgentProbe = (command: string, args: string[], options?: LocalAcpProbeOptions) => Promise<LocalAcpProbeResult>;
70
90
  /** How a strategy decides it is installed + ACP-capable on this machine. */
71
91
  type AcpDiscoveryProbe = {
72
92
  /** Args that print a version (probed first; failure = not installed). */
@@ -289,7 +309,11 @@ type AcpPromptContentBlock = {
289
309
  mimeType: string;
290
310
  data: string;
291
311
  };
292
- 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";
293
317
  type AcpAgentClientOptions = {
294
318
  /** The transport (real stdio or a fake). */
295
319
  transport: AcpJsonRpcTransport;
@@ -377,8 +401,9 @@ declare class AcpAgentClient implements AgentBackend {
377
401
  * Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
378
402
  * and maps them onto an ACP `session/new`. ACP supports:
379
403
  * • `cwd` → `session/new.cwd`.
380
- * • `model` → applied via `session/set_model` after the session opens, when
381
- * the agent advertises model selection (best-effort; debug-logged if not).
404
+ * • `model` → applied after the session opens, by whichever route the agent
405
+ * advertises: the `model` config option, or legacy `session/set_model`
406
+ * (best-effort; debug-logged if not). See `setModel`.
382
407
  * • `instructions` is NOT injected here — ACP `session/new` has no base-
383
408
  * instructions slot, matching the adapter's existing behavior. A host that
384
409
  * wants system framing sends it as leading turn text.
@@ -468,6 +493,25 @@ declare class AcpAgentClient implements AgentBackend {
468
493
  * releasing here never blocks resume. */
469
494
  archiveThread(threadId: string): Promise<void>;
470
495
  setMode(threadId: string, modeId: string): Promise<void>;
496
+ /** Apply a model selection by whichever route the agent actually advertises.
497
+ *
498
+ * ACP 1.3 has NO `session/set_model` — it is absent from `AGENT_METHODS`.
499
+ * An agent that offers model choice does it through a config option with
500
+ * `category: "model"` (Kimi's `session/new` returns exactly that, and nothing
501
+ * under `models`). Some agents still accept `session/set_model` as a vendor
502
+ * extension, and the `models.availableModels` shape is still honoured for
503
+ * them, but it is not what the protocol defines any more.
504
+ *
505
+ * Two reasons to prefer the config option when the agent offers it. The READ
506
+ * path already draws this distinction — `modelIdFromCapabilities` and
507
+ * `modelsFromCapabilities` both fall back to the config option — and the two
508
+ * disagreeing lets a host list models it cannot then select. And the config
509
+ * option's REPLY carries the agent's full refreshed config set, where
510
+ * `session/set_model` answers `{}` and refreshes only via an async
511
+ * notification. On Kimi the model DETERMINES which thought levels exist —
512
+ * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
513
+ * `applyReasoning` picks from `runtimeCapabilities` on the following
514
+ * `startTurn`, so the refresh this write leaves behind is what it reads. */
471
515
  setModel(threadId: string, modelId: string): Promise<void>;
472
516
  setConfigOption(threadId: string, optionId: string, value: string): Promise<void>;
473
517
  close(): Promise<void>;
@@ -707,7 +751,10 @@ type RejectedAcpAgentInstance = {
707
751
  /** Parsed from successful `--version` output when available. */
708
752
  version?: string;
709
753
  source: AcpAgentInstanceSource;
710
- reason: "version-probe-failed" | "acp-probe-failed" | "acp-help-mismatch";
754
+ /** `probe-timed-out` means the CLI did not answer within the probe budget —
755
+ * UNLIKE the other reasons it is not a verdict on the CLI, and a host may
756
+ * re-probe on a larger `probeTimeoutMs` instead of reporting it unusable. */
757
+ reason: "version-probe-failed" | "acp-probe-failed" | "acp-help-mismatch" | "probe-timed-out";
711
758
  };
712
759
  /** Every installed instance of one agent found on this machine. */
713
760
  type DiscoveredAcpAgentGroup = {
@@ -755,7 +802,23 @@ type LocalAcpDiscoveryOptions = {
755
802
  /** Include candidates known to exist but rejected by a discovery probe.
756
803
  * Defaults false to preserve the legacy installed-only result. */
757
804
  includeRejectedCandidates?: boolean;
805
+ /** Budget for ONE probe spawn (the default probe only — an injected `probe`
806
+ * owns its own timing). Defaults to `DEFAULT_ACP_PROBE_TIMEOUT_MS`. */
807
+ probeTimeoutMs?: number;
808
+ /** Abandon discovery in flight (re-triggered discovery, app quitting).
809
+ * Already-probed groups are still returned. */
810
+ signal?: AbortSignal;
758
811
  };
812
+ /**
813
+ * Budget for one ACP discovery probe spawn, in ms.
814
+ *
815
+ * Unchanged from the value this module has always used. Kept lower than
816
+ * codex-discovery's version-probe budget on purpose: `discoverStrategyInstances`
817
+ * walks its candidates SERIALLY, so the worst case here is (candidates x
818
+ * budget) rather than a single budget. Hosts that scan machines with many
819
+ * installed copies can raise it knowingly via `probeTimeoutMs`.
820
+ */
821
+ declare const DEFAULT_ACP_PROBE_TIMEOUT_MS = 5000;
759
822
  /**
760
823
  * Discover every installed instance of each agent (all `PATH` matches +
761
824
  * fallbacks + override that pass the probe), grouped per strategy. Groups with
@@ -890,4 +953,4 @@ declare class AcpRegistryService {
890
953
  declare function evaluateAcpDistributionPolicy(distribution: AcpRegistryDistribution, allowlist: AcpAllowlistDecision): AcpDistributionPolicy;
891
954
  declare function normalizeRegistry(raw: unknown): AcpRegistryAgent[];
892
955
 
893
- export { ACP_REGISTRY_URL, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientFactory, type AcpAgentClientOptions, AcpAgentClientPool, type AcpAgentClientPoolOptions, type AcpAgentConnection, type AcpAgentConnectionHandle, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, AcpConnection, type AcpConnectionOptions, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpHttpMcpServerConfig, type AcpJsonRpcTransport, type AcpLoadThreadOptions, type AcpMcpKeyValue, type AcpMcpKeyValueCollection, type AcpMcpServerConfig, type AcpMcpServerWireConfig, type AcpNormalizeResult, type AcpNormalizerOptions, AcpOneShotClient, type AcpOneShotClientOptions, type AcpOneShotRequest, type AcpOneShotResponse, type AcpPackageDistribution, type AcpPathExecutableLister, type AcpPromptContentBlock, type AcpRegistryAgent, type AcpRegistryAgentWithPolicy, type AcpRegistryAuthDescriptor, type AcpRegistryAuthMethod, type AcpRegistryDistribution, type AcpRegistryFetch, AcpRegistryService, type AcpRegistryServiceOptions, type AcpRegistrySnapshot, type AcpRuntimeAgentCapabilities, type AcpRuntimeAgentInfo, type AcpRuntimeCapabilities, type AcpRuntimeCapabilitiesHandler, type AcpRuntimeCapabilitiesSource, type AcpRuntimeConfigOption, type AcpRuntimeConfigOptionValue, type AcpRuntimeMcpCapabilities, type AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpSseMcpServerConfig, type AcpStartThreadOptions, type AcpStartTurnOptions, type AcpStdioMcpServerConfig, type AcpTitleHandler, type AcpVerificationStatus, BANNED_ACP_REGISTRY_IDS, BUILT_IN_ACP_STRATEGIES, DEFAULT_ACP_AGENT_ALLOWLIST, type DiscoveredAcpAgent, type DiscoveredAcpAgentGroup, type DiscoveredAcpAgentInstance, type LocalAcpAgentProbe, type LocalAcpDiscoveryOptions, type LocalAcpProbeResult, type RejectedAcpAgentInstance, acpRuntimeSupportsHttpMcp, acpRuntimeSupportsSessionLoad, acpRuntimeSupportsSseMcp, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, modelConfigOption, modelIdFromCapabilities, modelsFromCapabilities, normalizeAcpMcpServerConfig, normalizeAcpMcpServerConfigs, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, reasoningValueForThoughtLevel, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
956
+ export { ACP_REGISTRY_URL, ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientFactory, type AcpAgentClientOptions, AcpAgentClientPool, type AcpAgentClientPoolOptions, type AcpAgentConnection, type AcpAgentConnectionHandle, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, AcpConnection, type AcpConnectionOptions, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpHttpMcpServerConfig, type AcpJsonRpcTransport, type AcpLoadThreadOptions, type AcpMcpKeyValue, type AcpMcpKeyValueCollection, type AcpMcpServerConfig, type AcpMcpServerWireConfig, type AcpNormalizeResult, type AcpNormalizerOptions, AcpOneShotClient, type AcpOneShotClientOptions, type AcpOneShotRequest, type AcpOneShotResponse, type AcpPackageDistribution, type AcpPathExecutableLister, type AcpPromptContentBlock, type AcpRegistryAgent, type AcpRegistryAgentWithPolicy, type AcpRegistryAuthDescriptor, type AcpRegistryAuthMethod, type AcpRegistryDistribution, type AcpRegistryFetch, AcpRegistryService, type AcpRegistryServiceOptions, type AcpRegistrySnapshot, type AcpRuntimeAgentCapabilities, type AcpRuntimeAgentInfo, type AcpRuntimeCapabilities, type AcpRuntimeCapabilitiesHandler, type AcpRuntimeCapabilitiesSource, type AcpRuntimeConfigOption, type AcpRuntimeConfigOptionValue, type AcpRuntimeMcpCapabilities, type AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpSseMcpServerConfig, type AcpStartThreadOptions, type AcpStartTurnOptions, type AcpStdioMcpServerConfig, type AcpTitleHandler, type AcpVerificationStatus, BANNED_ACP_REGISTRY_IDS, BUILT_IN_ACP_STRATEGIES, DEFAULT_ACP_AGENT_ALLOWLIST, DEFAULT_ACP_PROBE_TIMEOUT_MS, type DiscoveredAcpAgent, type DiscoveredAcpAgentGroup, type DiscoveredAcpAgentInstance, type LocalAcpAgentProbe, type LocalAcpDiscoveryOptions, type LocalAcpProbeOptions, type LocalAcpProbeResult, type RejectedAcpAgentInstance, acpRuntimeSupportsHttpMcp, acpRuntimeSupportsSessionLoad, acpRuntimeSupportsSseMcp, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, modelConfigOption, modelIdFromCapabilities, modelsFromCapabilities, normalizeAcpMcpServerConfig, normalizeAcpMcpServerConfigs, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, reasoningValueForThoughtLevel, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
package/dist/index.js CHANGED
@@ -10,6 +10,27 @@ import {
10
10
  createCommandInvocation,
11
11
  prependCommandDirToPath
12
12
  } from "@pwrdrvr/agent-transport";
13
+ var ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX = "acp request timed out";
14
+ function withAcpRequestTimeout(method, timeoutMs, run) {
15
+ if (timeoutMs === void 0 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
16
+ return run();
17
+ }
18
+ const budget = Math.max(1, Math.floor(timeoutMs));
19
+ const call = run();
20
+ void call.catch(() => void 0);
21
+ let timer;
22
+ const expiry = new Promise((_, reject) => {
23
+ timer = setTimeout(
24
+ () => reject(
25
+ new Error(`${ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX} after ${budget}ms: ${method}`)
26
+ ),
27
+ budget
28
+ );
29
+ });
30
+ return Promise.race([call, expiry]).finally(() => {
31
+ if (timer !== void 0) clearTimeout(timer);
32
+ });
33
+ }
13
34
  var AcpConnection = class {
14
35
  constructor(options) {
15
36
  this.options = options;
@@ -101,10 +122,17 @@ var AcpConnection = class {
101
122
  };
102
123
  };
103
124
  }
104
- async request(method, params = {}, _timeoutMs) {
125
+ async request(method, params = {}, timeoutMs) {
105
126
  await this.ensureConnected();
106
127
  const conn = this.connection;
107
128
  if (!conn) throw new Error("acp connection unavailable");
129
+ return await withAcpRequestTimeout(
130
+ method,
131
+ timeoutMs,
132
+ () => this.dispatch(conn, method, params)
133
+ );
134
+ }
135
+ async dispatch(conn, method, params) {
108
136
  switch (method) {
109
137
  case "initialize":
110
138
  return await conn.initialize(params);
@@ -116,8 +144,9 @@ var AcpConnection = class {
116
144
  return await conn.prompt(params);
117
145
  case "session/set_mode":
118
146
  return await conn.setSessionMode(params);
119
- case "session/set_model":
120
- 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.
121
150
  case "session/cancel":
122
151
  await conn.cancel(params);
123
152
  return {};
@@ -1033,8 +1062,9 @@ var AcpAgentClient = class {
1033
1062
  * Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
1034
1063
  * and maps them onto an ACP `session/new`. ACP supports:
1035
1064
  * • `cwd` → `session/new.cwd`.
1036
- * • `model` → applied via `session/set_model` after the session opens, when
1037
- * the agent advertises model selection (best-effort; debug-logged if not).
1065
+ * • `model` → applied after the session opens, by whichever route the agent
1066
+ * advertises: the `model` config option, or legacy `session/set_model`
1067
+ * (best-effort; debug-logged if not). See `setModel`.
1038
1068
  * • `instructions` is NOT injected here — ACP `session/new` has no base-
1039
1069
  * instructions slot, matching the adapter's existing behavior. A host that
1040
1070
  * wants system framing sends it as leading turn text.
@@ -1412,7 +1442,32 @@ var AcpAgentClient = class {
1412
1442
  async setMode(threadId, modeId) {
1413
1443
  await this.setRuntimeOption(threadId, "mode", modeId, modeId);
1414
1444
  }
1445
+ /** Apply a model selection by whichever route the agent actually advertises.
1446
+ *
1447
+ * ACP 1.3 has NO `session/set_model` — it is absent from `AGENT_METHODS`.
1448
+ * An agent that offers model choice does it through a config option with
1449
+ * `category: "model"` (Kimi's `session/new` returns exactly that, and nothing
1450
+ * under `models`). Some agents still accept `session/set_model` as a vendor
1451
+ * extension, and the `models.availableModels` shape is still honoured for
1452
+ * them, but it is not what the protocol defines any more.
1453
+ *
1454
+ * Two reasons to prefer the config option when the agent offers it. The READ
1455
+ * path already draws this distinction — `modelIdFromCapabilities` and
1456
+ * `modelsFromCapabilities` both fall back to the config option — and the two
1457
+ * disagreeing lets a host list models it cannot then select. And the config
1458
+ * option's REPLY carries the agent's full refreshed config set, where
1459
+ * `session/set_model` answers `{}` and refreshes only via an async
1460
+ * notification. On Kimi the model DETERMINES which thought levels exist —
1461
+ * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
1462
+ * `applyReasoning` picks from `runtimeCapabilities` on the following
1463
+ * `startTurn`, so the refresh this write leaves behind is what it reads. */
1415
1464
  async setModel(threadId, modelId) {
1465
+ const advertisesModelsCapability = (this.runtimeCapabilities?.models?.availableModels?.length ?? 0) > 0;
1466
+ const option = modelConfigOption(this.runtimeCapabilities);
1467
+ if (!advertisesModelsCapability && option !== void 0) {
1468
+ await this.setRuntimeOption(threadId, "modelConfigOption", option.id, modelId);
1469
+ return;
1470
+ }
1416
1471
  await this.setRuntimeOption(threadId, "model", modelId, modelId);
1417
1472
  }
1418
1473
  async setConfigOption(threadId, optionId, value) {
@@ -1436,6 +1491,8 @@ var AcpAgentClient = class {
1436
1491
  }
1437
1492
  async initialize() {
1438
1493
  if (this.initialized) return;
1494
+ this.unsubscribeNotification?.();
1495
+ this.unsubscribeRequest?.();
1439
1496
  this.unsubscribeNotification = this.transport.onNotification((method, params) => {
1440
1497
  this.handleNotification(method, params);
1441
1498
  });
@@ -1567,7 +1624,16 @@ var AcpAgentClient = class {
1567
1624
  value
1568
1625
  );
1569
1626
  const runtimeCapabilities = this.captureRuntimeCapabilities("session-load", result);
1570
- const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : { currentModelId: value, updatedAt: this.now() };
1627
+ const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : source === "modelConfigOption" ? (
1628
+ // Record BOTH: it IS the config option's value, and it IS the
1629
+ // session's model. A consumer reading `currentModelId` must not
1630
+ // have to know which route the agent happened to expose.
1631
+ {
1632
+ currentModelId: value,
1633
+ configValues: { [optionId]: value },
1634
+ updatedAt: this.now()
1635
+ }
1636
+ ) : { currentModelId: value, updatedAt: this.now() };
1571
1637
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, requested);
1572
1638
  const effectiveCapabilities = runtimeCapabilities ?? this.runtimeCapabilities;
1573
1639
  if (effectiveCapabilities) {
@@ -1580,7 +1646,7 @@ var AcpAgentClient = class {
1580
1646
  this.emitThreadSettings(session, effectiveCapabilities, session.runtimeState);
1581
1647
  }
1582
1648
  async setRuntimeOptionOnTransport(protocolSessionId, source, optionId, value) {
1583
- if (source === "configOption") {
1649
+ if (source === "configOption" || source === "modelConfigOption") {
1584
1650
  return await this.transport.request("session/set_config_option", {
1585
1651
  sessionId: protocolSessionId,
1586
1652
  configId: optionId,
@@ -2314,12 +2380,17 @@ import {
2314
2380
  prependCommandDirToPath as prependCommandDirToPath2
2315
2381
  } from "@pwrdrvr/agent-transport";
2316
2382
  var execFile = promisify(execFileCallback);
2383
+ var DEFAULT_ACP_PROBE_TIMEOUT_MS = 5e3;
2317
2384
  async function discoverLocalAcpAgentInstances(options = {}) {
2318
2385
  const strategies = options.strategies ?? BUILT_IN_ACP_STRATEGIES;
2319
2386
  const env = options.env ?? process.env;
2320
2387
  const probe = options.probe ?? makeDefaultProbe(env);
2321
2388
  const listExecutables = options.listExecutables ?? defaultListExecutables;
2322
2389
  const now = options.now ?? Date.now;
2390
+ const probeOptions = {
2391
+ timeoutMs: options.probeTimeoutMs ?? DEFAULT_ACP_PROBE_TIMEOUT_MS,
2392
+ signal: options.signal
2393
+ };
2323
2394
  const groups = await Promise.all(
2324
2395
  strategies.map(
2325
2396
  (strategy) => discoverStrategyInstances(
@@ -2329,7 +2400,8 @@ async function discoverLocalAcpAgentInstances(options = {}) {
2329
2400
  env,
2330
2401
  listExecutables,
2331
2402
  options.overrides?.[strategy.id],
2332
- options.includeRejectedCandidates === true
2403
+ options.includeRejectedCandidates === true,
2404
+ probeOptions
2333
2405
  )
2334
2406
  )
2335
2407
  );
@@ -2353,22 +2425,27 @@ async function discoverLocalAcpAgents(options = {}) {
2353
2425
  return [agent];
2354
2426
  });
2355
2427
  }
2356
- async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false) {
2428
+ async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false, probeOptions = {}) {
2357
2429
  const candidates = candidateCommands(strategy, env, listExecutables, override);
2358
2430
  const instances = [];
2359
2431
  const rejectedInstances = [];
2360
2432
  for (const candidate of candidates) {
2361
- const [versionResult, helpResult] = await Promise.all([
2362
- runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs),
2363
- runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs)
2433
+ if (probeOptions.signal?.aborted) break;
2434
+ const [versionAttempt, helpAttempt] = await Promise.all([
2435
+ runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs, probeOptions),
2436
+ runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs, probeOptions)
2364
2437
  ]);
2438
+ if (probeOptions.signal?.aborted) break;
2439
+ const versionResult = versionAttempt.ok ? versionAttempt.result : void 0;
2440
+ const helpResult = helpAttempt.ok ? helpAttempt.result : void 0;
2365
2441
  const version = versionResult ? parseCliVersion(resultText(versionResult)) : void 0;
2366
2442
  if (!versionResult || !helpResult) {
2367
- if (includeRejectedCandidates && candidate.detected) {
2443
+ const timedOut = !versionAttempt.ok && versionAttempt.timedOut || !helpAttempt.ok && helpAttempt.timedOut;
2444
+ if (candidate.detected && (timedOut || includeRejectedCandidates)) {
2368
2445
  const rejected = {
2369
2446
  command: candidate.command,
2370
2447
  source: candidate.source,
2371
- reason: versionResult ? "acp-probe-failed" : "version-probe-failed"
2448
+ reason: timedOut ? "probe-timed-out" : versionResult ? "acp-probe-failed" : "version-probe-failed"
2372
2449
  };
2373
2450
  if (version !== void 0) rejected.version = version;
2374
2451
  rejectedInstances.push(rejected);
@@ -2531,25 +2608,78 @@ function ensureArgs(args, ensure) {
2531
2608
  }
2532
2609
  return result;
2533
2610
  }
2611
+ var AcpProbeTimeoutError = class extends Error {
2612
+ constructor(command, timeoutMs) {
2613
+ super(`acp probe did not answer within ${timeoutMs}ms: ${command}`);
2614
+ this.name = "AcpProbeTimeoutError";
2615
+ }
2616
+ };
2617
+ function isProbeTimeout(error) {
2618
+ if (error instanceof AcpProbeTimeoutError) return true;
2619
+ const name = error?.name;
2620
+ return error?.killed === true || name === "AbortError" || name === "TimeoutError";
2621
+ }
2622
+ function normalizeProbeTimeoutMs(timeoutMs) {
2623
+ if (timeoutMs === void 0 || !Number.isFinite(timeoutMs)) {
2624
+ return DEFAULT_ACP_PROBE_TIMEOUT_MS;
2625
+ }
2626
+ return Math.max(1, Math.floor(timeoutMs));
2627
+ }
2534
2628
  function makeDefaultProbe(env) {
2535
- return async (command, args) => {
2629
+ return async (command, args, options = {}) => {
2630
+ const timeoutMs = normalizeProbeTimeoutMs(options.timeoutMs);
2536
2631
  const launchEnv = prependCommandDirToPath2(command, env);
2537
2632
  const invocation = createCommandInvocation2({ command, args, env: launchEnv });
2538
- return await execFile(invocation.command, invocation.args, {
2539
- timeout: 5e3,
2540
- maxBuffer: 1024 * 1024,
2541
- // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2542
- // installed `qwen`) finds its sibling `node` during the probe.
2543
- env: launchEnv,
2544
- windowsVerbatimArguments: invocation.windowsVerbatimArguments
2633
+ const controller = new AbortController();
2634
+ let timedOut = false;
2635
+ const onAbort = () => controller.abort();
2636
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2637
+ const timer = setTimeout(() => {
2638
+ timedOut = true;
2639
+ controller.abort();
2640
+ }, timeoutMs);
2641
+ const expired = new Promise((resolve) => {
2642
+ controller.signal.addEventListener("abort", () => resolve("expired"), {
2643
+ once: true
2644
+ });
2545
2645
  });
2646
+ try {
2647
+ const execution = execFile(invocation.command, invocation.args, {
2648
+ timeout: timeoutMs,
2649
+ signal: controller.signal,
2650
+ maxBuffer: 1024 * 1024,
2651
+ // A Windows `.cmd` shim runs through `cmd.exe`; without this every
2652
+ // discovery pass flashes console windows over the host app.
2653
+ windowsHide: true,
2654
+ // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2655
+ // installed `qwen`) finds its sibling `node` during the probe.
2656
+ env: launchEnv,
2657
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
2658
+ }).then(
2659
+ (result) => ({ ok: true, result }),
2660
+ (error) => ({ ok: false, error })
2661
+ );
2662
+ const settled = await Promise.race([execution, expired]);
2663
+ if (settled === "expired") {
2664
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2665
+ throw new Error(`acp probe aborted: ${command}`);
2666
+ }
2667
+ if (!settled.ok) {
2668
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2669
+ throw settled.error;
2670
+ }
2671
+ return settled.result;
2672
+ } finally {
2673
+ clearTimeout(timer);
2674
+ options.signal?.removeEventListener("abort", onAbort);
2675
+ }
2546
2676
  };
2547
2677
  }
2548
- async function runProbe(probe, command, args) {
2678
+ async function runProbe(probe, command, args, options) {
2549
2679
  try {
2550
- return await probe(command, args);
2551
- } catch {
2552
- return void 0;
2680
+ return { ok: true, result: await probe(command, args, options) };
2681
+ } catch (error) {
2682
+ return { ok: false, timedOut: isProbeTimeout(error) };
2553
2683
  }
2554
2684
  }
2555
2685
  function resultText(result) {
@@ -2894,6 +3024,7 @@ function stringRecord(value) {
2894
3024
  }
2895
3025
  export {
2896
3026
  ACP_REGISTRY_URL,
3027
+ ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX,
2897
3028
  AcpAgentAllowlist,
2898
3029
  AcpAgentClient,
2899
3030
  AcpAgentClientPool,
@@ -2904,6 +3035,7 @@ export {
2904
3035
  BANNED_ACP_REGISTRY_IDS,
2905
3036
  BUILT_IN_ACP_STRATEGIES,
2906
3037
  DEFAULT_ACP_AGENT_ALLOWLIST,
3038
+ DEFAULT_ACP_PROBE_TIMEOUT_MS,
2907
3039
  acpRuntimeSupportsHttpMcp,
2908
3040
  acpRuntimeSupportsSessionLoad,
2909
3041
  acpRuntimeSupportsSseMcp,