@agen-ai/agent-runtime 0.4.0 → 0.4.1
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 +116 -8
- package/dist/adapterValidation.js +45 -5
- package/dist/artifacts.js +0 -1
- package/dist/contractErrors.d.ts +2 -2
- package/dist/contractErrors.js +5 -7
- package/dist/hostBindings/opening.d.ts +2 -0
- package/dist/hostBindings/opening.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/outputValidation.js +2 -2
- package/dist/providerDriver.js +0 -1
- package/dist/providerInstanceRegistry.js +2 -1
- package/dist/sessionInteractions/index.d.ts +2 -0
- package/dist/sessionInteractions/index.js +1 -0
- package/dist/sessionInteractions/lifetime.d.ts +52 -0
- package/dist/sessionInteractions/lifetime.js +251 -0
- package/dist/sessionInteractions/request.d.ts +42 -0
- package/dist/sessionInteractions/request.js +308 -0
- package/dist/sessionInteractions/types.d.ts +54 -0
- package/dist/sessionInteractions/types.js +10 -0
- package/dist/sessionInteractions/validation.d.ts +21 -0
- package/dist/sessionInteractions/validation.js +119 -0
- package/dist/sessionValidation.d.ts +4 -1
- package/dist/sessionValidation.js +29 -21
- package/dist/sessions.d.ts +3 -0
- package/dist/testing/conformance.js +0 -1
- package/dist/testing/fakeProvider.js +5 -6
- package/dist/turnExecution/requests.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { AgentProviderContractError, AgentProviderSessionCleanupError } from "../contractErrors.js";
|
|
2
|
+
import { SessionInteractionRequest } from "./request.js";
|
|
3
|
+
import {
|
|
4
|
+
captureSessionInteractionCandidate,
|
|
5
|
+
interactionContractError,
|
|
6
|
+
validateSessionInteractionCandidate,
|
|
7
|
+
validateSessionInteractionContext
|
|
8
|
+
} from "./validation.js";
|
|
9
|
+
function sessionInteractionError(code, message, uncertain = false) {
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
category: uncertain ? "unknown" : "invalid_request",
|
|
12
|
+
code,
|
|
13
|
+
message,
|
|
14
|
+
recovery: Object.freeze({ kind: uncertain ? "unknown" : "none" })
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function waitForInteractionWork(work, timeoutMs) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timer = setTimeout(() => reject(new Error("Session interaction cleanup did not finish before its deadline.")), timeoutMs);
|
|
20
|
+
work.then((result) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(result);
|
|
23
|
+
}, (error) => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
reject(error);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
class SessionInteractionOwner {
|
|
30
|
+
constructor(providerKey, sessionId, context, signal) {
|
|
31
|
+
this.providerKey = providerKey;
|
|
32
|
+
this.sessionId = sessionId;
|
|
33
|
+
try {
|
|
34
|
+
this.context = validateSessionInteractionContext(context);
|
|
35
|
+
} catch (cause) {
|
|
36
|
+
throw new AgentProviderContractError(
|
|
37
|
+
providerKey,
|
|
38
|
+
"invalid_session_interaction",
|
|
39
|
+
"Session interaction context is invalid.",
|
|
40
|
+
{ cause }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
this.limits = this.context.limits;
|
|
44
|
+
this.openingSignal = this.controller.signal;
|
|
45
|
+
this.failure = new Promise((resolve) => {
|
|
46
|
+
this.resolveFailure = resolve;
|
|
47
|
+
});
|
|
48
|
+
this.facade = Object.freeze({ limits: this.limits, onRequest: (candidate) => this.offer(candidate) });
|
|
49
|
+
if (signal) {
|
|
50
|
+
const aborted = () => this.fail(signal.reason ?? new DOMException("Session opening was aborted.", "AbortError"));
|
|
51
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
52
|
+
this.removeOpeningAbort = () => signal.removeEventListener("abort", aborted);
|
|
53
|
+
if (signal.aborted) aborted();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
facade;
|
|
57
|
+
openingSignal;
|
|
58
|
+
limits;
|
|
59
|
+
failure;
|
|
60
|
+
controller = new AbortController();
|
|
61
|
+
requests = /* @__PURE__ */ new Map();
|
|
62
|
+
work = /* @__PURE__ */ new Set();
|
|
63
|
+
state = "opening";
|
|
64
|
+
retainedBytes = 0;
|
|
65
|
+
fatal;
|
|
66
|
+
fenceCause;
|
|
67
|
+
resolveFailure;
|
|
68
|
+
removeOpeningAbort = () => {
|
|
69
|
+
};
|
|
70
|
+
automaticClose;
|
|
71
|
+
cleanupAttempt = null;
|
|
72
|
+
cleanupDeadline = 0;
|
|
73
|
+
cleanupComplete = false;
|
|
74
|
+
cleanupFailure;
|
|
75
|
+
adopted = false;
|
|
76
|
+
context;
|
|
77
|
+
track(promise) {
|
|
78
|
+
this.work.add(promise);
|
|
79
|
+
void promise.then(() => this.work.delete(promise), () => this.work.delete(promise));
|
|
80
|
+
return promise;
|
|
81
|
+
}
|
|
82
|
+
get accepting() {
|
|
83
|
+
return this.state === "opening" || this.state === "open";
|
|
84
|
+
}
|
|
85
|
+
requireUsable() {
|
|
86
|
+
if (this.fatal) throw this.fatal.cause;
|
|
87
|
+
if (!this.accepting) throw this.fenceCause ?? new AgentProviderContractError(
|
|
88
|
+
this.providerKey,
|
|
89
|
+
this.state === "closed" ? "session_closed" : "session_unusable",
|
|
90
|
+
"Session interactions no longer permit provider mutation."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
offer(candidate) {
|
|
94
|
+
let captured;
|
|
95
|
+
try {
|
|
96
|
+
captured = captureSessionInteractionCandidate(candidate, (terminal) => {
|
|
97
|
+
this.track(terminal);
|
|
98
|
+
});
|
|
99
|
+
const validated = validateSessionInteractionCandidate(captured, this.sessionId, this.limits.requestTimeoutMs);
|
|
100
|
+
this.requireUsable();
|
|
101
|
+
if (validated.signal.aborted) throw interactionContractError(this.providerKey, "Provider offered an unavailable session interaction.");
|
|
102
|
+
const bytes = Buffer.byteLength(JSON.stringify(validated.request), "utf8");
|
|
103
|
+
if (this.requests.has(validated.request.requestId) || this.requests.size >= this.limits.maxRetainedRequests || this.retainedBytes + bytes > this.limits.maxRetainedBytes) {
|
|
104
|
+
throw interactionContractError(this.providerKey, "Session interaction identity or retained capacity is unavailable.");
|
|
105
|
+
}
|
|
106
|
+
const request = new SessionInteractionRequest(this, validated, () => {
|
|
107
|
+
this.requests.delete(validated.request.requestId);
|
|
108
|
+
this.retainedBytes -= bytes;
|
|
109
|
+
});
|
|
110
|
+
this.requests.set(validated.request.requestId, request);
|
|
111
|
+
this.retainedBytes += bytes;
|
|
112
|
+
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
|
+
}
|
|
119
|
+
request.setCallback(callback);
|
|
120
|
+
this.track(callback);
|
|
121
|
+
void callback.catch((cause) => this.fail(cause));
|
|
122
|
+
return callback;
|
|
123
|
+
} catch (cause) {
|
|
124
|
+
const failure = cause instanceof AgentProviderContractError ? cause : new AgentProviderContractError(
|
|
125
|
+
this.providerKey,
|
|
126
|
+
"invalid_session_interaction",
|
|
127
|
+
"Provider offered an invalid session interaction.",
|
|
128
|
+
{ cause }
|
|
129
|
+
);
|
|
130
|
+
this.fail(failure);
|
|
131
|
+
const rejected = Promise.reject(failure);
|
|
132
|
+
void rejected.catch(() => {
|
|
133
|
+
});
|
|
134
|
+
return rejected;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
installClose(close) {
|
|
138
|
+
this.automaticClose = close;
|
|
139
|
+
}
|
|
140
|
+
adopt() {
|
|
141
|
+
this.requireUsable();
|
|
142
|
+
this.removeOpeningAbort();
|
|
143
|
+
this.state = "open";
|
|
144
|
+
this.adopted = true;
|
|
145
|
+
}
|
|
146
|
+
fence(cause, source) {
|
|
147
|
+
if (!this.accepting) return;
|
|
148
|
+
this.fenceCause = new AgentProviderContractError(
|
|
149
|
+
this.providerKey,
|
|
150
|
+
"session_unusable",
|
|
151
|
+
"Provider session cannot be reused after uncertain session interaction delivery.",
|
|
152
|
+
{ cause }
|
|
153
|
+
);
|
|
154
|
+
this.state = "mutation-fenced";
|
|
155
|
+
if (!this.adopted) {
|
|
156
|
+
this.resolveFailure(this.fenceCause);
|
|
157
|
+
this.controller.abort(new DOMException("Session opening lost confirmed interaction delivery.", "AbortError"));
|
|
158
|
+
}
|
|
159
|
+
const terminal = Object.freeze({ kind: "failed", error: sessionInteractionError(
|
|
160
|
+
"session_interaction_delivery_uncertain",
|
|
161
|
+
"A session interaction response could not be confirmed.",
|
|
162
|
+
true
|
|
163
|
+
) });
|
|
164
|
+
for (const request of this.requests.values()) if (request !== source) request.stop(terminal, false);
|
|
165
|
+
}
|
|
166
|
+
fail(cause) {
|
|
167
|
+
if (!this.fatal) {
|
|
168
|
+
this.fatal = { cause, observed: false };
|
|
169
|
+
this.resolveFailure(cause);
|
|
170
|
+
}
|
|
171
|
+
this.stop(Object.freeze({ kind: "failed", error: sessionInteractionError(
|
|
172
|
+
"session_interaction_failed",
|
|
173
|
+
"The session interaction owner failed."
|
|
174
|
+
) }));
|
|
175
|
+
if (this.adopted && this.automaticClose) {
|
|
176
|
+
void this.automaticClose().catch(() => {
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
stop(terminal = Object.freeze({ kind: "session_closed" })) {
|
|
181
|
+
if (this.state !== "closed" && this.state !== "cleanup-pending") this.state = "stopping";
|
|
182
|
+
this.removeOpeningAbort();
|
|
183
|
+
this.controller.abort(new DOMException("The session interaction owner was stopped.", "AbortError"));
|
|
184
|
+
for (const request of this.requests.values()) request.stop(terminal);
|
|
185
|
+
}
|
|
186
|
+
async join() {
|
|
187
|
+
while (this.work.size > 0) await Promise.allSettled([...this.work]);
|
|
188
|
+
await Promise.all([...this.requests.values()].map((request) => request.join()));
|
|
189
|
+
}
|
|
190
|
+
close(cleanup, explicit) {
|
|
191
|
+
this.stop();
|
|
192
|
+
if (!this.cleanupComplete && !this.cleanupAttempt) {
|
|
193
|
+
this.state = "cleanup-pending";
|
|
194
|
+
this.cleanupDeadline = Date.now() + this.limits.cleanupTimeoutMs;
|
|
195
|
+
const attempt = Promise.resolve().then(cleanup).then(() => {
|
|
196
|
+
this.cleanupComplete = true;
|
|
197
|
+
this.state = "closed";
|
|
198
|
+
});
|
|
199
|
+
this.cleanupAttempt = attempt;
|
|
200
|
+
void attempt.then(
|
|
201
|
+
() => {
|
|
202
|
+
if (this.cleanupAttempt === attempt) this.cleanupAttempt = null;
|
|
203
|
+
},
|
|
204
|
+
(cause) => {
|
|
205
|
+
this.cleanupFailure = { cause };
|
|
206
|
+
if (this.cleanupAttempt === attempt) this.cleanupAttempt = null;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const actual = this.cleanupAttempt ?? Promise.resolve();
|
|
211
|
+
return waitForInteractionWork(actual, this.cleanupComplete ? this.limits.cleanupTimeoutMs : Math.max(0, this.cleanupDeadline - Date.now())).then(() => {
|
|
212
|
+
if (explicit && this.fatal && !this.fatal.observed) {
|
|
213
|
+
this.fatal.observed = true;
|
|
214
|
+
if (this.cleanupFailure) throw new AggregateError(
|
|
215
|
+
[this.fatal.cause, this.cleanupFailure.cause],
|
|
216
|
+
"Session interactions failed; retained cleanup subsequently completed."
|
|
217
|
+
);
|
|
218
|
+
throw this.fatal.cause;
|
|
219
|
+
}
|
|
220
|
+
if (explicit) this.cleanupFailure = void 0;
|
|
221
|
+
}, (cleanupError) => {
|
|
222
|
+
const causes = [cleanupError];
|
|
223
|
+
if (explicit && this.fatal && !this.fatal.observed) {
|
|
224
|
+
this.fatal.observed = true;
|
|
225
|
+
causes.unshift(this.fatal.cause);
|
|
226
|
+
}
|
|
227
|
+
throw new AgentProviderSessionCleanupError({
|
|
228
|
+
providerKey: this.providerKey,
|
|
229
|
+
errors: causes,
|
|
230
|
+
retryCleanup: () => this.close(cleanup, true)
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function createSessionInteractionOwner(input) {
|
|
236
|
+
if (input.context === void 0) return void 0;
|
|
237
|
+
if (input.support.kind === "unsupported" || !input.support.openingOperations.includes(input.operation)) {
|
|
238
|
+
throw new AgentProviderContractError(
|
|
239
|
+
input.providerKey,
|
|
240
|
+
"session_interactions_unsupported",
|
|
241
|
+
"Provider does not support session interactions for this opening operation."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return new SessionInteractionOwner(input.providerKey, input.sessionId, input.context, input.signal);
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
SessionInteractionOwner,
|
|
248
|
+
createSessionInteractionOwner,
|
|
249
|
+
sessionInteractionError,
|
|
250
|
+
waitForInteractionWork
|
|
251
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type SessionInteractionOwner } from "./lifetime.js";
|
|
2
|
+
import type { AgentSessionInteraction, AgentSessionInteractionTerminal } from "./types.js";
|
|
3
|
+
import { type CapturedSessionInteractionCandidate } from "./validation.js";
|
|
4
|
+
type Candidate = CapturedSessionInteractionCandidate & Pick<AgentSessionInteraction, "request">;
|
|
5
|
+
export declare class SessionInteractionRequest {
|
|
6
|
+
private readonly owner;
|
|
7
|
+
private readonly candidate;
|
|
8
|
+
private readonly retire;
|
|
9
|
+
readonly handle: AgentSessionInteraction;
|
|
10
|
+
private state;
|
|
11
|
+
private readonly presentation;
|
|
12
|
+
private resolveTerminal;
|
|
13
|
+
private callback;
|
|
14
|
+
private callbackSettled;
|
|
15
|
+
private responseWork;
|
|
16
|
+
private rawTerminalSettled;
|
|
17
|
+
private nativeTerminal;
|
|
18
|
+
private lastAttempt;
|
|
19
|
+
private stopped;
|
|
20
|
+
private unavailable;
|
|
21
|
+
private retired;
|
|
22
|
+
private expiryTimer;
|
|
23
|
+
private terminalTimer;
|
|
24
|
+
private readonly nativeAborted;
|
|
25
|
+
constructor(owner: SessionInteractionOwner, candidate: Candidate, retire: () => void);
|
|
26
|
+
observe(): void;
|
|
27
|
+
setCallback(callback: Promise<void>): void;
|
|
28
|
+
private expire;
|
|
29
|
+
private protocolFailure;
|
|
30
|
+
private observeTerminal;
|
|
31
|
+
private expectTerminal;
|
|
32
|
+
private finalize;
|
|
33
|
+
stop(terminal: AgentSessionInteractionTerminal, nativeRetirementExpected?: boolean): void;
|
|
34
|
+
private refusal;
|
|
35
|
+
private uncertain;
|
|
36
|
+
private receiptContradiction;
|
|
37
|
+
private respond;
|
|
38
|
+
private maybeRetire;
|
|
39
|
+
join(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { parseAgentSessionDecisionResponseFor } from "@agen-ai/agent-protocol";
|
|
2
|
+
import { throwIfAgentOperationAborted } from "../foundation.js";
|
|
3
|
+
import { AgentProviderContractError } from "../contractErrors.js";
|
|
4
|
+
import { hasExactOwnKeys } from "../internal/objectShape.js";
|
|
5
|
+
import { validateAgentRequestResolutionResult } from "../turnExecution/receipts.js";
|
|
6
|
+
import { sessionInteractionError } from "./lifetime.js";
|
|
7
|
+
import { interactionContractError, validateSessionInteractionTerminal } from "./validation.js";
|
|
8
|
+
class SessionInteractionRequest {
|
|
9
|
+
constructor(owner, candidate, retire) {
|
|
10
|
+
this.owner = owner;
|
|
11
|
+
this.candidate = candidate;
|
|
12
|
+
this.retire = retire;
|
|
13
|
+
const terminal = new Promise((resolve) => {
|
|
14
|
+
this.resolveTerminal = resolve;
|
|
15
|
+
});
|
|
16
|
+
this.handle = Object.freeze({
|
|
17
|
+
request: candidate.request,
|
|
18
|
+
signal: this.presentation.signal,
|
|
19
|
+
terminal,
|
|
20
|
+
respond: (input) => this.respond(input)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
handle;
|
|
24
|
+
state = { kind: "pending" };
|
|
25
|
+
presentation = new AbortController();
|
|
26
|
+
resolveTerminal;
|
|
27
|
+
callback;
|
|
28
|
+
callbackSettled = false;
|
|
29
|
+
responseWork = 0;
|
|
30
|
+
rawTerminalSettled = false;
|
|
31
|
+
nativeTerminal;
|
|
32
|
+
lastAttempt;
|
|
33
|
+
stopped;
|
|
34
|
+
unavailable = false;
|
|
35
|
+
retired = false;
|
|
36
|
+
expiryTimer;
|
|
37
|
+
terminalTimer;
|
|
38
|
+
nativeAborted = () => {
|
|
39
|
+
this.unavailable = true;
|
|
40
|
+
this.presentation.abort(new DOMException("The provider interaction was stopped.", "AbortError"));
|
|
41
|
+
this.expectTerminal();
|
|
42
|
+
};
|
|
43
|
+
observe() {
|
|
44
|
+
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
|
+
void this.candidate.terminal.then((candidate) => {
|
|
47
|
+
this.rawTerminalSettled = true;
|
|
48
|
+
if (this.terminalTimer) clearTimeout(this.terminalTimer);
|
|
49
|
+
try {
|
|
50
|
+
this.nativeTerminal = validateSessionInteractionTerminal(candidate);
|
|
51
|
+
this.observeTerminal();
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
this.protocolFailure(cause);
|
|
54
|
+
}
|
|
55
|
+
this.maybeRetire();
|
|
56
|
+
}, (cause) => {
|
|
57
|
+
this.rawTerminalSettled = true;
|
|
58
|
+
if (this.terminalTimer) clearTimeout(this.terminalTimer);
|
|
59
|
+
this.protocolFailure(cause);
|
|
60
|
+
this.maybeRetire();
|
|
61
|
+
});
|
|
62
|
+
if (this.candidate.signal.aborted) this.nativeAborted();
|
|
63
|
+
}
|
|
64
|
+
setCallback(callback) {
|
|
65
|
+
this.callback = callback;
|
|
66
|
+
void callback.then(
|
|
67
|
+
() => {
|
|
68
|
+
this.callbackSettled = true;
|
|
69
|
+
this.maybeRetire();
|
|
70
|
+
},
|
|
71
|
+
() => {
|
|
72
|
+
this.callbackSettled = true;
|
|
73
|
+
this.maybeRetire();
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
expire() {
|
|
78
|
+
if (this.state.kind === "terminal") return;
|
|
79
|
+
this.stop(Object.freeze({ kind: "expired" }));
|
|
80
|
+
this.owner.fail(interactionContractError(this.owner.providerKey, "Session interaction expired before final delivery."));
|
|
81
|
+
}
|
|
82
|
+
protocolFailure(cause) {
|
|
83
|
+
const error = sessionInteractionError("session_interaction_failed", "Provider session interaction protocol failed.");
|
|
84
|
+
this.stop(Object.freeze({ kind: "failed", error }));
|
|
85
|
+
this.owner.fail(cause instanceof AgentProviderContractError ? cause : new AgentProviderContractError(
|
|
86
|
+
this.owner.providerKey,
|
|
87
|
+
"invalid_session_interaction",
|
|
88
|
+
"Provider session interaction protocol failed.",
|
|
89
|
+
{ cause }
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
observeTerminal() {
|
|
93
|
+
const terminal = this.nativeTerminal;
|
|
94
|
+
if (!terminal) return;
|
|
95
|
+
if (terminal.kind === "failed") {
|
|
96
|
+
this.stop(terminal);
|
|
97
|
+
this.owner.fail(interactionContractError(this.owner.providerKey, "Provider session interaction producer failed."));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (terminal.kind === "expired") {
|
|
101
|
+
this.stop(terminal);
|
|
102
|
+
this.owner.fail(interactionContractError(this.owner.providerKey, "Provider session interaction expired."));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (terminal.kind === "delivery_uncertain") this.owner.fence(terminal.error, this);
|
|
106
|
+
const attempt = this.lastAttempt;
|
|
107
|
+
if (terminal.kind !== "answered" && this.state.kind !== "terminal") {
|
|
108
|
+
this.stopped ??= terminal;
|
|
109
|
+
this.unavailable = true;
|
|
110
|
+
this.presentation.abort(new DOMException("The provider interaction is no longer answerable.", "AbortError"));
|
|
111
|
+
}
|
|
112
|
+
if (attempt && !attempt.actualReceipt) return;
|
|
113
|
+
if (attempt?.actualReceipt && this.receiptContradiction(attempt, attempt.actualReceipt)) {
|
|
114
|
+
throw interactionContractError(
|
|
115
|
+
this.owner.providerKey,
|
|
116
|
+
"Provider session interaction terminal contradicts its response delivery receipt."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (terminal.kind === "answered") {
|
|
120
|
+
if (!attempt || terminal.choiceId !== attempt.choiceId || attempt.actualReceipt?.status !== "delivered") {
|
|
121
|
+
throw interactionContractError(this.owner.providerKey, "Provider interaction acceptance disagrees with its delivery receipt.");
|
|
122
|
+
}
|
|
123
|
+
this.finalize(terminal, attempt);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (this.state.kind === "terminal") return;
|
|
127
|
+
this.finalize(this.stopped ?? terminal, attempt);
|
|
128
|
+
}
|
|
129
|
+
expectTerminal(deadline = Date.now() + this.owner.limits.responseTimeoutMs) {
|
|
130
|
+
if (this.rawTerminalSettled || this.terminalTimer) return;
|
|
131
|
+
const active = this.state.kind === "answering" ? this.state.attempt.deadline : this.state.kind === "terminal" && this.state.attempt ? this.state.attempt.deadline : deadline;
|
|
132
|
+
this.terminalTimer = setTimeout(() => {
|
|
133
|
+
this.terminalTimer = void 0;
|
|
134
|
+
if (!this.rawTerminalSettled) this.protocolFailure(interactionContractError(
|
|
135
|
+
this.owner.providerKey,
|
|
136
|
+
"Provider did not settle its stopped session interaction terminal."
|
|
137
|
+
));
|
|
138
|
+
}, Math.max(0, Math.min(active, deadline) - Date.now()));
|
|
139
|
+
}
|
|
140
|
+
finalize(terminal, attempt, nativeRetirementExpected = true) {
|
|
141
|
+
if (this.state.kind === "terminal") return;
|
|
142
|
+
this.state = { kind: "terminal", terminal, ...attempt === void 0 ? {} : { attempt } };
|
|
143
|
+
this.unavailable = true;
|
|
144
|
+
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
145
|
+
this.presentation.abort(new DOMException("The session interaction is no longer answerable.", "AbortError"));
|
|
146
|
+
this.resolveTerminal(terminal);
|
|
147
|
+
if (nativeRetirementExpected && (!attempt || attempt.actualReceipt || this.responseWork === 0)) this.expectTerminal();
|
|
148
|
+
this.maybeRetire();
|
|
149
|
+
}
|
|
150
|
+
stop(terminal, nativeRetirementExpected = true) {
|
|
151
|
+
this.stopped ??= terminal;
|
|
152
|
+
this.unavailable = true;
|
|
153
|
+
this.presentation.abort(new DOMException("The session interaction was stopped.", "AbortError"));
|
|
154
|
+
if (this.state.kind === "pending") this.finalize(this.stopped, void 0, nativeRetirementExpected);
|
|
155
|
+
if (nativeRetirementExpected) this.expectTerminal();
|
|
156
|
+
}
|
|
157
|
+
refusal(active = false) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
status: "rejected",
|
|
160
|
+
requestState: active ? "pending" : "unavailable",
|
|
161
|
+
error: sessionInteractionError(
|
|
162
|
+
active ? "session_interaction_response_active" : "session_interaction_unavailable",
|
|
163
|
+
active ? "A session interaction response is already active." : "The session interaction is no longer available."
|
|
164
|
+
)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
uncertain(attempt, cause) {
|
|
168
|
+
const error = sessionInteractionError(
|
|
169
|
+
"session_interaction_delivery_uncertain",
|
|
170
|
+
"Session interaction response delivery could not be confirmed.",
|
|
171
|
+
true
|
|
172
|
+
);
|
|
173
|
+
const receipt = Object.freeze({ status: "delivery_uncertain", error });
|
|
174
|
+
attempt.receipt ??= receipt;
|
|
175
|
+
this.finalize(Object.freeze({ kind: "delivery_uncertain", error }), attempt);
|
|
176
|
+
this.owner.fence(cause === void 0 ? error : new AgentProviderContractError(
|
|
177
|
+
this.owner.providerKey,
|
|
178
|
+
"invalid_session_interaction",
|
|
179
|
+
"Session interaction response delivery failed after provider entry.",
|
|
180
|
+
{ cause }
|
|
181
|
+
), this);
|
|
182
|
+
return receipt;
|
|
183
|
+
}
|
|
184
|
+
receiptContradiction(attempt, result) {
|
|
185
|
+
const terminal = this.nativeTerminal;
|
|
186
|
+
if (!terminal) return false;
|
|
187
|
+
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;
|
|
190
|
+
if (terminal.kind === "delivery_uncertain") return true;
|
|
191
|
+
return result.requestState === "pending" && terminal.kind === "rejected";
|
|
192
|
+
}
|
|
193
|
+
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 });
|
|
202
|
+
throwIfAgentOperationAborted(responseSignal);
|
|
203
|
+
const deadline = new AbortController();
|
|
204
|
+
const signal = AbortSignal.any([this.presentation.signal, deadline.signal, ...responseSignal ? [responseSignal] : []]);
|
|
205
|
+
if (Date.parse(this.handle.request.expiresAt) <= Date.now()) this.expire();
|
|
206
|
+
if (this.unavailable || !this.owner.accepting || this.state.kind === "terminal") return this.refusal();
|
|
207
|
+
if (this.state.kind === "answering") return this.refusal(true);
|
|
208
|
+
const attempt = { choiceId: response.choiceId, deadline: Date.now() + this.owner.limits.responseTimeoutMs };
|
|
209
|
+
this.lastAttempt = attempt;
|
|
210
|
+
this.state = { kind: "answering", attempt };
|
|
211
|
+
this.responseWork += 1;
|
|
212
|
+
let entered;
|
|
213
|
+
try {
|
|
214
|
+
entered = Promise.resolve(this.candidate.respond({ ...response, signal }));
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
entered = Promise.reject(cause);
|
|
217
|
+
}
|
|
218
|
+
const actual = this.owner.track(entered.then((candidate) => {
|
|
219
|
+
const result2 = validateAgentRequestResolutionResult(candidate, this.owner.providerKey);
|
|
220
|
+
attempt.actualReceipt = result2;
|
|
221
|
+
if (this.state.kind === "terminal") {
|
|
222
|
+
try {
|
|
223
|
+
this.observeTerminal();
|
|
224
|
+
} catch (cause) {
|
|
225
|
+
this.protocolFailure(cause);
|
|
226
|
+
}
|
|
227
|
+
this.expectTerminal(attempt.deadline);
|
|
228
|
+
}
|
|
229
|
+
return result2;
|
|
230
|
+
}));
|
|
231
|
+
void actual.then(
|
|
232
|
+
() => {
|
|
233
|
+
this.responseWork -= 1;
|
|
234
|
+
this.maybeRetire();
|
|
235
|
+
},
|
|
236
|
+
() => {
|
|
237
|
+
this.responseWork -= 1;
|
|
238
|
+
this.maybeRetire();
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
let timer;
|
|
242
|
+
const timeout = new Promise((resolve) => {
|
|
243
|
+
timer = setTimeout(
|
|
244
|
+
() => {
|
|
245
|
+
deadline.abort(new DOMException("Session interaction response timed out.", "TimeoutError"));
|
|
246
|
+
resolve(null);
|
|
247
|
+
},
|
|
248
|
+
Math.max(0, attempt.deadline - Date.now())
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
let result;
|
|
252
|
+
let responseFailure;
|
|
253
|
+
try {
|
|
254
|
+
result = await Promise.race([actual, timeout]);
|
|
255
|
+
} catch (cause) {
|
|
256
|
+
responseFailure = cause;
|
|
257
|
+
result = null;
|
|
258
|
+
}
|
|
259
|
+
if (timer) clearTimeout(timer);
|
|
260
|
+
if (result === null) return this.uncertain(attempt, responseFailure);
|
|
261
|
+
if (this.receiptContradiction(attempt, result)) {
|
|
262
|
+
const uncertain = this.uncertain(attempt);
|
|
263
|
+
this.protocolFailure(interactionContractError(
|
|
264
|
+
this.owner.providerKey,
|
|
265
|
+
"Provider session interaction terminal contradicts its response delivery receipt."
|
|
266
|
+
));
|
|
267
|
+
return uncertain;
|
|
268
|
+
}
|
|
269
|
+
attempt.receipt = result;
|
|
270
|
+
if (result.status === "delivery_uncertain") {
|
|
271
|
+
this.finalize(Object.freeze({ kind: "delivery_uncertain", error: result.error }), attempt);
|
|
272
|
+
this.owner.fence(result.error, this);
|
|
273
|
+
} else if (result.status === "delivered") {
|
|
274
|
+
this.finalize(Object.freeze({ kind: "answered", choiceId: attempt.choiceId }), attempt);
|
|
275
|
+
} else if (result.requestState === "unavailable") {
|
|
276
|
+
this.finalize(this.stopped ?? Object.freeze({ kind: "rejected", error: result.error }), attempt);
|
|
277
|
+
} else if (this.stopped || this.unavailable || !this.owner.accepting) {
|
|
278
|
+
result = this.refusal();
|
|
279
|
+
if (this.stopped || !this.owner.accepting) this.finalize(this.stopped ?? Object.freeze({ kind: "session_closed" }), attempt);
|
|
280
|
+
else {
|
|
281
|
+
this.state = { kind: "pending" };
|
|
282
|
+
this.expectTerminal(attempt.deadline);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
this.state = { kind: "pending" };
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
this.observeTerminal();
|
|
289
|
+
} catch (cause) {
|
|
290
|
+
this.protocolFailure(cause);
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
maybeRetire() {
|
|
295
|
+
if (this.retired || this.state.kind !== "terminal" || !this.callbackSettled || this.responseWork !== 0 || !this.rawTerminalSettled) return;
|
|
296
|
+
this.retired = true;
|
|
297
|
+
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
298
|
+
if (this.terminalTimer) clearTimeout(this.terminalTimer);
|
|
299
|
+
this.candidate.signal.removeEventListener("abort", this.nativeAborted);
|
|
300
|
+
this.retire();
|
|
301
|
+
}
|
|
302
|
+
async join() {
|
|
303
|
+
await Promise.allSettled([this.candidate.terminal, ...this.callback ? [this.callback] : []]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
export {
|
|
307
|
+
SessionInteractionRequest
|
|
308
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AgentError, AgentSessionDecisionChoiceId, AgentSessionDecisionRequest, AgentSessionDecisionResponse } from "@agen-ai/agent-protocol";
|
|
2
|
+
import type { MaybePromise } from "../foundation.js";
|
|
3
|
+
import type { AgentRequestResolutionResult } from "../sessions.js";
|
|
4
|
+
export type AgentSessionInteractionOpeningOperation = "create" | "resume" | "branch";
|
|
5
|
+
export type AgentSessionInteractionSupport = Readonly<{
|
|
6
|
+
kind: "unsupported";
|
|
7
|
+
}> | Readonly<{
|
|
8
|
+
kind: "supported";
|
|
9
|
+
openingOperations: readonly [AgentSessionInteractionOpeningOperation, ...AgentSessionInteractionOpeningOperation[]];
|
|
10
|
+
requestKinds: readonly ["decision"];
|
|
11
|
+
producerOwnership: "exclusive";
|
|
12
|
+
}>;
|
|
13
|
+
export interface AgentSessionInteractionLimits {
|
|
14
|
+
readonly maxRetainedRequests: number;
|
|
15
|
+
readonly maxRetainedBytes: number;
|
|
16
|
+
readonly requestTimeoutMs: number;
|
|
17
|
+
readonly responseTimeoutMs: number;
|
|
18
|
+
readonly cleanupTimeoutMs: number;
|
|
19
|
+
}
|
|
20
|
+
export interface AgentSessionDecisionResponseInput extends AgentSessionDecisionResponse {
|
|
21
|
+
readonly signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
export type AgentSessionInteractionTerminal = Readonly<{
|
|
24
|
+
kind: "answered";
|
|
25
|
+
choiceId: AgentSessionDecisionChoiceId;
|
|
26
|
+
}> | Readonly<{
|
|
27
|
+
kind: "rejected";
|
|
28
|
+
error: AgentError;
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
kind: "delivery_uncertain";
|
|
31
|
+
error: AgentError;
|
|
32
|
+
}> | Readonly<{
|
|
33
|
+
kind: "withdrawn";
|
|
34
|
+
}> | Readonly<{
|
|
35
|
+
kind: "expired";
|
|
36
|
+
}> | Readonly<{
|
|
37
|
+
kind: "session_closed";
|
|
38
|
+
}> | Readonly<{
|
|
39
|
+
kind: "failed";
|
|
40
|
+
error: AgentError;
|
|
41
|
+
}>;
|
|
42
|
+
export interface AgentSessionInteraction {
|
|
43
|
+
readonly request: AgentSessionDecisionRequest;
|
|
44
|
+
readonly signal: AbortSignal;
|
|
45
|
+
readonly terminal: Promise<AgentSessionInteractionTerminal>;
|
|
46
|
+
readonly respond: (input: AgentSessionDecisionResponseInput) => Promise<AgentRequestResolutionResult>;
|
|
47
|
+
}
|
|
48
|
+
export type AgentSessionInteractionHandler = (interaction: AgentSessionInteraction) => MaybePromise<void>;
|
|
49
|
+
export interface AgentSessionInteractionContext {
|
|
50
|
+
readonly onRequest: AgentSessionInteractionHandler;
|
|
51
|
+
readonly limits?: Partial<AgentSessionInteractionLimits>;
|
|
52
|
+
}
|
|
53
|
+
export declare const DEFAULT_AGENT_SESSION_INTERACTION_LIMITS: Readonly<AgentSessionInteractionLimits>;
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type AgentCapabilities, type AgentProviderKey, type AgentSessionId } from "@agen-ai/agent-protocol";
|
|
2
|
+
import { AgentProviderContractError } from "../contractErrors.js";
|
|
3
|
+
import type { AgentProviderAdapter } from "../sessions.js";
|
|
4
|
+
import { type AgentSessionInteraction, type AgentSessionInteractionContext, type AgentSessionInteractionLimits, type AgentSessionInteractionSupport, type AgentSessionInteractionTerminal } from "./types.js";
|
|
5
|
+
export declare function interactionContractError(providerKey: AgentProviderKey, message: string): AgentProviderContractError;
|
|
6
|
+
export declare function validateSessionInteractionSupport(capabilities: AgentCapabilities, adapter: AgentProviderAdapter): AgentSessionInteractionSupport;
|
|
7
|
+
export interface ValidatedSessionInteractionContext {
|
|
8
|
+
readonly onRequest: AgentSessionInteractionContext["onRequest"];
|
|
9
|
+
readonly limits: Readonly<AgentSessionInteractionLimits>;
|
|
10
|
+
}
|
|
11
|
+
export declare function validateSessionInteractionContext(input: AgentSessionInteractionContext): ValidatedSessionInteractionContext;
|
|
12
|
+
export interface CapturedSessionInteractionCandidate {
|
|
13
|
+
readonly request: unknown;
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
readonly terminal: Promise<unknown>;
|
|
16
|
+
readonly respond: AgentSessionInteraction["respond"];
|
|
17
|
+
}
|
|
18
|
+
export declare function captureSessionInteractionCandidate(input: AgentSessionInteraction, observe: (terminal: Promise<unknown>) => void): CapturedSessionInteractionCandidate;
|
|
19
|
+
export declare function validateSessionInteractionCandidate(input: CapturedSessionInteractionCandidate, sessionId: AgentSessionId, requestTimeoutMs: number): CapturedSessionInteractionCandidate & Pick<AgentSessionInteraction, "request">;
|
|
20
|
+
export declare function validateSessionInteractionTerminal(input: unknown): AgentSessionInteractionTerminal;
|
|
21
|
+
//# sourceMappingURL=validation.d.ts.map
|