@agen-ai/agent-runtime 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 (37) hide show
  1. package/README.md +154 -71
  2. package/dist/adapterValidation.d.ts +4 -2
  3. package/dist/adapterValidation.js +108 -59
  4. package/dist/contractErrors.d.ts +1 -1
  5. package/dist/contractErrors.js +7 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/nativeContinuations/admission.d.ts +17 -0
  9. package/dist/nativeContinuations/admission.js +250 -0
  10. package/dist/nativeContinuations/index.d.ts +2 -0
  11. package/dist/nativeContinuations/index.js +1 -0
  12. package/dist/nativeContinuations/types.d.ts +52 -0
  13. package/dist/nativeContinuations/types.js +0 -0
  14. package/dist/outputValidation.js +1 -0
  15. package/dist/providerCatalog.d.ts +1 -1
  16. package/dist/providerDriver.d.ts +4 -1
  17. package/dist/providerInstanceRegistry.d.ts +4 -4
  18. package/dist/sessionAdmission.d.ts +12 -0
  19. package/dist/sessionAdmission.js +54 -0
  20. package/dist/sessionInteractions/lifetime.d.ts +32 -3
  21. package/dist/sessionInteractions/lifetime.js +145 -13
  22. package/dist/sessionInteractions/request.d.ts +3 -2
  23. package/dist/sessionInteractions/request.js +98 -25
  24. package/dist/sessionInteractions/types.d.ts +68 -12
  25. package/dist/sessionInteractions/validation.d.ts +13 -9
  26. package/dist/sessionInteractions/validation.js +121 -60
  27. package/dist/sessionValidation.d.ts +78 -2
  28. package/dist/sessionValidation.js +213 -162
  29. package/dist/sessions.d.ts +33 -2
  30. package/dist/testing/conformance.d.ts +3 -1
  31. package/dist/testing/conformance.js +180 -85
  32. package/dist/testing/fakeProvider.js +2 -2
  33. package/dist/turnExecution/execution.d.ts +23 -6
  34. package/dist/turnExecution/execution.js +136 -32
  35. package/dist/turnExecution/requests.d.ts +15 -3
  36. package/dist/turnExecution/requests.js +93 -28
  37. 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
- this.facade = Object.freeze({ limits: this.limits, onRequest: (candidate) => this.offer(candidate) });
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.limits.requestTimeoutMs);
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 callback;
114
- try {
115
- callback = Promise.resolve(this.context.onRequest(request.handle));
116
- } catch (cause) {
117
- callback = Promise.reject(cause);
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) return void 0;
237
- if (input.support.kind === "unsupported" || !input.support.openingOperations.includes(input.operation)) {
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
- return new SessionInteractionOwner(input.providerKey, input.sessionId, input.context, input.signal);
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 CapturedSessionInteractionCandidate } from "./validation.js";
4
- type Candidate = CapturedSessionInteractionCandidate & Pick<AgentSessionInteraction, "request">;
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 { parseAgentSessionDecisionResponseFor } from "@agen-ai/agent-protocol";
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.choiceId !== attempt.choiceId || attempt.actualReceipt?.status !== "delivered") {
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 === "pending") this.finalize(this.stopped, void 0, nativeRetirementExpected);
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.choiceId !== attempt.choiceId || result.status !== "delivered";
189
- if (result.status === "delivered") return true;
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" || !hasExactOwnKeys(input, Object.hasOwn(input, "signal") ? ["choiceId", "signal"] : ["choiceId"])) {
195
- throw new TypeError("Session interaction response must contain a choice and an optional AbortSignal.");
196
- }
197
- const responseSignal = input.signal;
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 (Date.parse(this.handle.request.expiresAt) <= Date.now()) this.expire();
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 = { choiceId: response.choiceId, deadline: Date.now() + this.owner.limits.responseTimeoutMs };
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
- try {
214
- entered = Promise.resolve(this.candidate.respond({ ...response, signal }));
215
- } catch (cause) {
216
- entered = Promise.reject(cause);
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", choiceId: attempt.choiceId }), attempt);
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, AgentSessionDecisionChoiceId, AgentSessionDecisionRequest, AgentSessionDecisionResponse } from "@agen-ai/agent-protocol";
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
- openingOperations: readonly [AgentSessionInteractionOpeningOperation, ...AgentSessionInteractionOpeningOperation[]];
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 interface AgentSessionDecisionResponseInput extends AgentSessionDecisionResponse {
21
- readonly signal?: AbortSignal;
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
- choiceId: AgentSessionDecisionChoiceId;
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: AgentSessionDecisionRequest;
76
+ readonly request: AgentSessionRequest;
77
+ readonly deadline: AgentSessionInteractionDeadline;
44
78
  readonly signal: AbortSignal;
45
79
  readonly terminal: Promise<AgentSessionInteractionTerminal>;
46
- readonly respond: (input: AgentSessionDecisionResponseInput) => Promise<AgentRequestResolutionResult>;
80
+ readonly respond: (input: AgentSessionResponseInput) => Promise<AgentRequestResolutionResult>;
47
81
  }
48
82
  export type AgentSessionInteractionHandler = (interaction: AgentSessionInteraction) => MaybePromise<void>;
49
- export interface AgentSessionInteractionContext {
50
- readonly onRequest: AgentSessionInteractionHandler;
51
- readonly limits?: Partial<AgentSessionInteractionLimits>;
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