@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.
@@ -0,0 +1,119 @@
1
+ import path from "node:path";
2
+ import {
3
+ parseAgentError,
4
+ parseAgentSessionDecisionChoiceId,
5
+ parseAgentSessionDecisionRequest
6
+ } from "@agen-ai/agent-protocol";
7
+ import { AgentProviderContractError } from "../contractErrors.js";
8
+ import { hasExactOwnKeys } from "../internal/objectShape.js";
9
+ import {
10
+ DEFAULT_AGENT_SESSION_INTERACTION_LIMITS
11
+ } from "./types.js";
12
+ function record(input) {
13
+ if (input === null || typeof input !== "object" || Array.isArray(input)) throw new TypeError("Expected an interaction object.");
14
+ return input;
15
+ }
16
+ function interactionContractError(providerKey, message) {
17
+ return new AgentProviderContractError(providerKey, "invalid_session_interaction", message);
18
+ }
19
+ function validateSessionInteractionSupport(capabilities, adapter) {
20
+ const input = adapter.sessionInteractions;
21
+ if (input === void 0) return Object.freeze({ kind: "unsupported" });
22
+ try {
23
+ const value = record(input);
24
+ if (value.kind === "unsupported" && hasExactOwnKeys(value, ["kind"])) return Object.freeze({ kind: "unsupported" });
25
+ if (!hasExactOwnKeys(value, ["kind", "openingOperations", "requestKinds", "producerOwnership"]) || value.kind !== "supported" || value.producerOwnership !== "exclusive" || !Array.isArray(value.openingOperations) || value.openingOperations.length === 0 || !Array.isArray(value.requestKinds) || value.requestKinds.length !== 1 || value.requestKinds[0] !== "decision") throw new TypeError();
26
+ const operations = [];
27
+ for (const operation of value.openingOperations) {
28
+ if (operation !== "create" && operation !== "resume" && operation !== "branch") throw new TypeError();
29
+ if (operations.includes(operation)) throw new TypeError();
30
+ if (operation === "resume" && (!capabilities.sessions.resume || adapter.resumption.kind !== "supported")) throw new TypeError();
31
+ if (operation === "branch" && (capabilities.sessions.branch.kind !== "through_turn" || adapter.branching.kind !== "through_turn")) throw new TypeError();
32
+ operations.push(operation);
33
+ }
34
+ const order = ["create", "resume", "branch"];
35
+ if (operations.some((operation, index) => index > 0 && order.indexOf(operation) < order.indexOf(operations[index - 1]))) throw new TypeError();
36
+ const [first, ...rest] = operations;
37
+ if (first === void 0) throw new TypeError();
38
+ return Object.freeze({
39
+ kind: "supported",
40
+ producerOwnership: "exclusive",
41
+ openingOperations: Object.freeze([first, ...rest]),
42
+ requestKinds: Object.freeze(["decision"])
43
+ });
44
+ } catch {
45
+ throw interactionContractError(capabilities.providerKey, "Provider declared invalid session interaction support.");
46
+ }
47
+ }
48
+ const limitRanges = {
49
+ maxRetainedRequests: [1, 32],
50
+ maxRetainedBytes: [65536, 2097152],
51
+ requestTimeoutMs: [1e3, 18e5],
52
+ responseTimeoutMs: [1e3, 12e4],
53
+ cleanupTimeoutMs: [1e3, 12e4]
54
+ };
55
+ function validateSessionInteractionContext(input) {
56
+ const value = record(input);
57
+ const onRequest = value.onRequest;
58
+ if (!hasExactOwnKeys(value, Object.hasOwn(value, "limits") ? ["onRequest", "limits"] : ["onRequest"]) || typeof onRequest !== "function") throw new TypeError("Session interactions require a callable handler and known fields.");
59
+ const supplied = value.limits === void 0 ? {} : record(value.limits);
60
+ if (Reflect.ownKeys(supplied).some((key) => typeof key !== "string" || !Object.hasOwn(limitRanges, key))) {
61
+ throw new TypeError("Session interaction limits contain an unknown field.");
62
+ }
63
+ const limits = { ...DEFAULT_AGENT_SESSION_INTERACTION_LIMITS };
64
+ for (const key of Object.keys(limitRanges)) {
65
+ const number = supplied[key];
66
+ if (number === void 0) continue;
67
+ const [minimum, maximum] = limitRanges[key];
68
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < minimum || number > maximum) {
69
+ throw new RangeError(`Session interaction ${key} must be an integer between ${minimum} and ${maximum}.`);
70
+ }
71
+ limits[key] = number;
72
+ }
73
+ return Object.freeze({ onRequest: onRequest.bind(input), limits: Object.freeze(limits) });
74
+ }
75
+ function captureSessionInteractionCandidate(input, observe) {
76
+ const value = record(input);
77
+ const terminal = value.terminal;
78
+ const observation = Promise.resolve(terminal);
79
+ void observation.catch(() => {
80
+ });
81
+ observe(observation);
82
+ const request = value.request;
83
+ const signal = value.signal;
84
+ const respond = value.respond;
85
+ if (!hasExactOwnKeys(value, ["request", "signal", "terminal", "respond"]) || !(signal instanceof AbortSignal) || typeof respond !== "function" || !terminal || typeof record(terminal).then !== "function") throw new TypeError("Provider offered an invalid session interaction handle.");
86
+ return Object.freeze({
87
+ request,
88
+ signal,
89
+ terminal: observation,
90
+ respond: respond.bind(input)
91
+ });
92
+ }
93
+ function validateSessionInteractionCandidate(input, sessionId, requestTimeoutMs) {
94
+ const parsed = parseAgentSessionDecisionRequest(input.request);
95
+ if (parsed.sessionId !== sessionId || !path.isAbsolute(parsed.target.path) || path.normalize(parsed.target.path) !== parsed.target.path) {
96
+ throw new TypeError("Session interaction does not match its session or canonical workspace target.");
97
+ }
98
+ const expiry = Math.min(Date.parse(parsed.expiresAt), Date.now() + requestTimeoutMs);
99
+ if (expiry <= Date.now()) throw new TypeError("Provider offered an expired session interaction.");
100
+ const request = parseAgentSessionDecisionRequest({ ...parsed, expiresAt: new Date(expiry).toISOString() });
101
+ return Object.freeze({ ...input, request });
102
+ }
103
+ function validateSessionInteractionTerminal(input) {
104
+ const value = record(input);
105
+ if (value.kind === "answered" && hasExactOwnKeys(value, ["kind", "choiceId"])) {
106
+ return Object.freeze({ kind: "answered", choiceId: parseAgentSessionDecisionChoiceId(value.choiceId) });
107
+ }
108
+ if ((value.kind === "failed" || value.kind === "rejected" || value.kind === "delivery_uncertain") && hasExactOwnKeys(value, ["kind", "error"])) return Object.freeze({ kind: value.kind, error: parseAgentError(value.error) });
109
+ if ((value.kind === "withdrawn" || value.kind === "expired" || value.kind === "session_closed") && hasExactOwnKeys(value, ["kind"])) return Object.freeze({ kind: value.kind });
110
+ throw new TypeError("Provider returned an invalid session interaction terminal.");
111
+ }
112
+ export {
113
+ captureSessionInteractionCandidate,
114
+ interactionContractError,
115
+ validateSessionInteractionCandidate,
116
+ validateSessionInteractionContext,
117
+ validateSessionInteractionSupport,
118
+ validateSessionInteractionTerminal
119
+ };
@@ -2,6 +2,7 @@ import { type AgentCapabilities, type AgentProviderKey, type AgentSessionBinding
2
2
  import { type AgentHostBindingLease } from "./hostBindings/lifetime.js";
3
3
  import { type MaybePromise } from "./foundation.js";
4
4
  import type { AgentProviderSession, AgentSessionBindingCreatedObserver } from "./sessions.js";
5
+ import type { SessionInteractionOwner } from "./sessionInteractions/lifetime.js";
5
6
  export declare function validateAgentProviderSession(input: {
6
7
  readonly capabilities: AgentCapabilities;
7
8
  readonly sessionId: AgentSessionId;
@@ -9,6 +10,7 @@ export declare function validateAgentProviderSession(input: {
9
10
  readonly expectedBinding?: AgentSessionBinding;
10
11
  readonly sourceBinding?: AgentSessionBinding;
11
12
  readonly hostBindingLease?: AgentHostBindingLease;
13
+ readonly sessionInteractionOwner?: SessionInteractionOwner;
12
14
  }): AgentProviderSession;
13
15
  export declare function closeRejectedAgentProviderSession(session: AgentProviderSession | null, error: unknown, hostBinding?: Readonly<{
14
16
  providerKey: AgentProviderKey;
@@ -16,7 +18,7 @@ export declare function closeRejectedAgentProviderSession(session: AgentProvider
16
18
  }>, opening?: Readonly<{
17
19
  providerKey: AgentProviderKey;
18
20
  input: object;
19
- }>): Promise<never>;
21
+ }>, interactions?: SessionInteractionOwner): Promise<never>;
20
22
  export declare function openIdentityCreatingAgentProviderSession(input: {
21
23
  readonly capabilities: AgentCapabilities;
22
24
  readonly sessionId: AgentSessionId;
@@ -24,6 +26,7 @@ export declare function openIdentityCreatingAgentProviderSession(input: {
24
26
  readonly sourceBinding?: AgentSessionBinding;
25
27
  readonly hostBindingLease?: AgentHostBindingLease;
26
28
  readonly openingInput?: object;
29
+ readonly sessionInteractionOwner?: SessionInteractionOwner;
27
30
  readonly open: (observer: AgentSessionBindingCreatedObserver) => MaybePromise<AgentProviderSession>;
28
31
  }): Promise<AgentProviderSession>;
29
32
  //# sourceMappingURL=sessionValidation.d.ts.map
@@ -46,6 +46,11 @@ import {
46
46
  import { validateAgentTurnSteeringResult } from "./steeringValidation.js";
47
47
  import { createValidatedAgentTurnExecution } from "./turnExecution/execution.js";
48
48
  import { validateAgentProviderTurnInterruptionResult } from "./turnExecution/receipts.js";
49
+ function validateCloseReason(reason) {
50
+ if (reason !== void 0 && !["idle", "shutdown", "replaced", "contract_rejected", "error", "other"].includes(reason)) {
51
+ throw new TypeError("Provider session close reason is unsupported.");
52
+ }
53
+ }
49
54
  function validateSessionPorts(capabilities, session) {
50
55
  if (!hasExactOwnKeys(session, [
51
56
  "binding",
@@ -703,6 +708,7 @@ function validateAgentProviderSession(input) {
703
708
  );
704
709
  };
705
710
  const requireUsable = () => {
711
+ input.sessionInteractionOwner?.requireUsable();
706
712
  requireOpen();
707
713
  if (unusable) {
708
714
  throwAgentProviderContractError(
@@ -742,6 +748,7 @@ function validateAgentProviderSession(input) {
742
748
  await input2.observeOutput?.(candidate);
743
749
  };
744
750
  try {
751
+ requireUsable();
745
752
  const candidate = await input2.invoke(onProviderExecutionStarted, onOutput);
746
753
  if (!executionStarted) {
747
754
  throwAgentProviderContractError(
@@ -1386,39 +1393,34 @@ function validateAgentProviderSession(input) {
1386
1393
  return inspection;
1387
1394
  }
1388
1395
  });
1389
- const close = (closeInput) => {
1390
- if (closed) return Promise.resolve();
1391
- if (closePromise) return closePromise;
1396
+ const closeOwned = (closeInput, explicit) => {
1397
+ if (closed && !input.sessionInteractionOwner) return Promise.resolve();
1398
+ if (!input.sessionInteractionOwner && closePromise) return closePromise;
1392
1399
  throwIfAgentOperationAborted(closeInput.signal);
1393
- if (closeInput.reason !== void 0 && ![
1394
- "idle",
1395
- "shutdown",
1396
- "replaced",
1397
- "contract_rejected",
1398
- "error",
1399
- "other"
1400
- ].includes(closeInput.reason)) {
1401
- throw new TypeError("Provider session close reason is unsupported.");
1402
- }
1400
+ validateCloseReason(closeInput.reason);
1403
1401
  unusable = true;
1404
1402
  sessionLifetime.abort(new DOMException("The provider session was closed.", "AbortError"));
1405
1403
  input.hostBindingLease?.stop();
1406
1404
  const closingTurn = activeTurn;
1407
1405
  closingTurn?.owner.stop(new DOMException("The provider session was closed.", "AbortError"));
1408
- closePromise = Promise.resolve().then(async () => {
1406
+ const cleanup = async () => {
1409
1407
  if (!providerClosed) {
1410
1408
  await input.candidate.close(closeInput);
1411
1409
  providerClosed = true;
1412
1410
  }
1411
+ await input.sessionInteractionOwner?.join();
1413
1412
  await closingTurn?.owner.join();
1414
1413
  await input.hostBindingLease?.release();
1415
- }).then(() => {
1416
1414
  closed = true;
1417
- }).finally(() => {
1415
+ };
1416
+ if (input.sessionInteractionOwner) return input.sessionInteractionOwner.close(cleanup, explicit);
1417
+ closePromise = Promise.resolve().then(cleanup).finally(() => {
1418
1418
  closePromise = null;
1419
1419
  });
1420
1420
  return closePromise;
1421
1421
  };
1422
+ const close = (closeInput) => closeOwned(closeInput, true);
1423
+ input.sessionInteractionOwner?.installClose(() => closeOwned({ reason: "error" }, false));
1422
1424
  return Object.freeze({
1423
1425
  binding,
1424
1426
  startTurn,
@@ -1433,9 +1435,10 @@ function validateAgentProviderSession(input) {
1433
1435
  close
1434
1436
  });
1435
1437
  }
1436
- async function closeRejectedAgentProviderSession(session, error, hostBinding, opening) {
1438
+ async function closeRejectedAgentProviderSession(session, error, hostBinding, opening, interactions) {
1437
1439
  if (!session) throw error;
1438
1440
  hostBinding?.lease.stop();
1441
+ interactions?.stop();
1439
1442
  let providerClosed = false;
1440
1443
  const cleanup = async () => {
1441
1444
  if (!providerClosed) {
@@ -1445,24 +1448,26 @@ async function closeRejectedAgentProviderSession(session, error, hostBinding, op
1445
1448
  await session.close({ reason: "contract_rejected" });
1446
1449
  providerClosed = true;
1447
1450
  }
1451
+ await interactions?.join();
1448
1452
  await hostBinding?.lease.release();
1449
1453
  };
1450
1454
  try {
1451
- await cleanup();
1455
+ if (interactions) await interactions.close(cleanup, false);
1456
+ else await cleanup();
1452
1457
  } catch (cleanupError) {
1453
1458
  if (hostBinding) {
1454
1459
  const retained = createAgentHostBindingCleanupError({
1455
1460
  providerKey: hostBinding.providerKey,
1456
1461
  lease: hostBinding.lease,
1457
1462
  errors: [error, cleanupError],
1458
- retryCleanup: cleanup
1463
+ retryCleanup: () => interactions ? interactions.close(cleanup, false) : cleanup()
1459
1464
  });
1460
1465
  throw opening ? associateAgentSessionCleanupError(retained, opening.input) : retained;
1461
1466
  }
1462
1467
  if (opening) throw associateAgentSessionCleanupError(new AgentProviderSessionCleanupError({
1463
1468
  providerKey: opening.providerKey,
1464
1469
  errors: [error, cleanupError],
1465
- retryCleanup: cleanup
1470
+ retryCleanup: () => interactions ? interactions.close(cleanup, false) : cleanup()
1466
1471
  }), opening.input);
1467
1472
  throw new AggregateError(
1468
1473
  [error, cleanupError],
@@ -1533,10 +1538,12 @@ async function openIdentityCreatingAgentProviderSession(input) {
1533
1538
  capabilities: input.capabilities,
1534
1539
  sessionId: input.sessionId,
1535
1540
  candidate,
1541
+ ...input.sessionInteractionOwner === void 0 ? {} : { sessionInteractionOwner: input.sessionInteractionOwner },
1536
1542
  ...input.hostBindingLease === void 0 ? {} : { hostBindingLease: input.hostBindingLease },
1537
1543
  ...input.sourceBinding === void 0 ? {} : { sourceBinding: input.sourceBinding }
1538
1544
  });
1539
1545
  tracker.requireMatch(session.binding);
1546
+ input.sessionInteractionOwner?.adopt();
1540
1547
  return session;
1541
1548
  } catch (error) {
1542
1549
  return closeRejectedAgentProviderSession(
@@ -1546,7 +1553,8 @@ async function openIdentityCreatingAgentProviderSession(input) {
1546
1553
  providerKey: input.capabilities.providerKey,
1547
1554
  lease: input.hostBindingLease
1548
1555
  },
1549
- input.openingInput === void 0 ? void 0 : { providerKey: input.capabilities.providerKey, input: input.openingInput }
1556
+ input.openingInput === void 0 ? void 0 : { providerKey: input.capabilities.providerKey, input: input.openingInput },
1557
+ input.sessionInteractionOwner
1550
1558
  );
1551
1559
  }
1552
1560
  }
@@ -3,6 +3,7 @@ import type { MaybePromise } from "./foundation.js";
3
3
  import type { AgentSessionHostBinding, AgentSessionHostBindingSupport } from "./hostBindings/types.js";
4
4
  import type { AgentProviderOutput } from "./outputs.js";
5
5
  import type { AgentArtifactCandidate } from "./artifacts.js";
6
+ import type { AgentSessionInteractionContext, AgentSessionInteractionSupport } from "./sessionInteractions/types.js";
6
7
  import type { AgentEnvironmentObservationPort } from "./environment/types.js";
7
8
  export interface AgentProviderSessionContext {
8
9
  readonly sessionId: AgentSessionId;
@@ -10,6 +11,7 @@ export interface AgentProviderSessionContext {
10
11
  readonly configuration: AgentSessionConfiguration;
11
12
  readonly signal?: AbortSignal;
12
13
  readonly hostBinding?: AgentSessionHostBinding;
14
+ readonly interactions?: AgentSessionInteractionContext;
13
15
  }
14
16
  export type AgentSessionBindingCreatedObserver = (binding: AgentSessionBinding) => void;
15
17
  export interface AgentProviderCreateSessionInput extends AgentProviderSessionContext {
@@ -212,6 +214,7 @@ export type AgentProviderAuthentication = Readonly<{
212
214
  cancel: (input: AgentProviderAuthenticationCancelInput) => MaybePromise<AgentProviderOperationResult>;
213
215
  }>;
214
216
  export interface AgentProviderAdapter {
217
+ readonly sessionInteractions?: AgentSessionInteractionSupport;
215
218
  readonly hostBinding?: AgentSessionHostBindingSupport;
216
219
  readonly createSession: (input: AgentProviderCreateSessionInput) => MaybePromise<AgentProviderSession>;
217
220
  readonly resumption: AgentSessionResumption;
@@ -15,7 +15,6 @@ class AgentProviderConformanceError extends Error {
15
15
  this.check = check;
16
16
  this.name = "AgentProviderConformanceError";
17
17
  }
18
- check;
19
18
  }
20
19
  function requireCheck(condition, check, message) {
21
20
  if (!condition) throw new AgentProviderConformanceError(check, message);
@@ -44,7 +44,7 @@ function defaultCapabilities(providerKey) {
44
44
  }
45
45
  };
46
46
  return parseAgentCapabilities({
47
- protocolVersion: 10,
47
+ protocolVersion: 11,
48
48
  providerKey,
49
49
  sessions: { create: true, resume: true, branch: { kind: "through_turn" } },
50
50
  turns: {
@@ -55,6 +55,7 @@ function defaultCapabilities(providerKey) {
55
55
  requests: {
56
56
  approval: {
57
57
  kind: "supported",
58
+ providerManagedRemembrance: false,
58
59
  modes: [{ persistence: "once", scopeKinds: ["exact_action"] }]
59
60
  },
60
61
  elicitation: {
@@ -165,7 +166,7 @@ function sessionBinding(state, sessionId) {
165
166
  };
166
167
  }
167
168
  function eventBase(sessionId, turnId, occurredAt) {
168
- return { protocolVersion: 10, sessionId, turnId, occurredAt };
169
+ return { protocolVersion: 11, sessionId, turnId, occurredAt };
169
170
  }
170
171
  function fakeEnvironmentSnapshot(environmentId) {
171
172
  return parseAgentEnvironmentSnapshot({
@@ -251,15 +252,13 @@ function requestForTurn(turnId) {
251
252
  optionId: parseAgentApprovalOptionId("approval:allow-once"),
252
253
  label: "Allow once",
253
254
  decision: "approved",
254
- persistence: "once",
255
- scope: { kind: "exact_action" }
255
+ consequence: { kind: "scoped", persistence: "once", scope: { kind: "exact_action" } }
256
256
  },
257
257
  {
258
258
  optionId: parseAgentApprovalOptionId("approval:deny-once"),
259
259
  label: "Deny",
260
260
  decision: "denied",
261
- persistence: "once",
262
- scope: { kind: "exact_action" }
261
+ consequence: { kind: "scoped", persistence: "once", scope: { kind: "exact_action" } }
263
262
  }
264
263
  ]
265
264
  };
@@ -25,7 +25,7 @@ function createAgentTurnRequestAdmission(input) {
25
25
  };
26
26
  const suspend = () => {
27
27
  const suspendedGeneration = generation;
28
- const suspension = /* @__PURE__ */ Symbol("interruption");
28
+ const suspension = Symbol("interruption");
29
29
  suspendedGeneration.suspensions.add(suspension);
30
30
  for (const [requestId, pending] of input.pendingRequests) {
31
31
  suspendedGeneration.withheldRequests.set(requestId, pending);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agen-ai/agent-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Provider-neutral driver, instance, adapter, session, output, and conformance contracts for coding-agent runtimes.",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@agen-ai/agent-protocol": "^0.4.0"
34
+ "@agen-ai/agent-protocol": "^0.4.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "tsup": "^8.5.0"