@agen-ai/agent-runtime 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -43,10 +43,20 @@ Approval continuations are refusal-first. Before delegating to the candidate ada
43
43
  validated session proves that the request is still pending and unexpired and that a selected
44
44
  `optionId` was offered by that exact request. Provider-emitted approval requests must correlate to
45
45
  a live item or exact proposed-plan artifact, and every option must fit an advertised
46
- persistence/scope mode.
46
+ persistence/scope mode. Item identities are monotonic throughout a turn, including every request
47
+ continuation: an adapter cannot restart an observed identity, change its kind, erase prior progress
48
+ through an unknown-status update, regress it from in-progress to pending, or revive it after a
49
+ terminal event or status. An `item.completed` event must carry `completed`, `failed`, or `canceled`
50
+ status. Completion-only snapshots remain valid when no earlier lifecycle event was observed.
47
51
 
48
52
  The host should serialize mutating operations for a given session. Separate session objects may
49
53
  run concurrently, so provider implementations must isolate their conversation-local state.
54
+
55
+ Materialization-scoped context usage remains monotonic only within one native provider process. A
56
+ provider that replaces that process emits a `process.started` lifecycle output as the explicit
57
+ boundary before reporting usage from the replacement. Validation then clears only the
58
+ `materialization` occupancy, cumulative counters, and compaction allowance; logical `session`
59
+ measurements remain continuous.
50
60
  `runTurn` and `resolveRequest` both preserve consumer backpressure: the provider does not resume
51
61
  until the consumer requests the next output. Each stream must end at a completed turn or exactly
52
62
  one newly pending request. If a consumer abandons a stream, a provider throws, or either stream
@@ -136,7 +146,7 @@ turn/request ordering, request resolution, steering, interruption, configuration
136
146
  close, and idempotent disposal. Unsupported operations must remain explicit discriminants and
137
147
  must not expose handlers.
138
148
 
139
- The package is at `0.2.0` while the public SPI is being proven with external adapters. Minor
149
+ The package is at `0.2.2` while the public SPI is being proven with external adapters. Minor
140
150
  releases may include breaking changes during this beta period, and those changes will be called out
141
151
  in the release notes.
142
152
 
