@pwrdrvr/agent-acp 0.5.0 → 0.6.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
@@ -315,6 +315,11 @@ declare class AcpAgentClient implements AgentBackend {
315
315
  /** Built ONLY when a re-establish actually happens (skipped for a live
316
316
  * session), so the host doesn't rebuild the system prompt every turn. */
317
317
  buildInstructions?: () => string;
318
+ /** Per-thread MCP servers for THIS session. Lets ONE shared client serve
319
+ * surfaces with different tool sets (library vs sizzle): each surface
320
+ * passes its own servers, so its threads spawn its tools — overriding the
321
+ * client-level default. */
322
+ mcpServers?: AcpMcpServerConfig[];
318
323
  }): Promise<void>;
319
324
  /**
320
325
  * Mint a thread id WITHOUT spawning the agent or opening a session. The
@@ -326,6 +331,11 @@ declare class AcpAgentClient implements AgentBackend {
326
331
  * turn). The host id is a random UUID since there's no session GUID yet.
327
332
  */
328
333
  createDeferredThread(_options?: AgentStartThreadOptions): Promise<AgentBackendStartThreadResult>;
334
+ /** Warm the agent: spawn the process + run the `initialize` handshake WITHOUT
335
+ * opening a session, so the first thread/turn doesn't pay the multi-second
336
+ * spawn. Idempotent. Used by `AcpAgentClientPool` to hold a configured agent
337
+ * ready from app startup. */
338
+ connect(): Promise<void>;
329
339
  /** Shared `session/new` + session registration. Mints a new thread id unless
330
340
  * `bindThreadId` is given (resume), in which case the new ACP session is
331
341
  * bound to that existing host thread id. */
