@agen-ai/agent-runtime 0.1.0 → 0.2.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 CHANGED
@@ -9,6 +9,7 @@ its binding and conversation-local operations.
9
9
  The runtime depends only on `@agen-ai/agent-protocol`. It has no concept of tenants, SaaS
10
10
  workspaces, assigned users, database rows, persistence sequence, visibility, billing, host boots,
11
11
  leases, or storage policy. A host must authorize and select an instance before calling this SPI.
12
+ The current coordinated deployment tuple is Agent Protocol V7, private host V15/catalog V9, and Workspaces event V9.
12
13
 
13
14
  ## Entrypoints
14
15
 
@@ -38,6 +39,12 @@ exactly once before returning the matching session. Capability-dependent operati
38
39
  `supported`/`unsupported` discriminants, and the runtime rejects handlers that disagree with the
39
40
  instance capability declaration.
40
41
 
42
+ Approval continuations are refusal-first. Before delegating to the candidate adapter, the
43
+ validated session proves that the request is still pending and unexpired and that a selected
44
+ `optionId` was offered by that exact request. Provider-emitted approval requests must correlate to
45
+ a live item or exact proposed-plan artifact, and every option must fit an advertised
46
+ persistence/scope mode.
47
+
41
48
  The host should serialize mutating operations for a given session. Separate session objects may
42
49
  run concurrently, so provider implementations must isolate their conversation-local state.
43
50
  `runTurn` and `resolveRequest` both preserve consumer backpressure: the provider does not resume
@@ -53,6 +60,12 @@ turn terminalized; unless the result also carries a terminal event, the validate
53
60
  unusable and must be closed and rematerialized. A close failure makes the session unusable while
54
61
  leaving close itself retryable.
55
62
 
63
+ When context usage is advertised, the validated session enforces the declared measurement scopes
64
+ and cumulative fields across turns. Identical consecutive samples and decreasing cumulative
65
+ counters are rejected. Occupancy may decrease only after a completed advertised compaction item;
66
+ the next accepted sample consumes that allowance. Context output remains subject to the same
67
+ per-output backpressure and terminal ordering as every other provider event.
68
+
56
69
  When `capabilities.turns.steer` is true, the session exposes `steering.steerTurn`. Steering accepts
57
70
  the existing turn ID plus the same canonical `parts` and optional `summary` used to start a turn.
58
71
  It does not create or own an output stream: model output continues on the original `runTurn`
@@ -123,12 +136,13 @@ turn/request ordering, request resolution, steering, interruption, configuration
123
136
  close, and idempotent disposal. Unsupported operations must remain explicit discriminants and
124
137
  must not expose handlers.
125
138
 
126
- The package is at `0.1.0` while the public SPI is being proven with external adapters. Minor
139
+ The package is at `0.2.1` while the public SPI is being proven with external adapters. Minor
127
140
  releases may include breaking changes during this beta period, and those changes will be called out
128
141
  in the release notes.
129
142
 
130
- The current version tuple is Agent Protocol V6, private host V14/catalog V8, and Workspaces event V8.
131
- The runtime package version remains independent of Agent Protocol V6.
143
+ The public runtime implements Agent Protocol V7. Its package version remains independent of private
144
+ host, catalog, persistence, and member-projection versions. Version `0.2.0` directly replaces the
145
+ V6/`0.1.0` API; see the repository `MIGRATING-TO-0.2.md` guide.
132
146
 
133
147
  Run the clean packed-consumer proof before any release:
134
148
 
@@ -51,12 +51,43 @@ function isContentStreamCapabilitySupported(capabilities, streamKind) {
51
51
  }
52
52
  return unsupportedCapabilitySemantic(streamKind);
53
53
  }
