@pwrdrvr/agent-acp 0.7.0 → 0.9.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
@@ -1,7 +1,7 @@
1
+ import { Client } from '@zed-industries/agent-client-protocol';
1
2
  import { Logger, AgentBackend, NormalizedThreadEvent, Unsubscribe, AgentBackendToolCallHandler, AgentBackendApprovalHandler, AgentStartThreadOptions, AgentBackendStartThreadResult, AgentStartTurnOptions, NormalizedTokenUsage, NormalizedToolCall } from '@pwrdrvr/agent-core';
2
- import { JsonRpcId, JsonRpcObserver, JsonRpcTransport } from '@pwrdrvr/agent-transport';
3
+ import { JsonRpcId } from '@pwrdrvr/agent-transport';
3
4
 
4
- /** The bidirectional transport shape the ACP client drives. */
5
5
  interface AcpJsonRpcTransport {
6
6
  request(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
7
7
  notify?(method: string, params?: Record<string, unknown>): Promise<void>;
@@ -9,23 +9,53 @@ interface AcpJsonRpcTransport {
9
9
  onNotification(listener: (method: string, params: Record<string, unknown>) => void): () => void;
10
10
  onRequest?(listener: (method: string, params: Record<string, unknown>, id?: JsonRpcId) => Promise<unknown> | unknown): () => void;
11
11
  }
12
- type AcpStdioJsonRpcTransportOptions = {
12
+
13
+ /** The subset of the library's client→agent surface that `AcpConnection`
14
+ * drives. Injectable (via `createConnection`) so the adapter can be unit-tested
15
+ * without spawning a real agent. The library's `ClientSideConnection` satisfies
16
+ * this structurally (its methods take typed schema params; we pass records that
17
+ * match at runtime). */
18
+ interface AcpAgentConnection {
19
+ initialize(params: Record<string, unknown>): Promise<unknown>;
20
+ newSession(params: Record<string, unknown>): Promise<unknown>;
21
+ loadSession(params: Record<string, unknown>): Promise<unknown>;
22
+ prompt(params: Record<string, unknown>): Promise<unknown>;
23
+ cancel(params: Record<string, unknown>): Promise<void>;
24
+ setSessionMode(params: Record<string, unknown>): Promise<unknown>;
25
+ setSessionModel(params: Record<string, unknown>): Promise<unknown>;
26
+ authenticate(params: Record<string, unknown>): Promise<unknown>;
27
+ extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
28
+ extNotification(method: string, params: Record<string, unknown>): Promise<void>;
29
+ }
30
+ /** Built connection + a teardown handle (kills the child / closes streams). */
31
+ type AcpAgentConnectionHandle = {
32
+ connection: AcpAgentConnection;
33
+ dispose: () => void;
34
+ };
35
+ type AcpConnectionOptions = {
13
36
  command: string;
14
37
  args: string[];
15
38
  env?: NodeJS.ProcessEnv;
16
- requestTimeoutMs?: number;
17
- observer?: JsonRpcObserver;
18
39
  logger?: Logger;
19
- /** Override the underlying JSON-RPC transport (tests inject a fake). */
20
- transport?: JsonRpcTransport;
40
+ /** Test seam: build the underlying connection. Defaults to spawning the agent
41
+ * and wiring its stdio through the library's `ClientSideConnection`. */
42
+ createConnection?: (client: Client) => AcpAgentConnectionHandle;
21
43
  };
22
- declare class AcpStdioJsonRpcTransport implements AcpJsonRpcTransport {
23
- private readonly connection;
44
+ declare class AcpConnection implements AcpJsonRpcTransport {
45
+ private readonly options;
46
+ private readonly logger;
47
+ private connection;
48
+ private dispose;
49
+ private connecting;
24
50
  private readonly notificationListeners;
25
51
  private requestHandler;
26
- constructor(options: AcpStdioJsonRpcTransportOptions);
27
- connect(): Promise<void>;
28
- request(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
52
+ constructor(options: AcpConnectionOptions);
53
+ /** The `Client` handler the library invokes for agent→client traffic. */
54
+ private buildClient;
55
+ private ensureConnected;
56
+ private connect;
57
+ private defaultCreateConnection;
58
+ request(method: string, params?: Record<string, unknown>, _timeoutMs?: number): Promise<unknown>;
29
59
  notify(method: string, params?: Record<string, unknown>): Promise<void>;
30
60
  close(): Promise<void>;
31
61
  onNotification(listener: (method: string, params: Record<string, unknown>) => void): () => void;
@@ -349,6 +379,20 @@ declare class AcpAgentClient implements AgentBackend {
349
379
  turnId: string;
350
380
  }>;
351
381
  interruptTurn(threadId: string): Promise<void>;
382
+ /** Release a thread the host has archived/closed.
383
+ *
384
+ * ACP has NO protocol-level session delete/archive — sessions are
385
+ * connection-scoped and the spec offers only `session/cancel` (interrupt).
386
+ * So unlike the Codex backend (which sends `thread/archive`), there's no
387
+ * remote call to make here. What we MUST do is drop the live session
388
+ * LOCALLY: a long-lived pooled client (one process shared across surfaces,
389
+ * warmed at startup) would otherwise accumulate a dead `AcpSessionState` for
390
+ * every closed chat for the life of the app. Best-effort cancels an
391
+ * in-flight turn first so the agent stops working on a thread the user
392
+ * closed. Idempotent — no-ops if the session is already gone. A later
393
+ * `reopenThread` re-establishes a fresh session under the same threadId, so
394
+ * releasing here never blocks resume. */
395
+ archiveThread(threadId: string): Promise<void>;
352
396
  setMode(threadId: string, modeId: string): Promise<void>;
353
397
  setModel(threadId: string, modelId: string): Promise<void>;
354
398
  setConfigOption(threadId: string, optionId: string, value: string): Promise<void>;
@@ -743,4 +787,4 @@ declare class AcpRegistryService {
743
787
  declare function evaluateAcpDistributionPolicy(distribution: AcpRegistryDistribution, allowlist: AcpAllowlistDecision): AcpDistributionPolicy;
744
788
  declare function normalizeRegistry(raw: unknown): AcpRegistryAgent[];
745
789
 
746
- 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 };
790
+ 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 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, 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
@@ -1,54 +1,143 @@
1
- // src/acp-stdio-transport.ts
2
- import { noopLogger } from "@pwrdrvr/agent-core";
1
+ // src/acp-connection.ts
2
+ import { spawn } from "child_process";
3
+ import { Readable, Writable } from "stream";
3
4
  import {
4
- JsonRpcConnection,
5
- StdioJsonRpcTransport
6
- } from "@pwrdrvr/agent-transport";
7
- var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
8
- var AcpStdioJsonRpcTransport = class {
5
+ ClientSideConnection,
6
+ ndJsonStream
7
+ } from "@zed-industries/agent-client-protocol";
8
+ import { noopLogger } from "@pwrdrvr/agent-core";
9
+ var AcpConnection = class {
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.logger = options.logger ?? noopLogger;
13
+ }
14
+ options;
15
+ logger;
9
16
  connection;
17
+ dispose;
18
+ connecting;
10
19
  notificationListeners = /* @__PURE__ */ new Set();
11
20
  requestHandler;
12
- constructor(options) {
13
- const logger = options.logger ?? noopLogger;
14
- const transport = options.transport ?? new StdioJsonRpcTransport({
15
- command: options.command,
16
- args: options.args,
17
- ...options.env !== void 0 ? { env: options.env } : {},
18
- logger
19
- });
20
- this.connection = new JsonRpcConnection(
21
- transport,
22
- options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
23
- options.observer,
24
- { logger, logContext: { owner: "acp-stdio-transport" } }
25
- );
26
- this.connection.setNotificationHandler((method, params) => {
27
- const normalized = asRecord(params) ?? {};
28
- for (const listener of this.notificationListeners) {
29
- listener(method, normalized);
30
- }
31
- });
32
- this.connection.setRequestHandler(async (method, params, id) => {
33
- if (!this.requestHandler) {
34
- throw new Error(`ACP request handler unavailable for ${method}`);
21
+ /** The `Client` handler the library invokes for agent→client traffic. */
22
+ buildClient() {
23
+ const emit = (method, params) => {
24
+ for (const listener of this.notificationListeners) listener(method, params);
25
+ };
26
+ return {
27
+ requestPermission: async (params) => {
28
+ if (!this.requestHandler) {
29
+ throw new Error("ACP request handler unavailable for session/request_permission");
30
+ }
31
+ return await this.requestHandler(
32
+ "session/request_permission",
33
+ params
34
+ );
35
+ },
36
+ sessionUpdate: async (params) => {
37
+ emit("session/update", params);
38
+ },
39
+ extNotification: async (method, params) => {
40
+ emit(method, params);
41
+ },
42
+ extMethod: async (method, params) => {
43
+ if (!this.requestHandler) {
44
+ throw new Error(`ACP request handler unavailable for ${method}`);
45
+ }
46
+ return await this.requestHandler(method, params) ?? {};
35
47
  }
36
- return await this.requestHandler(method, asRecord(params) ?? {}, id);
37
- });
48
+ // No fs/terminal handlers: we don't advertise those client capabilities at
49
+ // `initialize`, so a conformant agent won't call them.
50
+ };
51
+ }
52
+ async ensureConnected() {
53
+ if (this.connection) return;
54
+ this.connecting ??= this.connect();
55
+ await this.connecting;
38
56
  }
39
57
  async connect() {
40
- await this.connection.connect();
58
+ const create = this.options.createConnection ?? this.defaultCreateConnection();
59
+ const handle = create(this.buildClient());
60
+ this.connection = handle.connection;
61
+ this.dispose = handle.dispose;
62
+ }
63
+ defaultCreateConnection() {
64
+ const { command, args, env } = this.options;
65
+ const logger = this.logger;
66
+ return (client) => {
67
+ const child = spawn(command, args, {
68
+ stdio: ["pipe", "pipe", "pipe"],
69
+ ...env !== void 0 ? { env } : {}
70
+ });
71
+ child.on("error", (error) => {
72
+ logger.error("acp agent process error", { command, message: error.message });
73
+ });
74
+ child.stderr?.on("data", (chunk) => {
75
+ const line = chunk.toString("utf8").trimEnd();
76
+ if (line.length > 0) logger.debug("acp agent stderr", { line });
77
+ });
78
+ const toAgentStdin = Writable.toWeb(child.stdin);
79
+ const fromAgentStdout = Readable.toWeb(child.stdout);
80
+ const stream = ndJsonStream(toAgentStdin, fromAgentStdout);
81
+ const connection = new ClientSideConnection(
82
+ (_agent) => client,
83
+ stream
84
+ );
85
+ return {
86
+ connection,
87
+ dispose: () => {
88
+ try {
89
+ child.stdin?.end();
90
+ } catch {
91
+ }
92
+ child.kill();
93
+ }
94
+ };
95
+ };
41
96
  }
42
- async request(method, params, timeoutMs) {
43
- await this.connection.connect();
44
- return await this.connection.request(method, params, timeoutMs);
97
+ async request(method, params = {}, _timeoutMs) {
98
+ await this.ensureConnected();
99
+ const conn = this.connection;
100
+ if (!conn) throw new Error("acp connection unavailable");
101
+ switch (method) {
102
+ case "initialize":
103
+ return await conn.initialize(params);
104
+ case "session/new":
105
+ return await conn.newSession(params);
106
+ case "session/load":
107
+ return await conn.loadSession(params);
108
+ case "session/prompt":
109
+ return await conn.prompt(params);
110
+ case "session/set_mode":
111
+ return await conn.setSessionMode(params);
112
+ case "session/set_model":
113
+ return await conn.setSessionModel(params);
114
+ case "session/cancel":
115
+ await conn.cancel(params);
116
+ return {};
117
+ case "authenticate":
118
+ return await conn.authenticate(params);
119
+ default:
120
+ return await conn.extMethod(method, params);
121
+ }
45
122
  }
46
- async notify(method, params) {
47
- await this.connection.connect();
48
- await this.connection.notify(method, params);
123
+ async notify(method, params = {}) {
124
+ await this.ensureConnected();
125
+ const conn = this.connection;
126
+ if (!conn) throw new Error("acp connection unavailable");
127
+ if (method === "session/cancel") {
128
+ await conn.cancel(params);
129
+ return;
130
+ }
131
+ await conn.extNotification(method, params);
49
132
  }
50
133
  async close() {
51
- await this.connection.close();
134
+ try {
135
+ this.dispose?.();
136
+ } finally {
137
+ this.connection = void 0;
138
+ this.dispose = void 0;
139
+ this.connecting = void 0;
140
+ }
52
141
  }
53
142
  onNotification(listener) {
54
143
  this.notificationListeners.add(listener);
@@ -65,9 +154,6 @@ var AcpStdioJsonRpcTransport = class {
65
154
  };
66
155
  }
67
156
  };
68
- function asRecord(value) {
69
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
70
- }
71
157
 
72
158
  // src/acp-client.ts
73
159
  import {
@@ -85,7 +171,7 @@ import {
85
171
  } from "@pwrdrvr/agent-core";
86
172
 
87
173
  // src/normalizer/content.ts
88
- function asRecord2(value) {
174
+ function asRecord(value) {
89
175
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
90
176
  }
91
177
  function readString(record, key) {
@@ -122,7 +208,7 @@ function readAcpContentText(value) {
122
208
  const parts = value.map((item) => readAcpContentText(item)).filter((item) => Boolean(item));
123
209
  return parts.length > 0 ? parts.join("\n") : void 0;
124
210
  }
125
- const content = asRecord2(value);
211
+ const content = asRecord(value);
126
212
  if (!content) {
127
213
  return void 0;
128
214
  }
@@ -143,7 +229,7 @@ function readFirstLocationPath(record) {
143
229
  return void 0;
144
230
  }
145
231
  for (const location of locations) {
146
- const path5 = readString(asRecord2(location), "path");
232
+ const path5 = readString(asRecord(location), "path");
147
233
  if (path5 && path5.trim()) {
148
234
  return path5;
149
235
  }
@@ -244,7 +330,7 @@ function toolCallFromUpdate(update, kind, sessionId) {
244
330
  }
245
331
  function readToolArgs(update) {
246
332
  const raw = update.rawInput ?? update.raw_input ?? update.input ?? update.arguments ?? update.args;
247
- return asRecord2(raw) ?? (raw === void 0 ? void 0 : raw);
333
+ return asRecord(raw) ?? (raw === void 0 ? void 0 : raw);
248
334
  }
249
335
  function readToolContentText(value) {
250
336
  return readContentText({ content: value }, "content");
@@ -484,7 +570,7 @@ function readPlanSteps(record) {
484
570
  if (typeof step === "string") {
485
571
  return [{ step, status: "pending" }];
486
572
  }
487
- const stepRecord = asRecord2(step);
573
+ const stepRecord = asRecord(step);
488
574
  if (!stepRecord) {
489
575
  return [];
490
576
  }
@@ -507,7 +593,7 @@ function readPromptText(content) {
507
593
 
508
594
  // src/normalizer/runtime-capabilities.ts
509
595
  function normalizeAcpRuntimeCapabilities(params) {
510
- const record = asRecord2(params.value);
596
+ const record = asRecord(params.value);
511
597
  if (!record) {
512
598
  return params.initialize;
513
599
  }
@@ -589,7 +675,7 @@ function acpSessionRuntimeStateFromUpdate(update, now) {
589
675
  return currentModeId ? { currentModeId, updatedAt: now } : void 0;
590
676
  }
591
677
  if (kind === "config_option_update") {
592
- const configOption = asRecord2(update.configOption ?? update.config_option) ?? update;
678
+ const configOption = asRecord(update.configOption ?? update.config_option) ?? update;
593
679
  const id = readString(configOption, "id") ?? readString(configOption, "configOptionId") ?? readString(configOption, "configId");
594
680
  const value = readString(configOption, "currentValue") ?? readString(configOption, "value");
595
681
  return id && value ? { configValues: { [id]: value }, updatedAt: now } : void 0;
@@ -619,7 +705,7 @@ function readConfigOptions(value) {
619
705
  return [];
620
706
  }
621
707
  return value.flatMap((item) => {
622
- const record = asRecord2(item);
708
+ const record = asRecord(item);
623
709
  const id = readString(record, "id") ?? readString(record, "configOptionId") ?? readString(record, "configId");
624
710
  if (!record || !id) {
625
711
  return [];
@@ -648,7 +734,7 @@ function readConfigOptionValues(value) {
648
734
  return [];
649
735
  }
650
736
  return value.flatMap((item) => {
651
- const record = asRecord2(item);
737
+ const record = asRecord(item);
652
738
  const optionValue = readString(record, "value") ?? readString(record, "id") ?? readString(record, "optionId");
653
739
  if (!record || !optionValue) {
654
740
  return [];
@@ -662,7 +748,7 @@ function readConfigOptionValues(value) {
662
748
  });
663
749
  }
664
750
  function readModes(value) {
665
- const record = asRecord2(value);
751
+ const record = asRecord(value);
666
752
  const modes = Array.isArray(record?.availableModes) ? record.availableModes.flatMap(readMode) : [];
667
753
  if (modes.length === 0) {
668
754
  return void 0;
@@ -673,7 +759,7 @@ function readModes(value) {
673
759
  return result;
674
760
  }
675
761
  function readMode(value) {
676
- const record = asRecord2(value);
762
+ const record = asRecord(value);
677
763
  const id = readString(record, "id") ?? readString(record, "modeId");
678
764
  if (!record || !id) {
679
765
  return [];
@@ -687,7 +773,7 @@ function readMode(value) {
687
773
  return [mode];
688
774
  }
689
775
  function readModels(value) {
690
- const record = asRecord2(value);
776
+ const record = asRecord(value);
691
777
  const models = Array.isArray(record?.availableModels) ? record.availableModels.flatMap(readModel) : [];
692
778
  if (models.length === 0) {
693
779
  return void 0;
@@ -698,7 +784,7 @@ function readModels(value) {
698
784
  return result;
699
785
  }
700
786
  function readModel(value) {
701
- const record = asRecord2(value);
787
+ const record = asRecord(value);
702
788
  const id = readString(record, "modelId") ?? readString(record, "id");
703
789
  if (!record || !id) {
704
790
  return [];
@@ -711,16 +797,16 @@ function readModel(value) {
711
797
  return [model];
712
798
  }
713
799
  function readAgentCapabilities(value, sessionCapabilitiesValue) {
714
- const record = asRecord2(value);
715
- const sessionCapabilities = asRecord2(sessionCapabilitiesValue);
716
- const sessionMeta = asRecord2(sessionCapabilities?._meta);
717
- const kimiMeta = asRecord2(sessionMeta?.kimi);
800
+ const record = asRecord(value);
801
+ const sessionCapabilities = asRecord(sessionCapabilitiesValue);
802
+ const sessionMeta = asRecord(sessionCapabilities?._meta);
803
+ const kimiMeta = asRecord(sessionMeta?.kimi);
718
804
  if (!record && !sessionCapabilities) {
719
805
  return void 0;
720
806
  }
721
- const loadSession = readBoolean(record, "loadSession") ?? readBoolean(record, "load_session") ?? readBoolean(asRecord2(record?.session), "load");
722
- const close = readBoolean(asRecord2(record?.session), "close");
723
- const cancel = readBoolean(asRecord2(record?.session), "cancel");
807
+ const loadSession = readBoolean(record, "loadSession") ?? readBoolean(record, "load_session") ?? readBoolean(asRecord(record?.session), "load");
808
+ const close = readBoolean(asRecord(record?.session), "close");
809
+ const cancel = readBoolean(asRecord(record?.session), "cancel");
724
810
  const sessionHistoryReplay = readBoolean(kimiMeta, "sessionHistoryReplay") ?? readBoolean(kimiMeta, "session_history_replay");
725
811
  const capabilities = {
726
812
  raw: record ?? sessionCapabilities
@@ -738,7 +824,7 @@ function readAgentCapabilities(value, sessionCapabilitiesValue) {
738
824
  return capabilities;
739
825
  }
740
826
  function readAgentInfo(value) {
741
- const record = asRecord2(value);
827
+ const record = asRecord(value);
742
828
  if (!record) {
743
829
  return void 0;
744
830
  }
@@ -960,7 +1046,7 @@ var AcpAgentClient = class {
960
1046
  cwd,
961
1047
  mcpServers
962
1048
  });
963
- const record = asRecord3(result);
1049
+ const record = asRecord2(result);
964
1050
  const protocolSessionId = readString2(record, "sessionId") ?? readString2(record, "session_id");
965
1051
  if (!protocolSessionId) {
966
1052
  throw new Error("ACP session/new did not return a session id");
@@ -1106,6 +1192,40 @@ var AcpAgentClient = class {
1106
1192
  });
1107
1193
  }
1108
1194
  }
1195
+ /** Release a thread the host has archived/closed.
1196
+ *
1197
+ * ACP has NO protocol-level session delete/archive — sessions are
1198
+ * connection-scoped and the spec offers only `session/cancel` (interrupt).
1199
+ * So unlike the Codex backend (which sends `thread/archive`), there's no
1200
+ * remote call to make here. What we MUST do is drop the live session
1201
+ * LOCALLY: a long-lived pooled client (one process shared across surfaces,
1202
+ * warmed at startup) would otherwise accumulate a dead `AcpSessionState` for
1203
+ * every closed chat for the life of the app. Best-effort cancels an
1204
+ * in-flight turn first so the agent stops working on a thread the user
1205
+ * closed. Idempotent — no-ops if the session is already gone. A later
1206
+ * `reopenThread` re-establishes a fresh session under the same threadId, so
1207
+ * releasing here never blocks resume. */
1208
+ async archiveThread(threadId) {
1209
+ const session = this.sessions.get(threadId);
1210
+ if (session === void 0) return;
1211
+ if (session.turnId !== void 0) {
1212
+ try {
1213
+ if (this.transport.notify) {
1214
+ await this.transport.notify("session/cancel", {
1215
+ sessionId: session.protocolSessionId
1216
+ });
1217
+ } else {
1218
+ await this.transport.request("session/cancel", {
1219
+ sessionId: session.protocolSessionId
1220
+ });
1221
+ }
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ this.sessions.delete(threadId);
1226
+ this.threadIdByProtocolId.delete(session.protocolSessionId);
1227
+ this.logger.debug("acp thread archived (session released locally)", { threadId });
1228
+ }
1109
1229
  async setMode(threadId, modeId) {
1110
1230
  await this.setRuntimeOption(threadId, "mode", modeId, modeId);
1111
1231
  }
@@ -1167,13 +1287,13 @@ var AcpAgentClient = class {
1167
1287
  if (method !== "session/update" && !vendorMethods.includes(method)) {
1168
1288
  return;
1169
1289
  }
1170
- const record = asRecord3(params);
1290
+ const record = asRecord2(params);
1171
1291
  if (!record) return;
1172
1292
  this.applySessionUpdate(record);
1173
1293
  }
1174
1294
  applySessionUpdate(params) {
1175
1295
  const protocolSessionId = readString2(params, "sessionId") ?? readString2(params, "session_id");
1176
- const update = asRecord3(params.update);
1296
+ const update = asRecord2(params.update);
1177
1297
  if (!protocolSessionId || !update) {
1178
1298
  return;
1179
1299
  }
@@ -1366,7 +1486,7 @@ var AcpAgentClient = class {
1366
1486
  }
1367
1487
  };
1368
1488
  function buildApprovalRequest(args) {
1369
- const toolCall = asRecord3(args.params.toolCall) ?? {};
1489
+ const toolCall = asRecord2(args.params.toolCall) ?? {};
1370
1490
  const title = typeof toolCall.title === "string" && toolCall.title.trim() ? toolCall.title.trim() : "ACP tool call";
1371
1491
  const toolCallId = readString2(toolCall, "toolCallId") ?? readString2(toolCall, "tool_call_id");
1372
1492
  const requestId = args.id == null ? toolCallId ?? `acp:${args.now()}` : String(args.id);
@@ -1402,7 +1522,7 @@ function readPermissionOptions(value) {
1402
1522
  return [];
1403
1523
  }
1404
1524
  return value.flatMap((option) => {
1405
- const record = asRecord3(option);
1525
+ const record = asRecord2(option);
1406
1526
  const optionId = readString2(record, "optionId");
1407
1527
  if (!record || !optionId) {
1408
1528
  return [];
@@ -1477,12 +1597,12 @@ function readString2(record, key) {
1477
1597
  const value = record?.[key];
1478
1598
  return typeof value === "string" ? value : void 0;
1479
1599
  }
1480
- function asRecord3(value) {
1600
+ function asRecord2(value) {
1481
1601
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1482
1602
  }
1483
1603
  function readAcpPromptUsage(result) {
1484
- const meta = asRecord3(asRecord3(result)?._meta);
1485
- const tokenCount = asRecord3(asRecord3(meta?.quota)?.token_count);
1604
+ const meta = asRecord2(asRecord2(result)?._meta);
1605
+ const tokenCount = asRecord2(asRecord2(meta?.quota)?.token_count);
1486
1606
  if (!tokenCount) return void 0;
1487
1607
  const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1488
1608
  const input = num(tokenCount.input_tokens);
@@ -2250,7 +2370,7 @@ function evaluateAcpDistributionPolicy(distribution, allowlist) {
2250
2370
  };
2251
2371
  }
2252
2372
  function normalizeRegistry(raw) {
2253
- const record = asRecord4(raw);
2373
+ const record = asRecord3(raw);
2254
2374
  const rawAgents = Array.isArray(record?.agents) ? record.agents : [];
2255
2375
  return rawAgents.flatMap((item) => {
2256
2376
  const agent = normalizeAgent(item);
@@ -2258,7 +2378,7 @@ function normalizeRegistry(raw) {
2258
2378
  });
2259
2379
  }
2260
2380
  function normalizeAgent(raw) {
2261
- const record = asRecord4(raw);
2381
+ const record = asRecord3(raw);
2262
2382
  if (!record || typeof record.id !== "string" || typeof record.name !== "string") {
2263
2383
  return void 0;
2264
2384
  }
@@ -2291,7 +2411,7 @@ function normalizeAgent(raw) {
2291
2411
  return agent;
2292
2412
  }
2293
2413
  function normalizeDistributions(raw) {
2294
- const record = asRecord4(raw);
2414
+ const record = asRecord3(raw);
2295
2415
  if (!record) {
2296
2416
  return [];
2297
2417
  }
@@ -2300,7 +2420,7 @@ function normalizeDistributions(raw) {
2300
2420
  if (npx) distributions.push(npx);
2301
2421
  const uvx = normalizePackageDistribution("uvx", record.uvx);
2302
2422
  if (uvx) distributions.push(uvx);
2303
- const binaryRecord = asRecord4(record.binary);
2423
+ const binaryRecord = asRecord3(record.binary);
2304
2424
  if (binaryRecord) {
2305
2425
  for (const [platform, value] of Object.entries(binaryRecord)) {
2306
2426
  const binary = normalizeBinaryDistribution(platform, value);
@@ -2310,7 +2430,7 @@ function normalizeDistributions(raw) {
2310
2430
  return distributions;
2311
2431
  }
2312
2432
  function normalizePackageDistribution(kind, raw) {
2313
- const record = asRecord4(raw);
2433
+ const record = asRecord3(raw);
2314
2434
  if (!record || typeof record.package !== "string") {
2315
2435
  return void 0;
2316
2436
  }
@@ -2322,7 +2442,7 @@ function normalizePackageDistribution(kind, raw) {
2322
2442
  };
2323
2443
  }
2324
2444
  function normalizeBinaryDistribution(platform, raw) {
2325
- const record = asRecord4(raw);
2445
+ const record = asRecord3(raw);
2326
2446
  if (!record || typeof record.archive !== "string" || typeof record.cmd !== "string") {
2327
2447
  return void 0;
2328
2448
  }
@@ -2344,7 +2464,7 @@ function normalizeAuth(raw) {
2344
2464
  if (!raw) {
2345
2465
  return { required: false, methods: [] };
2346
2466
  }
2347
- const record = asRecord4(raw);
2467
+ const record = asRecord3(raw);
2348
2468
  if (!record) {
2349
2469
  return { required: true, methods: ["unknown"], raw };
2350
2470
  }
@@ -2357,7 +2477,7 @@ function normalizeAuth(raw) {
2357
2477
  raw
2358
2478
  };
2359
2479
  }
2360
- function asRecord4(value) {
2480
+ function asRecord3(value) {
2361
2481
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2362
2482
  }
2363
2483
  function stringValue(value) {
@@ -2367,7 +2487,7 @@ function stringArray(value) {
2367
2487
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2368
2488
  }
2369
2489
  function stringRecord(value) {
2370
- const record = asRecord4(value);
2490
+ const record = asRecord3(value);
2371
2491
  if (!record) {
2372
2492
  return {};
2373
2493
  }
@@ -2382,17 +2502,17 @@ export {
2382
2502
  AcpAgentAllowlist,
2383
2503
  AcpAgentClient,
2384
2504
  AcpAgentClientPool,
2505
+ AcpConnection,
2385
2506
  AcpOneShotClient,
2386
2507
  AcpRegistryService,
2387
2508
  AcpSessionNormalizer,
2388
- AcpStdioJsonRpcTransport,
2389
2509
  BANNED_ACP_REGISTRY_IDS,
2390
2510
  BUILT_IN_ACP_STRATEGIES,
2391
2511
  DEFAULT_ACP_AGENT_ALLOWLIST,
2392
2512
  acpRuntimeSupportsSessionLoad,
2393
2513
  acpSessionRuntimeStateFromCapabilities,
2394
2514
  acpSessionRuntimeStateFromUpdate,
2395
- asRecord2 as asRecord,
2515
+ asRecord,
2396
2516
  buildAcpBackendId,
2397
2517
  buildStrategyTable,
2398
2518
  defaultAcpAgentAllowlist,