@agen-ai/provider-acp 0.4.0 → 0.4.2

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.
Files changed (81) hide show
  1. package/README.md +515 -12
  2. package/dist/capabilities.d.ts +3 -1
  3. package/dist/capabilities.js +5 -4
  4. package/dist/configuration/catalog.js +0 -2
  5. package/dist/configuration/errors.js +0 -1
  6. package/dist/connection/contracts.d.ts +24 -1
  7. package/dist/connection/errors.js +0 -1
  8. package/dist/connection/initialize.d.ts +7 -3
  9. package/dist/connection/initialize.js +64 -19
  10. package/dist/connection/negotiation.d.ts +3 -1
  11. package/dist/connection/negotiation.js +22 -1
  12. package/dist/contracts.d.ts +3 -0
  13. package/dist/driver.d.ts +1 -1
  14. package/dist/driver.js +6 -39
  15. package/dist/driverFactory.d.ts +8 -0
  16. package/dist/driverFactory.js +48 -0
  17. package/dist/host/interactions.d.ts +6 -1
  18. package/dist/host/interactions.js +42 -17
  19. package/dist/host/leases.js +0 -1
  20. package/dist/host/router.d.ts +3 -2
  21. package/dist/host/router.js +23 -21
  22. package/dist/host/terminals.js +0 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/instance.d.ts +7 -2
  25. package/dist/instance.js +113 -28
  26. package/dist/node/contracts.d.ts +6 -0
  27. package/dist/node/execution/errors.d.ts +11 -0
  28. package/dist/node/execution/errors.js +17 -0
  29. package/dist/node/execution/owner.d.ts +34 -0
  30. package/dist/node/execution/owner.js +162 -0
  31. package/dist/node/execution/types.d.ts +20 -0
  32. package/dist/node/execution/types.js +0 -0
  33. package/dist/node/index.d.ts +3 -1
  34. package/dist/node/index.js +16 -6
  35. package/dist/node/launch.d.ts +5 -8
  36. package/dist/node/launch.js +13 -1
  37. package/dist/permissions/contracts.d.ts +22 -0
  38. package/dist/permissions/contracts.js +0 -0
  39. package/dist/permissions/mapping.d.ts +13 -0
  40. package/dist/permissions/mapping.js +99 -0
  41. package/dist/permissions/selection.d.ts +5 -0
  42. package/dist/permissions/selection.js +96 -0
  43. package/dist/session/continuation.d.ts +33 -0
  44. package/dist/session/continuation.js +185 -0
  45. package/dist/session/create.d.ts +7 -1
  46. package/dist/session/create.js +3 -1
  47. package/dist/session/descendants.d.ts +29 -0
  48. package/dist/session/descendants.js +97 -0
  49. package/dist/session/errors.js +0 -1
  50. package/dist/session/mapping.d.ts +6 -0
  51. package/dist/session/mapping.js +5 -26
  52. package/dist/session/native.d.ts +1 -0
  53. package/dist/session/native.js +8 -0
  54. package/dist/session/opening.d.ts +1 -0
  55. package/dist/session/opening.js +3 -1
  56. package/dist/session/queue.js +0 -1
  57. package/dist/session/requests/elicitation.d.ts +1 -1
  58. package/dist/session/requests/elicitation.js +1 -1
  59. package/dist/session/requests/forms.js +0 -2
  60. package/dist/session/requests/grokWithdrawals.d.ts +28 -0
  61. package/dist/session/requests/grokWithdrawals.js +135 -0
  62. package/dist/session/requests/pending.d.ts +17 -4
  63. package/dist/session/requests/pending.js +63 -8
  64. package/dist/session/requests/resolution.js +4 -2
  65. package/dist/session/resources.js +5 -3
  66. package/dist/session/session.d.ts +12 -1
  67. package/dist/session/session.js +197 -47
  68. package/dist/session/turn.d.ts +6 -1
  69. package/dist/sessionInteractions/broker.d.ts +50 -0
  70. package/dist/sessionInteractions/broker.js +193 -0
  71. package/dist/sessionInteractions/contracts.d.ts +3 -0
  72. package/dist/sessionInteractions/contracts.js +0 -0
  73. package/dist/sessionInteractions/extensions.d.ts +4 -0
  74. package/dist/sessionInteractions/extensions.js +37 -0
  75. package/dist/sessionInteractions/grokFolderTrust.d.ts +42 -0
  76. package/dist/sessionInteractions/grokFolderTrust.js +106 -0
  77. package/dist/sessionInteractions/grokPlanReview.d.ts +16 -0
  78. package/dist/sessionInteractions/grokPlanReview.js +63 -0
  79. package/dist/sessionInteractions/request.d.ts +46 -0
  80. package/dist/sessionInteractions/request.js +173 -0
  81. package/package.json +4 -4
@@ -11,12 +11,11 @@ class AcpHostRouter {
11
11
  this.interactions = new AcpHostInteractions(input);
12
12
  this.terminals = new AcpHostTerminals(input.host, input.limits);
13
13
  }
14
- input;
15
14
  interactions;
16
15
  terminals;