@@ -356,6 +366,10 @@ declare class AcpAgentClient implements AgentBackend {
356
366
  private handleNotification;
357
367
  private applySessionUpdate;
358
368
  private handleAcpRequest;
369
+ /** Every MCP server name this client knows about — the client-level default
370
+ * plus the per-thread servers of every live session. Used to auto-approve a
371
+ * configured MCP tool regardless of which session's permission request it is. */
372
+ private knownMcpServerNames;
359
373
  private handlePermissionRequest;
360
374
  private setRuntimeOption;
361
375
  private setRuntimeOptionOnTransport;
@@ -375,6 +389,37 @@ declare class AcpAgentClient implements AgentBackend {
375
389
  supportsSessionLoad(): boolean;
376
390
  }
377
391
 
392
+ type AcpAgentClientFactory = () => AcpAgentClient;
393
+ type AcpAgentClientPoolOptions = {
394
+ /** Max time to wait for a client to warm (spawn + `initialize`) before an
395
+ * `acquire` rejects. Default 30s. */
396
+ warmTimeoutMs?: number;
397
+ logger?: Logger;
398
+ };
399
+ declare class AcpAgentClientPool {
400
+ private readonly entries;
401
+ private readonly warmTimeoutMs;
402
+ private readonly logger;
403
+ constructor(options?: AcpAgentClientPoolOptions);
404
+ /**
405
+ * Return the shared, warmed client for `key`, creating it via `factory` if it
406
+ * doesn't exist. Concurrent acquires for the same key share ONE spawn — they
407
+ * receive the same in-flight promise, which resolves when the agent is ready
408
+ * or rejects if warm-up fails / times out. A failed warm-up evicts the entry
409
+ * so a later acquire retries with a fresh client.
410
+ */
411
+ acquire(key: string, factory: AcpAgentClientFactory): Promise<AcpAgentClient>;
412
+ /** Fire-and-forget warm-up for app startup. Errors are logged, not thrown. */
413
+ warm(key: string, factory: AcpAgentClientFactory): void;
414
+ /** Whether a (warming or warm) entry exists for `key`. */
415
+ has(key: string): boolean;
416
+ /** Close + evict ONE key (e.g. its config changed). Safe if absent. */
417
+ release(key: string): Promise<void>;
418
+ /** Close + evict EVERY entry (app quit). */
419
+ closeAll(): Promise<void>;
420
+ private warmWithTimeout;
421
+ }
422
+
378
423
  type AcpOneShotClientOptions = {
379
424
  /** Connected (or lazily-connecting) ACP stdio transport for the agent. */
380
425
  transport: AcpJsonRpcTransport;
@@ -706,4 +751,4 @@ declare class AcpRegistryService {
706
751
  declare function evaluateAcpDistributionPolicy(distribution: AcpRegistryDistribution, allowlist: AcpAllowlistDecision): AcpDistributionPolicy;
707
752
  declare function normalizeRegistry(raw: unknown): AcpRegistryAgent[];
708
753
 
709
- export { ACP_REGISTRY_URL, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientOptions, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpJsonRpcTransport, type AcpMcpServerConfig, 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 AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpStartThreadOptions, type AcpStartTurnOptions, AcpStdioJsonRpcTransport, type AcpStdioJsonRpcTransportOptions, 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, acpRuntimeSupportsSessionLoad, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
754
+ export { ACP_REGISTRY_URL, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientFactory, type AcpAgentClientOptions, AcpAgentClientPool, type AcpAgentClientPoolOptions, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpJsonRpcTransport, type AcpMcpServerConfig, 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 AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpStartThreadOptions, type AcpStartTurnOptions, AcpStdioJsonRpcTransport, type AcpStdioJsonRpcTransportOptions, 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, acpRuntimeSupportsSessionLoad, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
package/dist/index.js CHANGED
@@ -912,7 +912,8 @@ var AcpAgentClient = class {
912
912
  const instructions = options.buildInstructions?.();
913
913
  await this.establishSession({
914
914
  bindThreadId: options.threadId,
915
- ...instructions !== void 0 ? { instructions } : {}
915
+ ...instructions !== void 0 ? { instructions } : {},
916
+ ...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
916
917
  });
917
918
  }
918
919
  /**
@@ -930,6 +931,13 @@ var AcpAgentClient = class {
930
931
  out.modelProvider = this.strategy.id;
931
932
  return out;
932
933
  }
934
+ /** Warm the agent: spawn the process + run the `initialize` handshake WITHOUT
935
+ * opening a session, so the first thread/turn doesn't pay the multi-second
936
+ * spawn. Idempotent. Used by `AcpAgentClientPool` to hold a configured agent
937
+ * ready from app startup. */
938
+ async connect() {
939
+ await this.initialize();
940
+ }
933
941
  /** Shared `session/new` + session registration. Mints a new thread id unless
934
942
  * `bindThreadId` is given (resume), in which case the new ACP session is
935
943
  * bound to that existing host thread id. */
@@ -967,7 +975,11 @@ var AcpAgentClient = class {
967
975
  turnId: void 0,
968
976
  runtimeState: void 0,
969
977
  pendingTurn: void 0,
970
- pendingInstructions: options.instructions
978
+ pendingInstructions: options.instructions,
979
+ // Remember the MCP servers attached to THIS session so its tools can be
980
+ // auto-approved even when the (pooled, shared) client has no client-level
981
+ // `mcpServers` default.
982
+ mcpServerNames: (options.mcpServers ?? this.defaultMcpServers).map((server) => server.name)
971
983
  };
972
984
  this.sessions.set(threadId, session);
973
985
  this.threadIdByProtocolId.set(protocolSessionId, threadId);
@@ -1209,14 +1221,21 @@ var AcpAgentClient = class {
1209
1221
  }
1210
1222
  throw new Error(`Unsupported ACP request: ${method}`);
1211
1223
  }
1224
+ /** Every MCP server name this client knows about — the client-level default
1225
+ * plus the per-thread servers of every live session. Used to auto-approve a
1226
+ * configured MCP tool regardless of which session's permission request it is. */
1227
+ knownMcpServerNames() {
1228
+ const names = new Set(this.defaultMcpServers.map((server) => server.name));
1229
+ for (const session of this.sessions.values()) {
1230
+ for (const name of session.mcpServerNames) names.add(name);
1231
+ }
1232
+ return [...names];
1233
+ }
1212
1234
  async handlePermissionRequest(params, id) {
1213
1235
  const protocolSessionId = readString2(params, "sessionId") ?? readString2(params, "session_id");
1214
1236
  const threadId = protocolSessionId ? this.threadIdByProtocolId.get(protocolSessionId) : void 0;
1215
1237
  const options = readPermissionOptions(params.options);
1216
- if (this.autoApproveConfiguredMcpTools && permissionTargetsConfiguredMcpServer(
1217
- params,
1218
- this.defaultMcpServers.map((server) => server.name)
1219
- )) {
1238
+ if (this.autoApproveConfiguredMcpTools && permissionTargetsConfiguredMcpServer(params, this.knownMcpServerNames())) {
1220
1239
  return autoApprovePermissionOutcome(options);
1221
1240
  }
1222
1241
  const handler = this.approvalHandler;
@@ -1497,9 +1516,90 @@ function readAcpPromptUsage(result) {
1497
1516
  return usage;
1498
1517
  }
1499
1518
 
1519
+ // src/acp-agent-client-pool.ts
1520
+ import { noopLogger as noopLogger3 } from "@pwrdrvr/agent-core";
1521
+ function errorMessage2(cause) {
1522
+ return cause instanceof Error ? cause.message : String(cause);
1523
+ }
1524
+ var AcpAgentClientPool = class {
1525
+ entries = /* @__PURE__ */ new Map();
1526
+ warmTimeoutMs;
1527
+ logger;
1528
+ constructor(options = {}) {
1529
+ this.warmTimeoutMs = options.warmTimeoutMs ?? 3e4;
1530
+ this.logger = options.logger ?? noopLogger3;
1531
+ }
1532
+ /**
1533
+ * Return the shared, warmed client for `key`, creating it via `factory` if it
1534
+ * doesn't exist. Concurrent acquires for the same key share ONE spawn — they
1535
+ * receive the same in-flight promise, which resolves when the agent is ready
1536
+ * or rejects if warm-up fails / times out. A failed warm-up evicts the entry
1537
+ * so a later acquire retries with a fresh client.
1538
+ */
1539
+ async acquire(key, factory) {
1540
+ const existing = this.entries.get(key);
1541
+ if (existing !== void 0) return existing.ready;
1542
+ const client = factory();
1543
+ const ready = this.warmWithTimeout(key, client);
1544
+ this.entries.set(key, { client, ready });
1545
+ return ready;
1546
+ }
1547
+ /** Fire-and-forget warm-up for app startup. Errors are logged, not thrown. */
1548
+ warm(key, factory) {
1549
+ void this.acquire(key, factory).catch((cause) => {
1550
+ this.logger.warn?.("acp pool: background warm-up failed", {
1551
+ key,
1552
+ message: errorMessage2(cause)
1553
+ });
1554
+ });
1555
+ }
1556
+ /** Whether a (warming or warm) entry exists for `key`. */
1557
+ has(key) {
1558
+ return this.entries.has(key);
1559
+ }
1560
+ /** Close + evict ONE key (e.g. its config changed). Safe if absent. */
1561
+ async release(key) {
1562
+ const entry = this.entries.get(key);
1563
+ if (entry === void 0) return;
1564
+ this.entries.delete(key);
1565
+ entry.ready.catch(() => void 0);
1566
+ await entry.client.close().catch(() => void 0);
1567
+ }
1568
+ /** Close + evict EVERY entry (app quit). */
1569
+ async closeAll() {
1570
+ const entries = [...this.entries.values()];
1571
+ this.entries.clear();
1572
+ for (const entry of entries) entry.ready.catch(() => void 0);
1573
+ await Promise.allSettled(entries.map((entry) => entry.client.close()));
1574
+ }
1575
+ async warmWithTimeout(key, client) {
1576
+ let timer;
1577
+ try {
1578
+ await Promise.race([
1579
+ client.connect(),
1580
+ new Promise((_, reject) => {
1581
+ timer = setTimeout(
1582
+ () => reject(
1583
+ new Error(`ACP agent "${key}" warm-up timed out after ${this.warmTimeoutMs}ms`)
1584
+ ),
1585
+ this.warmTimeoutMs
1586
+ );
1587
+ })
1588
+ ]);
1589
+ return client;
1590
+ } catch (cause) {
1591
+ if (this.entries.get(key)?.client === client) this.entries.delete(key);
1592
+ await client.close().catch(() => void 0);
1593
+ throw cause;
1594
+ } finally {
1595
+ if (timer !== void 0) clearTimeout(timer);
1596
+ }
1597
+ }
1598
+ };
1599
+
1500
1600
  // src/acp-oneshot-client.ts
1501
1601
  import {
1502
- noopLogger as noopLogger3
1602
+ noopLogger as noopLogger4
1503
1603
  } from "@pwrdrvr/agent-core";
1504
1604
  var AcpOneShotClient = class {
1505
1605
  client;
@@ -1510,7 +1610,7 @@ var AcpOneShotClient = class {
1510
1610
  queue = Promise.resolve();
1511
1611
  constructor(options) {
1512
1612
  this.strategy = options.strategy;
1513
- const logger = options.logger ?? noopLogger3;
1613
+ const logger = options.logger ?? noopLogger4;
1514
1614
  this.client = new AcpAgentClient({
1515
1615
  transport: options.transport,
1516
1616
  strategy: options.strategy,
@@ -2299,6 +2399,7 @@ export {
2299
2399
  ACP_REGISTRY_URL,
2300
2400
  AcpAgentAllowlist,
2301
2401
  AcpAgentClient,
2402
+ AcpAgentClientPool,
2302
2403
  AcpOneShotClient,
2303
2404
  AcpRegistryService,
2304
2405
  AcpSessionNormalizer,