54
+ function isApprovalRequestCapabilitySupported(capabilities, event) {
55
+ const request = event.payload.request;
56
+ if (request.requestKind !== "approval") return false;
57
+ const capability = capabilities.requests.approval;
58
+ return capability.kind === "supported" && request.options.every(
59
+ (option) => capability.modes.some(
60
+ (mode) => mode.persistence === option.persistence && mode.scopeKinds.includes(option.scope.kind)
61
+ )
62
+ );
63
+ }
64
+ function isContextUsageCapabilitySupported(capabilities, event) {
65
+ const capability = capabilities.context.usage;
66
+ if (capability.kind !== "supported" || !capability.measurementScopes.includes(
67
+ event.payload.measurementScope
68
+ )) {
69
+ return false;
70
+ }
71
+ if (event.payload.cumulative !== void 0 && Object.keys(event.payload.cumulative).some(
72
+ (field) => !capability.cumulativeFields.includes(
73
+ field
74
+ )
75
+ )) {
76
+ return false;
77
+ }
78
+ return event.payload.compaction === void 0 || capabilities.context.compaction.kind === "supported";
79
+ }
54
80
  function isEventCapabilitySupported(capabilities, event) {
55
81
  switch (event.type) {
56
82
  case "item.started":
57
83
  case "item.updated":
58
- case "item.completed":
84
+ case "item.completed": {
85
+ if (event.payload.itemKind === "context_compaction") {
86
+ const compaction = capabilities.context.compaction;
87
+ return compaction.kind === "supported" && (event.payload.details.trigger === "unknown" || compaction.triggers.includes(event.payload.details.trigger));
88
+ }
59
89
  return isItemKindCapabilitySupported(capabilities, event.payload.itemKind);
90
+ }
60
91
  case "content.delta":
61
92
  return capabilities.output.streaming && isContentStreamCapabilitySupported(
62
93
  capabilities,
@@ -72,9 +103,11 @@ function isEventCapabilitySupported(capabilities, event) {
72
103
  event.payload.artifact.kind
73
104
  );
74
105
  case "request.opened":
75
- return event.payload.request.requestKind === "approval" ? capabilities.requests.approval : capabilities.requests.elicitation.kind === "structured" || capabilities.requests.elicitation.kind === "text" && event.payload.request.fields.every(
106
+ return event.payload.request.requestKind === "approval" ? isApprovalRequestCapabilitySupported(capabilities, event) : capabilities.requests.elicitation.kind === "structured" || capabilities.requests.elicitation.kind === "text" && event.payload.request.fields.every(
76
107
  (field) => field.kind === "text"
77
108
  );
109
+ case "context.usage.updated":
110
+ return isContextUsageCapabilitySupported(capabilities, event);
78
111
  case "turn.started":
79
112
  case "turn.state_changed":
80
113
  case "turn.completed":
@@ -64,8 +64,74 @@ const FINAL_DIFF_TRAILING_EVENT_TYPES = [
64
64
  "runtime.warning",
65
65
  "turn.completed"
66
66
  ];
67
+ function assertApprovalSubjectCorrelation(input) {
68
+ const request = input.event.payload.request;
69
+ if (request.requestKind !== "approval") return;
70
+ if (request.subject.kind === "plan") {
71
+ if (input.state.proposedPlans.get(request.subject.artifactId) !== request.requestId) {
72
+ invalidTurnSequence(
73
+ input.providerKey,
74
+ "Provider approval request does not identify a proposed plan from this turn."
75
+ );
76
+ }
77
+ return;
78
+ }
79
+ const status = input.state.observedItems.get(request.subject.itemId);
80
+ if (status !== "pending" && status !== "in_progress") {
81
+ invalidTurnSequence(
82
+ input.providerKey,
83
+ "Provider approval request does not identify a live item from this turn."
84
+ );
85
+ }
86
+ }
87
+ function observeContextUsage(input) {
88
+ const previous = input.state.latestByScope.get(input.usage.measurementScope);
89
+ if (previous !== void 0) {
90
+ if (JSON.stringify(previous) === JSON.stringify(input.usage)) {
91
+ invalidTurnSequence(
92
+ input.providerKey,
93
+ "Provider emitted a duplicate context usage sample."
94
+ );
95
+ }
96
+ if (input.usage.usedTokens < previous.usedTokens && !input.state.compactionReadyScopes.has(input.usage.measurementScope)) {
97
+ invalidTurnSequence(
98
+ input.providerKey,
99
+ "Provider context occupancy cannot decrease without completed compaction."
100
+ );
101
+ }
102
+ }
103
+ const cumulative = input.usage.cumulative;
104
+ if (cumulative !== void 0) {
105
+ const previousCumulative = input.state.cumulativeByScope.get(
106
+ input.usage.measurementScope
107
+ );
108
+ for (const field of Object.keys(cumulative)) {
109
+ const value = cumulative[field];
110
+ const previousValue = previousCumulative?.[field];
111
+ if (value !== void 0 && previousValue !== void 0 && value < previousValue) {
112
+ invalidTurnSequence(
113
+ input.providerKey,
114
+ "Provider context cumulative counters cannot decrease."
115
+ );
116
+ }
117
+ }
118
+ input.state.cumulativeByScope.set(input.usage.measurementScope, {
119
+ ...previousCumulative,
120
+ ...cumulative
121
+ });
122
+ }
123
+ input.state.latestByScope.set(input.usage.measurementScope, input.usage);
124
+ input.state.compactionReadyScopes.delete(input.usage.measurementScope);
125
+ }
67
126
  function observeTurnEvent(input) {
68
- const { event, openedRequestIds, pendingRequests, providerKey, state } = input;
127
+ const {
128
+ contextUsageState,
129
+ event,
130
+ openedRequestIds,
131
+ pendingRequests,
132
+ providerKey,
133
+ state
134
+ } = input;
69
135
  if (state.terminal) {
70
136
  invalidTurnSequence(
71
137
  providerKey,
@@ -108,6 +174,24 @@ function observeTurnEvent(input) {
108
174
  );
109
175
  }
110
176
  }
177
+ if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
178
+ state.observedItems.set(event.payload.itemId, event.payload.status);
179
+ if (event.payload.itemKind === "context_compaction" && event.type === "item.completed" && event.payload.status === "completed") {
180
+ for (const scope of ["session", "materialization"]) {
181
+ contextUsageState.compactionReadyScopes.add(scope);
182
+ }
183
+ }
184
+ }
185
+ if (event.type === "turn.plan.proposed") {
186
+ state.proposedPlans.set(event.payload.artifactId, event.payload.requestId);
187
+ }
188
+ if (event.type === "context.usage.updated") {
189
+ observeContextUsage({
190
+ providerKey,
191
+ usage: event.payload,
192
+ state: contextUsageState
193
+ });
194
+ }
111
195
  if (event.type === "request.opened") {
112
196
  if (pendingRequests.size > 0) {
113
197
  invalidTurnSequence(
@@ -122,6 +206,7 @@ function observeTurnEvent(input) {
122
206
  "Provider reused a request ID within a session."
123
207
  );
124
208
  }
209
+ assertApprovalSubjectCorrelation({ providerKey, event, state });
125
210
  openedRequestIds.add(requestId);
126
211
  pendingRequests.set(requestId, {
127
212
  request: event.payload.request,
@@ -182,7 +267,8 @@ function observeTurnOperationOutputs(input) {
182
267
  event: output.event,
183
268
  state: input.state,
184
269
  pendingRequests: input.pendingRequests,
185
- openedRequestIds: input.openedRequestIds
270
+ openedRequestIds: input.openedRequestIds,
271
+ contextUsageState: input.contextUsageState
186
272
  });
187
273
  }
188
274
  }
@@ -196,6 +282,8 @@ function reconstructWaitingTurn(input) {
196
282
  return {
197
283
  state: {
198
284
  fileChangeMode: input.fileChangeMode,
285
+ observedItems: /* @__PURE__ */ new Map(),
286
+ proposedPlans: /* @__PURE__ */ new Map(),
199
287
  started: true,
200
288
  terminal: false,
201
289
  waiting: true,
@@ -289,6 +377,11 @@ function validateAgentProviderSession(input) {
289
377
  validateSessionPorts(capabilities, input.candidate);
290
378
  const pendingRequests = /* @__PURE__ */ new Map();
291
379
  const openedRequestIds = /* @__PURE__ */ new Set();
380
+ const contextUsageState = {
381
+ latestByScope: /* @__PURE__ */ new Map(),
382
+ cumulativeByScope: /* @__PURE__ */ new Map(),
383
+ compactionReadyScopes: /* @__PURE__ */ new Set()
384
+ };
292
385
  let activeTurn = null;
293
386
  let closePromise = null;
294
387
  let closed = false;
@@ -370,6 +463,8 @@ function validateAgentProviderSession(input) {
370
463
  pendingRequests: /* @__PURE__ */ new Map(),
371
464
  state: {
372
465
  fileChangeMode: capabilities.output.fileChanges,
466
+ observedItems: /* @__PURE__ */ new Map(),
467
+ proposedPlans: /* @__PURE__ */ new Map(),
373
468
  started: false,
374
469
  terminal: false,
375
470
  waiting: false,
@@ -394,7 +489,8 @@ function validateAgentProviderSession(input) {
394
489
  event: output.event,
395
490
  state: currentTurn.state,
396
491
  pendingRequests: currentTurn.pendingRequests,
397
- openedRequestIds
492
+ openedRequestIds,
493
+ contextUsageState
398
494
  });
399
495
  }
400
496
  yield output;
@@ -443,6 +539,13 @@ function validateAgentProviderSession(input) {
443
539
  "Provider request resolution does not match the opened request."
444
540
  );
445
541
  }
542
+ if (pending.request.expiresAt !== void 0 && Date.parse(pending.request.expiresAt) <= Date.now() && resolution.disposition !== "canceled") {
543
+ throwAgentProviderContractError(
544
+ providerKey,
545
+ "request_resolution_mismatch",
546
+ "Provider request resolution identifies an expired request."
547
+ );
548
+ }
446
549
  const nextPendingRequests = new Map(pendingRequests);
447
550
  nextPendingRequests.delete(resolution.requestId);
448
551
  if (activeTurn !== null) {
@@ -457,6 +560,8 @@ function validateAgentProviderSession(input) {
457
560
  pendingRequests: nextPendingRequests,
458
561
  state: {
459
562
  fileChangeMode: capabilities.output.fileChanges,
563
+ observedItems: /* @__PURE__ */ new Map(),
564
+ proposedPlans: /* @__PURE__ */ new Map(),
460
565
  started: true,
461
566
  terminal: false,
462
567
  waiting: false,
@@ -481,7 +586,8 @@ function validateAgentProviderSession(input) {
481
586
  event: output.event,
482
587
  state: currentTurn.state,
483
588
  pendingRequests: currentTurn.pendingRequests,
484
- openedRequestIds
589
+ openedRequestIds,
590
+ contextUsageState
485
591
  });
486
592
  }
487
593
  yield output;
@@ -555,7 +661,8 @@ function validateAgentProviderSession(input) {
555
661
  outputs: result.outputs,
556
662
  state: targetedTurn.state,
557
663
  pendingRequests: targetedTurn.pendingRequests,
558
- openedRequestIds
664
+ openedRequestIds,
665
+ contextUsageState
559
666
  });
560
667
  if (terminalized && !targetedTurn.state.terminal) {
561
668
  completeTurnSequence({
@@ -2,6 +2,8 @@ import {
2
2
  parseAgentCapabilities,
3
3
  parseAgentInstanceId,
4
4
  parseAgentIsoDateTime,
5
+ parseAgentItemId,
6
+ parseAgentApprovalOptionId,
5
7
  parseAgentProviderConversationId,
6
8
  parseAgentProviderHistoryAnchor,
7
9
  parseAgentProviderKey,
@@ -33,7 +35,7 @@ function defaultCapabilities(providerKey) {
33
35
  }
34
36
  };
35
37
  return parseAgentCapabilities({
36
- protocolVersion: 6,
38
+ protocolVersion: 7,
37
39
  providerKey,
38
40
  sessions: { create: true, resume: true, branch: { kind: "through_turn" } },
39
41
  turns: {
@@ -41,7 +43,17 @@ function defaultCapabilities(providerKey) {
41
43
  interrupt: true,
42
44
  steer: { kind: "supported", input }
43
45
  },
44
- requests: { approval: true, elicitation: { kind: "structured" } },
46
+ requests: {
47
+ approval: {
48
+ kind: "supported",
49
+ modes: [{ persistence: "once", scopeKinds: ["exact_action"] }]
50
+ },
51
+ elicitation: { kind: "structured" }
52
+ },
53
+ context: {
54
+ usage: { kind: "unsupported" },
55
+ compaction: { kind: "unsupported" }
56
+ },
45
57
  input,
46
58
  output: {
47
59
  streaming: true,
@@ -81,14 +93,34 @@ function sessionBinding(state, sessionId) {
81
93
  };
82
94
  }
83
95
  function eventBase(sessionId, turnId, occurredAt) {
84
- return { protocolVersion: 6, sessionId, turnId, occurredAt };
96
+ return { protocolVersion: 7, sessionId, turnId, occurredAt };
85
97
  }
86
98
  function requestForTurn(turnId) {
87
99
  return {
88
100
  requestKind: "approval",
89
101
  requestId: parseAgentRequestId(`fake-request:${turnId}`),
90
102
  prompt: "Approve the deterministic fake operation?",
91
- subject: { kind: "other", title: "Fake provider operation" }
103
+ subject: {
104
+ kind: "other",
105
+ title: "Fake provider operation",
106
+ itemId: parseAgentItemId(`fake-item:${turnId}`)
107
+ },
108
+ options: [
109
+ {
110
+ optionId: parseAgentApprovalOptionId("approval:allow-once"),
111
+ label: "Allow once",
112
+ decision: "approved",
113
+ persistence: "once",
114
+ scope: { kind: "exact_action" }
115
+ },
116
+ {
117
+ optionId: parseAgentApprovalOptionId("approval:deny-once"),
118
+ label: "Deny",
119
+ decision: "denied",
120
+ persistence: "once",
121
+ scope: { kind: "exact_action" }
122
+ }
123
+ ]
92
124
  };
93
125
  }
94
126
  function completedResult(outputs = []) {
@@ -109,6 +141,15 @@ function fakeSession(input) {
109
141
  type: "turn.started",
110
142
  payload: { message: "Fake provider turn started." }
111
143
  });
144
+ yield createAgentEventOutput({
145
+ ...eventBase(input.context.sessionId, turnInput.turnId, occurredAt),
146
+ type: "item.started",
147
+ payload: {
148
+ itemId: parseAgentItemId(`fake-item:${turnInput.turnId}`),
149
+ itemKind: "assistant_message",
150
+ status: "in_progress"
151
+ }
152
+ });
112
153
  if (input.capabilities.output.streaming) {
113
154
  yield createAgentEventOutput({
114
155
  ...eventBase(input.context.sessionId, turnInput.turnId, occurredAt),
@@ -120,7 +161,7 @@ function fakeSession(input) {
120
161
  }
121
162
  });
122
163
  }
123
- if (input.capabilities.requests.approval) {
164
+ if (input.capabilities.requests.approval.kind === "supported") {
124
165
  const request = requestForTurn(turnInput.turnId);
125
166
  pendingRequests.set(request.requestId, {
126
167
  request,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agen-ai/agent-runtime",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -27,7 +27,7 @@
27
27
  "./package.json": "./package.json"
28
28
  },
29
29
  "dependencies": {
30
- "@agen-ai/agent-protocol": "^0.1.0"
30
+ "@agen-ai/agent-protocol": "^0.2.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "tsup": "^8.5.0"