@agen-ai/agent-runtime 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 +176 -74
- package/dist/adapterValidation.d.ts +4 -2
- package/dist/adapterValidation.js +108 -59
- package/dist/contractErrors.d.ts +1 -1
- package/dist/contractErrors.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/nativeContinuations/admission.d.ts +17 -0
- package/dist/nativeContinuations/admission.js +250 -0
- package/dist/nativeContinuations/index.d.ts +2 -0
- package/dist/nativeContinuations/index.js +1 -0
- package/dist/nativeContinuations/types.d.ts +52 -0
- package/dist/nativeContinuations/types.js +0 -0
- package/dist/outputValidation.js +1 -0
- package/dist/providerCatalog.d.ts +1 -1
- package/dist/providerDriver.d.ts +4 -1
- package/dist/providerInstanceRegistry.d.ts +4 -4
- package/dist/sessionAdmission.d.ts +14 -0
- package/dist/sessionAdmission.js +67 -0
- package/dist/sessionInteractions/lifetime.d.ts +32 -3
- package/dist/sessionInteractions/lifetime.js +145 -13
- package/dist/sessionInteractions/request.d.ts +3 -2
- package/dist/sessionInteractions/request.js +98 -25
- package/dist/sessionInteractions/types.d.ts +68 -12
- package/dist/sessionInteractions/validation.d.ts +13 -9
- package/dist/sessionInteractions/validation.js +121 -60
- package/dist/sessionValidation.d.ts +80 -2
- package/dist/sessionValidation.js +348 -283
- package/dist/sessions.d.ts +33 -2
- package/dist/testing/conformance.d.ts +3 -1
- package/dist/testing/conformance.js +180 -85
- package/dist/testing/fakeProvider.js +2 -2
- package/dist/turnExecution/execution.d.ts +23 -6
- package/dist/turnExecution/execution.js +136 -32
- package/dist/turnExecution/requests.d.ts +15 -3
- package/dist/turnExecution/requests.js +93 -28
- package/package.json +2 -2
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
captureSessionInteractionCandidate,
|
|
5
5
|
interactionContractError,
|
|
6
6
|
validateSessionInteractionCandidate,
|
|
7
|
-
validateSessionInteractionContext
|
|
7
|
+
validateSessionInteractionContext,
|
|
8
|
+
validateNativeContinuationContext
|
|
8
9
|
} from "./validation.js";
|
|
9
10
|
function sessionInteractionError(code, message, uncertain = false) {
|
|
10
11
|
return Object.freeze({
|
|
@@ -27,9 +28,15 @@ function waitForInteractionWork(work, timeoutMs) {
|
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
class SessionInteractionOwner {
|
|
30
|
-
constructor(providerKey, sessionId, context, signal) {
|
|
31
|
+
constructor(providerKey, sessionId, context, validationOwner, operation, support, nativeSupport, continuations, signal) {
|
|
31
32
|
this.providerKey = providerKey;
|
|
32
33
|
this.sessionId = sessionId;
|
|
34
|
+
this.validationOwner = validationOwner;
|
|
35
|
+
this.operation = operation;
|
|
36
|
+
this.support = support;
|
|
37
|
+
this.nativeSupport = nativeSupport;
|
|
38
|
+
this.admission = validationOwner.sessionAdmission;
|
|
39
|
+
this.continuations = validateNativeContinuationContext(continuations);
|
|
33
40
|
try {
|
|
34
41
|
this.context = validateSessionInteractionContext(context);
|
|
35
42
|
} catch (cause) {
|
|
@@ -45,7 +52,9 @@ class SessionInteractionOwner {
|
|
|
45
52
|
this.failure = new Promise((resolve) => {
|
|
46
53
|
this.resolveFailure = resolve;
|
|
47
54
|
});
|
|
48
|
-
|
|
55
|
+
const { requestTimeoutMs: _timeout, ...nonHumanLimits } = this.limits;
|
|
56
|
+
const onRequest = (candidate) => this.offer(candidate);
|
|
57
|
+
this.facade = this.context.humanWait === "bounded" ? Object.freeze({ humanWait: Object.freeze({ kind: "bounded" }), limits: this.limits, onRequest }) : Object.freeze({ humanWait: Object.freeze({ kind: "until_withdrawn_or_closed" }), limits: Object.freeze(nonHumanLimits), onRequest });
|
|
49
58
|
if (signal) {
|
|
50
59
|
const aborted = () => this.fail(signal.reason ?? new DOMException("Session opening was aborted.", "AbortError"));
|
|
51
60
|
signal.addEventListener("abort", aborted, { once: true });
|
|
@@ -53,6 +62,7 @@ class SessionInteractionOwner {
|
|
|
53
62
|
if (signal.aborted) aborted();
|
|
54
63
|
}
|
|
55
64
|
}
|
|
65
|
+
admission;
|
|
56
66
|
facade;
|
|
57
67
|
openingSignal;
|
|
58
68
|
limits;
|
|
@@ -71,8 +81,18 @@ class SessionInteractionOwner {
|
|
|
71
81
|
cleanupAttempt = null;
|
|
72
82
|
cleanupDeadline = 0;
|
|
73
83
|
cleanupComplete = false;
|
|
84
|
+
retainedCleanup;
|
|
74
85
|
cleanupFailure;
|
|
75
86
|
adopted = false;
|
|
87
|
+
bound = false;
|
|
88
|
+
returned = false;
|
|
89
|
+
waiting = /* @__PURE__ */ new Map();
|
|
90
|
+
associationTimer;
|
|
91
|
+
prebindingCount = 0;
|
|
92
|
+
prebindingBytes = 0;
|
|
93
|
+
preparations = /* @__PURE__ */ new Set();
|
|
94
|
+
stoppedExecution = null;
|
|
95
|
+
continuations;
|
|
76
96
|
context;
|
|
77
97
|
track(promise) {
|
|
78
98
|
this.work.add(promise);
|
|
@@ -84,6 +104,7 @@ class SessionInteractionOwner {
|
|
|
84
104
|
}
|
|
85
105
|
requireUsable() {
|
|
86
106
|
if (this.fatal) throw this.fatal.cause;
|
|
107
|
+
this.validationOwner.requireUsable();
|
|
87
108
|
if (!this.accepting) throw this.fenceCause ?? new AgentProviderContractError(
|
|
88
109
|
this.providerKey,
|
|
89
110
|
this.state === "closed" ? "session_closed" : "session_unusable",
|
|
@@ -96,13 +117,18 @@ class SessionInteractionOwner {
|
|
|
96
117
|
captured = captureSessionInteractionCandidate(candidate, (terminal) => {
|
|
97
118
|
this.track(terminal);
|
|
98
119
|
});
|
|
99
|
-
const validated = validateSessionInteractionCandidate(captured, this.sessionId, this.
|
|
120
|
+
const validated = validateSessionInteractionCandidate(captured, this.sessionId, this.context);
|
|
100
121
|
this.requireUsable();
|
|
122
|
+
if (this.support.kind !== "supported" || !this.support.mappings.some((entry) => entry.openingOperation === this.operation && entry.requestKind === validated.request.kind)) {
|
|
123
|
+
throw interactionContractError(this.providerKey, "Provider offered an undeclared session request kind.");
|
|
124
|
+
}
|
|
125
|
+
if (validated.kind === "prepared") this.requireContinuation(validated.request.kind);
|
|
101
126
|
if (validated.signal.aborted) throw interactionContractError(this.providerKey, "Provider offered an unavailable session interaction.");
|
|
102
127
|
const bytes = Buffer.byteLength(JSON.stringify(validated.request), "utf8");
|
|
103
128
|
if (this.requests.has(validated.request.requestId) || this.requests.size >= this.limits.maxRetainedRequests || this.retainedBytes + bytes > this.limits.maxRetainedBytes) {
|
|
104
129
|
throw interactionContractError(this.providerKey, "Session interaction identity or retained capacity is unavailable.");
|
|
105
130
|
}
|
|
131
|
+
this.admission.claimRequest(validated.request.requestId, "invalid_session_interaction");
|
|
106
132
|
const request = new SessionInteractionRequest(this, validated, () => {
|
|
107
133
|
this.requests.delete(validated.request.requestId);
|
|
108
134
|
this.retainedBytes -= bytes;
|
|
@@ -110,15 +136,45 @@ class SessionInteractionOwner {
|
|
|
110
136
|
this.requests.set(validated.request.requestId, request);
|
|
111
137
|
this.retainedBytes += bytes;
|
|
112
138
|
request.observe();
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
139
|
+
let resolveCallback;
|
|
140
|
+
let rejectCallback;
|
|
141
|
+
const callback = new Promise((resolve, reject) => {
|
|
142
|
+
resolveCallback = resolve;
|
|
143
|
+
rejectCallback = reject;
|
|
144
|
+
});
|
|
145
|
+
const present = () => {
|
|
146
|
+
this.waiting.delete(present);
|
|
147
|
+
if (!this.accepting || request.handle.signal.aborted) {
|
|
148
|
+
resolveCallback();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
Promise.resolve(this.context.onRequest(request.handle)).then(resolveCallback, rejectCallback);
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
rejectCallback(cause);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
119
157
|
request.setCallback(callback);
|
|
120
158
|
this.track(callback);
|
|
121
159
|
void callback.catch((cause) => this.fail(cause));
|
|
160
|
+
if (validated.kind === "direct" || this.canPresent(validated.request.kind)) present();
|
|
161
|
+
else {
|
|
162
|
+
this.waiting.set(present, validated.request.kind);
|
|
163
|
+
if (!this.bound) {
|
|
164
|
+
this.prebindingCount += 1;
|
|
165
|
+
this.prebindingBytes += bytes;
|
|
166
|
+
if (this.prebindingCount > 16 || this.prebindingBytes > 1048576) throw new AgentProviderContractError(
|
|
167
|
+
this.providerKey,
|
|
168
|
+
"native_continuation_limit_exceeded",
|
|
169
|
+
"Prebinding association exceeded its retained material budget."
|
|
170
|
+
);
|
|
171
|
+
this.associationTimer ??= setTimeout(() => this.fail(new AgentProviderContractError(
|
|
172
|
+
this.providerKey,
|
|
173
|
+
"invalid_native_continuation",
|
|
174
|
+
"Prebinding association did not finish before its deadline."
|
|
175
|
+
)), 3e4);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
122
178
|
return callback;
|
|
123
179
|
} catch (cause) {
|
|
124
180
|
const failure = cause instanceof AgentProviderContractError ? cause : new AgentProviderContractError(
|
|
@@ -134,8 +190,48 @@ class SessionInteractionOwner {
|
|
|
134
190
|
return rejected;
|
|
135
191
|
}
|
|
136
192
|
}
|
|
193
|
+
requireContinuation(kind) {
|
|
194
|
+
if (!this.continuations || this.nativeSupport.kind !== "supported" || !this.nativeSupport.mappings.some((entry) => entry.openingOperation === this.operation && entry.requestKind === kind)) {
|
|
195
|
+
throw new AgentProviderContractError(this.providerKey, "native_continuations_unsupported", "Provider native continuation requires its exact supported tuple and a host output handler.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
canPresent(kind) {
|
|
199
|
+
return this.bound && this.nativeSupport.kind === "supported" && this.nativeSupport.mappings.some((entry) => entry.openingOperation === this.operation && entry.requestKind === kind && (entry.timing !== "after_open_return" || this.returned));
|
|
200
|
+
}
|
|
201
|
+
bind() {
|
|
202
|
+
this.requireUsable();
|
|
203
|
+
this.bound = true;
|
|
204
|
+
if (this.associationTimer) clearTimeout(this.associationTimer);
|
|
205
|
+
this.prebindingCount = 0;
|
|
206
|
+
this.prebindingBytes = 0;
|
|
207
|
+
this.presentWaiting();
|
|
208
|
+
}
|
|
209
|
+
markReturned() {
|
|
210
|
+
this.returned = true;
|
|
211
|
+
this.presentWaiting();
|
|
212
|
+
}
|
|
213
|
+
presentWaiting() {
|
|
214
|
+
for (const [present, kind] of this.waiting) if (!this.accepting || this.canPresent(kind)) present();
|
|
215
|
+
}
|
|
216
|
+
async cleanupFailedPreparation() {
|
|
217
|
+
if (this.cleanupComplete) return;
|
|
218
|
+
if (this.retainedCleanup) return this.close(this.retainedCleanup, false);
|
|
219
|
+
if (this.automaticClose) return this.automaticClose();
|
|
220
|
+
throw new Error("Opening cleanup has not acquired its native session owner.");
|
|
221
|
+
}
|
|
222
|
+
quiesce() {
|
|
223
|
+
if (this.adopted && this.automaticClose) void this.automaticClose().catch(() => {
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
retainPreparation(cleanup) {
|
|
227
|
+
this.preparations.add(cleanup);
|
|
228
|
+
return () => {
|
|
229
|
+
this.preparations.delete(cleanup);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
137
232
|
installClose(close) {
|
|
138
233
|
this.automaticClose = close;
|
|
234
|
+
this.validationOwner.installClose(close);
|
|
139
235
|
}
|
|
140
236
|
adopt() {
|
|
141
237
|
this.requireUsable();
|
|
@@ -145,6 +241,7 @@ class SessionInteractionOwner {
|
|
|
145
241
|
}
|
|
146
242
|
fence(cause, source) {
|
|
147
243
|
if (!this.accepting) return;
|
|
244
|
+
this.validationOwner.fence();
|
|
148
245
|
this.fenceCause = new AgentProviderContractError(
|
|
149
246
|
this.providerKey,
|
|
150
247
|
"session_unusable",
|
|
@@ -179,15 +276,23 @@ class SessionInteractionOwner {
|
|
|
179
276
|
}
|
|
180
277
|
stop(terminal = Object.freeze({ kind: "session_closed" })) {
|
|
181
278
|
if (this.state !== "closed" && this.state !== "cleanup-pending") this.state = "stopping";
|
|
279
|
+
this.stoppedExecution ??= this.validationOwner.activeTurn;
|
|
280
|
+
this.validationOwner.stop();
|
|
281
|
+
this.stoppedExecution?.owner.stop(new DOMException("The session interaction owner was stopped.", "AbortError"));
|
|
182
282
|
this.removeOpeningAbort();
|
|
283
|
+
if (this.associationTimer) clearTimeout(this.associationTimer);
|
|
284
|
+
this.presentWaiting();
|
|
183
285
|
this.controller.abort(new DOMException("The session interaction owner was stopped.", "AbortError"));
|
|
184
286
|
for (const request of this.requests.values()) request.stop(terminal);
|
|
185
287
|
}
|
|
186
288
|
async join() {
|
|
289
|
+
await Promise.all([...this.preparations].map((cleanup) => cleanup()));
|
|
187
290
|
while (this.work.size > 0) await Promise.allSettled([...this.work]);
|
|
188
291
|
await Promise.all([...this.requests.values()].map((request) => request.join()));
|
|
292
|
+
await this.stoppedExecution?.owner.join();
|
|
189
293
|
}
|
|
190
294
|
close(cleanup, explicit) {
|
|
295
|
+
this.retainedCleanup ??= cleanup;
|
|
191
296
|
this.stop();
|
|
192
297
|
if (!this.cleanupComplete && !this.cleanupAttempt) {
|
|
193
298
|
this.state = "cleanup-pending";
|
|
@@ -233,15 +338,42 @@ class SessionInteractionOwner {
|
|
|
233
338
|
}
|
|
234
339
|
}
|
|
235
340
|
function createSessionInteractionOwner(input) {
|
|
236
|
-
if (input.context === void 0)
|
|
237
|
-
|
|
341
|
+
if (input.context === void 0) {
|
|
342
|
+
if (input.continuations !== void 0) throw new AgentProviderContractError(input.providerKey, "native_continuations_unsupported", "Native continuations require session interaction handlers.");
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
if (input.support.kind === "unsupported" || !input.support.mappings.some((entry) => entry.openingOperation === input.operation)) {
|
|
238
346
|
throw new AgentProviderContractError(
|
|
239
347
|
input.providerKey,
|
|
240
348
|
"session_interactions_unsupported",
|
|
241
349
|
"Provider does not support session interactions for this opening operation."
|
|
242
350
|
);
|
|
243
351
|
}
|
|
244
|
-
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = validateSessionInteractionContext(input.context);
|
|
355
|
+
} catch (cause) {
|
|
356
|
+
throw new AgentProviderContractError(input.providerKey, "invalid_session_interaction", "Session interaction context is invalid.", { cause });
|
|
357
|
+
}
|
|
358
|
+
if (!input.support.humanWait.includes(parsed.humanWait)) throw new AgentProviderContractError(
|
|
359
|
+
input.providerKey,
|
|
360
|
+
"session_interactions_unsupported",
|
|
361
|
+
"Provider does not support the selected human wait policy."
|
|
362
|
+
);
|
|
363
|
+
if (input.continuations !== void 0 && (input.nativeSupport.kind !== "supported" || !input.nativeSupport.mappings.some((entry) => entry.openingOperation === input.operation))) {
|
|
364
|
+
throw new AgentProviderContractError(input.providerKey, "native_continuations_unsupported", "Provider does not support continuations for this opening operation.");
|
|
365
|
+
}
|
|
366
|
+
return new SessionInteractionOwner(
|
|
367
|
+
input.providerKey,
|
|
368
|
+
input.sessionId,
|
|
369
|
+
input.context,
|
|
370
|
+
input.validationOwner,
|
|
371
|
+
input.operation,
|
|
372
|
+
input.support,
|
|
373
|
+
input.nativeSupport,
|
|
374
|
+
input.continuations,
|
|
375
|
+
input.signal
|
|
376
|
+
);
|
|
245
377
|
}
|
|
246
378
|
export {
|
|
247
379
|
SessionInteractionOwner,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SessionInteractionOwner } from "./lifetime.js";
|
|
2
2
|
import type { AgentSessionInteraction, AgentSessionInteractionTerminal } from "./types.js";
|
|
3
|
-
import { type
|
|
4
|
-
type Candidate =
|
|
3
|
+
import { type ValidatedSessionInteractionCandidate } from "./validation.js";
|
|
4
|
+
type Candidate = ValidatedSessionInteractionCandidate;
|
|
5
5
|
export declare class SessionInteractionRequest {
|
|
6
6
|
private readonly owner;
|
|
7
7
|
private readonly candidate;
|
|
@@ -25,6 +25,7 @@ export declare class SessionInteractionRequest {
|
|
|
25
25
|
constructor(owner: SessionInteractionOwner, candidate: Candidate, retire: () => void);
|
|
26
26
|
observe(): void;
|
|
27
27
|
setCallback(callback: Promise<void>): void;
|
|
28
|
+
private scheduleExpiry;
|
|
28
29
|
private expire;
|
|
29
30
|
private protocolFailure;
|
|
30
31
|
private observeTerminal;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseAgentSessionResponseFor } from "@agen-ai/agent-protocol";
|
|
2
2
|
import { throwIfAgentOperationAborted } from "../foundation.js";
|
|
3
3
|
import { AgentProviderContractError } from "../contractErrors.js";
|
|
4
|
-
import { hasExactOwnKeys } from "../internal/objectShape.js";
|
|
5
4
|
import { validateAgentRequestResolutionResult } from "../turnExecution/receipts.js";
|
|
6
5
|
import { sessionInteractionError } from "./lifetime.js";
|
|
6
|
+
import { dispatchPreparedSessionResponse, mintSessionResponseAttemptId } from "../nativeContinuations/admission.js";
|
|
7
7
|
import { interactionContractError, validateSessionInteractionTerminal } from "./validation.js";
|
|
8
|
+
const MAX_EXPIRY_TIMER_DELAY_MS = 2147483647;
|
|
8
9
|
class SessionInteractionRequest {
|
|
9
10
|
constructor(owner, candidate, retire) {
|
|
10
11
|
this.owner = owner;
|
|
@@ -15,6 +16,7 @@ class SessionInteractionRequest {
|
|
|
15
16
|
});
|
|
16
17
|
this.handle = Object.freeze({
|
|
17
18
|
request: candidate.request,
|
|
19
|
+
deadline: candidate.deadline,
|
|
18
20
|
signal: this.presentation.signal,
|
|
19
21
|
terminal,
|
|
20
22
|
respond: (input) => this.respond(input)
|
|
@@ -42,7 +44,6 @@ class SessionInteractionRequest {
|
|
|
42
44
|
};
|
|
43
45
|
observe() {
|
|
44
46
|
this.candidate.signal.addEventListener("abort", this.nativeAborted, { once: true });
|
|
45
|
-
this.expiryTimer = setTimeout(() => this.expire(), Math.max(0, Date.parse(this.handle.request.expiresAt) - Date.now()));
|
|
46
47
|
void this.candidate.terminal.then((candidate) => {
|
|
47
48
|
this.rawTerminalSettled = true;
|
|
48
49
|
if (this.terminalTimer) clearTimeout(this.terminalTimer);
|
|
@@ -60,6 +61,7 @@ class SessionInteractionRequest {
|
|
|
60
61
|
this.maybeRetire();
|
|
61
62
|
});
|
|
62
63
|
if (this.candidate.signal.aborted) this.nativeAborted();
|
|
64
|
+
this.scheduleExpiry();
|
|
63
65
|
}
|
|
64
66
|
setCallback(callback) {
|
|
65
67
|
this.callback = callback;
|
|
@@ -74,9 +76,21 @@ class SessionInteractionRequest {
|
|
|
74
76
|
}
|
|
75
77
|
);
|
|
76
78
|
}
|
|
79
|
+
scheduleExpiry() {
|
|
80
|
+
if (this.candidate.deadlineAt === void 0 || this.state.kind === "terminal") return;
|
|
81
|
+
const remainingMs = this.candidate.deadlineAt - Date.now();
|
|
82
|
+
if (remainingMs <= 0) {
|
|
83
|
+
this.expire();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.expiryTimer = setTimeout(() => {
|
|
87
|
+
this.expiryTimer = void 0;
|
|
88
|
+
this.scheduleExpiry();
|
|
89
|
+
}, Math.min(remainingMs, MAX_EXPIRY_TIMER_DELAY_MS));
|
|
90
|
+
}
|
|
77
91
|
expire() {
|
|
78
92
|
if (this.state.kind === "terminal") return;
|
|
79
|
-
this.stop(Object.freeze({ kind: "expired" }));
|
|
93
|
+
this.stop(Object.freeze({ kind: "expired", source: this.candidate.deadline.kind === "deadline" ? this.candidate.deadline.source : "provider" }));
|
|
80
94
|
this.owner.fail(interactionContractError(this.owner.providerKey, "Session interaction expired before final delivery."));
|
|
81
95
|
}
|
|
82
96
|
protocolFailure(cause) {
|
|
@@ -108,6 +122,7 @@ class SessionInteractionRequest {
|
|
|
108
122
|
this.stopped ??= terminal;
|
|
109
123
|
this.unavailable = true;
|
|
110
124
|
this.presentation.abort(new DOMException("The provider interaction is no longer answerable.", "AbortError"));
|
|
125
|
+
this.finalize(this.stopped, attempt);
|
|
111
126
|
}
|
|
112
127
|
if (attempt && !attempt.actualReceipt) return;
|
|
113
128
|
if (attempt?.actualReceipt && this.receiptContradiction(attempt, attempt.actualReceipt)) {
|
|
@@ -117,7 +132,7 @@ class SessionInteractionRequest {
|
|
|
117
132
|
);
|
|
118
133
|
}
|
|
119
134
|
if (terminal.kind === "answered") {
|
|
120
|
-
if (!attempt || terminal.
|
|
135
|
+
if (!attempt || JSON.stringify(terminal.response) !== JSON.stringify(attempt.response) || attempt.actualReceipt?.status !== "delivered") {
|
|
121
136
|
throw interactionContractError(this.owner.providerKey, "Provider interaction acceptance disagrees with its delivery receipt.");
|
|
122
137
|
}
|
|
123
138
|
this.finalize(terminal, attempt);
|
|
@@ -151,7 +166,7 @@ class SessionInteractionRequest {
|
|
|
151
166
|
this.stopped ??= terminal;
|
|
152
167
|
this.unavailable = true;
|
|
153
168
|
this.presentation.abort(new DOMException("The session interaction was stopped.", "AbortError"));
|
|
154
|
-
if (this.state.kind
|
|
169
|
+
if (this.state.kind !== "terminal") this.finalize(this.stopped, void 0, nativeRetirementExpected);
|
|
155
170
|
if (nativeRetirementExpected) this.expectTerminal();
|
|
156
171
|
}
|
|
157
172
|
refusal(active = false) {
|
|
@@ -179,45 +194,86 @@ class SessionInteractionRequest {
|
|
|
179
194
|
"Session interaction response delivery failed after provider entry.",
|
|
180
195
|
{ cause }
|
|
181
196
|
), this);
|
|
197
|
+
if (this.candidate.kind === "prepared") this.owner.quiesce();
|
|
182
198
|
return receipt;
|
|
183
199
|
}
|
|
184
200
|
receiptContradiction(attempt, result) {
|
|
185
201
|
const terminal = this.nativeTerminal;
|
|
186
202
|
if (!terminal) return false;
|
|
187
203
|
if (result.status === "delivery_uncertain") return terminal.kind === "rejected";
|
|
188
|
-
if (terminal.kind === "answered") return terminal.
|
|
189
|
-
if (result.status === "delivered") return
|
|
204
|
+
if (terminal.kind === "answered") return JSON.stringify(terminal.response) !== JSON.stringify(attempt.response) || result.status !== "delivered";
|
|
205
|
+
if (result.status === "delivered") return terminal.kind === "rejected" || terminal.kind === "delivery_uncertain";
|
|
190
206
|
if (terminal.kind === "delivery_uncertain") return true;
|
|
191
207
|
return result.requestState === "pending" && terminal.kind === "rejected";
|
|
192
208
|
}
|
|
193
209
|
async respond(input) {
|
|
194
|
-
if (input === null || typeof input !== "object" ||
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
if (responseSignal !== void 0 && !(responseSignal instanceof AbortSignal)) {
|
|
199
|
-
throw new TypeError("Session interaction response signal must be an AbortSignal.");
|
|
200
|
-
}
|
|
201
|
-
const response = parseAgentSessionDecisionResponseFor(this.handle.request, { choiceId: input.choiceId });
|
|
210
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) throw new TypeError("Session response must be an object.");
|
|
211
|
+
const { signal: responseSignal, ...rawResponse } = input;
|
|
212
|
+
if (responseSignal !== void 0 && !(responseSignal instanceof AbortSignal)) throw new TypeError("Session interaction response signal must be an AbortSignal.");
|
|
213
|
+
const response = parseAgentSessionResponseFor(this.handle.request, rawResponse);
|
|
202
214
|
throwIfAgentOperationAborted(responseSignal);
|
|
203
215
|
const deadline = new AbortController();
|
|
204
216
|
const signal = AbortSignal.any([this.presentation.signal, deadline.signal, ...responseSignal ? [responseSignal] : []]);
|
|
205
|
-
if (
|
|
217
|
+
if (this.candidate.deadlineAt !== void 0 && this.candidate.deadlineAt <= Date.now()) this.expire();
|
|
206
218
|
if (this.unavailable || !this.owner.accepting || this.state.kind === "terminal") return this.refusal();
|
|
207
219
|
if (this.state.kind === "answering") return this.refusal(true);
|
|
208
|
-
const attempt = {
|
|
220
|
+
const attempt = { response: projectResponse(response), deadline: Date.now() + this.owner.limits.responseTimeoutMs, entered: false };
|
|
209
221
|
this.lastAttempt = attempt;
|
|
210
222
|
this.state = { kind: "answering", attempt };
|
|
211
223
|
this.responseWork += 1;
|
|
212
224
|
let entered;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
const requireCurrent = () => {
|
|
226
|
+
if (this.state.kind !== "answering" || this.state.attempt !== attempt || this.unavailable) throw new DOMException("Session response is no longer eligible.", "AbortError");
|
|
227
|
+
if (this.candidate.deadlineAt !== void 0 && this.candidate.deadlineAt <= Date.now()) {
|
|
228
|
+
this.expire();
|
|
229
|
+
throw new DOMException("Session response expired before entry.", "AbortError");
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
if (this.candidate.kind === "prepared") {
|
|
233
|
+
entered = dispatchPreparedSessionResponse({
|
|
234
|
+
owner: this.owner,
|
|
235
|
+
candidate: this.candidate,
|
|
236
|
+
response,
|
|
237
|
+
attemptId: mintSessionResponseAttemptId(),
|
|
238
|
+
signal,
|
|
239
|
+
requireCurrent,
|
|
240
|
+
onDispatch: (deadlineAt) => {
|
|
241
|
+
attempt.entered = true;
|
|
242
|
+
attempt.deadline = deadlineAt;
|
|
243
|
+
},
|
|
244
|
+
observeActualReceipt: (work) => {
|
|
245
|
+
this.responseWork += 1;
|
|
246
|
+
const actual2 = this.owner.track(work.then((result2) => {
|
|
247
|
+
attempt.actualReceipt = result2;
|
|
248
|
+
try {
|
|
249
|
+
this.observeTerminal();
|
|
250
|
+
} catch (cause) {
|
|
251
|
+
this.protocolFailure(cause);
|
|
252
|
+
}
|
|
253
|
+
}));
|
|
254
|
+
void actual2.then(
|
|
255
|
+
() => {
|
|
256
|
+
this.responseWork -= 1;
|
|
257
|
+
this.maybeRetire();
|
|
258
|
+
},
|
|
259
|
+
() => {
|
|
260
|
+
this.responseWork -= 1;
|
|
261
|
+
this.maybeRetire();
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
attempt.entered = true;
|
|
268
|
+
try {
|
|
269
|
+
entered = Promise.resolve(this.candidate.respond({ ...response, signal }));
|
|
270
|
+
} catch (cause) {
|
|
271
|
+
entered = Promise.reject(cause);
|
|
272
|
+
}
|
|
217
273
|
}
|
|
218
274
|
const actual = this.owner.track(entered.then((candidate) => {
|
|
219
275
|
const result2 = validateAgentRequestResolutionResult(candidate, this.owner.providerKey);
|
|
220
|
-
attempt.actualReceipt = result2;
|
|
276
|
+
if (this.candidate.kind === "direct") attempt.actualReceipt = result2;
|
|
221
277
|
if (this.state.kind === "terminal") {
|
|
222
278
|
try {
|
|
223
279
|
this.observeTerminal();
|
|
@@ -240,6 +296,7 @@ class SessionInteractionRequest {
|
|
|
240
296
|
);
|
|
241
297
|
let timer;
|
|
242
298
|
const timeout = new Promise((resolve) => {
|
|
299
|
+
if (this.candidate.kind === "prepared") return;
|
|
243
300
|
timer = setTimeout(
|
|
244
301
|
() => {
|
|
245
302
|
deadline.abort(new DOMException("Session interaction response timed out.", "TimeoutError"));
|
|
@@ -253,6 +310,11 @@ class SessionInteractionRequest {
|
|
|
253
310
|
try {
|
|
254
311
|
result = await Promise.race([actual, timeout]);
|
|
255
312
|
} catch (cause) {
|
|
313
|
+
if (!attempt.entered) {
|
|
314
|
+
if (this.state.kind === "answering" && this.state.attempt === attempt) this.state = { kind: "pending" };
|
|
315
|
+
this.lastAttempt = void 0;
|
|
316
|
+
throw cause;
|
|
317
|
+
}
|
|
256
318
|
responseFailure = cause;
|
|
257
319
|
result = null;
|
|
258
320
|
}
|
|
@@ -270,12 +332,12 @@ class SessionInteractionRequest {
|
|
|
270
332
|
if (result.status === "delivery_uncertain") {
|
|
271
333
|
this.finalize(Object.freeze({ kind: "delivery_uncertain", error: result.error }), attempt);
|
|
272
334
|
this.owner.fence(result.error, this);
|
|
335
|
+
if (this.candidate.kind === "prepared") this.owner.quiesce();
|
|
273
336
|
} else if (result.status === "delivered") {
|
|
274
|
-
this.finalize(Object.freeze({ kind: "answered",
|
|
337
|
+
this.finalize(this.stopped ?? Object.freeze({ kind: "answered", response: attempt.response }), attempt);
|
|
275
338
|
} else if (result.requestState === "unavailable") {
|
|
276
339
|
this.finalize(this.stopped ?? Object.freeze({ kind: "rejected", error: result.error }), attempt);
|
|
277
340
|
} else if (this.stopped || this.unavailable || !this.owner.accepting) {
|
|
278
|
-
result = this.refusal();
|
|
279
341
|
if (this.stopped || !this.owner.accepting) this.finalize(this.stopped ?? Object.freeze({ kind: "session_closed" }), attempt);
|
|
280
342
|
else {
|
|
281
343
|
this.state = { kind: "pending" };
|
|
@@ -303,6 +365,17 @@ class SessionInteractionRequest {
|
|
|
303
365
|
await Promise.allSettled([this.candidate.terminal, ...this.callback ? [this.callback] : []]);
|
|
304
366
|
}
|
|
305
367
|
}
|
|
368
|
+
function projectResponse(response) {
|
|
369
|
+
if (response.kind === "form") return Object.freeze({ kind: "form", action: response.action, choiceId: response.choiceId });
|
|
370
|
+
if (response.kind === "plan_review") return Object.freeze({
|
|
371
|
+
kind: "plan_review",
|
|
372
|
+
action: response.action,
|
|
373
|
+
choiceId: response.choiceId,
|
|
374
|
+
planId: response.planId,
|
|
375
|
+
revision: response.revision
|
|
376
|
+
});
|
|
377
|
+
return response;
|
|
378
|
+
}
|
|
306
379
|
export {
|
|
307
380
|
SessionInteractionRequest
|
|
308
381
|
};
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import type { AgentError,
|
|
1
|
+
import type { AgentError, AgentSessionAcceptedResponse, AgentSessionDecisionResponse, AgentSessionRequest, AgentSessionRequestKind, AgentSessionResponse, AgentSessionFormRequest, AgentSessionPlanReviewRequest } from "@agen-ai/agent-protocol";
|
|
2
2
|
import type { MaybePromise } from "../foundation.js";
|
|
3
3
|
import type { AgentRequestResolutionResult } from "../sessions.js";
|
|
4
|
+
import type { AgentPreparedSessionResponse, AgentSessionResponseAttemptId } from "../nativeContinuations/types.js";
|
|
4
5
|
export type AgentSessionInteractionOpeningOperation = "create" | "resume" | "branch";
|
|
6
|
+
export type AgentSessionHumanWaitKind = "bounded" | "until_withdrawn_or_closed";
|
|
7
|
+
export type AgentSessionHumanWaitPolicy = Readonly<{
|
|
8
|
+
kind: "bounded";
|
|
9
|
+
}> | Readonly<{
|
|
10
|
+
kind: "until_withdrawn_or_closed";
|
|
11
|
+
}>;
|
|
12
|
+
export interface AgentSessionInteractionMapping {
|
|
13
|
+
readonly openingOperation: AgentSessionInteractionOpeningOperation;
|
|
14
|
+
readonly requestKind: AgentSessionRequestKind;
|
|
15
|
+
}
|
|
5
16
|
export type AgentSessionInteractionSupport = Readonly<{
|
|
6
17
|
kind: "unsupported";
|
|
7
18
|
}> | Readonly<{
|
|
8
19
|
kind: "supported";
|
|
9
|
-
|
|
10
|
-
requestKinds: readonly ["decision"];
|
|
20
|
+
mappings: readonly [AgentSessionInteractionMapping, ...AgentSessionInteractionMapping[]];
|
|
11
21
|
producerOwnership: "exclusive";
|
|
22
|
+
humanWait: readonly [AgentSessionHumanWaitKind, ...AgentSessionHumanWaitKind[]];
|
|
12
23
|
}>;
|
|
13
24
|
export interface AgentSessionInteractionLimits {
|
|
14
25
|
readonly maxRetainedRequests: number;
|
|
@@ -17,12 +28,33 @@ export interface AgentSessionInteractionLimits {
|
|
|
17
28
|
readonly responseTimeoutMs: number;
|
|
18
29
|
readonly cleanupTimeoutMs: number;
|
|
19
30
|
}
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
export type AgentSessionInteractionOptions = Readonly<{
|
|
32
|
+
humanWait?: Readonly<{
|
|
33
|
+
kind: "bounded";
|
|
34
|
+
}>;
|
|
35
|
+
limits?: Partial<AgentSessionInteractionLimits>;
|
|
36
|
+
}> | Readonly<{
|
|
37
|
+
humanWait: Readonly<{
|
|
38
|
+
kind: "until_withdrawn_or_closed";
|
|
39
|
+
}>;
|
|
40
|
+
limits?: Partial<Omit<AgentSessionInteractionLimits, "requestTimeoutMs">>;
|
|
41
|
+
}>;
|
|
42
|
+
export type AgentSessionInteractionDeadline = Readonly<{
|
|
43
|
+
kind: "no_deadline";
|
|
44
|
+
}> | Readonly<{
|
|
45
|
+
kind: "deadline";
|
|
46
|
+
expiresAt: import("@agen-ai/agent-protocol").AgentIsoDateTime;
|
|
47
|
+
source: "provider" | "host_policy";
|
|
48
|
+
}>;
|
|
49
|
+
export type AgentSessionResponseInput = AgentSessionResponse & Readonly<{
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}>;
|
|
52
|
+
export type AgentSessionDecisionResponseInput = AgentSessionDecisionResponse & Readonly<{
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}>;
|
|
23
55
|
export type AgentSessionInteractionTerminal = Readonly<{
|
|
24
56
|
kind: "answered";
|
|
25
|
-
|
|
57
|
+
response: AgentSessionAcceptedResponse;
|
|
26
58
|
}> | Readonly<{
|
|
27
59
|
kind: "rejected";
|
|
28
60
|
error: AgentError;
|
|
@@ -33,6 +65,7 @@ export type AgentSessionInteractionTerminal = Readonly<{
|
|
|
33
65
|
kind: "withdrawn";
|
|
34
66
|
}> | Readonly<{
|
|
35
67
|
kind: "expired";
|
|
68
|
+
source: "provider" | "host_policy";
|
|
36
69
|
}> | Readonly<{
|
|
37
70
|
kind: "session_closed";
|
|
38
71
|
}> | Readonly<{
|
|
@@ -40,15 +73,38 @@ export type AgentSessionInteractionTerminal = Readonly<{
|
|
|
40
73
|
error: AgentError;
|
|
41
74
|
}>;
|
|
42
75
|
export interface AgentSessionInteraction {
|
|
43
|
-
readonly request:
|
|
76
|
+
readonly request: AgentSessionRequest;
|
|
77
|
+
readonly deadline: AgentSessionInteractionDeadline;
|
|
44
78
|
readonly signal: AbortSignal;
|
|
45
79
|
readonly terminal: Promise<AgentSessionInteractionTerminal>;
|
|
46
|
-
readonly respond: (input:
|
|
80
|
+
readonly respond: (input: AgentSessionResponseInput) => Promise<AgentRequestResolutionResult>;
|
|
47
81
|
}
|
|
48
82
|
export type AgentSessionInteractionHandler = (interaction: AgentSessionInteraction) => MaybePromise<void>;
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
export type AgentSessionInteractionContext = AgentSessionInteractionOptions & Readonly<{
|
|
84
|
+
onRequest: AgentSessionInteractionHandler;
|
|
85
|
+
}>;
|
|
86
|
+
export interface AgentProviderDirectSessionInteraction {
|
|
87
|
+
readonly kind: "direct";
|
|
88
|
+
readonly request: AgentSessionRequest;
|
|
89
|
+
readonly signal: AbortSignal;
|
|
90
|
+
readonly terminal: Promise<AgentSessionInteractionTerminal>;
|
|
91
|
+
readonly respond: (input: AgentSessionResponseInput) => Promise<AgentRequestResolutionResult>;
|
|
92
|
+
}
|
|
93
|
+
export interface AgentProviderPrepareSessionResponseInput {
|
|
94
|
+
readonly response: AgentSessionResponse;
|
|
95
|
+
readonly responseAttemptId: AgentSessionResponseAttemptId;
|
|
96
|
+
readonly signal: AbortSignal;
|
|
52
97
|
}
|
|
98
|
+
export interface AgentProviderPreparedSessionInteraction {
|
|
99
|
+
readonly kind: "prepared";
|
|
100
|
+
readonly request: AgentSessionFormRequest | AgentSessionPlanReviewRequest;
|
|
101
|
+
readonly signal: AbortSignal;
|
|
102
|
+
readonly terminal: Promise<AgentSessionInteractionTerminal>;
|
|
103
|
+
readonly prepareResponse: (input: AgentProviderPrepareSessionResponseInput) => MaybePromise<AgentPreparedSessionResponse>;
|
|
104
|
+
}
|
|
105
|
+
export type AgentProviderSessionInteraction = AgentProviderDirectSessionInteraction | AgentProviderPreparedSessionInteraction;
|
|
106
|
+
export type AgentProviderSessionInteractionContext = AgentSessionInteractionOptions & Readonly<{
|
|
107
|
+
onRequest: (interaction: AgentProviderSessionInteraction) => MaybePromise<void>;
|
|
108
|
+
}>;
|
|
53
109
|
export declare const DEFAULT_AGENT_SESSION_INTERACTION_LIMITS: Readonly<AgentSessionInteractionLimits>;
|
|
54
110
|
//# sourceMappingURL=types.d.ts.map
|