@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
@@ -50,25 +50,104 @@ async function advanceToStartedTurn(session, input) {
50
50
  );
51
51
  outputs.push(next.value);
52
52
  if (next.value.kind === "event" && next.value.event.type === "turn.started") {
53
- return { execution, iterator, outputs };
53
+ return { session, execution, iterator, outputs };
54
54
  }
55
55
  }
56
56
  } catch (error) {
57
- await iterator.return?.();
57
+ const closing = session.close({ reason: "contract_rejected" });
58
+ const settled = await Promise.allSettled([closing, iterator.return?.()]);
59
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
60
+ if (failures.length > 0) throw new AggregateError([error, ...failures], "Conformance opening and cleanup failed.", { cause: error });
58
61
  throw error;
59
62
  }
60
63
  }
61
- async function collectRemainingOutputs(started, resolutionFor) {
62
- for (; ; ) {
63
- const next = await started.iterator.next();
64
- if (next.done) return started.outputs;
65
- started.outputs.push(next.value);
66
- if (next.value.kind === "event" && next.value.event.type === "request.opened" && resolutionFor) {
67
- const request = next.value.event.payload.request;
68
- const receipt = await started.execution.resolveRequest({ resolution: parseAgentRequestResolutionFor(request, resolutionFor(request)) });
69
- requireCheck(receipt.status === "delivered", "request_resolution", "Matching request answer was not accepted by the provider handler.");
64
+ async function collectRemainingOutputs(started, resolutionFor, beforeClose) {
65
+ const requests = /* @__PURE__ */ new Map();
66
+ const tasks = [];
67
+ let acceptingRequests = true;
68
+ let deliveredRequests = 0;
69
+ let failTask;
70
+ const taskFailure = new Promise((_resolve, reject) => {
71
+ failTask = reject;
72
+ });
73
+ const abortRequests = () => {
74
+ for (const controller of requests.values()) {
75
+ controller.abort(new DOMException("Conformance request is no longer answerable.", "AbortError"));
76
+ }
77
+ };
78
+ const resolve = async (request, signal) => {
79
+ if (!resolutionFor) return;
80
+ let resolution;
81
+ try {
82
+ resolution = await resolutionFor(request, { signal });
83
+ } catch (error) {
84
+ if (signal.aborted && isAgentOperationAbortError(error)) return;
85
+ throw error;
70
86
  }
87
+ if (resolution === void 0 || signal.aborted) return;
88
+ const receipt = await started.execution.resolveRequest({
89
+ resolution: parseAgentRequestResolutionFor(request, resolution)
90
+ });
91
+ requireCheck(
92
+ receipt.status === "delivered",
93
+ "request_resolution",
94
+ "Matching request answer was not accepted by the provider handler."
95
+ );
96
+ deliveredRequests += 1;
97
+ };
98
+ const reader = (async () => {
99
+ for (; ; ) {
100
+ const next = await started.iterator.next();
101
+ if (next.done || !acceptingRequests) return;
102
+ started.outputs.push(next.value);
103
+ if (next.value.kind !== "event") continue;
104
+ const event = next.value.event;
105
+ if (event.type === "request.opened" && resolutionFor) {
106
+ const request = event.payload.request;
107
+ const controller = new AbortController();
108
+ requests.set(request.requestId, controller);
109
+ const task = resolve(request, controller.signal);
110
+ tasks.push(task);
111
+ void task.catch((error) => {
112
+ acceptingRequests = false;
113
+ failTask(error);
114
+ });
115
+ } else if (event.type === "request.unavailable") {
116
+ requests.get(event.payload.requestId)?.abort(
117
+ new DOMException("The provider retired this conformance request.", "AbortError")
118
+ );
119
+ requests.delete(event.payload.requestId);
120
+ } else if (event.type === "turn.completed") {
121
+ abortRequests();
122
+ return;
123
+ }
124
+ }
125
+ })();
126
+ const checked = reader.then(async () => {
127
+ if (acceptingRequests) await beforeClose?.();
128
+ });
129
+ const failures = /* @__PURE__ */ new Set();
130
+ try {
131
+ await Promise.race([checked, taskFailure]);
132
+ } catch (error) {
133
+ failures.add(error);
134
+ }
135
+ acceptingRequests = false;
136
+ abortRequests();
137
+ const closing = started.session.close({ reason: failures.size > 0 ? "contract_rejected" : "idle" });
138
+ const [closed, read, inspected, returned] = await Promise.allSettled([closing, reader, checked, started.iterator.return?.()]);
139
+ if (closed?.status === "rejected") failures.add(closed.reason);
140
+ for (const result of [read, inspected]) {
141
+ if (result?.status === "rejected" && !(failures.size > 0 && isAgentOperationAbortError(result.reason))) failures.add(result.reason);
71
142
  }
143
+ if (returned?.status === "rejected") failures.add(returned.reason);
144
+ const joined = await Promise.allSettled(tasks);
145
+ for (const result of joined) {
146
+ if (result.status === "rejected") failures.add(result.reason);
147
+ }
148
+ if (failures.size === 1) throw failures.values().next().value;
149
+ if (failures.size > 1) throw new AggregateError(failures, "Conformance and owned cleanup failed.", { cause: failures.values().next().value });
150
+ return { outputs: started.outputs, deliveredRequests };
72
151
  }
73
152
  function requireOperationStatus(result, statuses, check, message) {
74
153
  requireCheck(statuses.includes(result.status), check, message);
@@ -110,6 +189,7 @@ async function runAgentProviderConformance(scenario) {
110
189
  });
111
190
  let disposed = false;
112
191
  let createdSession = null;
192
+ let failure;
113
193
  try {
114
194
  const instance = registry.requireInstance(scenario.definition.instanceId);
115
195
  requireCheck(
@@ -276,55 +356,7 @@ async function runAgentProviderConformance(scenario) {
276
356
  );
277
357
  checks.push("abort");
278
358
  }
279
- const startedTurn = await advanceToStartedTurn(created, scenario.turn);
280
- let turnOutputs;
281
- try {
282
- if (instance.capabilities.turns.steer.kind === "supported") {
283
- requireCheck(
284
- created.steering.kind === "supported",
285
- "steering",
286
- "Steering capability has no handler."
287
- );
288
- const result = await created.steering.steerTurn({
289
- turnId: scenario.turn.turnId,
290
- ...scenario.steeringInput ?? {
291
- parts: [
292
- { type: "text", text: "Additional conformance input." }
293
- ]
294
- }
295
- });
296
- requireCheck(
297
- result.status === "delivered",
298
- "steering",
299
- "Steering did not deliver to the live turn."
300
- );
301
- } else {
302
- requireCheck(
303
- created.steering.kind === "unsupported",
304
- "steering",
305
- "Unsupported steering exposed a handler."
306
- );
307
- }
308
- turnOutputs = await collectRemainingOutputs(startedTurn, scenario.resolutionFor);
309
- } catch (error) {
310
- await startedTurn.iterator.return?.();
311
- await created.close({ reason: "contract_rejected" });
312
- throw error;
313
- }
314
- const events = semanticEvents(turnOutputs);
315
- requireCheck(
316
- events[0]?.type === "turn.started",
317
- "turn_order",
318
- "Turn did not start first."
319
- );
320
- requireCheck(
321
- hasStableTurnBoundary(events),
322
- "turn_order",
323
- "Turn did not terminate or enter a waiting state."
324
- );
325
- checks.push("turn_order");
326
- if (events.some((event) => event.type === "request.opened")) checks.push("request_resolution");
327
- checks.push("steering");
359
+ const turnChecksIndex = checks.length;
328
360
  if (instance.capabilities.turns.interrupt) {
329
361
  let interruptedBindingCount = 0;
330
362
  const interrupted = await instance.adapter.createSession({
@@ -362,15 +394,15 @@ async function runAgentProviderConformance(scenario) {
362
394
  "interruption",
363
395
  "Live turn interruption did not succeed."
364
396
  );
365
- const outputs = await collectRemainingOutputs(liveTurn);
397
+ const { outputs } = await collectRemainingOutputs(liveTurn);
366
398
  requireCheck(
367
399
  semanticEvents(outputs).some((event) => event.type === "turn.completed"),
368
400
  "interruption",
369
401
  "Interrupted turn did not produce native terminal output."
370
402
  );
371
403
  } finally {
372
- await liveTurn?.iterator.return?.();
373
- await interrupted.close({ reason: "other" });
404
+ const closing = interrupted.close({ reason: "other" });
405
+ await Promise.all([closing, liveTurn?.iterator.return?.()]);
374
406
  }
375
407
  } else {
376
408
  requireCheck(
@@ -526,28 +558,83 @@ async function runAgentProviderConformance(scenario) {
526
558
  );
527
559
  }
528
560
  checks.push("collaboration");
529
- if (instance.capabilities.generatedResources.kind === "supported") {
530
- requireCheck(
531
- created.generatedResources.kind === "supported" && scenario.generatedResourceId !== void 0,
532
- "generated_resources",
533
- "Supported generated resources require an access port and fixture."
534
- );
535
- const resource = await created.generatedResources.getGeneratedResource({
536
- resourceId: scenario.generatedResourceId
561
+ const turnChecks = [];
562
+ const startedTurn = await advanceToStartedTurn(created, scenario.turn);
563
+ let turnResult;
564
+ try {
565
+ if (instance.capabilities.turns.steer.kind === "supported") {
566
+ requireCheck(
567
+ created.steering.kind === "supported",
568
+ "steering",
569
+ "Steering capability has no handler."
570
+ );
571
+ const result = await created.steering.steerTurn({
572
+ turnId: scenario.turn.turnId,
573
+ ...scenario.steeringInput ?? {
574
+ parts: [
575
+ { type: "text", text: "Additional conformance input." }
576
+ ]
577
+ }
578
+ });
579
+ requireCheck(
580
+ result.status === "delivered",
581
+ "steering",
582
+ "Steering did not deliver to the live turn."
583
+ );
584
+ } else {
585
+ requireCheck(
586
+ created.steering.kind === "unsupported",
587
+ "steering",
588
+ "Unsupported steering exposed a handler."
589
+ );
590
+ }
591
+ turnResult = await collectRemainingOutputs(startedTurn, scenario.resolutionFor, async () => {
592
+ if (instance.capabilities.generatedResources.kind === "supported") {
593
+ requireCheck(
594
+ created.generatedResources.kind === "supported" && scenario.generatedResourceId !== void 0,
595
+ "generated_resources",
596
+ "Supported generated resources require an access port and fixture."
597
+ );
598
+ const resource = await created.generatedResources.getGeneratedResource({
599
+ resourceId: scenario.generatedResourceId
600
+ });
601
+ requireCheck(
602
+ resource.descriptor.resourceId === scenario.generatedResourceId,
603
+ "generated_resources",
604
+ "Generated resource access returned another resource."
605
+ );
606
+ } else {
607
+ requireCheck(
608
+ created.generatedResources.kind === "unsupported",
609
+ "generated_resources",
610
+ "Unsupported generated resources exposed an access port."
611
+ );
612
+ }
613
+ checks.push("generated_resources");
537
614
  });
538
- requireCheck(
539
- resource.descriptor.resourceId === scenario.generatedResourceId,
540
- "generated_resources",
541
- "Generated resource access returned another resource."
542
- );
543
- } else {
544
- requireCheck(
545
- created.generatedResources.kind === "unsupported",
546
- "generated_resources",
547
- "Unsupported generated resources exposed an access port."
548
- );
615
+ } catch (error) {
616
+ const closing = created.close({ reason: "contract_rejected" });
617
+ const settled = await Promise.allSettled([closing, startedTurn.iterator.return?.()]);
618
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
619
+ if (failures.length > 0) throw new AggregateError([error, ...failures], "Conformance turn and cleanup failed.", { cause: error });
620
+ throw error;
549
621
  }
550
- checks.push("generated_resources");
622
+ const events = semanticEvents(turnResult.outputs);
623
+ requireCheck(
624
+ events[0]?.type === "turn.started",
625
+ "turn_order",
626
+ "Turn did not start first."
627
+ );
628
+ requireCheck(
629
+ hasStableTurnBoundary(events),
630
+ "turn_order",
631
+ "Turn did not terminate or enter a waiting state."
632
+ );
633
+ turnChecks.push("turn_order");
634
+ if (turnResult.deliveredRequests > 0) turnChecks.push("request_resolution");
635
+ if (events.some((event) => event.type === "request.unavailable")) turnChecks.push("request_unavailability");
636
+ turnChecks.push("steering");
637
+ checks.splice(turnChecksIndex, 0, ...turnChecks);
551
638
  await created.close({ reason: "idle" });
552
639
  await created.close({ reason: "idle" });
553
640
  checks.push("idempotent_session_close");
@@ -620,9 +707,17 @@ async function runAgentProviderConformance(scenario) {
620
707
  providerKey: scenario.definition.providerKey,
621
708
  checks: Object.freeze(checks)
622
709
  });
710
+ } catch (error) {
711
+ failure = { error };
712
+ throw error;
623
713
  } finally {
624
- await createdSession?.close({ reason: "contract_rejected" });
625
- if (!disposed) await registry.dispose();
714
+ try {
715
+ await createdSession?.close({ reason: "contract_rejected" });
716
+ if (!disposed) await registry.dispose();
717
+ } catch (error) {
718
+ if (failure) throw new AggregateError([failure.error, error], "Conformance and final cleanup failed.", { cause: failure.error });
719
+ throw error;
720
+ }
626
721
  }
627
722
  }
628
723
  export {
@@ -44,7 +44,7 @@ function defaultCapabilities(providerKey) {
44
44
  }
45
45
  };
46
46
  return parseAgentCapabilities({
47
- protocolVersion: 11,
47
+ protocolVersion: 12,
48
48
  providerKey,
49
49
  sessions: { create: true, resume: true, branch: { kind: "through_turn" } },
50
50
  turns: {
@@ -166,7 +166,7 @@ function sessionBinding(state, sessionId) {
166
166
  };
167
167
  }
168
168
  function eventBase(sessionId, turnId, occurredAt) {
169
- return { protocolVersion: 11, sessionId, turnId, occurredAt };
169
+ return { protocolVersion: 12, sessionId, turnId, occurredAt };
170
170
  }
171
171
  function fakeEnvironmentSnapshot(environmentId) {
172
172
  return parseAgentEnvironmentSnapshot({
@@ -1,26 +1,43 @@
1
- import { type AgentProviderKey } from "@agen-ai/agent-protocol";
1
+ import { type AgentProviderKey, type AgentTurnId } from "@agen-ai/agent-protocol";
2
2
  import type { AgentProviderOutput } from "../outputs.js";
3
+ import type { AgentSessionAdmission } from "../sessionAdmission.js";
3
4
  import type { AgentProviderStartTurnInput, AgentProviderTurnExecution } from "../sessions.js";
4
- import { type PendingAgentRequest, type SuspendedAgentTurnRequests } from "./requests.js";
5
+ import { type AgentTurnRequestAdmission, type PendingAgentRequest, type SuspendedAgentTurnRequests } from "./requests.js";
5
6
  export interface ValidatedAgentTurnExecution {
6
7
  readonly execution: AgentProviderTurnExecution;
7
8
  readonly hasEntered: () => boolean;
8
9
  readonly stop: (reason: Error) => void;
9
10
  readonly join: () => Promise<void>;
10
11
  readonly suspendRequests: () => SuspendedAgentTurnRequests;
12
+ readonly completed: Promise<void>;
11
13
  }
12
- export declare function createValidatedAgentTurnExecution(input: Readonly<{
14
+ type ExecutionOptions = Readonly<{
13
15
  providerKey: AgentProviderKey;
14
- turnInput: AgentProviderStartTurnInput;
15
16
  pendingRequests: Map<string, PendingAgentRequest>;
17
+ sessionAdmission: AgentSessionAdmission;
16
18
  inFlightOperations: Set<Promise<void>>;
17
- start: (input: AgentProviderStartTurnInput) => AgentProviderTurnExecution;
18
19
  requireUsable: () => void;
19
20
  isTerminal: () => boolean;
20
- observe: (output: AgentProviderOutput) => AgentProviderOutput;
21
+ canRestoreRequest: (pending: PendingAgentRequest) => boolean;
22
+ observe: (output: AgentProviderOutput, requests: AgentTurnRequestAdmission) => AgentProviderOutput;
21
23
  assertCompleted: () => void;
22
24
  release: () => void;
23
25
  retire: () => void;
24
26
  abandon: () => Promise<void>;
27
+ }>;
28
+ export declare function createValidatedAgentTurnExecution(input: ExecutionOptions & Readonly<{
29
+ turnInput: AgentProviderStartTurnInput;
30
+ start: (input: AgentProviderStartTurnInput) => AgentProviderTurnExecution;
25
31
  }>): ValidatedAgentTurnExecution;
32
+ export interface AdoptedAgentTurnExecution extends ValidatedAgentTurnExecution {
33
+ readonly readerReady: Promise<void>;
34
+ readonly admitDispatch: () => void;
35
+ readonly settleNoWork: () => void;
36
+ readonly markStarted: () => void;
37
+ }
38
+ export declare function adoptValidatedAgentTurnExecution(input: ExecutionOptions & Readonly<{
39
+ turnId: AgentTurnId;
40
+ createCandidate: (signal: AbortSignal) => AgentProviderTurnExecution;
41
+ }>): AdoptedAgentTurnExecution;
42
+ export {};
26
43
  //# sourceMappingURL=execution.d.ts.map