@agen-ai/agent-runtime 0.3.0 → 0.4.0
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 +182 -24
- package/dist/adapterValidation.d.ts +1 -1
- package/dist/adapterValidation.js +162 -95
- package/dist/contractErrors.d.ts +15 -2
- package/dist/contractErrors.js +39 -1
- package/dist/hostBindings/index.d.ts +4 -0
- package/dist/hostBindings/index.js +9 -0
- package/dist/hostBindings/lifetime.d.ts +25 -0
- package/dist/hostBindings/lifetime.js +147 -0
- package/dist/hostBindings/opening.d.ts +12 -0
- package/dist/hostBindings/opening.js +24 -0
- package/dist/hostBindings/schema.d.ts +9 -0
- package/dist/hostBindings/schema.js +169 -0
- package/dist/hostBindings/types.d.ts +71 -0
- package/dist/hostBindings/types.js +0 -0
- package/dist/hostBindings/validation.d.ts +14 -0
- package/dist/hostBindings/validation.js +107 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/internal/objectShape.d.ts +2 -0
- package/dist/internal/objectShape.js +7 -0
- package/dist/outputValidation.js +1 -0
- package/dist/sessionCleanup.d.ts +9 -0
- package/dist/sessionCleanup.js +17 -0
- package/dist/sessionValidation.d.ts +12 -2
- package/dist/sessionValidation.js +134 -305
- package/dist/sessions.d.ts +28 -4
- package/dist/testing/conformance.d.ts +3 -3
- package/dist/testing/conformance.js +24 -30
- package/dist/testing/fakeProvider.js +56 -45
- package/dist/turnExecution/execution.d.ts +26 -0
- package/dist/turnExecution/execution.js +214 -0
- package/dist/turnExecution/receipts.d.ts +6 -0
- package/dist/turnExecution/receipts.js +39 -0
- package/dist/turnExecution/requests.d.ts +19 -0
- package/dist/turnExecution/requests.js +56 -0
- package/package.json +2 -2
package/dist/sessions.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentAuthenticationFlow, AgentCollaborationControlInput, AgentCollaborationNode, AgentCollaborationSpawnInput, AgentConfigurationCatalog, AgentConfigurationSelectionInput, AgentError, AgentGeneratedResourceDescriptor, AgentGeneratedResourceId, AgentManagedContentCatalog, AgentOperationCatalog, AgentOperationInvocation, AgentOperationResult, AgentRequestResolution, AgentSessionBinding, AgentSessionBranchSource, AgentSessionConfiguration, AgentSessionId, AgentTurnId, AgentTurnInputContent, AgentTurnInteractionMode, AgentTurnInputPart, AgentTurnInterruptionReason } from "@agen-ai/agent-protocol";
|
|
2
2
|
import type { MaybePromise } from "./foundation.js";
|
|
3
|
+
import type { AgentSessionHostBinding, AgentSessionHostBindingSupport } from "./hostBindings/types.js";
|
|
3
4
|
import type { AgentProviderOutput } from "./outputs.js";
|
|
4
5
|
import type { AgentArtifactCandidate } from "./artifacts.js";
|
|
5
6
|
import type { AgentEnvironmentObservationPort } from "./environment/types.js";
|
|
@@ -8,6 +9,7 @@ export interface AgentProviderSessionContext {
|
|
|
8
9
|
readonly workingDirectory: string;
|
|
9
10
|
readonly configuration: AgentSessionConfiguration;
|
|
10
11
|
readonly signal?: AbortSignal;
|
|
12
|
+
readonly hostBinding?: AgentSessionHostBinding;
|
|
11
13
|
}
|
|
12
14
|
export type AgentSessionBindingCreatedObserver = (binding: AgentSessionBinding) => void;
|
|
13
15
|
export interface AgentProviderCreateSessionInput extends AgentProviderSessionContext {
|
|
@@ -22,7 +24,7 @@ export interface AgentProviderBranchSessionInput extends AgentProviderSessionCon
|
|
|
22
24
|
}
|
|
23
25
|
export type AgentProviderExecutionStartedObserver = () => void;
|
|
24
26
|
export type AgentProviderOutputObserver = (output: AgentProviderOutput) => MaybePromise<void>;
|
|
25
|
-
export interface
|
|
27
|
+
export interface AgentProviderStartTurnInput {
|
|
26
28
|
readonly turnId: AgentTurnId;
|
|
27
29
|
readonly interactionMode: AgentTurnInteractionMode;
|
|
28
30
|
readonly parts: readonly AgentTurnInputPart[];
|
|
@@ -34,6 +36,23 @@ export interface AgentProviderRunTurnInput {
|
|
|
34
36
|
export interface AgentProviderResolveRequestInput {
|
|
35
37
|
readonly resolution: AgentRequestResolution;
|
|
36
38
|
readonly signal?: AbortSignal;
|
|
39
|
+
readonly onProviderExecutionStarted?: AgentProviderExecutionStartedObserver;
|
|
40
|
+
}
|
|
41
|
+
/** Acceptance of the canonical answer or step, not model consumption or turn completion. */
|
|
42
|
+
export type AgentRequestResolutionResult = Readonly<{
|
|
43
|
+
status: "delivered";
|
|
44
|
+
}> | Readonly<{
|
|
45
|
+
status: "rejected";
|
|
46
|
+
requestState: "pending" | "unavailable";
|
|
47
|
+
error: AgentError;
|
|
48
|
+
}> | Readonly<{
|
|
49
|
+
status: "delivery_uncertain";
|
|
50
|
+
error: AgentError;
|
|
51
|
+
}>;
|
|
52
|
+
export interface AgentProviderTurnExecution {
|
|
53
|
+
readonly turnId: AgentTurnId;
|
|
54
|
+
readonly outputs: AsyncIterable<AgentProviderOutput>;
|
|
55
|
+
readonly resolveRequest: (input: AgentProviderResolveRequestInput) => Promise<AgentRequestResolutionResult>;
|
|
37
56
|
}
|
|
38
57
|
export interface AgentProviderInterruptTurnInput {
|
|
39
58
|
readonly turnId: AgentTurnId;
|
|
@@ -89,6 +108,11 @@ export interface AgentProviderOperationResult {
|
|
|
89
108
|
readonly outputs?: readonly AgentProviderOutput[];
|
|
90
109
|
readonly error?: AgentError;
|
|
91
110
|
}
|
|
111
|
+
export interface AgentProviderTurnInterruptionResult {
|
|
112
|
+
readonly status: AgentProviderOperationStatus;
|
|
113
|
+
readonly error?: AgentError;
|
|
114
|
+
readonly outputs?: never;
|
|
115
|
+
}
|
|
92
116
|
export type AgentTurnSteeringResult = Readonly<{
|
|
93
117
|
status: "delivered";
|
|
94
118
|
}> | Readonly<{
|
|
@@ -102,7 +126,7 @@ export type AgentTurnInterruption = Readonly<{
|
|
|
102
126
|
kind: "unsupported";
|
|
103
127
|
}> | Readonly<{
|
|
104
128
|
kind: "supported";
|
|
105
|
-
interruptTurn: (input: AgentProviderInterruptTurnInput) => MaybePromise<
|
|
129
|
+
interruptTurn: (input: AgentProviderInterruptTurnInput) => MaybePromise<AgentProviderTurnInterruptionResult>;
|
|
106
130
|
}>;
|
|
107
131
|
export type AgentTurnSteering = Readonly<{
|
|
108
132
|
kind: "unsupported";
|
|
@@ -145,8 +169,7 @@ export type AgentGeneratedResourceAccess = Readonly<{
|
|
|
145
169
|
}>;
|
|
146
170
|
export interface AgentProviderSession {
|
|
147
171
|
readonly binding: AgentSessionBinding;
|
|
148
|
-
readonly
|
|
149
|
-
readonly resolveRequest: (input: AgentProviderResolveRequestInput) => AsyncIterable<AgentProviderOutput>;
|
|
172
|
+
readonly startTurn: (input: AgentProviderStartTurnInput) => AgentProviderTurnExecution;
|
|
150
173
|
readonly interruption: AgentTurnInterruption;
|
|
151
174
|
readonly steering: AgentTurnSteering;
|
|
152
175
|
readonly configuration: AgentSessionConfigurationControl;
|
|
@@ -189,6 +212,7 @@ export type AgentProviderAuthentication = Readonly<{
|
|
|
189
212
|
cancel: (input: AgentProviderAuthenticationCancelInput) => MaybePromise<AgentProviderOperationResult>;
|
|
190
213
|
}>;
|
|
191
214
|
export interface AgentProviderAdapter {
|
|
215
|
+
readonly hostBinding?: AgentSessionHostBindingSupport;
|
|
192
216
|
readonly createSession: (input: AgentProviderCreateSessionInput) => MaybePromise<AgentProviderSession>;
|
|
193
217
|
readonly resumption: AgentSessionResumption;
|
|
194
218
|
readonly branching: AgentSessionBranching;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentCollaborationSpawnInput, type AgentConfigurationSelectionInput, type AgentEnvironmentId, type AgentGeneratedResourceId, type AgentOperationInvocation, type AgentRequest, type AgentRequestResolution, type AgentSessionBinding, type AgentSessionBranchSource, type AgentSessionConfiguration, type AgentSessionId, type AgentTurnId, type AgentTurnInputContent } from "@agen-ai/agent-protocol";
|
|
2
2
|
import type { AgentProviderDriver, AgentProviderInstanceDefinition } from "../providerDriver.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentProviderStartTurnInput } from "../sessions.js";
|
|
4
4
|
export interface AgentProviderConformanceScenario {
|
|
5
5
|
readonly driver: AgentProviderDriver;
|
|
6
6
|
readonly definition: AgentProviderInstanceDefinition;
|
|
@@ -16,8 +16,8 @@ export interface AgentProviderConformanceScenario {
|
|
|
16
16
|
readonly branchSessionId?: AgentSessionId;
|
|
17
17
|
readonly abortedSessionId: AgentSessionId;
|
|
18
18
|
readonly interruptionSessionId: AgentSessionId;
|
|
19
|
-
readonly turn:
|
|
20
|
-
readonly interruptionTurn:
|
|
19
|
+
readonly turn: AgentProviderStartTurnInput;
|
|
20
|
+
readonly interruptionTurn: AgentProviderStartTurnInput;
|
|
21
21
|
readonly resolutionFor: (request: AgentRequest) => AgentRequestResolution;
|
|
22
22
|
readonly branchSource?: (binding: AgentSessionBinding, turnId: AgentTurnId) => AgentSessionBranchSource;
|
|
23
23
|
readonly steeringInput?: AgentTurnInputContent;
|
|
@@ -38,7 +38,8 @@ async function checkEnvironmentWatch(input) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
async function advanceToStartedTurn(session, input) {
|
|
41
|
-
const
|
|
41
|
+
const execution = session.startTurn(input);
|
|
42
|
+
const iterator = execution.outputs[Symbol.asyncIterator]();
|
|
42
43
|
const outputs = [];
|
|
43
44
|
try {
|
|
44
45
|
for (; ; ) {
|
|
@@ -50,7 +51,7 @@ async function advanceToStartedTurn(session, input) {
|
|
|
50
51
|
);
|
|
51
52
|
outputs.push(next.value);
|
|
52
53
|
if (next.value.kind === "event" && next.value.event.type === "turn.started") {
|
|
53
|
-
return { iterator, outputs };
|
|
54
|
+
return { execution, iterator, outputs };
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
} catch (error) {
|
|
@@ -58,18 +59,18 @@ async function advanceToStartedTurn(session, input) {
|
|
|
58
59
|
throw error;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
async function collectRemainingOutputs(started) {
|
|
62
|
+
async function collectRemainingOutputs(started, resolutionFor) {
|
|
62
63
|
for (; ; ) {
|
|
63
64
|
const next = await started.iterator.next();
|
|
64
65
|
if (next.done) return started.outputs;
|
|
65
66
|
started.outputs.push(next.value);
|
|
67
|
+
if (next.value.kind === "event" && next.value.event.type === "request.opened" && resolutionFor) {
|
|
68
|
+
const request = next.value.event.payload.request;
|
|
69
|
+
const receipt = await started.execution.resolveRequest({ resolution: parseAgentRequestResolutionFor(request, resolutionFor(request)) });
|
|
70
|
+
requireCheck(receipt.status === "delivered", "request_resolution", "Matching request answer was not accepted by the provider handler.");
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
|
-
async function collectOutputs(outputs) {
|
|
69
|
-
const collected = [];
|
|
70
|
-
for await (const output of outputs) collected.push(output);
|
|
71
|
-
return collected;
|
|
72
|
-
}
|
|
73
74
|
function requireOperationStatus(result, statuses, check, message) {
|
|
74
75
|
requireCheck(statuses.includes(result.status), check, message);
|
|
75
76
|
}
|
|
@@ -78,6 +79,12 @@ function semanticEvents(outputs) {
|
|
|
78
79
|
(output) => output.kind === "event" ? [output.event] : []
|
|
79
80
|
);
|
|
80
81
|
}
|
|
82
|
+
function hasStableTurnBoundary(events) {
|
|
83
|
+
const boundary = events.findLast(
|
|
84
|
+
(event) => event.type === "turn.completed" || event.type === "turn.state_changed"
|
|
85
|
+
);
|
|
86
|
+
return boundary?.type === "turn.completed" || boundary?.type === "turn.state_changed" && boundary.payload.state === "waiting_for_request";
|
|
87
|
+
}
|
|
81
88
|
async function runAgentProviderConformance(scenario) {
|
|
82
89
|
const checks = [];
|
|
83
90
|
try {
|
|
@@ -299,7 +306,7 @@ async function runAgentProviderConformance(scenario) {
|
|
|
299
306
|
"Unsupported steering exposed a handler."
|
|
300
307
|
);
|
|
301
308
|
}
|
|
302
|
-
turnOutputs = await collectRemainingOutputs(startedTurn);
|
|
309
|
+
turnOutputs = await collectRemainingOutputs(startedTurn, scenario.resolutionFor);
|
|
303
310
|
} catch (error) {
|
|
304
311
|
await startedTurn.iterator.return?.();
|
|
305
312
|
await created.close({ reason: "contract_rejected" });
|
|
@@ -311,32 +318,13 @@ async function runAgentProviderConformance(scenario) {
|
|
|
311
318
|
"turn_order",
|
|
312
319
|
"Turn did not start first."
|
|
313
320
|
);
|
|
314
|
-
const lastEvent = events.at(-1);
|
|
315
321
|
requireCheck(
|
|
316
|
-
|
|
322
|
+
hasStableTurnBoundary(events),
|
|
317
323
|
"turn_order",
|
|
318
324
|
"Turn did not terminate or enter a waiting state."
|
|
319
325
|
);
|
|
320
326
|
checks.push("turn_order");
|
|
321
|
-
|
|
322
|
-
(event) => event.type === "request.opened"
|
|
323
|
-
);
|
|
324
|
-
if (openedRequest?.type === "request.opened") {
|
|
325
|
-
const resolution = parseAgentRequestResolutionFor(
|
|
326
|
-
openedRequest.payload.request,
|
|
327
|
-
scenario.resolutionFor(openedRequest.payload.request)
|
|
328
|
-
);
|
|
329
|
-
const resolutionOutputs = await collectOutputs(
|
|
330
|
-
created.resolveRequest({ resolution })
|
|
331
|
-
);
|
|
332
|
-
const resolutionBoundary = semanticEvents(resolutionOutputs).at(-1);
|
|
333
|
-
requireCheck(
|
|
334
|
-
resolutionBoundary?.type === "turn.completed" || resolutionBoundary?.type === "turn.state_changed" && resolutionBoundary.payload.state === "waiting_for_request",
|
|
335
|
-
"request_resolution",
|
|
336
|
-
"Matching request resolution did not reach a stable turn boundary."
|
|
337
|
-
);
|
|
338
|
-
checks.push("request_resolution");
|
|
339
|
-
}
|
|
327
|
+
if (events.some((event) => event.type === "request.opened")) checks.push("request_resolution");
|
|
340
328
|
checks.push("steering");
|
|
341
329
|
if (instance.capabilities.turns.interrupt) {
|
|
342
330
|
let interruptedBindingCount = 0;
|
|
@@ -375,6 +363,12 @@ async function runAgentProviderConformance(scenario) {
|
|
|
375
363
|
"interruption",
|
|
376
364
|
"Live turn interruption did not succeed."
|
|
377
365
|
);
|
|
366
|
+
const outputs = await collectRemainingOutputs(liveTurn);
|
|
367
|
+
requireCheck(
|
|
368
|
+
semanticEvents(outputs).some((event) => event.type === "turn.completed"),
|
|
369
|
+
"interruption",
|
|
370
|
+
"Interrupted turn did not produce native terminal output."
|
|
371
|
+
);
|
|
378
372
|
} finally {
|
|
379
373
|
await liveTurn?.iterator.return?.();
|
|
380
374
|
await interrupted.close({ reason: "other" });
|
|
@@ -44,7 +44,7 @@ function defaultCapabilities(providerKey) {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
return parseAgentCapabilities({
|
|
47
|
-
protocolVersion:
|
|
47
|
+
protocolVersion: 10,
|
|
48
48
|
providerKey,
|
|
49
49
|
sessions: { create: true, resume: true, branch: { kind: "through_turn" } },
|
|
50
50
|
turns: {
|
|
@@ -165,7 +165,7 @@ function sessionBinding(state, sessionId) {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
function eventBase(sessionId, turnId, occurredAt) {
|
|
168
|
-
return { protocolVersion:
|
|
168
|
+
return { protocolVersion: 10, sessionId, turnId, occurredAt };
|
|
169
169
|
}
|
|
170
170
|
function fakeEnvironmentSnapshot(environmentId) {
|
|
171
171
|
return parseAgentEnvironmentSnapshot({
|
|
@@ -280,7 +280,8 @@ function fakeSession(input) {
|
|
|
280
280
|
);
|
|
281
281
|
};
|
|
282
282
|
let closed = false;
|
|
283
|
-
const
|
|
283
|
+
const interruptedTurns = /* @__PURE__ */ new Set();
|
|
284
|
+
const outputTurn = async function* (turnInput) {
|
|
284
285
|
throwIfAgentOperationAborted(turnInput.signal);
|
|
285
286
|
input.state.turnIds.push(turnInput.turnId);
|
|
286
287
|
const occurredAt = input.now();
|
|
@@ -289,6 +290,14 @@ function fakeSession(input) {
|
|
|
289
290
|
type: "turn.started",
|
|
290
291
|
payload: { message: "Fake provider turn started." }
|
|
291
292
|
});
|
|
293
|
+
if (interruptedTurns.has(turnInput.turnId)) {
|
|
294
|
+
yield createAgentEventOutput({
|
|
295
|
+
...eventBase(input.context.sessionId, turnInput.turnId, input.now()),
|
|
296
|
+
type: "turn.completed",
|
|
297
|
+
payload: { outcome: "canceled", reason: "Fake provider interrupted." }
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
292
301
|
yield createAgentEventOutput({
|
|
293
302
|
...eventBase(input.context.sessionId, turnInput.turnId, occurredAt),
|
|
294
303
|
type: "item.started",
|
|
@@ -311,10 +320,13 @@ function fakeSession(input) {
|
|
|
311
320
|
}
|
|
312
321
|
if (input.capabilities.requests.approval.kind === "supported") {
|
|
313
322
|
const request = requestForTurn(turnInput.turnId);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
323
|
+
let release;
|
|
324
|
+
const answered = new Promise((resolve) => {
|
|
325
|
+
release = resolve;
|
|
317
326
|
});
|
|
327
|
+
pendingRequests.set(request.requestId, { request, turnId: turnInput.turnId, release });
|
|
328
|
+
const aborted = () => release();
|
|
329
|
+
turnInput.signal?.addEventListener("abort", aborted, { once: true });
|
|
318
330
|
yield createAgentEventOutput({
|
|
319
331
|
...eventBase(input.context.sessionId, turnInput.turnId, occurredAt),
|
|
320
332
|
type: "request.opened",
|
|
@@ -325,17 +337,30 @@ function fakeSession(input) {
|
|
|
325
337
|
type: "turn.state_changed",
|
|
326
338
|
payload: { state: "waiting_for_request", requestId: request.requestId }
|
|
327
339
|
});
|
|
328
|
-
|
|
340
|
+
try {
|
|
341
|
+
if (!turnInput.signal?.aborted) await answered;
|
|
342
|
+
throwIfAgentOperationAborted(turnInput.signal);
|
|
343
|
+
if (closed) return;
|
|
344
|
+
} finally {
|
|
345
|
+
turnInput.signal?.removeEventListener("abort", aborted);
|
|
346
|
+
pendingRequests.delete(request.requestId);
|
|
347
|
+
}
|
|
348
|
+
yield createAgentEventOutput({
|
|
349
|
+
...eventBase(input.context.sessionId, turnInput.turnId, input.now()),
|
|
350
|
+
type: "turn.state_changed",
|
|
351
|
+
payload: { state: "running" }
|
|
352
|
+
});
|
|
329
353
|
}
|
|
330
354
|
yield createAgentEventOutput({
|
|
331
355
|
...eventBase(input.context.sessionId, turnInput.turnId, occurredAt),
|
|
332
356
|
type: "turn.completed",
|
|
333
|
-
payload: { outcome: "completed", reason: "Fake provider completed." }
|
|
357
|
+
payload: { outcome: interruptedTurns.has(turnInput.turnId) ? "canceled" : "completed", reason: "Fake provider completed." }
|
|
334
358
|
});
|
|
335
359
|
};
|
|
336
360
|
const close = async () => {
|
|
337
361
|
if (closed) return;
|
|
338
362
|
closed = true;
|
|
363
|
+
for (const pending of pendingRequests.values()) pending.release();
|
|
339
364
|
input.state.closeCounts.set(
|
|
340
365
|
input.context.sessionId,
|
|
341
366
|
(input.state.closeCounts.get(input.context.sessionId) ?? 0) + 1
|
|
@@ -343,49 +368,35 @@ function fakeSession(input) {
|
|
|
343
368
|
};
|
|
344
369
|
return {
|
|
345
370
|
binding: input.binding,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
type: "turn.state_changed",
|
|
362
|
-
payload: { state: "running" }
|
|
363
|
-
});
|
|
364
|
-
yield createAgentEventOutput({
|
|
365
|
-
...eventBase(input.context.sessionId, pending.turnId, occurredAt),
|
|
366
|
-
type: "turn.completed",
|
|
367
|
-
payload: { outcome: "completed", reason: "Fake request resolved." }
|
|
368
|
-
});
|
|
371
|
+
startTurn(turnInput) {
|
|
372
|
+
return {
|
|
373
|
+
turnId: turnInput.turnId,
|
|
374
|
+
outputs: outputTurn(turnInput),
|
|
375
|
+
async resolveRequest(requestInput) {
|
|
376
|
+
throwIfAgentOperationAborted(requestInput.signal);
|
|
377
|
+
const pending = pendingRequests.get(requestInput.resolution.requestId);
|
|
378
|
+
if (!pending || pending.turnId !== turnInput.turnId) throw new TypeError("Fake provider has no matching pending request.");
|
|
379
|
+
const resolution = parseAgentRequestResolutionFor(pending.request, requestInput.resolution);
|
|
380
|
+
input.state.resolutions.push(resolution);
|
|
381
|
+
pendingRequests.delete(resolution.requestId);
|
|
382
|
+
pending.release();
|
|
383
|
+
return { status: "delivered" };
|
|
384
|
+
}
|
|
385
|
+
};
|
|
369
386
|
},
|
|
370
387
|
interruption: input.capabilities.turns.interrupt ? {
|
|
371
388
|
kind: "supported",
|
|
372
389
|
interruptTurn: async (interruptionInput) => {
|
|
373
390
|
throwIfAgentOperationAborted(interruptionInput.signal);
|
|
374
391
|
input.state.interruptedTurnIds.push(interruptionInput.turnId);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
payload: {
|
|
384
|
-
outcome: "canceled",
|
|
385
|
-
reason: interruptionInput.reason
|
|
386
|
-
}
|
|
387
|
-
})
|
|
388
|
-
]);
|
|
392
|
+
interruptedTurns.add(interruptionInput.turnId);
|
|
393
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
394
|
+
if (pending.turnId === interruptionInput.turnId) {
|
|
395
|
+
pendingRequests.delete(requestId);
|
|
396
|
+
pending.release();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { status: "accepted" };
|
|
389
400
|
}
|
|
390
401
|
} : { kind: "unsupported" },
|
|
391
402
|
steering: input.capabilities.turns.steer.kind === "supported" ? {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AgentProviderKey } from "@agen-ai/agent-protocol";
|
|
2
|
+
import type { AgentProviderOutput } from "../outputs.js";
|
|
3
|
+
import type { AgentProviderStartTurnInput, AgentProviderTurnExecution } from "../sessions.js";
|
|
4
|
+
import { type PendingAgentRequest, type SuspendedAgentTurnRequests } from "./requests.js";
|
|
5
|
+
export interface ValidatedAgentTurnExecution {
|
|
6
|
+
readonly execution: AgentProviderTurnExecution;
|
|
7
|
+
readonly hasEntered: () => boolean;
|
|
8
|
+
readonly stop: (reason: Error) => void;
|
|
9
|
+
readonly join: () => Promise<void>;
|
|
10
|
+
readonly suspendRequests: () => SuspendedAgentTurnRequests;
|
|
11
|
+
}
|
|
12
|
+
export declare function createValidatedAgentTurnExecution(input: Readonly<{
|
|
13
|
+
providerKey: AgentProviderKey;
|
|
14
|
+
turnInput: AgentProviderStartTurnInput;
|
|
15
|
+
pendingRequests: Map<string, PendingAgentRequest>;
|
|
16
|
+
inFlightOperations: Set<Promise<void>>;
|
|
17
|
+
start: (input: AgentProviderStartTurnInput) => AgentProviderTurnExecution;
|
|
18
|
+
requireUsable: () => void;
|
|
19
|
+
isTerminal: () => boolean;
|
|
20
|
+
observe: (output: AgentProviderOutput) => AgentProviderOutput;
|
|
21
|
+
assertCompleted: () => void;
|
|
22
|
+
release: () => void;
|
|
23
|
+
retire: () => void;
|
|
24
|
+
abandon: () => Promise<void>;
|
|
25
|
+
}>): ValidatedAgentTurnExecution;
|
|
26
|
+
//# sourceMappingURL=execution.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseAgentRequestResolution,
|
|
3
|
+
parseAgentRequestResolutionFor
|
|
4
|
+
} from "@agen-ai/agent-protocol";
|
|
5
|
+
import { AgentProviderDelegatedOperationError, throwAgentProviderContractError } from "../contractErrors.js";
|
|
6
|
+
import { throwIfAgentOperationAborted } from "../foundation.js";
|
|
7
|
+
import { hasExactOwnKeys } from "../internal/objectShape.js";
|
|
8
|
+
import { validateAgentRequestResolutionResult } from "./receipts.js";
|
|
9
|
+
import { createAgentTurnRequestAdmission } from "./requests.js";
|
|
10
|
+
function createValidatedAgentTurnExecution(input) {
|
|
11
|
+
let state = { kind: "reserved" };
|
|
12
|
+
let source;
|
|
13
|
+
const requests = createAgentTurnRequestAdmission({
|
|
14
|
+
pendingRequests: input.pendingRequests,
|
|
15
|
+
canRestore: () => state.kind === "entered" && !input.isTerminal()
|
|
16
|
+
});
|
|
17
|
+
let acquired = false;
|
|
18
|
+
let pulling = false;
|
|
19
|
+
let returnPromise;
|
|
20
|
+
const lifetime = new AbortController();
|
|
21
|
+
const nativeOperations = /* @__PURE__ */ new Set();
|
|
22
|
+
const fail = (code, message) => throwAgentProviderContractError(input.providerKey, code, message);
|
|
23
|
+
const abortError = () => {
|
|
24
|
+
try {
|
|
25
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof Error) return error;
|
|
28
|
+
}
|
|
29
|
+
return new DOMException("The turn execution was stopped.", "AbortError");
|
|
30
|
+
};
|
|
31
|
+
const detachAbort = () => input.turnInput.signal?.removeEventListener("abort", abort);
|
|
32
|
+
const stop = (reason) => {
|
|
33
|
+
if (state.kind === "settled" || state.kind === "stopped") return;
|
|
34
|
+
const entered = state.kind === "entered" || state.kind === "entering";
|
|
35
|
+
state = { kind: "stopped", reason };
|
|
36
|
+
requests.withdraw();
|
|
37
|
+
lifetime.abort(reason);
|
|
38
|
+
detachAbort();
|
|
39
|
+
if (entered) input.retire();
|
|
40
|
+
else input.release();
|
|
41
|
+
};
|
|
42
|
+
const abort = () => {
|
|
43
|
+
let reason = new DOMException("The turn execution was aborted.", "AbortError");
|
|
44
|
+
try {
|
|
45
|
+
throwIfAgentOperationAborted(input.turnInput.signal);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof Error) reason = error;
|
|
48
|
+
}
|
|
49
|
+
stop(reason);
|
|
50
|
+
};
|
|
51
|
+
input.turnInput.signal?.addEventListener("abort", abort, { once: true });
|
|
52
|
+
const track = (operation) => {
|
|
53
|
+
nativeOperations.add(operation);
|
|
54
|
+
void operation.then(() => nativeOperations.delete(operation), () => nativeOperations.delete(operation));
|
|
55
|
+
return operation;
|
|
56
|
+
};
|
|
57
|
+
const join = async () => {
|
|
58
|
+
while (nativeOperations.size > 0 || input.inFlightOperations.size > 0) {
|
|
59
|
+
await Promise.allSettled([...nativeOperations, ...input.inFlightOperations]);
|
|
60
|
+
}
|
|
61
|
+
if (source?.return) await source.return();
|
|
62
|
+
};
|
|
63
|
+
const requireAnswerable = () => {
|
|
64
|
+
input.requireUsable();
|
|
65
|
+
if (state.kind !== "entered" || input.isTerminal()) {
|
|
66
|
+
fail("request_resolution_mismatch", "Request no longer belongs to an answerable turn execution.");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const resolveRequest = async (requestInput) => {
|
|
70
|
+
requireAnswerable();
|
|
71
|
+
throwIfAgentOperationAborted(requestInput.signal);
|
|
72
|
+
const resolution = parseAgentRequestResolution(requestInput.resolution);
|
|
73
|
+
const pending = input.pendingRequests.get(resolution.requestId);
|
|
74
|
+
if (!pending || pending.turnId !== input.turnInput.turnId) {
|
|
75
|
+
return fail("request_resolution_mismatch", "Resolution does not identify an answerable request in this execution.");
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
parseAgentRequestResolutionFor(pending.request, resolution);
|
|
79
|
+
} catch {
|
|
80
|
+
return fail("request_resolution_mismatch", "Resolution does not match the opened request.");
|
|
81
|
+
}
|
|
82
|
+
if (pending.request.expiresAt !== void 0 && Date.parse(pending.request.expiresAt) <= Date.now() && resolution.disposition !== "canceled") {
|
|
83
|
+
return fail("request_resolution_mismatch", "Resolution identifies an expired request.");
|
|
84
|
+
}
|
|
85
|
+
if (state.kind !== "entered") return fail("request_resolution_mismatch", "Turn execution is no longer live.");
|
|
86
|
+
const candidate = state.candidate;
|
|
87
|
+
const restore = requests.claim(resolution.requestId, pending);
|
|
88
|
+
let settle;
|
|
89
|
+
const settlement = new Promise((resolve) => {
|
|
90
|
+
settle = resolve;
|
|
91
|
+
});
|
|
92
|
+
input.inFlightOperations.add(settlement);
|
|
93
|
+
try {
|
|
94
|
+
try {
|
|
95
|
+
requestInput.onProviderExecutionStarted?.();
|
|
96
|
+
throwIfAgentOperationAborted(requestInput.signal);
|
|
97
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
98
|
+
requireAnswerable();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
restore();
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
const signal = requestInput.signal === void 0 ? lifetime.signal : AbortSignal.any([lifetime.signal, requestInput.signal]);
|
|
106
|
+
result = validateAgentRequestResolutionResult(await candidate.resolveRequest({ resolution, signal }), input.providerKey);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
input.retire();
|
|
109
|
+
throw new AgentProviderDelegatedOperationError(input.providerKey, "resolve_request", error);
|
|
110
|
+
}
|
|
111
|
+
if (result.status === "rejected" && result.requestState === "pending") restore();
|
|
112
|
+
if (result.status === "delivery_uncertain") input.retire();
|
|
113
|
+
return result;
|
|
114
|
+
} finally {
|
|
115
|
+
input.inFlightOperations.delete(settlement);
|
|
116
|
+
settle();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const next = async () => {
|
|
120
|
+
if (state.kind === "stopped") throw state.reason;
|
|
121
|
+
if (state.kind === "settled") return { done: true, value: void 0 };
|
|
122
|
+
if (pulling) return fail("concurrent_turn", "Turn output permits only one outstanding pull.");
|
|
123
|
+
pulling = true;
|
|
124
|
+
let removeAbort = () => {
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
if (state.kind === "reserved") input.requireUsable();
|
|
128
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
129
|
+
if (state.kind === "reserved") {
|
|
130
|
+
try {
|
|
131
|
+
input.turnInput.onProviderExecutionStarted?.();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
state = { kind: "settled" };
|
|
134
|
+
detachAbort();
|
|
135
|
+
input.release();
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
139
|
+
input.requireUsable();
|
|
140
|
+
const { onProviderExecutionStarted: _observer, ...turnInput } = input.turnInput;
|
|
141
|
+
state = { kind: "entering" };
|
|
142
|
+
try {
|
|
143
|
+
const candidate = input.start({ ...turnInput, signal: lifetime.signal });
|
|
144
|
+
if (candidate === null || typeof candidate !== "object" || candidate.turnId !== turnInput.turnId || !hasExactOwnKeys(candidate, ["turnId", "outputs", "resolveRequest"]) || typeof candidate.resolveRequest !== "function" || !candidate.outputs || typeof candidate.outputs[Symbol.asyncIterator] !== "function") {
|
|
145
|
+
return fail("invalid_operation_result", "Provider returned an invalid turn execution.");
|
|
146
|
+
}
|
|
147
|
+
source = candidate.outputs[Symbol.asyncIterator]();
|
|
148
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
149
|
+
state = { kind: "entered", candidate, iterator: source };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
stop(error instanceof Error ? error : new Error("Provider turn entry failed.", { cause: error }));
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (state.kind !== "entered") throw abortError();
|
|
156
|
+
const nativePull = track(Promise.resolve().then(() => state.kind === "entered" ? state.iterator.next() : Promise.reject(abortError())));
|
|
157
|
+
const stopped = new Promise((_, reject) => {
|
|
158
|
+
const onAbort = () => reject(abortError());
|
|
159
|
+
lifetime.signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
removeAbort = () => lifetime.signal.removeEventListener("abort", onAbort);
|
|
161
|
+
});
|
|
162
|
+
const result = await Promise.race([nativePull, stopped]);
|
|
163
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
164
|
+
if (!result.done) {
|
|
165
|
+
const output = input.observe(result.value);
|
|
166
|
+
if (output.kind === "event" && output.event.type === "request.opened") requests.observeRequest();
|
|
167
|
+
return { done: false, value: output };
|
|
168
|
+
}
|
|
169
|
+
while (input.inFlightOperations.size > 0) {
|
|
170
|
+
await Promise.race([Promise.all(input.inFlightOperations), stopped]);
|
|
171
|
+
}
|
|
172
|
+
throwIfAgentOperationAborted(lifetime.signal);
|
|
173
|
+
input.assertCompleted();
|
|
174
|
+
state = { kind: "settled" };
|
|
175
|
+
detachAbort();
|
|
176
|
+
input.release();
|
|
177
|
+
return { done: true, value: void 0 };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (state.kind === "entered" || state.kind === "entering") stop(error instanceof Error ? error : new Error("Provider output failed.", { cause: error }));
|
|
180
|
+
throw error;
|
|
181
|
+
} finally {
|
|
182
|
+
removeAbort();
|
|
183
|
+
pulling = false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const iterator = {
|
|
187
|
+
next,
|
|
188
|
+
return() {
|
|
189
|
+
if (returnPromise) return returnPromise;
|
|
190
|
+
if (state.kind === "settled") return Promise.resolve({ done: true, value: void 0 });
|
|
191
|
+
const entered = state.kind === "entered" || source !== void 0;
|
|
192
|
+
stop(new DOMException("The turn output consumer returned.", "AbortError"));
|
|
193
|
+
returnPromise = (entered ? input.abandon() : Promise.resolve()).then(() => ({ done: true, value: void 0 }));
|
|
194
|
+
return returnPromise;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const outputs = Object.freeze({
|
|
198
|
+
[Symbol.asyncIterator]() {
|
|
199
|
+
if (acquired) return fail("concurrent_turn", "Turn output has exactly one consumer.");
|
|
200
|
+
acquired = true;
|
|
201
|
+
return iterator;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
execution: Object.freeze({ turnId: input.turnInput.turnId, outputs, resolveRequest }),
|
|
206
|
+
hasEntered: () => state.kind === "entering" || state.kind === "entered",
|
|
207
|
+
stop,
|
|
208
|
+
join,
|
|
209
|
+
suspendRequests: requests.suspend
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
createValidatedAgentTurnExecution
|
|
214
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AgentProviderKey } from "@agen-ai/agent-protocol";
|
|
2
|
+
import { type AgentProviderOutputValidationContext } from "../outputValidation.js";
|
|
3
|
+
import type { AgentProviderTurnInterruptionResult, AgentRequestResolutionResult } from "../sessions.js";
|
|
4
|
+
export declare function validateAgentProviderTurnInterruptionResult(candidate: AgentProviderTurnInterruptionResult, context: AgentProviderOutputValidationContext): AgentProviderTurnInterruptionResult;
|
|
5
|
+
export declare function validateAgentRequestResolutionResult(candidate: unknown, providerKey: AgentProviderKey): AgentRequestResolutionResult;
|
|
6
|
+
//# sourceMappingURL=receipts.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseAgentError } from "@agen-ai/agent-protocol";
|
|
2
|
+
import { throwAgentProviderContractError } from "../contractErrors.js";
|
|
3
|
+
import { hasExactOwnKeys } from "../internal/objectShape.js";
|
|
4
|
+
import { validateAgentProviderOperationResult } from "../outputValidation.js";
|
|
5
|
+
function validateAgentProviderTurnInterruptionResult(candidate, context) {
|
|
6
|
+
if (candidate === null || typeof candidate !== "object" || !hasExactOwnKeys(candidate, candidate.status === "failed" ? ["status", "error"] : ["status"])) {
|
|
7
|
+
return throwAgentProviderContractError(
|
|
8
|
+
context.providerKey,
|
|
9
|
+
"invalid_operation_result",
|
|
10
|
+
"Provider interruption must return an output-free operation receipt."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const result = validateAgentProviderOperationResult(candidate, context);
|
|
14
|
+
return Object.freeze({ status: result.status, ...result.error === void 0 ? {} : { error: result.error } });
|
|
15
|
+
}
|
|
16
|
+
function validateAgentRequestResolutionResult(candidate, providerKey) {
|
|
17
|
+
const invalid = () => throwAgentProviderContractError(
|
|
18
|
+
providerKey,
|
|
19
|
+
"invalid_operation_result",
|
|
20
|
+
"Provider returned an invalid request delivery receipt."
|
|
21
|
+
);
|
|
22
|
+
if (candidate === null || typeof candidate !== "object" || !("status" in candidate)) return invalid();
|
|
23
|
+
if (candidate.status === "delivered" && hasExactOwnKeys(candidate, ["status"])) {
|
|
24
|
+
return Object.freeze({ status: "delivered" });
|
|
25
|
+
}
|
|
26
|
+
if (!("error" in candidate)) return invalid();
|
|
27
|
+
const error = parseAgentError(candidate.error);
|
|
28
|
+
if (candidate.status === "delivery_uncertain" && hasExactOwnKeys(candidate, ["status", "error"])) {
|
|
29
|
+
return Object.freeze({ status: "delivery_uncertain", error });
|
|
30
|
+
}
|
|
31
|
+
if (candidate.status === "rejected" && hasExactOwnKeys(candidate, ["status", "requestState", "error"]) && "requestState" in candidate && (candidate.requestState === "pending" || candidate.requestState === "unavailable")) {
|
|
32
|
+
return Object.freeze({ status: "rejected", requestState: candidate.requestState, error });
|
|
33
|
+
}
|
|
34
|
+
return invalid();
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
validateAgentProviderTurnInterruptionResult,
|
|
38
|
+
validateAgentRequestResolutionResult
|
|
39
|
+
};
|