@@ -84,7 +84,7 @@ function isEventCapabilitySupported(capabilities, event) {
84
84
  case "item.completed": {
85
85
  if (event.payload.itemKind === "context_compaction") {
86
86
  const compaction = capabilities.context.compaction;
87
- return compaction.kind === "supported" && compaction.triggers.includes(event.payload.details.trigger);
87
+ return compaction.kind === "supported" && (event.payload.details.trigger === "unknown" || compaction.triggers.includes(event.payload.details.trigger));
88
88
  }
89
89
  return isItemKindCapabilitySupported(capabilities, event.payload.itemKind);
90
90
  }
@@ -64,8 +64,53 @@ const FINAL_DIFF_TRAILING_EVENT_TYPES = [
64
64
  "runtime.warning",
65
65
  "turn.completed"
66
66
  ];
67
+ const TERMINAL_ITEM_STATUSES = /* @__PURE__ */ new Set([
68
+ "completed",
69
+ "failed",
70
+ "canceled"
71
+ ]);
72
+ function observeItemLifecycle(input) {
73
+ const { event, providerKey, state } = input;
74
+ const previous = state.observedItems.get(event.payload.itemId);
75
+ if (event.type === "item.completed" && !TERMINAL_ITEM_STATUSES.has(event.payload.status)) {
76
+ invalidTurnSequence(
77
+ providerKey,
78
+ "Provider completed an item without a terminal item status."
79
+ );
80
+ }
81
+ if (previous?.terminal) {
82
+ invalidTurnSequence(
83
+ providerKey,
84
+ "Provider emitted an item lifecycle event after the item became terminal."
85
+ );
86
+ }
87
+ if (previous?.itemKind !== void 0 && previous.itemKind !== event.payload.itemKind) {
88
+ invalidTurnSequence(
89
+ providerKey,
90
+ "Provider changed the kind of an existing item identity."
91
+ );
92
+ }
93
+ if (previous !== void 0 && event.type === "item.started") {
94
+ invalidTurnSequence(
95
+ providerKey,
96
+ "Provider started an item identity more than once."
97
+ );
98
+ }
99
+ if (previous?.inProgressObserved && event.payload.status === "pending") {
100
+ invalidTurnSequence(
101
+ providerKey,
102
+ "Provider regressed an in-progress item to pending."
103
+ );
104
+ }
105
+ state.observedItems.set(event.payload.itemId, {
106
+ itemKind: event.payload.itemKind,
107
+ status: event.payload.status,
108
+ inProgressObserved: previous?.inProgressObserved === true || event.payload.status === "in_progress",
109
+ terminal: event.type === "item.completed" || TERMINAL_ITEM_STATUSES.has(event.payload.status)
110
+ });
111
+ }
67
112
  function assertApprovalSubjectCorrelation(input) {
68
- const request = input.event.payload.request;
113
+ const { request } = input;
69
114
  if (request.requestKind !== "approval") return;
70
115
  if (request.subject.kind === "plan") {
71
116
  if (input.state.proposedPlans.get(request.subject.artifactId) !== request.requestId) {
@@ -76,14 +121,24 @@ function assertApprovalSubjectCorrelation(input) {
76
121
  }
77
122
  return;
78
123
  }
79
- const status = input.state.observedItems.get(request.subject.itemId);
80
- if (status !== "pending" && status !== "in_progress") {
124
+ const item = input.state.observedItems.get(request.subject.itemId);
125
+ const itemIsLive = item !== void 0 && !item.terminal && (item.status === "pending" || item.status === "in_progress" || item.status === "unknown" && item.inProgressObserved);
126
+ if (!itemIsLive) {
81
127
  invalidTurnSequence(
82
128
  input.providerKey,
83
129
  "Provider approval request does not identify a live item from this turn."
84
130
  );
85
131
  }
86
132
  }
133
+ function assertPendingApprovalSubjectCorrelations(input) {
134
+ for (const pending of input.pendingRequests.values()) {
135
+ assertApprovalSubjectCorrelation({
136
+ providerKey: input.providerKey,
137
+ request: pending.request,
138
+ state: input.state
139
+ });
140
+ }
141
+ }
87
142
  function observeContextUsage(input) {
88
143
  const previous = input.state.latestByScope.get(input.usage.measurementScope);
89
144
  if (previous !== void 0) {
@@ -93,15 +148,6 @@ function observeContextUsage(input) {
93
148
  "Provider emitted a duplicate context usage sample."
94
149
  );
95
150
  }
96
- for (const [field, value] of Object.entries(input.usage.cumulative ?? {})) {
97
- const previousValue = previous.cumulative?.[field];
98
- if (previousValue !== void 0 && value < previousValue) {
99
- invalidTurnSequence(
100
- input.providerKey,
101
- "Provider context cumulative counters cannot decrease."
102
- );
103
- }
104
- }
105
151
  if (input.usage.usedTokens < previous.usedTokens && !input.state.compactionReadyScopes.has(input.usage.measurementScope)) {
106
152
  invalidTurnSequence(
107
153
  input.providerKey,
@@ -109,9 +155,34 @@ function observeContextUsage(input) {
109
155
  );
110
156
  }
111
157
  }
158
+ const cumulative = input.usage.cumulative;
159
+ if (cumulative !== void 0) {
160
+ const previousCumulative = input.state.cumulativeByScope.get(
161
+ input.usage.measurementScope
162
+ );
163
+ for (const field of Object.keys(cumulative)) {
164
+ const value = cumulative[field];
165
+ const previousValue = previousCumulative?.[field];
166
+ if (value !== void 0 && previousValue !== void 0 && value < previousValue) {
167
+ invalidTurnSequence(
168
+ input.providerKey,
169
+ "Provider context cumulative counters cannot decrease."
170
+ );
171
+ }
172
+ }
173
+ input.state.cumulativeByScope.set(input.usage.measurementScope, {
174
+ ...previousCumulative,
175
+ ...cumulative
176
+ });
177
+ }
112
178
  input.state.latestByScope.set(input.usage.measurementScope, input.usage);
113
179
  input.state.compactionReadyScopes.delete(input.usage.measurementScope);
114
180
  }
181
+ function observeContextMaterializationBoundary(state) {
182
+ state.latestByScope.delete("materialization");
183
+ state.cumulativeByScope.delete("materialization");
184
+ state.compactionReadyScopes.delete("materialization");
185
+ }
115
186
  function observeTurnEvent(input) {
116
187
  const {
117
188
  contextUsageState,
@@ -164,7 +235,12 @@ function observeTurnEvent(input) {
164
235
  }
165
236
  }
166
237
  if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
167
- state.observedItems.set(event.payload.itemId, event.payload.status);
238
+ observeItemLifecycle({ providerKey, event, state });
239
+ assertPendingApprovalSubjectCorrelations({
240
+ providerKey,
241
+ pendingRequests,
242
+ state
243
+ });
168
244
  if (event.payload.itemKind === "context_compaction" && event.type === "item.completed" && event.payload.status === "completed") {
169
245
  for (const scope of ["session", "materialization"]) {
170
246
  contextUsageState.compactionReadyScopes.add(scope);
@@ -173,6 +249,11 @@ function observeTurnEvent(input) {
173
249
  }
174
250
  if (event.type === "turn.plan.proposed") {
175
251
  state.proposedPlans.set(event.payload.artifactId, event.payload.requestId);
252
+ assertPendingApprovalSubjectCorrelations({
253
+ providerKey,
254
+ pendingRequests,
255
+ state
256
+ });
176
257
  }
177
258
  if (event.type === "context.usage.updated") {
178
259
  observeContextUsage({
@@ -195,7 +276,11 @@ function observeTurnEvent(input) {
195
276
  "Provider reused a request ID within a session."
196
277
  );
197
278
  }
198
- assertApprovalSubjectCorrelation({ providerKey, event, state });
279
+ assertApprovalSubjectCorrelation({
280
+ providerKey,
281
+ request: event.payload.request,
282
+ state
283
+ });
199
284
  openedRequestIds.add(requestId);
200
285
  pendingRequests.set(requestId, {
201
286
  request: event.payload.request,
@@ -215,6 +300,20 @@ function observeTurnEvent(input) {
215
300
  completeTurnSequence({ providerKey, state, pendingRequests });
216
301
  }
217
302
  }
303
+ function observeTurnOutput(input) {
304
+ if (input.output.kind === "lifecycle" && input.output.lifecycle.type === "process.started") {
305
+ observeContextMaterializationBoundary(input.contextUsageState);
306
+ }
307
+ if (input.output.kind !== "event") return;
308
+ observeTurnEvent({
309
+ providerKey: input.providerKey,
310
+ event: input.output.event,
311
+ state: input.state,
312
+ pendingRequests: input.pendingRequests,
313
+ openedRequestIds: input.openedRequestIds,
314
+ contextUsageState: input.contextUsageState
315
+ });
316
+ }
218
317
  function assertStableTurnBoundary(input) {
219
318
  const terminal = input.state.terminal && !input.state.waiting;
220
319
  const waiting = !input.state.terminal && input.state.waiting && input.pendingRequests.size === 1;
@@ -250,10 +349,9 @@ function interruptionTerminalizesTurn(input) {
250
349
  }
251
350
  function observeTurnOperationOutputs(input) {
252
351
  for (const output of input.outputs ?? []) {
253
- if (output.kind !== "event") continue;
254
- observeTurnEvent({
352
+ observeTurnOutput({
255
353
  providerKey: input.providerKey,
256
- event: output.event,
354
+ output,
257
355
  state: input.state,
258
356
  pendingRequests: input.pendingRequests,
259
357
  openedRequestIds: input.openedRequestIds,
@@ -267,17 +365,9 @@ function reconstructWaitingTurn(input) {
267
365
  ([, pending]) => pending.turnId === input.turnId
268
366
  )
269
367
  );
270
- if (targetedPendingRequests.size === 0) return null;
368
+ if (targetedPendingRequests.size === 0 || input.state === void 0 || input.state.terminal || !input.state.waiting) return null;
271
369
  return {
272
- state: {
273
- fileChangeMode: input.fileChangeMode,
274
- observedItems: /* @__PURE__ */ new Map(),
275
- proposedPlans: /* @__PURE__ */ new Map(),
276
- started: true,
277
- terminal: false,
278
- waiting: true,
279
- finalDiffObserved: false
280
- },
370
+ state: input.state,
281
371
  pendingRequests: targetedPendingRequests
282
372
  };
283
373
  }
@@ -365,9 +455,11 @@ function validateAgentProviderSession(input) {
365
455
  }
366
456
  validateSessionPorts(capabilities, input.candidate);
367
457
  const pendingRequests = /* @__PURE__ */ new Map();
458
+ const waitingTurnStates = /* @__PURE__ */ new Map();
368
459
  const openedRequestIds = /* @__PURE__ */ new Set();
369
460
  const contextUsageState = {
370
461
  latestByScope: /* @__PURE__ */ new Map(),
462
+ cumulativeByScope: /* @__PURE__ */ new Map(),
371
463
  compactionReadyScopes: /* @__PURE__ */ new Set()
372
464
  };
373
465
  let activeTurn = null;
@@ -471,16 +563,14 @@ function validateAgentProviderSession(input) {
471
563
  candidate,
472
564
  outputContext(parsedInput.turnId)
473
565
  );
474
- if (output.kind === "event") {
475
- observeTurnEvent({
476
- providerKey,
477
- event: output.event,
478
- state: currentTurn.state,
479
- pendingRequests: currentTurn.pendingRequests,
480
- openedRequestIds,
481
- contextUsageState
482
- });
483
- }
566
+ observeTurnOutput({
567
+ providerKey,
568
+ output,
569
+ state: currentTurn.state,
570
+ pendingRequests: currentTurn.pendingRequests,
571
+ openedRequestIds,
572
+ contextUsageState
573
+ });
484
574
  yield output;
485
575
  }
486
576
  while (currentTurn.inFlightOperations.size > 0) {
@@ -499,6 +589,9 @@ function validateAgentProviderSession(input) {
499
589
  });
500
590
  if (currentTurn.state.waiting) {
501
591
  replacePendingRequests(pendingRequests, currentTurn.pendingRequests);
592
+ waitingTurnStates.set(currentTurn.turnId, currentTurn.state);
593
+ } else {
594
+ waitingTurnStates.delete(currentTurn.turnId);
502
595
  }
503
596
  reachedStableBoundary = true;
504
597
  } finally {
@@ -527,7 +620,7 @@ function validateAgentProviderSession(input) {
527
620
  "Provider request resolution does not match the opened request."
528
621
  );
529
622
  }
530
- if (pending.request.expiresAt !== void 0 && Date.parse(pending.request.expiresAt) <= Date.now()) {
623
+ if (pending.request.expiresAt !== void 0 && Date.parse(pending.request.expiresAt) <= Date.now() && resolution.disposition !== "canceled") {
531
624
  throwAgentProviderContractError(
532
625
  providerKey,
533
626
  "request_resolution_mismatch",
@@ -543,18 +636,19 @@ function validateAgentProviderSession(input) {
543
636
  "Provider session already has an active turn."
544
637
  );
545
638
  }
639
+ const waitingTurnState = waitingTurnStates.get(pending.turnId);
640
+ if (waitingTurnState === void 0 || waitingTurnState.terminal || !waitingTurnState.waiting) {
641
+ throwAgentProviderContractError(
642
+ providerKey,
643
+ "request_resolution_mismatch",
644
+ "Provider request resolution does not identify a waiting turn."
645
+ );
646
+ }
647
+ waitingTurnState.waiting = false;
546
648
  const currentTurn = {
547
649
  turnId: pending.turnId,
548
650
  pendingRequests: nextPendingRequests,
549
- state: {
550
- fileChangeMode: capabilities.output.fileChanges,
551
- observedItems: /* @__PURE__ */ new Map(),
552
- proposedPlans: /* @__PURE__ */ new Map(),
553
- started: true,
554
- terminal: false,
555
- waiting: false,
556
- finalDiffObserved: false
557
- },
651
+ state: waitingTurnState,
558
652
  inFlightOperations: /* @__PURE__ */ new Set()
559
653
  };
560
654
  let reachedStableBoundary = false;
@@ -568,16 +662,14 @@ function validateAgentProviderSession(input) {
568
662
  candidate,
569
663
  outputContext(pending.turnId)
570
664
  );
571
- if (output.kind === "event") {
572
- observeTurnEvent({
573
- providerKey,
574
- event: output.event,
575
- state: currentTurn.state,
576
- pendingRequests: currentTurn.pendingRequests,
577
- openedRequestIds,
578
- contextUsageState
579
- });
580
- }
665
+ observeTurnOutput({
666
+ providerKey,
667
+ output,
668
+ state: currentTurn.state,
669
+ pendingRequests: currentTurn.pendingRequests,
670
+ openedRequestIds,
671
+ contextUsageState
672
+ });
581
673
  yield output;
582
674
  }
583
675
  while (currentTurn.inFlightOperations.size > 0) {
@@ -589,6 +681,11 @@ function validateAgentProviderSession(input) {
589
681
  pendingRequests: currentTurn.pendingRequests
590
682
  });
591
683
  replacePendingRequests(pendingRequests, currentTurn.pendingRequests);
684
+ if (currentTurn.state.waiting) {
685
+ waitingTurnStates.set(currentTurn.turnId, currentTurn.state);
686
+ } else {
687
+ waitingTurnStates.delete(currentTurn.turnId);
688
+ }
592
689
  reachedStableBoundary = true;
593
690
  } finally {
594
691
  if (!reachedStableBoundary) unusable = true;
@@ -610,7 +707,7 @@ function validateAgentProviderSession(input) {
610
707
  const targetedWaitingTurn = targetedActiveTurn === null ? reconstructWaitingTurn({
611
708
  pendingRequests,
612
709
  turnId: parsed.turnId,
613
- fileChangeMode: capabilities.output.fileChanges
710
+ state: waitingTurnStates.get(parsed.turnId)
614
711
  }) : null;
615
712
  const targetedTurn = targetedActiveTurn ?? targetedWaitingTurn;
616
713
  if (targetedTurn === null) {
@@ -642,6 +739,7 @@ function validateAgentProviderSession(input) {
642
739
  parsed.turnId
643
740
  );
644
741
  targetedTurn.state.waiting = false;
742
+ waitingTurnStates.delete(parsed.turnId);
645
743
  }
646
744
  try {
647
745
  observeTurnOperationOutputs({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agen-ai/agent-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.2.0"
30
+ "@agen-ai/agent-protocol": "^0.2.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "tsup": "^8.5.0"