@pwrdrvr/agent-acp 0.14.0 → 0.15.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
@@ -27,6 +27,10 @@ interface AcpAgentConnection {
27
27
  request(method: string, params: Record<string, unknown>): Promise<unknown>;
28
28
  notify(method: string, params: Record<string, unknown>): Promise<void>;
29
29
  }
30
+ /** Prefix on the rejection when a request outlived its budget. Lets a caller
31
+ * tell "the agent did not answer in time" apart from "the agent answered with
32
+ * an error" — the two want different recovery (retry / restart vs. surface). */
33
+ declare const ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX = "acp request timed out";
30
34
  /** Built connection + a teardown handle (kills the child / closes streams). */
31
35
  type AcpAgentConnectionHandle = {
32
36
  connection: AcpAgentConnection;
@@ -55,7 +59,8 @@ declare class AcpConnection implements AcpJsonRpcTransport {
55
59
  private ensureConnected;
56
60
  private connect;
57
61
  private defaultCreateConnection;
58
- request(method: string, params?: Record<string, unknown>, _timeoutMs?: number): Promise<unknown>;
62
+ request(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
63
+ private dispatch;
59
64
  notify(method: string, params?: Record<string, unknown>): Promise<void>;
60
65
  close(): Promise<void>;
61
66
  onNotification(listener: (method: string, params: Record<string, unknown>) => void): () => void;
@@ -66,7 +71,14 @@ type LocalAcpProbeResult = {
66
71
  stdout?: string | Buffer;
67
72
  stderr?: string | Buffer;
68
73
  };
69
- type LocalAcpAgentProbe = (command: string, args: string[]) => Promise<LocalAcpProbeResult>;
74
+ type LocalAcpProbeOptions = {
75
+ /** Budget for this probe spawn. An injected probe should honor it so a slow
76
+ * CLI is reported as "did not answer in time", not as "not installed". */
77
+ timeoutMs?: number | undefined;
78
+ /** Fires when discovery is abandoned mid-probe. */
79
+ signal?: AbortSignal | undefined;
80
+ };
81
+ type LocalAcpAgentProbe = (command: string, args: string[], options?: LocalAcpProbeOptions) => Promise<LocalAcpProbeResult>;
70
82
  /** How a strategy decides it is installed + ACP-capable on this machine. */
71
83
  type AcpDiscoveryProbe = {
72
84
  /** Args that print a version (probed first; failure = not installed). */
@@ -707,7 +719,10 @@ type RejectedAcpAgentInstance = {
707
719
  /** Parsed from successful `--version` output when available. */
708
720
  version?: string;
709
721
  source: AcpAgentInstanceSource;
710
- reason: "version-probe-failed" | "acp-probe-failed" | "acp-help-mismatch";
722
+ /** `probe-timed-out` means the CLI did not answer within the probe budget —
723
+ * UNLIKE the other reasons it is not a verdict on the CLI, and a host may
724
+ * re-probe on a larger `probeTimeoutMs` instead of reporting it unusable. */
725
+ reason: "version-probe-failed" | "acp-probe-failed" | "acp-help-mismatch" | "probe-timed-out";
711
726
  };
712
727
  /** Every installed instance of one agent found on this machine. */
713
728
  type DiscoveredAcpAgentGroup = {
@@ -755,7 +770,23 @@ type LocalAcpDiscoveryOptions = {
755
770
  /** Include candidates known to exist but rejected by a discovery probe.
756
771
  * Defaults false to preserve the legacy installed-only result. */
757
772
  includeRejectedCandidates?: boolean;
773
+ /** Budget for ONE probe spawn (the default probe only — an injected `probe`
774
+ * owns its own timing). Defaults to `DEFAULT_ACP_PROBE_TIMEOUT_MS`. */
775
+ probeTimeoutMs?: number;
776
+ /** Abandon discovery in flight (re-triggered discovery, app quitting).
777
+ * Already-probed groups are still returned. */
778
+ signal?: AbortSignal;
758
779
  };
780
+ /**
781
+ * Budget for one ACP discovery probe spawn, in ms.
782
+ *
783
+ * Unchanged from the value this module has always used. Kept lower than
784
+ * codex-discovery's version-probe budget on purpose: `discoverStrategyInstances`
785
+ * walks its candidates SERIALLY, so the worst case here is (candidates x
786
+ * budget) rather than a single budget. Hosts that scan machines with many
787
+ * installed copies can raise it knowingly via `probeTimeoutMs`.
788
+ */
789
+ declare const DEFAULT_ACP_PROBE_TIMEOUT_MS = 5000;
759
790
  /**
760
791
  * Discover every installed instance of each agent (all `PATH` matches +
761
792
  * fallbacks + override that pass the probe), grouped per strategy. Groups with
@@ -890,4 +921,4 @@ declare class AcpRegistryService {
890
921
  declare function evaluateAcpDistributionPolicy(distribution: AcpRegistryDistribution, allowlist: AcpAllowlistDecision): AcpDistributionPolicy;
891
922
  declare function normalizeRegistry(raw: unknown): AcpRegistryAgent[];
892
923
 
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 };
924
+ 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);
@@ -1436,6 +1464,8 @@ var AcpAgentClient = class {
1436
1464
  }
1437
1465
  async initialize() {
1438
1466
  if (this.initialized) return;
1467
+ this.unsubscribeNotification?.();
1468
+ this.unsubscribeRequest?.();
1439
1469
  this.unsubscribeNotification = this.transport.onNotification((method, params) => {
1440
1470
  this.handleNotification(method, params);
1441
1471
  });
@@ -2314,12 +2344,17 @@ import {
2314
2344
  prependCommandDirToPath as prependCommandDirToPath2
2315
2345
  } from "@pwrdrvr/agent-transport";
2316
2346
  var execFile = promisify(execFileCallback);
2347
+ var DEFAULT_ACP_PROBE_TIMEOUT_MS = 5e3;
2317
2348
  async function discoverLocalAcpAgentInstances(options = {}) {
2318
2349
  const strategies = options.strategies ?? BUILT_IN_ACP_STRATEGIES;
2319
2350
  const env = options.env ?? process.env;
2320
2351
  const probe = options.probe ?? makeDefaultProbe(env);
2321
2352
  const listExecutables = options.listExecutables ?? defaultListExecutables;
2322
2353
  const now = options.now ?? Date.now;
2354
+ const probeOptions = {
2355
+ timeoutMs: options.probeTimeoutMs ?? DEFAULT_ACP_PROBE_TIMEOUT_MS,
2356
+ signal: options.signal
2357
+ };
2323
2358
  const groups = await Promise.all(
2324
2359
  strategies.map(
2325
2360
  (strategy) => discoverStrategyInstances(
@@ -2329,7 +2364,8 @@ async function discoverLocalAcpAgentInstances(options = {}) {
2329
2364
  env,
2330
2365
  listExecutables,
2331
2366
  options.overrides?.[strategy.id],
2332
- options.includeRejectedCandidates === true
2367
+ options.includeRejectedCandidates === true,
2368
+ probeOptions
2333
2369
  )
2334
2370
  )
2335
2371
  );
@@ -2353,22 +2389,27 @@ async function discoverLocalAcpAgents(options = {}) {
2353
2389
  return [agent];
2354
2390
  });
2355
2391
  }
2356
- async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false) {
2392
+ async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false, probeOptions = {}) {
2357
2393
  const candidates = candidateCommands(strategy, env, listExecutables, override);
2358
2394
  const instances = [];
2359
2395
  const rejectedInstances = [];
2360
2396
  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)
2397
+ if (probeOptions.signal?.aborted) break;
2398
+ const [versionAttempt, helpAttempt] = await Promise.all([
2399
+ runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs, probeOptions),
2400
+ runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs, probeOptions)
2364
2401
  ]);
2402
+ if (probeOptions.signal?.aborted) break;
2403
+ const versionResult = versionAttempt.ok ? versionAttempt.result : void 0;
2404
+ const helpResult = helpAttempt.ok ? helpAttempt.result : void 0;
2365
2405
  const version = versionResult ? parseCliVersion(resultText(versionResult)) : void 0;
2366
2406
  if (!versionResult || !helpResult) {
2367
- if (includeRejectedCandidates && candidate.detected) {
2407
+ const timedOut = !versionAttempt.ok && versionAttempt.timedOut || !helpAttempt.ok && helpAttempt.timedOut;
2408
+ if (candidate.detected && (timedOut || includeRejectedCandidates)) {
2368
2409
  const rejected = {
2369
2410
  command: candidate.command,
2370
2411
  source: candidate.source,
2371
- reason: versionResult ? "acp-probe-failed" : "version-probe-failed"
2412
+ reason: timedOut ? "probe-timed-out" : versionResult ? "acp-probe-failed" : "version-probe-failed"
2372
2413
  };
2373
2414
  if (version !== void 0) rejected.version = version;
2374
2415
  rejectedInstances.push(rejected);
@@ -2531,25 +2572,78 @@ function ensureArgs(args, ensure) {
2531
2572
  }
2532
2573
  return result;
2533
2574
  }
2575
+ var AcpProbeTimeoutError = class extends Error {
2576
+ constructor(command, timeoutMs) {
2577
+ super(`acp probe did not answer within ${timeoutMs}ms: ${command}`);
2578
+ this.name = "AcpProbeTimeoutError";
2579
+ }
2580
+ };
2581
+ function isProbeTimeout(error) {
2582
+ if (error instanceof AcpProbeTimeoutError) return true;
2583
+ const name = error?.name;
2584
+ return error?.killed === true || name === "AbortError" || name === "TimeoutError";
2585
+ }
2586
+ function normalizeProbeTimeoutMs(timeoutMs) {
2587
+ if (timeoutMs === void 0 || !Number.isFinite(timeoutMs)) {
2588
+ return DEFAULT_ACP_PROBE_TIMEOUT_MS;
2589
+ }
2590
+ return Math.max(1, Math.floor(timeoutMs));
2591
+ }
2534
2592
  function makeDefaultProbe(env) {
2535
- return async (command, args) => {
2593
+ return async (command, args, options = {}) => {
2594
+ const timeoutMs = normalizeProbeTimeoutMs(options.timeoutMs);
2536
2595
  const launchEnv = prependCommandDirToPath2(command, env);
2537
2596
  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
2597
+ const controller = new AbortController();
2598
+ let timedOut = false;
2599
+ const onAbort = () => controller.abort();
2600
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2601
+ const timer = setTimeout(() => {
2602
+ timedOut = true;
2603
+ controller.abort();
2604
+ }, timeoutMs);
2605
+ const expired = new Promise((resolve) => {
2606
+ controller.signal.addEventListener("abort", () => resolve("expired"), {
2607
+ once: true
2608
+ });
2545
2609
  });
2610
+ try {
2611
+ const execution = execFile(invocation.command, invocation.args, {
2612
+ timeout: timeoutMs,
2613
+ signal: controller.signal,
2614
+ maxBuffer: 1024 * 1024,
2615
+ // A Windows `.cmd` shim runs through `cmd.exe`; without this every
2616
+ // discovery pass flashes console windows over the host app.
2617
+ windowsHide: true,
2618
+ // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2619
+ // installed `qwen`) finds its sibling `node` during the probe.
2620
+ env: launchEnv,
2621
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
2622
+ }).then(
2623
+ (result) => ({ ok: true, result }),
2624
+ (error) => ({ ok: false, error })
2625
+ );
2626
+ const settled = await Promise.race([execution, expired]);
2627
+ if (settled === "expired") {
2628
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2629
+ throw new Error(`acp probe aborted: ${command}`);
2630
+ }
2631
+ if (!settled.ok) {
2632
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2633
+ throw settled.error;
2634
+ }
2635
+ return settled.result;
2636
+ } finally {
2637
+ clearTimeout(timer);
2638
+ options.signal?.removeEventListener("abort", onAbort);
2639
+ }
2546
2640
  };
2547
2641
  }
2548
- async function runProbe(probe, command, args) {
2642
+ async function runProbe(probe, command, args, options) {
2549
2643
  try {
2550
- return await probe(command, args);
2551
- } catch {
2552
- return void 0;
2644
+ return { ok: true, result: await probe(command, args, options) };
2645
+ } catch (error) {
2646
+ return { ok: false, timedOut: isProbeTimeout(error) };
2553
2647
  }
2554
2648
  }
2555
2649
  function resultText(result) {
@@ -2894,6 +2988,7 @@ function stringRecord(value) {
2894
2988
  }
2895
2989
  export {
2896
2990
  ACP_REGISTRY_URL,
2991
+ ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX,
2897
2992
  AcpAgentAllowlist,
2898
2993
  AcpAgentClient,
2899
2994
  AcpAgentClientPool,
@@ -2904,6 +2999,7 @@ export {
2904
2999
  BANNED_ACP_REGISTRY_IDS,
2905
3000
  BUILT_IN_ACP_STRATEGIES,
2906
3001
  DEFAULT_ACP_AGENT_ALLOWLIST,
3002
+ DEFAULT_ACP_PROBE_TIMEOUT_MS,
2907
3003
  acpRuntimeSupportsHttpMcp,
2908
3004
  acpRuntimeSupportsSessionLoad,
2909
3005
  acpRuntimeSupportsSseMcp,