17
16
  requests = /* @__PURE__ */ new Map();
18
17
  binding;
19
- pending = 0;
18
+ pending = /* @__PURE__ */ new Map();
20
19
  bytes = 0;
21
20
  bindSession(binding) {
22
21
  if (this.binding && (binding.nativeSessionId !== this.binding.nativeSessionId || binding.sessionId !== this.binding.sessionId || binding.workingDirectory !== this.binding.workingDirectory)) {
@@ -55,32 +54,27 @@ class AcpHostRouter {
55
54
  };
56
55
  }
57
56
  admit(context) {
57
+ const params = hostObject(context.params);
58
+ if (context.method === "session/request_permission" || context.method === "elicitation/create" && Object.hasOwn(params, "sessionId")) return this.route(context, params);
58
59
  const bytes = Buffer.byteLength(JSON.stringify(context.params), "utf8");
59
- if (this.pending >= this.input.limits.maxPendingRequests || this.bytes + bytes > this.input.limits.maxQueuedBytes) {
60
+ if (this.pending.size >= this.input.limits.maxPendingRequests || this.bytes + bytes > this.input.limits.maxQueuedBytes) {
60
61
  throw new AcpRpcHandlerError(-32603, "The host request capacity was exceeded.");
61
62
  }
62
- this.pending += 1;
63
63
  this.bytes += bytes;
64
- let result;
65
- try {
66
- result = this.route(context);
67
- } catch (error) {
68
- this.pending -= 1;
64
+ const retirement = context.settled.then(() => this.interactions.joinRequest(context.settled)).then(() => {
65
+ this.pending.delete(context.settled);
69
66
  this.bytes -= bytes;
70
- throw error;
71
- }
67
+ });
68
+ this.pending.set(context.settled, retirement);
69
+ const result = this.route(context, params);
72
70
  return result.then((value) => {
73
71
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > Math.min(this.input.limits.maxQueuedBytes, 8 * 1024 * 1024) - 512) {
74
72
  throw invalidHostRequest();
75
73
  }
76
74
  return value;
77
- }).finally(() => {
78
- this.pending -= 1;
79
- this.bytes -= bytes;
80
75
  });
81
76
  }
82
- route(context) {
83
- const params = hostObject(context.params);
77
+ route(context, params) {
84
78
  if (context.method === "elicitation/create") {
85
79
  const session2 = Object.hasOwn(params, "sessionId");
86
80
  const request = Object.hasOwn(params, "requestId");
@@ -144,20 +138,28 @@ class AcpHostRouter {
144
138
  const id = hostText(hostObject(value).elicitationId, 256);
145
139
  return this.interactions.complete(id);
146
140
  }
147
- sessionElicitation(context) {
141
+ sessionElicitation(context, observeCallback) {
148
142
  const bound = this.requireSession(hostObject(context.params), context.signal);
149
143
  return this.interactions.request(context, {
150
144
  kind: "session",
151
145
  sessionId: bound.sessionId,
152
146
  workingDirectory: bound.workingDirectory
153
- }, context.signal);
147
+ }, context.signal, observeCallback);
154
148
  }
155
149
  async close() {
156
150
  for (const request of this.requests.values()) request.abort();
157
151
  this.requests.clear();
158
- const results = await Promise.allSettled([this.interactions.close(), this.terminals.close()]);
159
- const failures = results.filter((result) => result.status === "rejected");
160
- if (failures.length) throw new AggregateError(failures.map((result) => result.reason), "ACP host facility cleanup failed.");
152
+ const deadline = hostDeadline(new AbortController().signal, this.input.limits.hostOperationTimeoutMs);
153
+ const join = async () => {
154
+ while (this.pending.size > 0) await Promise.all(this.pending.values());
155
+ };
156
+ try {
157
+ const results = await Promise.allSettled([this.interactions.close(), this.terminals.close(), awaitHost(join(), deadline.signal)]);
158
+ const failures = results.filter((result) => result.status === "rejected");
159
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason), "ACP host facility cleanup failed.");
160
+ } finally {
161
+ deadline.close();
162
+ }
161
163
  }
162
164
  }
