@agen-ai/provider-acp 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -14
- package/dist/capabilities.js +2 -2
- package/dist/connection/contracts.d.ts +3 -1
- package/dist/connection/initialize.js +25 -9
- package/dist/connection/negotiation.d.ts +2 -1
- package/dist/connection/negotiation.js +8 -1
- package/dist/host/interactions.d.ts +6 -1
- package/dist/host/interactions.js +42 -16
- package/dist/host/router.d.ts +3 -2
- package/dist/host/router.js +23 -20
- package/dist/instance.js +19 -9
- package/dist/node/contracts.d.ts +2 -2
- package/dist/node/index.js +1 -1
- package/dist/node/launch.js +3 -3
- package/dist/session/continuation.d.ts +33 -0
- package/dist/session/continuation.js +185 -0
- package/dist/session/descendants.d.ts +29 -0
- package/dist/session/descendants.js +97 -0
- package/dist/session/mapping.js +1 -1
- package/dist/session/native.d.ts +1 -0
- package/dist/session/native.js +8 -0
- package/dist/session/opening.js +1 -1
- package/dist/session/requests/elicitation.d.ts +1 -1
- package/dist/session/requests/elicitation.js +1 -1
- package/dist/session/requests/grokWithdrawals.d.ts +28 -0
- package/dist/session/requests/grokWithdrawals.js +135 -0
- package/dist/session/requests/pending.d.ts +17 -4
- package/dist/session/requests/pending.js +63 -7
- package/dist/session/requests/resolution.js +4 -2
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +140 -32
- package/dist/session/turn.d.ts +6 -1
- package/dist/sessionInteractions/broker.d.ts +11 -3
- package/dist/sessionInteractions/broker.js +35 -13
- package/dist/sessionInteractions/extensions.d.ts +2 -1
- package/dist/sessionInteractions/extensions.js +21 -1
- package/dist/sessionInteractions/grokFolderTrust.d.ts +0 -1
- package/dist/sessionInteractions/grokFolderTrust.js +0 -1
- package/dist/sessionInteractions/grokPlanReview.d.ts +16 -0
- package/dist/sessionInteractions/grokPlanReview.js +63 -0
- package/dist/sessionInteractions/request.d.ts +15 -7
- package/dist/sessionInteractions/request.js +46 -18
- package/package.json +4 -4
package/dist/session/session.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
298
|
+
turnId,
|
|
212
299
|
mapping: new TurnMapping({
|
|
213
300
|
sessionId: this.input.context.sessionId,
|
|
214
|
-
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.
|
|
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.
|
|
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 (
|
|
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();
|
package/dist/session/turn.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
48
|
-
const
|
|
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:
|
|
58
|
-
|
|
59
|
-
|
|
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 === "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|