@agen-ai/provider-acp 0.4.1 → 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 (43) hide show
  1. package/README.md +174 -14
  2. package/dist/capabilities.js +2 -2
  3. package/dist/connection/contracts.d.ts +3 -1
  4. package/dist/connection/initialize.js +25 -9
  5. package/dist/connection/negotiation.d.ts +2 -1
  6. package/dist/connection/negotiation.js +8 -1
  7. package/dist/host/interactions.d.ts +6 -1
  8. package/dist/host/interactions.js +42 -16
  9. package/dist/host/router.d.ts +3 -2
  10. package/dist/host/router.js +23 -20
  11. package/dist/instance.js +19 -9
  12. package/dist/node/contracts.d.ts +2 -2
  13. package/dist/node/index.js +1 -1
  14. package/dist/node/launch.js +3 -3
  15. package/dist/session/continuation.d.ts +33 -0
  16. package/dist/session/continuation.js +185 -0
  17. package/dist/session/descendants.d.ts +29 -0
  18. package/dist/session/descendants.js +97 -0
  19. package/dist/session/mapping.js +1 -1
  20. package/dist/session/native.d.ts +1 -0
  21. package/dist/session/native.js +8 -0
  22. package/dist/session/opening.js +1 -1
  23. package/dist/session/requests/elicitation.d.ts +1 -1
  24. package/dist/session/requests/elicitation.js +1 -1
  25. package/dist/session/requests/grokWithdrawals.d.ts +28 -0
  26. package/dist/session/requests/grokWithdrawals.js +135 -0
  27. package/dist/session/requests/pending.d.ts +17 -4
  28. package/dist/session/requests/pending.js +63 -7
  29. package/dist/session/requests/resolution.js +4 -2
  30. package/dist/session/session.d.ts +8 -0
  31. package/dist/session/session.js +140 -32
  32. package/dist/session/turn.d.ts +6 -1
  33. package/dist/sessionInteractions/broker.d.ts +11 -3
  34. package/dist/sessionInteractions/broker.js +35 -13
  35. package/dist/sessionInteractions/extensions.d.ts +2 -1
  36. package/dist/sessionInteractions/extensions.js +21 -1
  37. package/dist/sessionInteractions/grokFolderTrust.d.ts +0 -1
  38. package/dist/sessionInteractions/grokFolderTrust.js +0 -1
  39. package/dist/sessionInteractions/grokPlanReview.d.ts +16 -0
  40. package/dist/sessionInteractions/grokPlanReview.js +63 -0
  41. package/dist/sessionInteractions/request.d.ts +15 -7
  42. package/dist/sessionInteractions/request.js +46 -18
  43. package/package.json +4 -4
@@ -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,10 +92,17 @@ 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
  }