163
165
  export {
@@ -8,8 +8,6 @@ class AcpHostTerminals {
8
8
  this.limits = limits;
9
9
  this.leases = new AcpHostLeases(limits.hostOperationTimeoutMs);
10
10
  }
11
- host;
12
- limits;
13
11
  terminals = /* @__PURE__ */ new Map();
14
12
  leases;
15
13
  async request(method, params, session) {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { createAcpProviderDriver } from './driver.js';
2
2
  export { DEFAULT_ACP_PROVIDER_LIMITS, type AcpProviderLimits } from './limits.js';
3
3
  export { AcpProviderConnectionError } from './connection/errors.js';
4
4
  export { AcpProviderSessionError } from './session/errors.js';
5
+ export type { AcpPermissionOptionSignature, AcpPermissionMenuSignature, AcpPermissionMappingSelection, } from './permissions/contracts.js';
5
6
  export type { AcpConnectionStreams, AcpConnectionLease, AcpConnectionFactoryContext, AcpConnectionFactory, AcpSessionMcpBindingContext, AcpSessionMcpBindingLease, AcpSessionMcpBindingFactory, AcpSessionMcpBindingOptions, CreateAcpProviderDriverOptions, } from './contracts.js';
6
7
  export type { AcpAuthenticationMethods, AcpHostFacilities, AcpHostInteractionScope, AcpHostInteractionInput, AcpHostFormInput, AcpHostFormResponse, AcpHostUrlInput, AcpHostUrlDecision, AcpHostUrlInteractionLease, AcpHostSessionInput, AcpHostFileReadInput, AcpHostFileWriteInput, AcpHostTerminalCreateInput, AcpHostTerminalLease, AcpHostTerminalOutput, AcpHostTerminalExitStatus, AcpHostAuthenticationTerminalInput, AcpHostAuthenticationTerminalLease, } from './host/contracts.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,13 +1,16 @@
1
1
  import { type AgentInstanceId, type AgentProviderKey } from '@agen-ai/agent-protocol';
2
2
  import type { AcpStandardMcpServer, AcpUnknownExtensionPolicy } from '@agen-ai/acp-transport';
3
3
  import { type MaterializedAgentProviderInstance } from '@agen-ai/agent-runtime';
4
- import type { AcpConnectionFactory, AcpSessionMcpBindingOptions } from './contracts.js';
4
+ import type { AcpSessionMcpBindingOptions } from './contracts.js';
5
5
  import type { AcpAuthenticationMethods, AcpHostFacilities } from './host/contracts.js';
6
6
  import type { AcpProviderLimits } from './limits.js';
7
+ import type { AcpConnectionSource } from './connection/contracts.js';
8
+ import type { AcpSessionInteractionSelection } from './sessionInteractions/contracts.js';
9
+ import type { AcpPermissionMappingSelection } from './permissions/contracts.js';
7
10
  export declare function createAcpProviderInstance(input: {
8
11
  readonly providerKey: AgentProviderKey;
9
12
  readonly instanceId: AgentInstanceId;
10
- readonly createConnection: AcpConnectionFactory;
13
+ readonly connectionSource: AcpConnectionSource;
11
14
  readonly unknownExtensionPolicy: AcpUnknownExtensionPolicy;
12
15
  readonly limits: AcpProviderLimits;
13
16
  readonly usedStreams: WeakSet<object>;
@@ -15,6 +18,8 @@ export declare function createAcpProviderInstance(input: {
15
18
  readonly authentication: AcpAuthenticationMethods;
16
19
  readonly host: AcpHostFacilities;
17
20
  readonly mcpServers: readonly AcpStandardMcpServer[];
21
+ readonly permissionMapping: AcpPermissionMappingSelection;
18
22
  readonly sessionMcpBinding?: AcpSessionMcpBindingOptions;
23
+ readonly sessionInteractions?: AcpSessionInteractionSelection;
19
24
  }): Promise<MaterializedAgentProviderInstance>;
20
25
  //# sourceMappingURL=instance.d.ts.map
package/dist/instance.js CHANGED
@@ -6,7 +6,9 @@ import {
6
6
  createAgentProviderReadiness,
7
7
  createAgentProviderSessionCleanupError,
8
8
  isAgentProviderSessionCleanupErrorFor,
9
- throwIfAgentOperationAborted
9
+ throwIfAgentOperationAborted,
10
+ AgentProviderContractError,
11
+ DEFAULT_AGENT_SESSION_INTERACTION_LIMITS
10
12
  } from "@agen-ai/agent-runtime";
11
13
  import { createAcpAuthentication } from "./auth/controller.js";
12
14
  import { selectedAuthenticationMethods } from "./auth/methods.js";
@@ -17,18 +19,33 @@ import { negotiatedEnvelope } from "./connection/negotiation.js";
17
19
  import { assertAcpMcpSupport, supportsAcpSessionMcpBinding } from "./mcp.js";
18
20
  import { createAcpProviderSession } from "./session/create.js";
19
21
  import { AcpSessionResources } from "./session/resources.js";
22
+ import { awaitHost, hostDeadline } from "./host/lifecycle.js";
23
+ import { isNodeAcpExecutionCleanupError, NodeAcpExecutionCleanupError } from "./node/execution/errors.js";
20
24
  async function createAcpProviderInstance(input) {
21
25
  const lifetime = new AbortController();
22
- const connect = (signal) => initializeAcpConnection({
23
- createConnection: input.createConnection,
24
- unknownExtensionPolicy: input.unknownExtensionPolicy,
25
- context: { instanceId: input.instanceId, signal },
26
- limits: input.limits,
27
- usedStreams: input.usedStreams,
28
- configuration: input.configuration,
29
- authentication: input.authentication,
30
- host: input.host
31
- });
26
+ const executions = /* @__PURE__ */ new Set();
27
+ const connect = (signal) => {
28
+ throwIfAgentOperationAborted(signal);
29
+ const source = input.connectionSource.kind === "factory" ? input.connectionSource : { kind: "reserved", owner: input.connectionSource.reserve() };
30
+ if (source.kind === "reserved") {
31
+ executions.add(source.owner);
32
+ source.owner.whenDrained(() => {
33
+ executions.delete(source.owner);
34
+ });
35
+ }
36
+ return initializeAcpConnection({
37
+ source,
38
+ unknownExtensionPolicy: input.unknownExtensionPolicy,
39
+ context: { instanceId: input.instanceId, signal },
40
+ limits: input.limits,
41
+ usedStreams: input.usedStreams,
42
+ configuration: input.configuration,
43
+ authentication: input.authentication,
44
+ host: input.host,
45
+ permissionMapping: input.permissionMapping,
46
+ ...input.sessionInteractions ? { sessionInteractions: input.sessionInteractions } : {}
47
+ });
48
+ };
32
49
  let eager = await connect(lifetime.signal);
33
50
  const negotiated = eager.negotiated;
34
51
  let methods;
@@ -36,11 +53,13 @@ async function createAcpProviderInstance(input) {
36
53
  try {
37
54
  assertAcpMcpSupport(input.mcpServers, negotiated);
38
55
  methods = selectedAuthenticationMethods(negotiated, input.authentication, input.host);
39
- capabilities = acpProviderCapabilities(input.providerKey, negotiated, input.configuration, [...methods.keys()]);
56
+ capabilities = acpProviderCapabilities(input.providerKey, negotiated, input.configuration, [...methods.keys()], input.sessionInteractions, input.permissionMapping);
40
57
  } catch (cause) {
58
+ if (eager.execution?.cleanupFailure) throw eager.execution.retainedError(cause, eager.execution.cleanupFailure.error);
41
59
  try {
42
60
  await eager.close();
43
61
  } catch (cleanupError) {
62
+ if (eager.execution) throw eager.execution.retainedError(cause, cleanupError);
44
63
  throw new AggregateError([cause, cleanupError], "ACP materialization and cleanup failed.");
45
64
  }
46
65
  throw cause;
@@ -52,7 +71,8 @@ async function createAcpProviderInstance(input) {
52
71
  const creatingIds = /* @__PURE__ */ new Set();
53
72
  const pending = /* @__PURE__ */ new Set();
54
73
  let disposePromise;
55
- const cleanupPending = () => [...resources].some((resource) => resource.cleanupPending);
74
+ let disposeDeadline = 0;
75
+ const cleanupPending = () => [...resources].some((resource) => resource.cleanupPending) || [...executions].some((execution) => execution.cleanupPending);
56
76
  const bindingSupported = supportsAcpSessionMcpBinding(input.sessionMcpBinding, negotiated);
57
77
  const now = () => (/* @__PURE__ */ new Date()).toISOString();
58
78
  const retire = async (connection) => {
@@ -60,6 +80,10 @@ async function createAcpProviderInstance(input) {
60
80
  await connection.close();
61
81
  connections.delete(connection);
62
82
  };
83
+ const closeConnection = async (connection) => {
84
+ await connection.close();
85
+ if (input.sessionInteractions) connections.delete(connection);
86
+ };
63
87
  const acquire = async (signal) => {
64
88
  const reserved = eager;
65
89
  eager = void 0;
@@ -67,14 +91,22 @@ async function createAcpProviderInstance(input) {
67
91
  connections.add(connection);
68
92
  try {
69
93
  throwIfAgentOperationAborted(signal);
94
+ if (input.sessionInteractions && connection.stopped.aborted) throw connection.stopped.reason;
70
95
  if (negotiatedEnvelope(connection.negotiated) !== envelope) {
71
96
  throw new AcpProviderConnectionError("acp_provider_negotiation_changed", "The ACP peer changed its initialized behavior envelope.");
72
97
  }
73
98
  return connection;
74
99
  } catch (cause) {
100
+ const retained = (cleanupError) => new NodeAcpExecutionCleanupError({
101
+ cause,
102
+ cleanupError,
103
+ retryCleanup: () => retire(connection)
104
+ });
105
+ if (connection.execution?.cleanupFailure) throw retained(connection.execution.cleanupFailure.error);
75
106
  try {
76
107
  await retire(connection);
77
108
  } catch (cleanupError) {
109
+ if (connection.execution) throw retained(cleanupError);
78
110
  throw new AggregateError([cause, cleanupError], "ACP negotiation and cleanup failed.");
79
111
  }
80
112
  throw cause;
@@ -101,6 +133,16 @@ async function createAcpProviderInstance(input) {
101
133
  const open = async (context, resume) => {
102
134
  throwIfAgentOperationAborted(context.signal);
103
135
  throwIfAgentOperationAborted(lifetime.signal);
136
+ if (input.sessionInteractions?.mapping === "grok-folder-trust-v1" && resume) throw new AgentProviderContractError(
137
+ input.providerKey,
138
+ "session_interactions_unsupported",
139
+ "The selected ACP session interaction mapping supports creation only."
140
+ );
141
+ if (input.sessionInteractions && (input.sessionInteractions.mapping === "grok-folder-trust-v1" || resume) && context.interactions === void 0) throw new AgentProviderContractError(
142
+ input.providerKey,
143
+ "invalid_session_interaction",
144
+ "The selected ACP session interaction mapping requires a handler before opening."
145
+ );
104
146
  if (authentication.active) throw new AcpProviderConnectionError("acp_authentication_active", "Authentication owns the ACP instance.");
105
147
  if (authentication.cleanupPending) throw new AcpProviderConnectionError("acp_authentication_cleanup_pending", "Authentication terminal cleanup must complete before session mutation.");
106
148
  if (cleanupPending()) throw new AcpProviderConnectionError("acp_session_cleanup_pending", "Session resource cleanup must complete before session mutation.");
@@ -126,6 +168,14 @@ async function createAcpProviderInstance(input) {
126
168
  let opened;
127
169
  try {
128
170
  connection = await acquire(signal);
171
+ if (context.interactions) {
172
+ connection.sessionInteractions?.prepareOpening({
173
+ sessionId: parsed.sessionId,
174
+ workingDirectory: context.workingDirectory,
175
+ interactions: context.interactions,
176
+ signal
177
+ });
178
+ }
129
179
  const ownedConnection = connection;
130
180
  resource = new AcpSessionResources({
131
181
  instanceId: input.instanceId,
@@ -145,32 +195,39 @@ async function createAcpProviderInstance(input) {
145
195
  throwIfAgentOperationAborted(signal);
146
196
  const closeResources = () => resource.close();
147
197
  const sessionCapabilities = negotiated.agentCapabilities.sessionCapabilities;
148
- const session = await createAcpProviderSession({
198
+ opened = await createAcpProviderSession({
149
199
  context: { ...context, sessionId: parsed.sessionId, configuration: parsed.configuration, signal },
150
200
  openingInput: context,
151
201
  ...parsed.operation === "resume" ? { resume: {
152
202
  binding: parsed.binding,
153
- method: sessionCapabilities?.resume != null ? "session/resume" : "session/load"
203
+ method: input.sessionInteractions?.mapping === "grok-build-1.0.60-session-requests-v1" ? "session/load" : sessionCapabilities?.resume != null ? "session/resume" : "session/load"
154
204
  } } : {},
155
205
  mcpServers,
156
206
  connection,
157
207
  capabilities,
158
208
  limits: input.limits,
159
209
  now,
210
+ permissionMapping: input.permissionMapping,
160
211
  onClosed: () => {
161
212
  sessions.delete(parsed.sessionId);
162
213
  connections.delete(ownedConnection);
163
214
  },
164
215
  closeResources
165
216
  });
166
- opened = session;
167
217
  throwIfAgentOperationAborted(signal);
168
- sessions.set(parsed.sessionId, session);
169
- return session;
218
+ if (context.interactions) connection.sessionInteractions?.adopt();
219
+ sessions.set(parsed.sessionId, opened.session);
220
+ return opened.session;
170
221
  } catch (cause) {
171
222
  if (isAgentProviderSessionCleanupErrorFor(cause, context) && cause.providerKey === input.providerKey) throw cause;
223
+ if (isNodeAcpExecutionCleanupError(cause)) throw createAgentProviderSessionCleanupError({
224
+ providerKey: input.providerKey,
225
+ openingInput: context,
226
+ errors: [cause],
227
+ retryCleanup: cause.retryCleanup
228
+ });
172
229
  if (resource || connection) {
173
- const cleanup = () => opened ? opened.close({ reason: "contract_rejected" }) : resource ? resource.close() : retire(connection);
230
+ const cleanup = () => opened ? opened.cleanup() : resource ? resource.close() : retire(connection);
174
231
  try {
175
232
  await cleanup();
176
233
  } catch (cleanupError) {
@@ -193,16 +250,34 @@ async function createAcpProviderInstance(input) {
193
250
  creatingIds.delete(parsed.sessionId);
194
251
  }
195
252
  };
253
+ const adapter = Object.freeze({
254
+ ...input.sessionInteractions ? { sessionInteractions: Object.freeze({
255
+ kind: "supported",
256
+ mappings: input.sessionInteractions.mapping === "grok-folder-trust-v1" ? Object.freeze([Object.freeze({ openingOperation: "create", requestKind: "decision" })]) : Object.freeze([Object.freeze({ openingOperation: "resume", requestKind: "plan_review" })]),
257
+ producerOwnership: "exclusive",
258
+ humanWait: Object.freeze(["bounded", "until_withdrawn_or_closed"])
259
+ }) } : {},
260
+ ...input.sessionInteractions?.mapping === "grok-build-1.0.60-session-requests-v1" ? {
261
+ nativeContinuations: Object.freeze({
262
+ kind: "supported",
263
+ producerOwnership: "exclusive",
264
+ mappings: Object.freeze([Object.freeze({
265
+ openingOperation: "resume",
266
+ requestKind: "plan_review",
267
+ timing: "after_open_return"
268
+ })])
269
+ })
270
+ } : {},
271
+ hostBinding: bindingSupported ? Object.freeze({ kind: "supported", toolTransport: "mcp", instructionPlacements: Object.freeze(["tool_context"]) }) : Object.freeze({ kind: "unsupported" }),
272
+ createSession: (context) => open(context, false),
273
+ resumption: capabilities.sessions.resume ? Object.freeze({ kind: "supported", resumeSession: (context) => open(context, true) }) : Object.freeze({ kind: "unsupported" }),
274
+ branching: { kind: "unsupported" },
275
+ authentication: authentication.port
276
+ });
196
277
  return Object.freeze({
197
278
  instanceId: input.instanceId,
198
279
  capabilities,
199
- adapter: Object.freeze({
200
- hostBinding: bindingSupported ? Object.freeze({ kind: "supported", toolTransport: "mcp", instructionPlacements: Object.freeze(["tool_context"]) }) : Object.freeze({ kind: "unsupported" }),
201
- createSession: (context) => open(context, false),
202
- resumption: capabilities.sessions.resume ? Object.freeze({ kind: "supported", resumeSession: (context) => open(context, true) }) : Object.freeze({ kind: "unsupported" }),
203
- branching: { kind: "unsupported" },
204
- authentication: authentication.port
205
- }),
280
+ adapter,
206
281
  accountQuota: { kind: "unsupported" },
207
282
  environment: { kind: "unsupported" },
208
283
  checkReadiness(check = {}) {
@@ -217,18 +292,24 @@ async function createAcpProviderInstance(input) {
217
292
  },
218
293
  dispose() {
219
294
  if (!disposePromise) {
295
+ disposeDeadline = Date.now() + Math.min(input.limits.hostOperationTimeoutMs, DEFAULT_AGENT_SESSION_INTERACTION_LIMITS.cleanupTimeoutMs);
220
296
  disposePromise = Promise.resolve().then(async () => {
221
297
  const failures = /* @__PURE__ */ new Set();
222
298
  const closingConnections = new Set(connections);
299
+ if (input.sessionInteractions) for (const connection of closingConnections) {
300
+ const reason = connection.stopped.reason;
301
+ if (connection.stopped.aborted && reason instanceof Error && !(reason instanceof AcpProviderConnectionError && reason.reasonCode === "acp_provider_connection_closed")) failures.add(reason);
302
+ }
223
303
  const closing = await Promise.allSettled([
224
304
  authentication.close(),
225
305
  ...[...sessions.values()].map((session) => session.close({ reason: "shutdown" })),
226
306
  ...[...resources].map((resource) => resource.close()),
227
- ...[...closingConnections].map((connection) => connection.close())
307
+ ...[...closingConnections].map(closeConnection),
308
+ ...[...executions].map((execution) => execution.cleanup())
228
309
  ]);
229
310
  for (const result of closing) if (result.status === "rejected") failures.add(result.reason);
230
311
  await Promise.allSettled([...pending]);
231
- const remaining = await Promise.allSettled([...connections].filter((connection) => !closingConnections.has(connection)).map((connection) => connection.close()));
312
+ const remaining = await Promise.allSettled([...connections].filter((connection) => !closingConnections.has(connection)).map(closeConnection));
232
313
  for (const result of remaining) if (result.status === "rejected") failures.add(result.reason);
233
314
  if (failures.size) throw new AggregateError(failures, "ACP instance cleanup failed.");
234
315
  connections.clear();
@@ -239,6 +320,10 @@ async function createAcpProviderInstance(input) {
239
320
  });
240
321
  lifetime.abort(new AcpProviderConnectionError("acp_provider_instance_disposed", "The ACP instance was disposed."));
241
322
  }
323
+ if (input.sessionInteractions || input.connectionSource.kind === "reserved") {
324
+ const deadline = hostDeadline(new AbortController().signal, Math.max(0, disposeDeadline - Date.now()));
325
+ return awaitHost(disposePromise, deadline.signal).finally(() => deadline.close());
326
+ }
242
327
  return disposePromise;
243
328
  }
244
329
  });
@@ -1,5 +1,6 @@
1
1
  import type { AcpProviderLimits } from '../limits.js';
2
2
  import type { CreateAcpProviderDriverOptions } from '../contracts.js';
3
+ import type { NodeAcpExecutionBoundary } from './execution/types.js';
3
4
  export interface NodeAcpLaunchOptions {
4
5
  readonly executable: string;
5
6
  readonly args?: readonly string[];
@@ -10,6 +11,11 @@ export interface NodeAcpLaunchOptions {
10
11
  }
11
12
  export interface CreateNodeAcpProviderDriverOptions extends Omit<CreateAcpProviderDriverOptions, 'createConnection' | 'limits'> {
12
13
  readonly launch: NodeAcpLaunchOptions;
14
+ readonly executionBoundary?: NodeAcpExecutionBoundary;
15
+ /** Requires a direct native executable, exact no-leader arguments and a handler for each selected opening tuple. */
16
+ readonly sessionInteractions?: Readonly<{
17
+ readonly mapping: 'grok-folder-trust-v1' | 'grok-build-1.0.60-session-requests-v1';
18
+ }>;
13
19
  readonly limits?: Partial<AcpProviderLimits>;
14
20
  }
15
21
  //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1,11 @@
1
+ export declare class NodeAcpExecutionCleanupError extends Error {
2
+ readonly cleanupError: unknown;
3
+ readonly retryCleanup: () => Promise<void>;
4
+ constructor(input: Readonly<{
5
+ cause: unknown;
6
+ cleanupError: unknown;
7
+ retryCleanup: () => Promise<void>;
8
+ }>);
9
+ }
10
+ export declare function isNodeAcpExecutionCleanupError(error: unknown): error is NodeAcpExecutionCleanupError;
11
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,17 @@
1
+ class NodeAcpExecutionCleanupError extends Error {
2
+ cleanupError;
3
+ retryCleanup;
4
+ constructor(input) {
5
+ super("The owned Node ACP execution cleanup remains incomplete.", { cause: input.cause });
6
+ this.name = "NodeAcpExecutionCleanupError";
7
+ this.cleanupError = input.cleanupError;
8
+ this.retryCleanup = input.retryCleanup;
9
+ }
10
+ }
11
+ function isNodeAcpExecutionCleanupError(error) {
12
+ return error instanceof NodeAcpExecutionCleanupError;
13
+ }
14
+ export {
15
+ NodeAcpExecutionCleanupError,
16
+ isNodeAcpExecutionCleanupError
17
+ };
@@ -0,0 +1,34 @@
1
+ import type { AcpConnectionFactoryContext, AcpConnectionLease } from '../../contracts.js';
2
+ import { NodeAcpExecutionCleanupError } from './errors.js';
3
+ import type { NodeAcpExecutionBoundary, NodeAcpExecutionInput, NodeAcpExecutionReservation } from './types.js';
4
+ export declare class NodeAcpExecutionOwner {
5
+ private readonly cleanupTimeoutMs;
6
+ private state;
7
+ private startup;
8
+ private releaseState;
9
+ private readonly releaseComplete;
10
+ private failureNotice;
11
+ private cleanupAttempt;
12
+ private observation;
13
+ private cleanupWork;
14
+ private onDrained;
15
+ private failure;
16
+ private readonly startReservation;
17
+ private readonly releaseReservation;
18
+ constructor(reservation: NodeAcpExecutionReservation, cleanupTimeoutMs: number);
19
+ get cleanupPending(): boolean;
20
+ get cleanupFailure(): Readonly<{
21
+ error: unknown;
22
+ }> | undefined;
23
+ retainCleanup(work: () => Promise<void>): void;
24
+ whenDrained(callback: () => void): void;
25
+ start(context: AcpConnectionFactoryContext): Promise<AcpConnectionLease>;
26
+ /** Actual release is unbounded here; one outer observation owns the full connection deadline. */
27
+ release(): Promise<void>;
28
+ private beginRelease;
29
+ cleanup(): Promise<void>;
30
+ retainedError(cause: unknown, cleanupError: unknown): NodeAcpExecutionCleanupError;
31
+ joinCleanup(): Promise<void>;
32
+ }
33
+ export declare function executionReservationSource(boundary: NodeAcpExecutionBoundary, input: NodeAcpExecutionInput): () => NodeAcpExecutionOwner;
34
+ //# sourceMappingURL=owner.d.ts.map
@@ -0,0 +1,162 @@
1
+ import { AcpProviderConnectionError } from "../../connection/errors.js";
2
+ import { NodeAcpExecutionCleanupError } from "./errors.js";
3
+ class NodeAcpExecutionOwner {
4
+ constructor(reservation, cleanupTimeoutMs) {
5
+ this.cleanupTimeoutMs = cleanupTimeoutMs;
6
+ const release = reservation.release;
7
+ const start = reservation.start;
8
+ if (typeof release !== "function" || typeof start !== "function") {
9
+ throw new TypeError("An execution reservation must provide callable start and release methods.");
10
+ }
11
+ this.startReservation = (context) => Reflect.apply(start, reservation, [context]);
12
+ this.releaseReservation = () => Reflect.apply(release, reservation, []);
13
+ }
14
+ state = "reserved";
15
+ startup;
16
+ releaseState = { kind: "idle" };
17
+ releaseComplete = Promise.withResolvers();
18
+ failureNotice = Promise.withResolvers();
19
+ cleanupAttempt;
20
+ observation;
21
+ cleanupWork = () => this.release();
22
+ onDrained = () => {
23
+ };
24
+ failure;
25
+ startReservation;
26
+ releaseReservation;
27
+ get cleanupPending() {
28
+ return this.state === "stopping";
29
+ }
30
+ get cleanupFailure() {
31
+ return this.failure;
32
+ }
33
+ retainCleanup(work) {
34
+ this.cleanupWork = work;
35
+ }
36
+ whenDrained(callback) {
37
+ this.onDrained = callback;
38
+ }
39
+ start(context) {
40
+ if (this.state !== "reserved") return Promise.reject(new TypeError("Execution startup is once-only and cannot follow release."));
41
+ this.state = "active";
42
+ this.startup = Promise.resolve().then(() => {
43
+ if (this.state !== "active") throw new DOMException("Execution startup was stopped.", "AbortError");
44
+ context.signal?.throwIfAborted();
45
+ return this.startReservation(Object.freeze({ ...context }));
46
+ });
47
+ void this.startup.catch(() => {
48
+ });
49
+ return this.startup.then((streams) => {
50
+ const stdout = streams.stdout;
51
+ const stdin = streams.stdin;
52
+ const stderr = streams.stderr;
53
+ return { ownership: "owned", stdout, stdin, ...stderr === void 0 ? {} : { stderr }, release: () => this.release() };
54
+ });
55
+ }
56
+ /** Actual release is unbounded here; one outer observation owns the full connection deadline. */
57
+ release() {
58
+ if (this.state === "released") return Promise.resolve();
59
+ this.state = "stopping";
60
+ if (this.releaseState.kind === "idle") this.beginRelease();
61
+ return this.releaseComplete.promise;
62
+ }
63
+ beginRelease() {
64
+ if (this.releaseState.kind === "running" || this.releaseState.kind === "complete") return;
65
+ const operation = Promise.withResolvers();
66
+ const actual = operation.promise.then(async () => {
67
+ await this.startup?.catch(() => void 0);
68
+ this.releaseState = { kind: "complete" };
69
+ this.releaseComplete.resolve();
70
+ }).catch((error) => {
71
+ this.releaseState = { kind: "failed", error };
72
+ this.failure = { error };
73
+ this.failureNotice.resolve(error);
74
+ });
75
+ this.releaseState = { kind: "running", actual };
76
+ try {
77
+ Promise.resolve(this.releaseReservation()).then(operation.resolve, operation.reject);
78
+ } catch (error) {
79
+ operation.reject(error);
80
+ }
81
+ }
82
+ cleanup() {
83
+ if (this.state === "released") return Promise.resolve();
84
+ this.state = "stopping";
85
+ if (this.observation) return this.observation;
86
+ const deadline = performance.now() + this.cleanupTimeoutMs;
87
+ this.failureNotice = Promise.withResolvers();
88
+ let initiate;
89
+ let actual = this.cleanupAttempt;
90
+ if (!actual) {
91
+ const operation = Promise.withResolvers();
92
+ actual = operation.promise.then(() => {
93
+ this.state = "released";
94
+ this.failure = void 0;
95
+ this.onDrained();
96
+ }).catch((error) => {
97
+ this.failure = { error };
98
+ this.cleanupAttempt = void 0;
99
+ throw error;
100
+ });
101
+ this.cleanupAttempt = actual;
102
+ initiate = () => {
103
+ try {
104
+ Promise.resolve(this.cleanupWork()).then(operation.resolve, operation.reject);
105
+ } catch (error) {
106
+ operation.reject(error);
107
+ }
108
+ };
109
+ }
110
+ const cleanup = actual;
111
+ this.observation = new Promise((resolve, reject) => {
112
+ const timeout = setTimeout(() => reject(new AcpProviderConnectionError(
113
+ "acp_node_execution_cleanup_timeout",
114
+ "The owned Node ACP execution cleanup exceeded its observation deadline."
115
+ )), Math.max(0, deadline - performance.now()));
116
+ cleanup.then(() => {
117
+ clearTimeout(timeout);
118
+ resolve();
119
+ }, (error) => {
120
+ clearTimeout(timeout);
121
+ reject(error);
122
+ });
123
+ this.failureNotice.promise.then((error) => {
124
+ clearTimeout(timeout);
125
+ reject(error);
126
+ });
127
+ }).catch((error) => {
128
+ this.failure ??= { error };
129
+ throw error;
130
+ }).finally(() => {
131
+ this.observation = void 0;
132
+ });
133
+ if (this.releaseState.kind === "failed") this.beginRelease();
134
+ initiate?.();
135
+ return this.observation;
136
+ }
137
+ retainedError(cause, cleanupError) {
138
+ return new NodeAcpExecutionCleanupError({ cause, cleanupError, retryCleanup: () => this.cleanup() });
139
+ }
140
+ joinCleanup() {
141
+ if (this.cleanupAttempt) return this.cleanupAttempt;
142
+ if (this.state === "released") return Promise.resolve();
143
+ return Promise.reject(this.failure?.error ?? new Error("Execution cleanup has not started."));
144
+ }
145
+ }
146
+ function executionReservationSource(boundary, input) {
147
+ const reserve = boundary?.reserve;
148
+ if (typeof reserve !== "function") throw new TypeError("A Node ACP execution boundary must provide a synchronous reserve method.");
149
+ const used = /* @__PURE__ */ new WeakSet();
150
+ return () => {
151
+ const reservation = Reflect.apply(reserve, boundary, [input]);
152
+ if (!reservation || typeof reservation !== "object" || "then" in reservation || used.has(reservation)) {
153
+ throw new TypeError("Execution reserve must return a fresh synchronous inert reservation.");
154
+ }
155
+ used.add(reservation);
156
+ return new NodeAcpExecutionOwner(reservation, 2 * input.shutdownTimeoutMs);
157
+ };
158
+ }
159
+ export {
160
+ NodeAcpExecutionOwner,
161
+ executionReservationSource
162
+ };