@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
@@ -8,7 +8,6 @@ class AcpSessionResources {
8
8
  constructor(input) {
9
9
  this.input = input;
10
10
  }
11
- input;
12
11
  lifetime = new AbortController();
13
12
  acquisition;
14
13
  releaseMcp;
@@ -64,9 +63,12 @@ class AcpSessionResources {
64
63
  close() {
65
64
  if (this.closed) return Promise.resolve();
66
65
  this.lifetime.abort(new DOMException("ACP session resources were closed.", "AbortError"));
66
+ const nativeCleanup = this.nativeClosed ? Promise.resolve() : this.input.connection.close();
67
+ void nativeCleanup.catch(() => {
68
+ });
67
69
  this.cleanup ??= Promise.resolve().then(async () => {
68
70
  if (!this.nativeClosed) {
69
- await this.input.connection.close();
71
+ await (this.input.connection.execution?.joinCleanup() ?? nativeCleanup);
70
72
  this.nativeClosed = true;
71
73
  }
72
74
  if (this.acquisition) {
@@ -82,7 +84,7 @@ class AcpSessionResources {
82
84
  this.cleanup = void 0;
83
85
  });
84
86
  const deadline = hostDeadline(new AbortController().signal, this.input.limits.hostOperationTimeoutMs);
85
- return awaitHost(this.cleanup, deadline.signal).finally(() => {
87
+ return awaitHost(Promise.all([nativeCleanup, this.cleanup]).then(() => void 0), deadline.signal).finally(() => {
86
88
  deadline.close();
87
89
  });
88
90
  }
@@ -15,10 +15,15 @@ export declare class SessionState {
15
15
  private nativeSessionId;
16
16
  private turn;
17
17
  private failure;
18
+ private failureObserved;
19
+ private cleanupFailure;
18
20
  private closed;
19
21
  private closePromise;
20
22
  private itemSequence;
21
23
  private requestSequence;
24
+ private readonly continuations;
25
+ private readonly descendants;
26
+ private readonly childWithdrawals;
22
27
  constructor(input: CreateAcpProviderSessionInput);
23
28
  open(): Promise<AgentProviderSession>;
24
29
  private requireUsable;
@@ -29,7 +34,12 @@ export declare class SessionState {
29
34
  private observeMetadata;
30
35
  private admitRequest;
31
36
  private clearTurnTimers;
37
+ private streamContinuation;
32
38
  startTurn(input: AgentProviderStartTurnInput): AgentProviderTurnExecution;
39
+ private createNativeTurn;
40
+ private execution;
41
+ private ownedStream;
42
+ private completeNativeTurn;
33
43
  private launchTurn;
34
44
  private emit;
35
45
  private drain;
@@ -38,6 +48,7 @@ export declare class SessionState {
38
48
  private resolveRequest;
39
49
  private cancelNative;
40
50
  interruptTurn(input: AgentProviderInterruptTurnInput): Promise<AgentProviderTurnInterruptionResult>;
41
- close(): Promise<void>;
51
+ close(observeFailure?: boolean): Promise<void>;
52
+ private observeFailure;
42
53
  }
43
54
  //# sourceMappingURL=session.d.ts.map
@@ -8,6 +8,10 @@ import {
8
8
  import {
9
9
  throwIfAgentOperationAborted
10
10
  } from "@agen-ai/agent-runtime";
11
+ import { GrokPlanContinuations } from "./continuation.js";
12
+ import { GrokDescendantObservations } from "./descendants.js";
13
+ import { GrokChildPermissionWithdrawals } from "./requests/grokWithdrawals.js";
14
+ import { awaitHost } from "../host/lifecycle.js";
11
15
  import { AcpProviderSessionError, sessionError } from "./errors.js";
12
16
  import { TurnMapping } from "./mapping.js";
13
17
  import { createAcpSessionConfiguration } from "../configuration/control.js";
@@ -26,11 +30,55 @@ class SessionState {
26
30
  this.input = input;
27
31
  this.queue = new SessionQueue(input.limits);
28
32
  this.replay = new AcpHistoryReplay(input.limits);
33
+ if (input.connection.sessionInteractions?.selectedLifecycle) {
34
+ this.childWithdrawals = new GrokChildPermissionWithdrawals({
35
+ reserveState: (value, extraBytes) => this.queue.reserveState(value, extraBytes),
36
+ currentTurn: () => this.turn,
37
+ timeoutMs: input.limits.cancellationTimeoutMs,
38
+ withdraw: (request) => this.requests.withdraw(request, new AcpRpcHandlerError(-32800, "Request cancelled")),
39
+ fail: (error) => this.fail(error)
40
+ });
41
+ this.descendants = new GrokDescendantObservations({
42
+ reserveState: (value, extraBytes) => this.queue.reserveState(value, extraBytes),
43
+ rootSessionId: () => this.nativeSessionId,
44
+ activeTurn: () => this.turn !== void 0 && !this.turn.nativeSettled && !this.turn.terminal,
45
+ observeHistory: (params) => this.replay.observe(params),
46
+ withdrawals: this.childWithdrawals
47
+ });
48
+ }
49
+ this.continuations = new GrokPlanContinuations({
50
+ queue: this.queue,
51
+ selected: input.connection.sessionInteractions?.selectedLifecycle === true,
52
+ requireUsable: () => this.requireUsable(),
53
+ assertNativeSession: (params) => this.assertNativeSession(params),
54
+ createNativeTurn: (turnId) => this.createNativeTurn(turnId),
55
+ execution: (turn, stream) => this.execution(turn, stream),
56
+ stream: (turn, ready) => this.streamContinuation(turn, ready),
57
+ completeNativeTurn: (turn, payload) => this.completeNativeTurn(turn, payload),
58
+ close: () => this.close()
59
+ });
29
60
  this.operations = createAcpStandardSessionOperations(input.connection.client);
30
61
  this.requests = new PendingSessionRequests({
31
- maxPendingRequests: input.limits.maxPendingRequests,
62
+ maxPendingRequests: input.connection.sessionInteractions?.selectedLifecycle ? Math.min(input.limits.maxPendingRequests, 16) : input.limits.maxPendingRequests,
63
+ maxRequestMaterialBytes: input.connection.sessionInteractions?.selectedLifecycle ? Math.min(input.limits.maxQueuedBytes, 1048576) : input.limits.maxQueuedBytes,
32
64
  admit: (request, bytes) => this.queue.admit({ kind: "request", request }, bytes),
33
- fail: (error) => this.fail(error)
65
+ fail: (error) => this.fail(error),
66
+ unavailable: (request, requestId) => {
67
+ try {
68
+ this.queue.admit({ kind: "unavailable", request, requestId }, nativeBytes(requestId) + 64, true);
69
+ } catch (cause) {
70
+ this.fail(sessionError(cause));
71
+ }
72
+ },
73
+ settled: (request) => {
74
+ this.childWithdrawals?.settle(request);
75
+ const turn = this.turn;
76
+ if (turn?.waiting === request && !request.unavailable) {
77
+ turn.waiting = void 0;
78
+ this.queue.wakeReader();
79
+ }
80
+ },
81
+ admitted: (request) => this.childWithdrawals?.attach(request)
34
82
  });
35
83
  this.configuration = createAcpSessionConfiguration({
36
84
  capability: input.capabilities.configuration,
@@ -44,14 +92,20 @@ class SessionState {
44
92
  this.environment = createAcpSessionEnvironment({ limits: input.limits, now: input.now, signal: input.connection.stopped });
45
93
  this.onStopped = () => this.fail(sessionError(input.connection.stopped.reason));
46
94
  this.detach = input.connection.attachSession({
95
+ joinCallbacks: () => this.requests.join(),
96
+ onExtensionNotification: (context) => {
97
+ this.requireUsable();
98
+ if (this.descendants?.admit(context, this.replay.active)) return;
99
+ this.continuations.admitNotification(context);
100
+ },
47
101
  onNotification: (context) => this.admitNotification(context),
48
102
  onPermissionRequest: (context) => this.admitRequest(context, "permission"),
49
103
  onElicitationRequest: (context) => this.admitRequest(context, "form")
50
104
  });
105
+ input.connection.sessionInteractions?.attachContinuationOwner((prepared) => this.continuations.prepareResponse(prepared));
51
106
  input.connection.stopped.addEventListener("abort", this.onStopped, { once: true });
52
107
  if (input.connection.stopped.aborted) this.onStopped();
53
108
  }
54
- input;
55
109
  queue;
56
110
  operations;
57
111
  requests;
@@ -65,18 +119,25 @@ class SessionState {
65
119
  nativeSessionId;
66
120
  turn;
67
121
  failure;
122
+ failureObserved = false;
123
+ cleanupFailure;
68
124
  closed = false;
69
125
  closePromise;
70
126
  itemSequence = 0;
71
127
  requestSequence = 0;
128
+ continuations;
129
+ descendants;
130
+ childWithdrawals;
72
131
  async open() {
73
132
  this.requireUsable();
74
133
  throwIfAgentOperationAborted(this.input.context.signal);
134
+ const sessionInteractions = this.input.context.interactions ? this.input.connection.sessionInteractions : void 0;
75
135
  await openAcpNativeSession(this.input, {
76
136
  replay: this.replay,
77
137
  bind: (binding2) => {
78
138
  this.binding = binding2;
79
139
  this.nativeSessionId = binding2.conversationId;
140
+ sessionInteractions?.bindNativeSession(binding2.conversationId);
80
141
  this.bindHostSession();
81
142
  },
82
143
  observeSetup: (result) => {
@@ -91,7 +152,10 @@ class SessionState {
91
152
  }
92
153
  }
93
154
  this.configuration.observeSetup(result);
94
- }
155
+ },
156
+ ...sessionInteractions ? {
157
+ completeOpeningResponse: () => sessionInteractions.completeOpeningResponse()
158
+ } : {}
95
159
  });
96
160
  this.requireUsable();
97
161
  if (!this.binding) throw new AcpProviderSessionError("acp_binding_missing", "ACP did not return a session binding.");
@@ -108,7 +172,7 @@ class SessionState {
108
172
  environment: this.environment.port,
109
173
  collaboration: Object.freeze({ kind: "unsupported" }),
110
174
  generatedResources: Object.freeze({ kind: "unsupported" }),
111
- close: async () => this.close()
175
+ close: async () => this.close(true)
112
176
  });
113
177
  }
114
178
  requireUsable() {
@@ -121,23 +185,31 @@ class SessionState {
121
185
  }
122
186
  }
123
187
  fail(error) {
124
- if (this.failure) return;
188
+ if (this.failure || this.closed) return;
125
189
  this.failure = error;
190
+ this.input.connection.sessionInteractions?.stop({ kind: "failed", cause: error });
126
191
  this.turn?.hostInteractions.abort(error);
127
192
  this.queue.fail(error);
128
193
  this.requests.cancelAll();
129
194
  this.environment.close();
130
195
  this.turn?.settled.reject(error);
196
+ this.continuations.stop(error);
197
+ this.descendants?.stop();
198
+ this.childWithdrawals?.stop(error);
131
199
  this.clearTurnTimers();
132
- void this.input.connection.close().catch(() => void 0);
200
+ void this.input.connection.close().catch((cause) => {
201
+ this.cleanupFailure = cause;
202
+ });
133
203
  }
134
204
  admitNotification(context) {
135
205
  this.requireUsable();
206
+ if (this.descendants?.admit(context, this.replay.active)) return;
136
207
  const params = nativeObject(context.params, "session notification");
137
208
  this.assertNativeSession(params);
138
209
  const update = nativeObject(params.update, "session update");
139
210
  const kind = nativeString(update.sessionUpdate, "session update kind");
140
211
  this.replay.observe(params);
212
+ const liveOutput = this.continuations.admitOutput(params, update, this.replay.active);
141
213
  if (SESSION_METADATA_UPDATES.has(kind)) {
142
214
  if (this.nativeSessionId === void 0) {
143
215
  const entry = this.queue.admit({ kind: "update", params }, nativeBytes(params) * 2);
@@ -147,7 +219,7 @@ class SessionState {
147
219
  this.observeMetadata(update);
148
220
  return;
149
221
  }
150
- if (this.replay.active) return;
222
+ if (this.replay.active || !liveOutput) return;
151
223
  if (!this.turn || this.turn.nativeSettled || this.turn.terminal) {
152
224
  throw new AcpProviderSessionError("acp_update_without_prompt", "ACP emitted turn content without an active native prompt.");
153
225
  }
@@ -188,22 +260,47 @@ class SessionState {
188
260
  clearTurnTimers() {
189
261
  for (const cleanup of this.turn?.cleanup.splice(0) ?? []) cleanup();
190
262
  }
263
+ async *streamContinuation(turn, ready) {
264
+ let stable = false;
265
+ try {
266
+ if (!await ready) {
267
+ stable = true;
268
+ return;
269
+ }
270
+ yield* this.emit(turn.mapping.event("turn.started", {}));
271
+ yield* this.drain(turn);
272
+ await this.requests.join();
273
+ stable = true;
274
+ } catch (error) {
275
+ this.fail(sessionError(error));
276
+ throw error;
277
+ } finally {
278
+ if (!stable && !this.closed) this.fail(new AcpProviderSessionError("acp_turn_abandoned", "The native continuation reader was abandoned."));
279
+ }
280
+ }
191
281
  startTurn(input) {
192
282
  this.requireUsable();
193
283
  throwIfAgentOperationAborted(input.signal);
194
- if (this.turn && !this.turn.terminal) throw new AcpProviderSessionError("acp_turn_active", "The ACP session already has an active or waiting turn.");
284
+ if (this.turn && !this.turn.sourceSettled) throw new AcpProviderSessionError("acp_turn_active", "The ACP session already has an active or waiting turn.");
195
285
  if (input.interactionMode !== "default") {
196
286
  throw new AcpProviderSessionError("acp_input_unsupported", "ACP supports only the default interaction mode.");
197
287
  }
198
288
  const prompt = prepareAcpPrompt(input.parts, this.input.capabilities);
289
+ const turn = this.createNativeTurn(input.turnId);
290
+ return this.execution(turn, () => this.streamTurn(turn, input, prompt));
291
+ }
292
+ createNativeTurn(turnId) {
293
+ this.requireUsable();
294
+ if (this.turn && !this.turn.sourceSettled) throw new AcpProviderSessionError("acp_turn_active", "The ACP session already has an active or waiting turn.");
199
295
  const settled = deferred();
200
296
  void settled.promise.catch(() => void 0);
201
297
  const turn = {
202
- turnId: input.turnId,
298
+ turnId,
203
299
  mapping: new TurnMapping({
204
300
  sessionId: this.input.context.sessionId,
205
- turnId: input.turnId,
301
+ turnId,
206
302
  workingDirectory: this.input.context.workingDirectory,
303
+ permissionMapping: this.input.permissionMapping,
207
304
  now: this.input.now,
208
305
  nextItemId: () => ++this.itemSequence,
209
306
  nextRequestId: () => ++this.requestSequence,
@@ -216,22 +313,41 @@ class SessionState {
216
313
  nativeStarted: false,
217
314
  nativeSettled: false,
218
315
  terminal: false,
316
+ sourceSettled: false,
219
317
  waiting: void 0,
220
318
  transition: void 0,
221
319
  cancelReason: void 0,
222
320
  cancellation: void 0
223
321
  };
224
322
  this.turn = turn;
323
+ return turn;
324
+ }
325
+ execution(turn, stream) {
225
326
  let acquired = false;
226
327
  const outputs = {
227
328
  [Symbol.asyncIterator]: () => {
228
329
  if (acquired) throw new AcpProviderSessionError("acp_turn_stream_owned", "The ACP turn output already has a consumer.");
229
330
  acquired = true;
230
- return this.streamTurn(turn, input, prompt);
331
+ return this.ownedStream(turn, stream);
231
332
  }
232
333
  };
233
334
  return Object.freeze({ turnId: turn.turnId, outputs, resolveRequest: (resolution) => this.resolveRequest(turn, resolution) });
234
335
  }
336
+ async *ownedStream(turn, stream) {
337
+ yield* stream();
338
+ turn.sourceSettled = true;
339
+ }
340
+ completeNativeTurn(turn, payload) {
341
+ turn.nativeSettled = true;
342
+ if (this.requests.size > 0) {
343
+ this.fail(new AcpProviderSessionError("acp_terminal_with_request", "ACP completed native work with unanswered interaction requests."));
344
+ return;
345
+ }
346
+ turn.hostInteractions.abort(new DOMException("The ACP native execution settled.", "AbortError"));
347
+ this.clearTurnTimers();
348
+ this.queue.admit({ kind: "terminal", payload }, nativeBytes(payload), true);
349
+ turn.settled.resolve(payload);
350
+ }
235
351
  launchTurn(turn, input, prompt) {
236
352
  this.requireUsable();
237
353
  throwIfAgentOperationAborted(input.signal);
@@ -244,16 +360,8 @@ class SessionState {
244
360
  }, {
245
361
  timeoutMs: this.input.limits.promptTimeoutMs,
246
362
  onResponseReceived: (response) => {
247
- turn.nativeSettled = true;
248
- if (this.requests.size > 0) {
249
- this.fail(new AcpProviderSessionError("acp_terminal_with_request", "ACP completed a prompt with unanswered interaction requests."));
250
- return;
251
- }
252
- turn.hostInteractions.abort(new DOMException("The ACP native prompt settled.", "AbortError"));
253
- this.clearTurnTimers();
254
363
  const payload = acpTurnCompletion(response, turn.cancelReason);
255
- this.queue.admit({ kind: "terminal", payload }, nativeBytes(payload), true);
256
- turn.settled.resolve(payload);
364
+ this.completeNativeTurn(turn, payload);
257
365
  }
258
366
  }).then(() => void 0, (error) => {
259
367
  if (!turn.nativeSettled) this.fail(sessionError(error));
@@ -319,10 +427,7 @@ class SessionState {
319
427
  }
320
428
  } else if (message.kind === "request") {
321
429
  const request = message.request;
322
- if (request.answered || turn.cancelReason) {
323
- entry.release();
324
- continue;
325
- }
430
+ if (request.answered || turn.cancelReason) continue;
326
431
  if (request.kind === "permission") {
327
432
  const mappedTool = turn.mapping.tool(nativeObject(request.params.toolCall, "permission tool"), true);
328
433
  let next = mappedTool.next();
@@ -330,26 +435,42 @@ class SessionState {
330
435
  if (turn.terminal) return "terminal";
331
436
  yield* this.emit(next.value);
332
437
  if (turn.terminal) return "terminal";
438
+ if (request.answered || turn.cancelReason) break;
333
439
  next = mappedTool.next();
334
440
  }
441
+ if (!next.done) continue;
335
442
  const permission = turn.mapping.permission(request.params, next.value);
336
443
  request.mapped = { kind: "permission", permission, request: permission.request };
444
+ if (permission.warning) {
445
+ yield* this.emit(turn.mapping.event("runtime.warning", permission.warning));
446
+ if (turn.terminal) return "terminal";
447
+ }
337
448
  } else {
338
449
  if (request.params.toolCallId != null && !turn.mapping.hasTool(nativeString(request.params.toolCallId, "elicitation tool identity"))) {
339
450
  this.requests.reject(request, new AcpRpcHandlerError(-32602, "ACP form tool identity was not observed in this turn."));
340
451
  continue;
341
452
  }
342
453
  if (request.host) {
343
- void (async () => request.host())().then((result) => this.requests.answer(request, result), (error) => this.requests.reject(request, sessionError(error)));
454
+ void this.requests.invokeHost(request).then((result) => this.requests.answer(request, result), (error) => this.requests.reject(request, sessionError(error)));
344
455
  continue;
345
456
  }
346
457
  const flow = new AcpFormFlow(request.form, () => parseAgentRequestId(`acp:request:${++this.requestSequence}`));
347
458
  request.mapped = { kind: "form", flow, request: flow.request };
348
459
  }
460
+ if (request.answered || turn.cancelReason) continue;
349
461
  turn.waiting = request;
350
462
  yield* this.offerRequest(turn, request);
351
463
  if (turn.terminal) return "terminal";
352
464
  continue;
465
+ } else if (message.kind === "unavailable") {
466
+ try {
467
+ const request = message.request;
468
+ yield* this.emit(turn.mapping.event("request.unavailable", { requestId: message.requestId, reason: "withdrawn" }));
469
+ if (turn.waiting === request) turn.waiting = void 0;
470
+ yield* this.emit(turn.mapping.event("turn.state_changed", { state: "running" }));
471
+ } finally {
472
+ entry.release();
473
+ }
353
474
  } else {
354
475
  try {
355
476
  if (this.requests.size > 0) throw new AcpProviderSessionError("acp_terminal_with_request", "ACP completed a prompt with unanswered interaction requests.");
@@ -361,6 +482,7 @@ class SessionState {
361
482
  turn.terminal = true;
362
483
  turn.waiting = void 0;
363
484
  turn.mapping.release();
485
+ this.continuations.complete(turn);
364
486
  this.clearTurnTimers();
365
487
  return "terminal";
366
488
  } finally {
@@ -371,6 +493,8 @@ class SessionState {
371
493
  }
372
494
  async *offerRequest(turn, request) {
373
495
  const canonical = request.mapped.request;
496
+ if (!this.requests.canAnswer(request)) return;
497
+ request.publishedRequestId = canonical.requestId;
374
498
  yield* this.emit(turn.mapping.event("request.opened", { request: canonical }));
375
499
  if (turn.terminal) return;
376
500
  yield* this.emit(turn.mapping.event("turn.state_changed", { state: "waiting_for_request", requestId: canonical.requestId }));
@@ -381,6 +505,7 @@ class SessionState {
381
505
  this.launchTurn(turn, input, prompt);
382
506
  yield* this.emit(turn.mapping.event("turn.started", {}));
383
507
  yield* this.drain(turn);
508
+ await this.requests.join();
384
509
  stable = true;
385
510
  } catch (error) {
386
511
  this.fail(sessionError(error));
@@ -395,6 +520,9 @@ class SessionState {
395
520
  if (this.turn !== turn || turn.terminal || !request?.mapped) {
396
521
  throw new AcpProviderSessionError("acp_request_not_pending", "The ACP resolution does not identify an available pending request.");
397
522
  }
523
+ const gate = this.childWithdrawals?.answerGate(request);
524
+ if (gate) await awaitHost(gate, input.signal ?? this.input.connection.stopped);
525
+ throwIfAgentOperationAborted(input.signal);
398
526
  const result = resolveAcpRequest(this.requests, turn, request, request.mapped, input);
399
527
  if (result.status === "delivered") this.queue.wakeReader();
400
528
  return result;
@@ -451,27 +579,49 @@ class SessionState {
451
579
  throw error;
452
580
  }
453
581
  }
454
- async close() {
455
- if (this.closePromise) return this.closePromise;
456
- this.closed = true;
457
- this.closePromise = Promise.resolve().then(async () => {
458
- await this.input.closeResources();
459
- this.input.onClosed();
460
- }).catch((error2) => {
461
- this.closePromise = void 0;
462
- throw error2;
463
- });
464
- const error = new AcpProviderSessionError("acp_session_closed", "The ACP session is closed.");
465
- this.turn?.hostInteractions.abort(error);
466
- this.queue.fail(error);
467
- this.requests.cancelAll();
468
- this.turn?.settled.reject(error);
469
- this.clearTurnTimers();
470
- this.environment.close();
471
- this.turn?.mapping.release();
472
- this.detach();
473
- this.input.connection.stopped.removeEventListener("abort", this.onStopped);
474
- return this.closePromise;
582
+ async close(observeFailure = false) {
583
+ if (!this.closePromise) {
584
+ this.closed = true;
585
+ this.closePromise = Promise.resolve().then(async () => {
586
+ await this.input.closeResources();
587
+ this.input.onClosed();
588
+ }).catch((error2) => {
589
+ this.cleanupFailure = error2;
590
+ this.closePromise = void 0;
591
+ throw error2;
592
+ });
593
+ this.input.connection.sessionInteractions?.stop({ kind: "session_closed" });
594
+ const error = new AcpProviderSessionError("acp_session_closed", "The ACP session is closed.");
595
+ this.turn?.hostInteractions.abort(error);
596
+ this.queue.fail(error);
597
+ this.requests.cancelAll();
598
+ this.turn?.settled.reject(error);
599
+ this.continuations.stop(error);
600
+ this.descendants?.stop();
601
+ this.childWithdrawals?.stop(error);
602
+ this.clearTurnTimers();
603
+ this.environment.close();
604
+ this.turn?.mapping.release();
605
+ this.detach();
606
+ this.input.connection.stopped.removeEventListener("abort", this.onStopped);
607
+ }
608
+ try {
609
+ await this.closePromise;
610
+ } catch (cause) {
611
+ const failure2 = this.observeFailure(observeFailure);
612
+ if (failure2) throw new AggregateError([failure2, cause], "ACP session failure and cleanup remain owned.");
613
+ throw cause;
614
+ }
615
+ const failure = this.observeFailure(observeFailure);
616
+ if (failure) {
617
+ if (this.cleanupFailure !== void 0) throw new AggregateError([failure, this.cleanupFailure], "ACP session failed before cleanup completed.");
618
+ throw failure;
619
+ }
620
+ }
621
+ observeFailure(explicit) {
622
+ if (!explicit || !this.input.connection.sessionInteractions || !this.failure || this.failureObserved) return void 0;
623
+ this.failureObserved = true;
624
+ return this.failure;
475
625
  }
476
626
  }
477
627
  export {
@@ -1,5 +1,5 @@
1
1
  import type { AcpJsonObject } from '@agen-ai/acp-transport';
2
- import type { AgentTurnCompletedPayload, AgentTurnId } from '@agen-ai/agent-protocol';
2
+ import type { AgentRequestId, AgentTurnCompletedPayload, AgentTurnId } from '@agen-ai/agent-protocol';
3
3
  import type { TurnMapping } from './mapping.js';
4
4
  import type { deferred } from './queue.js';
5
5
  import type { PendingSessionRequest } from './requests/pending.js';
@@ -9,6 +9,10 @@ export type SessionMessage = Readonly<{
9
9
  }> | Readonly<{
10
10
  kind: 'request';
11
11
  request: PendingSessionRequest;
12
+ }> | Readonly<{
13
+ kind: 'unavailable';
14
+ request: PendingSessionRequest;
15
+ requestId: AgentRequestId;
12
16
  }> | Readonly<{
13
17
  kind: 'terminal';
14
18
  payload: AgentTurnCompletedPayload;
@@ -23,6 +27,7 @@ export interface NativeTurn {
23
27
  nativeStarted: boolean;
24
28
  nativeSettled: boolean;
25
29
  terminal: boolean;
30
+ sourceSettled: boolean;
26
31
  waiting: PendingSessionRequest | undefined;
27
32
  transition: Readonly<{
28
33
  nextRequest?: PendingSessionRequest;
@@ -0,0 +1,50 @@
1
+ import type { AcpJsonValue, AcpReverseRequestContext } from '@agen-ai/acp-transport';
2
+ import type { AgentProviderConversationId, AgentSessionId } from '@agen-ai/agent-protocol';
3
+ import { type AgentProviderSessionInteractionContext } from '@agen-ai/agent-runtime';
4
+ import type { AcpProviderLimits } from '../limits.js';
5
+ import { type PreparePlanContinuation } from './request.js';
6
+ import type { AcpSessionInteractionSelection } from './contracts.js';
7
+ export type SessionInteractionStop = Readonly<{
8
+ kind: 'session_closed';
9
+ }> | Readonly<{
10
+ kind: 'failed';
11
+ cause: Error;
12
+ }>;
13
+ /** One connection owns one opening generation and one retained native session request. */
14
+ export declare class SessionInteractionBroker {
15
+ private readonly input;
16
+ private state;
17
+ private requestNumber;
18
+ private expectedNativeId;
19
+ private prepareContinuation;
20
+ private retained;
21
+ private readonly entered;
22
+ private openingTimer;
23
+ private readonly onStopped;
24
+ readonly handleRequest: (context: AcpReverseRequestContext) => Promise<AcpJsonValue>;
25
+ constructor(input: Readonly<{
26
+ stopped: AbortSignal;
27
+ limits: AcpProviderLimits;
28
+ selection: AcpSessionInteractionSelection;
29
+ onFailure: (cause: Error) => void;
30
+ }>);
31
+ attachContinuationOwner(prepare: PreparePlanContinuation): void;
32
+ humanTimeoutMs(): number | undefined;
33
+ get selectedLifecycle(): boolean;
34
+ prepareOpening(input: Readonly<{
35
+ sessionId: AgentSessionId;
36
+ workingDirectory: string;
37
+ interactions: AgentProviderSessionInteractionContext;
38
+ signal?: AbortSignal;
39
+ }>): void;
40
+ onOpeningDispatched(): void;
41
+ bindNativeSession(nativeId: AgentProviderConversationId): void;
42
+ completeOpeningResponse(): void;
43
+ adopt(): void;
44
+ stop(reason: SessionInteractionStop): void;
45
+ private clearOpeningTimer;
46
+ private stateError;
47
+ private throwIfStopped;
48
+ join(): Promise<void>;
49
+ }
50
+ //# sourceMappingURL=broker.d.ts.map