@@ -70,16 +125,19 @@ class SessionState {
70
125
  closePromise;
71
126
  itemSequence = 0;
72
127
  requestSequence = 0;
128
+ continuations;
129
+ descendants;
130
+ childWithdrawals;
73
131
  async open() {
74
132
  this.requireUsable();
75
133
  throwIfAgentOperationAborted(this.input.context.signal);
76
- const sessionInteractions = this.input.connection.sessionInteractions;
134
+ const sessionInteractions = this.input.context.interactions ? this.input.connection.sessionInteractions : void 0;
77
135
  await openAcpNativeSession(this.input, {
78
136
  replay: this.replay,
79
137
  bind: (binding2) => {
80
138
  this.binding = binding2;
81
139
  this.nativeSessionId = binding2.conversationId;
82
- this.input.connection.sessionInteractions?.bindNativeSession(binding2.conversationId);
140
+ sessionInteractions?.bindNativeSession(binding2.conversationId);
83
141
  this.bindHostSession();
84
142
  },
85
143
  observeSetup: (result) => {
@@ -127,7 +185,7 @@ class SessionState {
127
185
  }
128
186
  }
129
187
  fail(error) {
130
- if (this.failure) return;
188
+ if (this.failure || this.closed) return;
131
189
  this.failure = error;
132
190
  this.input.connection.sessionInteractions?.stop({ kind: "failed", cause: error });
133
191
  this.turn?.hostInteractions.abort(error);
@@ -135,6 +193,9 @@ class SessionState {
135
193
  this.requests.cancelAll();
136
194
  this.environment.close();
137
195
  this.turn?.settled.reject(error);
196
+ this.continuations.stop(error);
197
+ this.descendants?.stop();
198
+ this.childWithdrawals?.stop(error);
138
199
  this.clearTurnTimers();
139
200
  void this.input.connection.close().catch((cause) => {
140
201
  this.cleanupFailure = cause;
@@ -142,11 +203,13 @@ class SessionState {
142
203
  }
143
204
  admitNotification(context) {
144
205
  this.requireUsable();
206
+ if (this.descendants?.admit(context, this.replay.active)) return;
145
207
  const params = nativeObject(context.params, "session notification");
146
208
  this.assertNativeSession(params);
147
209
  const update = nativeObject(params.update, "session update");
148
210
  const kind = nativeString(update.sessionUpdate, "session update kind");
149
211
  this.replay.observe(params);
212
+ const liveOutput = this.continuations.admitOutput(params, update, this.replay.active);
150
213
  if (SESSION_METADATA_UPDATES.has(kind)) {
151
214
  if (this.nativeSessionId === void 0) {
152
215
  const entry = this.queue.admit({ kind: "update", params }, nativeBytes(params) * 2);
@@ -156,7 +219,7 @@ class SessionState {
156
219
  this.observeMetadata(update);
157
220
  return;
158
221
  }
159
- if (this.replay.active) return;
222
+ if (this.replay.active || !liveOutput) return;
160
223
  if (!this.turn || this.turn.nativeSettled || this.turn.terminal) {
161
224
  throw new AcpProviderSessionError("acp_update_without_prompt", "ACP emitted turn content without an active native prompt.");
162
225
  }
@@ -197,21 +260,45 @@ class SessionState {
197
260
  clearTurnTimers() {
198
261
  for (const cleanup of this.turn?.cleanup.splice(0) ?? []) cleanup();
199
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
+ }
200
281
  startTurn(input) {
201
282
  this.requireUsable();
202
283
  throwIfAgentOperationAborted(input.signal);
203
- 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.");
204
285
  if (input.interactionMode !== "default") {
205
286
  throw new AcpProviderSessionError("acp_input_unsupported", "ACP supports only the default interaction mode.");
206
287
  }
207
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.");
208
295
  const settled = deferred();
209
296
  void settled.promise.catch(() => void 0);
210
297
  const turn = {
211
- turnId: input.turnId,
298
+ turnId,
212
299
  mapping: new TurnMapping({
213
300
  sessionId: this.input.context.sessionId,
214
- turnId: input.turnId,
301
+ turnId,
215
302
  workingDirectory: this.input.context.workingDirectory,
216
303
  permissionMapping: this.input.permissionMapping,
217
304
  now: this.input.now,
@@ -226,22 +313,41 @@ class SessionState {
226
313
  nativeStarted: false,
227
314
  nativeSettled: false,
228
315
  terminal: false,
316
+ sourceSettled: false,
229
317
  waiting: void 0,
230
318
  transition: void 0,
231
319
  cancelReason: void 0,
232
320
  cancellation: void 0
233
321
  };
234
322
  this.turn = turn;
323
+ return turn;
324
+ }
325
+ execution(turn, stream) {
235
326
  let acquired = false;
236
327
  const outputs = {
237
328
  [Symbol.asyncIterator]: () => {
238
329
  if (acquired) throw new AcpProviderSessionError("acp_turn_stream_owned", "The ACP turn output already has a consumer.");
239
330
  acquired = true;
240
- return this.streamTurn(turn, input, prompt);
331
+ return this.ownedStream(turn, stream);
241
332
  }
242
333
  };
243
334
  return Object.freeze({ turnId: turn.turnId, outputs, resolveRequest: (resolution) => this.resolveRequest(turn, resolution) });
244
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
+ }
245
351
  launchTurn(turn, input, prompt) {
246
352
  this.requireUsable();
247
353
  throwIfAgentOperationAborted(input.signal);
@@ -254,16 +360,8 @@ class SessionState {
254
360
  }, {
255
361
  timeoutMs: this.input.limits.promptTimeoutMs,
256
362
  onResponseReceived: (response) => {
257
- turn.nativeSettled = true;
258
- if (this.requests.size > 0) {
259
- this.fail(new AcpProviderSessionError("acp_terminal_with_request", "ACP completed a prompt with unanswered interaction requests."));
260
- return;
261
- }
262
- turn.hostInteractions.abort(new DOMException("The ACP native prompt settled.", "AbortError"));
263
- this.clearTurnTimers();
264
363
  const payload = acpTurnCompletion(response, turn.cancelReason);
265
- this.queue.admit({ kind: "terminal", payload }, nativeBytes(payload), true);
266
- turn.settled.resolve(payload);
364
+ this.completeNativeTurn(turn, payload);
267
365
  }
268
366
  }).then(() => void 0, (error) => {
269
367
  if (!turn.nativeSettled) this.fail(sessionError(error));
@@ -329,10 +427,7 @@ class SessionState {
329
427
  }
330
428
  } else if (message.kind === "request") {
331
429
  const request = message.request;
332
- if (request.answered || turn.cancelReason) {
333
- entry.release();
334
- continue;
335
- }
430
+ if (request.answered || turn.cancelReason) continue;
336
431
  if (request.kind === "permission") {
337
432
  const mappedTool = turn.mapping.tool(nativeObject(request.params.toolCall, "permission tool"), true);
338
433
  let next = mappedTool.next();
@@ -343,10 +438,7 @@ class SessionState {
343
438
  if (request.answered || turn.cancelReason) break;
344
439
  next = mappedTool.next();
345
440
  }
346
- if (!next.done) {
347
- entry.release();
348
- continue;
349
- }
441
+ if (!next.done) continue;
350
442
  const permission = turn.mapping.permission(request.params, next.value);
351
443
  request.mapped = { kind: "permission", permission, request: permission.request };
352
444
  if (permission.warning) {
@@ -359,20 +451,26 @@ class SessionState {
359
451
  continue;
360
452
  }
361
453
  if (request.host) {
362
- 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)));
363
455
  continue;
364
456
  }
365
457
  const flow = new AcpFormFlow(request.form, () => parseAgentRequestId(`acp:request:${++this.requestSequence}`));
366
458
  request.mapped = { kind: "form", flow, request: flow.request };
367
459
  }
368
- if (request.answered || turn.cancelReason) {
369
- entry.release();
370
- continue;
371
- }
460
+ if (request.answered || turn.cancelReason) continue;
372
461
  turn.waiting = request;
373
462
  yield* this.offerRequest(turn, request);
374
463
  if (turn.terminal) return "terminal";
375
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
+ }
376
474
  } else {
377
475
  try {
378
476
  if (this.requests.size > 0) throw new AcpProviderSessionError("acp_terminal_with_request", "ACP completed a prompt with unanswered interaction requests.");
@@ -384,6 +482,7 @@ class SessionState {
384
482
  turn.terminal = true;
385
483
  turn.waiting = void 0;
386
484
  turn.mapping.release();
485
+ this.continuations.complete(turn);
387
486
  this.clearTurnTimers();
388
487
  return "terminal";
389
488
  } finally {
@@ -394,6 +493,8 @@ class SessionState {
394
493
  }
395
494
  async *offerRequest(turn, request) {
396
495
  const canonical = request.mapped.request;
496
+ if (!this.requests.canAnswer(request)) return;
497
+ request.publishedRequestId = canonical.requestId;
397
498
  yield* this.emit(turn.mapping.event("request.opened", { request: canonical }));
398
499
  if (turn.terminal) return;
399
500
  yield* this.emit(turn.mapping.event("turn.state_changed", { state: "waiting_for_request", requestId: canonical.requestId }));
@@ -404,6 +505,7 @@ class SessionState {
404
505
  this.launchTurn(turn, input, prompt);
405
506
  yield* this.emit(turn.mapping.event("turn.started", {}));
406
507
  yield* this.drain(turn);
508
+ await this.requests.join();
407
509
  stable = true;
408
510
  } catch (error) {
409
511
  this.fail(sessionError(error));
@@ -418,6 +520,9 @@ class SessionState {
418
520
  if (this.turn !== turn || turn.terminal || !request?.mapped) {
419
521
  throw new AcpProviderSessionError("acp_request_not_pending", "The ACP resolution does not identify an available pending request.");
420
522
  }
523
+ const gate = this.childWithdrawals?.answerGate(request);
524
+ if (gate) await awaitHost(gate, input.signal ?? this.input.connection.stopped);
525
+ throwIfAgentOperationAborted(input.signal);
421
526
  const result = resolveAcpRequest(this.requests, turn, request, request.mapped, input);
422
527
  if (result.status === "delivered") this.queue.wakeReader();
423
528
  return result;
@@ -491,6 +596,9 @@ class SessionState {
491
596
  this.queue.fail(error);
492
597
  this.requests.cancelAll();
493
598
  this.turn?.settled.reject(error);
599
+ this.continuations.stop(error);
600
+ this.descendants?.stop();
601
+ this.childWithdrawals?.stop(error);
494
602
  this.clearTurnTimers();
495
603
  this.environment.close();
496
604
  this.turn?.mapping.release();
@@ -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;
@@ -1,18 +1,22 @@
1
1
  import type { AcpJsonValue, AcpReverseRequestContext } from '@agen-ai/acp-transport';
2
2
  import type { AgentProviderConversationId, AgentSessionId } from '@agen-ai/agent-protocol';
3
- import { type AgentSessionInteractionContext } from '@agen-ai/agent-runtime';
3
+ import { type AgentProviderSessionInteractionContext } from '@agen-ai/agent-runtime';
4
4
  import type { AcpProviderLimits } from '../limits.js';
5
+ import { type PreparePlanContinuation } from './request.js';
6
+ import type { AcpSessionInteractionSelection } from './contracts.js';
5
7
  export type SessionInteractionStop = Readonly<{
6
8
  kind: 'session_closed';
7
9
  }> | Readonly<{
8
10
  kind: 'failed';
9
11
  cause: Error;
10
12
  }>;
11
- /** One connection owns one create generation and one retained native decision. */
13
+ /** One connection owns one opening generation and one retained native session request. */
12
14
  export declare class SessionInteractionBroker {
13
15
  private readonly input;
14
16
  private state;
15
17
  private requestNumber;
18
+ private expectedNativeId;
19
+ private prepareContinuation;
16
20
  private retained;
17
21
  private readonly entered;
18
22
  private openingTimer;
@@ -21,12 +25,16 @@ export declare class SessionInteractionBroker {
21
25
  constructor(input: Readonly<{
22
26
  stopped: AbortSignal;
23
27
  limits: AcpProviderLimits;
28
+ selection: AcpSessionInteractionSelection;
24
29
  onFailure: (cause: Error) => void;
25
30
  }>);
31
+ attachContinuationOwner(prepare: PreparePlanContinuation): void;
32
+ humanTimeoutMs(): number | undefined;
33
+ get selectedLifecycle(): boolean;
26
34
  prepareOpening(input: Readonly<{
27
35
  sessionId: AgentSessionId;
28
36
  workingDirectory: string;
29
- interactions: AgentSessionInteractionContext;
37
+ interactions: AgentProviderSessionInteractionContext;
30
38
  signal?: AbortSignal;
31
39
  }>): void;
32
40
  onOpeningDispatched(): void;
@@ -5,6 +5,7 @@ import {
5
5
  import { AcpProviderSessionError } from "../session/errors.js";
6
6
  import { folderTrustDecision, GROK_FOLDER_TRUST_METHOD, parseNativeFolderTrustRequest } from "./grokFolderTrust.js";
7
7
  import { NativeSessionDecision, sessionDecisionError } from "./request.js";
8
+ import { GROK_PLAN_REVIEW_METHOD, parseNativePlanReviewRequest, planReviewRequest } from "./grokPlanReview.js";
8
9
  class SessionInteractionBroker {
9
10
  constructor(input) {
10
11
  this.input = input;
@@ -13,6 +14,8 @@ class SessionInteractionBroker {
13
14
  }
14
15
  state = { kind: "idle" };
15
16
  requestNumber = 0;
17
+ expectedNativeId;
18
+ prepareContinuation;
16
19
  retained;
17
20
  entered = /* @__PURE__ */ new Set();
18
21
  openingTimer;
@@ -25,11 +28,12 @@ class SessionInteractionBroker {
25
28
  };
26
29
  handleRequest = (context) => {
27
30
  try {
28
- if (context.method !== GROK_FOLDER_TRUST_METHOD) throw failure(
31
+ const plan = this.input.selection.mapping === "grok-build-1.0.60-session-requests-v1";
32
+ if (context.method !== (plan ? GROK_PLAN_REVIEW_METHOD : GROK_FOLDER_TRUST_METHOD)) throw failure(
29
33
  "acp_session_interaction_invalid",
30
34
  "The reverse request does not belong to the selected session mapping."
31
35
  );
32
- const native = parseNativeFolderTrustRequest(context.params);
36
+ const native = plan ? parseNativePlanReviewRequest(context.params) : parseNativeFolderTrustRequest(context.params);
33
37
  const state = this.state;
34
38
  if (state.kind === "stopped") throw state.cause;
35
39
  if (state.kind === "idle" || state.kind === "prepared") throw failure(
@@ -40,12 +44,13 @@ class SessionInteractionBroker {
40
44
  "acp_session_interaction_capacity",
41
45
  "The connection already retains a native session decision."
42
46
  );
43
- if (native.cwd !== state.owner.workingDirectory || "nativeId" in state && native.sessionId !== state.nativeId) throw failure(
47
+ if ("cwd" in native && native.cwd !== state.owner.workingDirectory || this.expectedNativeId !== void 0 && native.sessionId !== this.expectedNativeId) throw failure(
44
48
  "acp_session_interaction_binding",
45
49
  "The native decision does not match its exact session binding."
46
50
  );
47
- const expiresAt = Date.now() + state.owner.limits.requestTimeoutMs;
48
- const request = folderTrustDecision({ native, sessionId: state.owner.sessionId, requestNumber: ++this.requestNumber, expiresAt });
51
+ const hostDeadlineAt = state.owner.interactions.humanWait?.kind === "until_withdrawn_or_closed" ? void 0 : Date.now() + state.owner.limits.requestTimeoutMs;
52
+ const requestNumber = ++this.requestNumber;
53
+ const request = "planContent" in native ? planReviewRequest(native, state.owner.sessionId, requestNumber) : folderTrustDecision({ native, sessionId: state.owner.sessionId, requestNumber });
49
54
  const descriptorBytes = Buffer.byteLength(JSON.stringify(request), "utf8");
50
55
  const nativeBytes = Buffer.byteLength(JSON.stringify(native), "utf8");
51
56
  if (descriptorBytes > state.owner.limits.maxRetainedBytes || descriptorBytes + nativeBytes > this.input.limits.maxQueuedBytes) throw failure(
@@ -54,9 +59,13 @@ class SessionInteractionBroker {
54
59
  );
55
60
  const decision = new NativeSessionDecision({
56
61
  request,
57
- nativeContext: Object.freeze({ id: context.id, signal: context.signal }),
58
- expiresAt,
59
- accepting: () => this.state.kind === "active" || this.state.kind === "open",
62
+ nativeContext: context,
63
+ ...hostDeadlineAt === void 0 ? {} : { hostDeadlineAt },
64
+ ...plan ? { prepareContinuation: (input) => {
65
+ if (!this.prepareContinuation) throw failure("acp_continuation_owner_missing", "The plan review has no prepared execution owner.");
66
+ return this.prepareContinuation(input);
67
+ } } : {},
68
+ accepting: () => this.state.kind === "open" || !plan && this.state.kind === "active",
60
69
  fail: (cause) => this.stop({ kind: "failed", cause }),
61
70
  retire: () => {
62
71
  if (this.retained?.decision === decision) this.retained = void 0;
@@ -66,7 +75,7 @@ class SessionInteractionBroker {
66
75
  this.retained = { decision, nativeId: native.sessionId };
67
76
  this.entered.add(decision);
68
77
  decision.observeNativeSignal();
69
- if (this.state.kind === "active" || this.state.kind === "open") decision.present(state.owner.interactions.onRequest);
78
+ if (this.state.kind === "open" || !plan && this.state.kind === "active") decision.present(state.owner.interactions.onRequest);
70
79
  return decision.nativeReply;
71
80
  } catch (cause) {
72
81
  const error = cause instanceof Error ? cause : failure(
@@ -81,6 +90,17 @@ class SessionInteractionBroker {
81
90
  return rejected;
82
91
  }
83
92
  };
93
+ attachContinuationOwner(prepare) {
94
+ if (this.prepareContinuation) throw this.stateError();
95
+ this.prepareContinuation = prepare;
96
+ }
97
+ humanTimeoutMs() {
98
+ if (this.state.kind === "idle" || this.state.kind === "stopped") return DEFAULT_AGENT_SESSION_INTERACTION_LIMITS.requestTimeoutMs;
99
+ return this.state.owner.interactions.humanWait?.kind === "until_withdrawn_or_closed" ? void 0 : this.state.owner.limits.requestTimeoutMs;
100
+ }
101
+ get selectedLifecycle() {
102
+ return this.input.selection.mapping === "grok-build-1.0.60-session-requests-v1";
103
+ }
84
104
  prepareOpening(input) {
85
105
  throwIfAgentOperationAborted(input.signal);
86
106
  if (this.state.kind !== "idle") throw this.stateError();
@@ -99,7 +119,7 @@ class SessionInteractionBroker {
99
119
  }
100
120
  onOpeningDispatched() {
101
121
  if (this.state.kind !== "prepared") throw this.stateError();
102
- this.state = { kind: "dispatched", owner: this.state.owner };
122
+ this.state = this.expectedNativeId === void 0 ? { kind: "dispatched", owner: this.state.owner } : { kind: "bound", owner: this.state.owner, nativeId: this.expectedNativeId };
103
123
  this.openingTimer = setTimeout(
104
124
  () => this.stop({ kind: "failed", cause: failure(
105
125
  "acp_session_interaction_opening_timeout",
@@ -109,26 +129,28 @@ class SessionInteractionBroker {
109
129
  );
110
130
  }
111
131
  bindNativeSession(nativeId) {
112
- if (this.state.kind !== "dispatched") throw this.stateError();
132
+ if (this.state.kind !== "dispatched" && this.state.kind !== "prepared") throw this.stateError();
113
133
  if (this.retained && this.retained.nativeId !== nativeId) {
114
134
  const cause = failure("acp_session_interaction_binding", "The buffered decision claimed a different native session.");
115
135
  this.stop({ kind: "failed", cause });
116
136
  throw cause;
117
137
  }
118
- this.state = { kind: "bound", owner: this.state.owner, nativeId };
138
+ this.expectedNativeId = nativeId;
139
+ if (this.state.kind === "dispatched") this.state = { kind: "bound", owner: this.state.owner, nativeId };
119
140
  }
120
141
  completeOpeningResponse() {
121
142
  if (this.state.kind !== "bound") throw this.stateError();
122
143
  this.clearOpeningTimer();
123
144
  const owner = this.state.owner;
124
145
  this.state = { kind: "active", owner, nativeId: this.state.nativeId };
125
- this.retained?.decision.present(owner.interactions.onRequest);
146
+ if (this.input.selection.mapping === "grok-folder-trust-v1") this.retained?.decision.present(owner.interactions.onRequest);
126
147
  this.throwIfStopped();
127
148
  }
128
149
  adopt() {
129
150
  if (this.state.kind !== "active") throw this.stateError();
130
151
  this.state.owner.removeAbort();
131
152
  this.state = { kind: "open", owner: this.state.owner, nativeId: this.state.nativeId };
153
+ this.retained?.decision.present(this.state.owner.interactions.onRequest);
132
154
  }
133
155
  stop(reason) {
134
156
  if (this.state.kind === "stopped") return;
@@ -1,3 +1,4 @@
1
1
  import { type AcpExtensionRegistry } from '@agen-ai/acp-transport';
2
- export declare function createSessionInteractionExtensionRegistry(): AcpExtensionRegistry;
2
+ import type { AcpSessionInteractionSelection } from './contracts.js';
3
+ export declare function createSessionInteractionExtensionRegistry(selection: AcpSessionInteractionSelection): AcpExtensionRegistry;
3
4
  //# sourceMappingURL=extensions.d.ts.map
@@ -4,7 +4,27 @@ import {
4
4
  validateNativeFolderTrustRequest,
5
5
  validateNativeFolderTrustResult
6
6
  } from "./grokFolderTrust.js";
7
- function createSessionInteractionExtensionRegistry() {
7
+ import {
8
+ GROK_PLAN_REVIEW_METHOD,
9
+ GROK_QUEUE_METHOD,
10
+ GROK_SESSION_NOTIFICATION_METHOD,
11
+ validateNativePlanReviewRequest,
12
+ validateNativePlanReviewResult
13
+ } from "./grokPlanReview.js";
14
+ function sessionNotification(input) {
15
+ return input !== null && typeof input === "object" && !Array.isArray(input) && "sessionId" in input && typeof input.sessionId === "string" && input.sessionId.length > 0 && input.sessionId.length <= 256;
16
+ }
17
+ function createSessionInteractionExtensionRegistry(selection) {
18
+ if (selection.mapping === "grok-build-1.0.60-session-requests-v1") return createAcpExtensionRegistry([
19
+ {
20
+ method: GROK_PLAN_REVIEW_METHOD,
21
+ direction: "inbound_request",
22
+ validateParams: validateNativePlanReviewRequest,
23
+ validateResult: validateNativePlanReviewResult
24
+ },
25
+ { method: GROK_QUEUE_METHOD, direction: "inbound_notification", validateParams: (input) => sessionNotification(input) && "entries" in input && Array.isArray(input.entries) && input.entries.length <= 256 },
26
+ { method: GROK_SESSION_NOTIFICATION_METHOD, direction: "inbound_notification", validateParams: (input) => sessionNotification(input) && "update" in input && input.update !== null && typeof input.update === "object" && !Array.isArray(input.update) && "sessionUpdate" in input.update && typeof input.update.sessionUpdate === "string" }
27
+ ]);
8
28
  return createAcpExtensionRegistry([{
9
29
  method: GROK_FOLDER_TRUST_METHOD,
10
30
  direction: "inbound_request",
@@ -37,7 +37,6 @@ export declare function folderTrustDecision(input: Readonly<{
37
37
  native: NativeFolderTrustRequest;
38
38
  sessionId: AgentSessionId;
39
39
  requestNumber: number;
40
- expiresAt: number;
41
40
  }>): AgentSessionDecisionRequest;
42
41
  export {};
43
42
  //# sourceMappingURL=grokFolderTrust.d.ts.map
@@ -76,7 +76,6 @@ function folderTrustDecision(input) {
76
76
  title: "Allow workspace configuration?",
77
77
  description: `Detected configuration: ${detected}. Workspace configuration can start tools or hooks and affect provider behavior. These detected categories do not restrict the scope of an authorization.`,
78
78
  target: { kind: "workspace", path: input.native.workspace },
79
- expiresAt: new Date(input.expiresAt).toISOString(),
80
79
  choices: [
81
80
  {
82
81
  choiceId: "grant",
@@ -0,0 +1,16 @@
1
+ import { type AgentProviderConversationId, type AgentSessionId, type AgentSessionPlanReviewRequest, type AgentSessionPlanReviewResponse } from '@agen-ai/agent-protocol';
2
+ import type { AcpJsonValue } from '@agen-ai/acp-transport';
3
+ export declare const GROK_PLAN_REVIEW_METHOD = "_x.ai/exit_plan_mode";
4
+ export declare const GROK_QUEUE_METHOD = "_x.ai/queue/changed";
5
+ export declare const GROK_SESSION_NOTIFICATION_METHOD = "_x.ai/session_notification";
6
+ export interface NativePlanReviewRequest {
7
+ readonly sessionId: AgentProviderConversationId;
8
+ readonly toolCallId: string;
9
+ readonly planContent: string;
10
+ }
11
+ export declare function parseNativePlanReviewRequest(input: unknown): NativePlanReviewRequest;
12
+ export declare function validateNativePlanReviewRequest(input: unknown): boolean;
13
+ export declare function validateNativePlanReviewResult(input: unknown): boolean;
14
+ export declare function planReviewRequest(native: NativePlanReviewRequest, sessionId: AgentSessionId, requestNumber: number): AgentSessionPlanReviewRequest;
15
+ export declare function nativePlanReviewReply(response: AgentSessionPlanReviewResponse): AcpJsonValue;
16
+ //# sourceMappingURL=grokPlanReview.d.ts.map