@agen-ai/agent-runtime 0.2.1 → 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.1` 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
 
@@ -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) {
@@ -123,6 +178,11 @@ function observeContextUsage(input) {
123
178
  input.state.latestByScope.set(input.usage.measurementScope, input.usage);
124
179
  input.state.compactionReadyScopes.delete(input.usage.measurementScope);
125
180
  }
181
+ function observeContextMaterializationBoundary(state) {
182
+ state.latestByScope.delete("materialization");
183
+ state.cumulativeByScope.delete("materialization");
184
+ state.compactionReadyScopes.delete("materialization");
185
+ }
126
186
  function observeTurnEvent(input) {
127
187
  const {
128
188
  contextUsageState,
@@ -175,7 +235,12 @@ function observeTurnEvent(input) {
175
235
  }
176
236
  }
177
237
  if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
178
- state.observedItems.set(event.payload.itemId, event.payload.status);
238
+ observeItemLifecycle({ providerKey, event, state });
239
+ assertPendingApprovalSubjectCorrelations({
240
+ providerKey,
241
+ pendingRequests,
242
+ state
243
+ });
179
244
  if (event.payload.itemKind === "context_compaction" && event.type === "item.completed" && event.payload.status === "completed") {
180
245
  for (const scope of ["session", "materialization"]) {
181
246
  contextUsageState.compactionReadyScopes.add(scope);
@@ -184,6 +249,11 @@ function observeTurnEvent(input) {
184
249
  }
185
250
  if (event.type === "turn.plan.proposed") {
186
251
  state.proposedPlans.set(event.payload.artifactId, event.payload.requestId);
252
+ assertPendingApprovalSubjectCorrelations({
253
+ providerKey,
254
+ pendingRequests,
255
+ state
256
+ });
187
257
  }
188
258
  if (event.type === "context.usage.updated") {
189
259
  observeContextUsage({
@@ -206,7 +276,11 @@ function observeTurnEvent(input) {
206
276
  "Provider reused a request ID within a session."
207
277
  );
208
278
  }
209
- assertApprovalSubjectCorrelation({ providerKey, event, state });
279
+ assertApprovalSubjectCorrelation({
280
+ providerKey,
281
+ request: event.payload.request,
282
+ state
283
+ });
210
284
  openedRequestIds.add(requestId);
211
285
  pendingRequests.set(requestId, {
212
286
  request: event.payload.request,
@@ -226,6 +300,20 @@ function observeTurnEvent(input) {
226
300
  completeTurnSequence({ providerKey, state, pendingRequests });
227
301
  }
228
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
+ }
229
317
  function assertStableTurnBoundary(input) {
230
318
  const terminal = input.state.terminal && !input.state.waiting;
231
319
  const waiting = !input.state.terminal && input.state.waiting && input.pendingRequests.size === 1;
@@ -261,10 +349,9 @@ function interruptionTerminalizesTurn(input) {
261
349
  }
262
350
  function observeTurnOperationOutputs(input) {
263
351
  for (const output of input.outputs ?? []) {
264
- if (output.kind !== "event") continue;
265
- observeTurnEvent({
352
+ observeTurnOutput({
266
353
  providerKey: input.providerKey,
267
- event: output.event,
354
+ output,
268
355
  state: input.state,
269
356
  pendingRequests: input.pendingRequests,
270
357
  openedRequestIds: input.openedRequestIds,
@@ -278,17 +365,9 @@ function reconstructWaitingTurn(input) {
278
365
  ([, pending]) => pending.turnId === input.turnId
279
366
  )
280
367
  );
281
- if (targetedPendingRequests.size === 0) return null;
368
+ if (targetedPendingRequests.size === 0 || input.state === void 0 || input.state.terminal || !input.state.waiting) return null;
282
369
  return {
283
- state: {
284
- fileChangeMode: input.fileChangeMode,
285
- observedItems: /* @__PURE__ */ new Map(),
286
- proposedPlans: /* @__PURE__ */ new Map(),
287
- started: true,
288
- terminal: false,
289
- waiting: true,
290
- finalDiffObserved: false
291
- },
370
+ state: input.state,
292
371
  pendingRequests: targetedPendingRequests
293
372
  };
294
373
  }
@@ -376,6 +455,7 @@ function validateAgentProviderSession(input) {
376
455
  }
377
456
  validateSessionPorts(capabilities, input.candidate);
378
457
  const pendingRequests = /* @__PURE__ */ new Map();
458
+ const waitingTurnStates = /* @__PURE__ */ new Map();
379
459
  const openedRequestIds = /* @__PURE__ */ new Set();
380
460
  const contextUsageState = {
381
461
  latestByScope: /* @__PURE__ */ new Map(),
@@ -483,16 +563,14 @@ function validateAgentProviderSession(input) {
483
563
  candidate,
484
564
  outputContext(parsedInput.turnId)
485
565
  );
486
- if (output.kind === "event") {
487
- observeTurnEvent({
488
- providerKey,
489
- event: output.event,
490
- state: currentTurn.state,
491
- pendingRequests: currentTurn.pendingRequests,
492
- openedRequestIds,
493
- contextUsageState
494
- });
495
- }
566
+ observeTurnOutput({
567
+ providerKey,
568
+ output,
569
+ state: currentTurn.state,
570
+ pendingRequests: currentTurn.pendingRequests,
571
+ openedRequestIds,
572
+ contextUsageState
573
+ });
496
574
  yield output;
497
575
  }
498
576
  while (currentTurn.inFlightOperations.size > 0) {
@@ -511,6 +589,9 @@ function validateAgentProviderSession(input) {
511
589
  });
512
590
  if (currentTurn.state.waiting) {
513
591
  replacePendingRequests(pendingRequests, currentTurn.pendingRequests);
592
+ waitingTurnStates.set(currentTurn.turnId, currentTurn.state);
593
+ } else {
594
+ waitingTurnStates.delete(currentTurn.turnId);
514
595
  }
515
596
  reachedStableBoundary = true;
516
597
  } finally {
@@ -555,18 +636,19 @@ function validateAgentProviderSession(input) {
555
636
  "Provider session already has an active turn."
556
637
  );
557
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;
558
648
  const currentTurn = {
559
649
  turnId: pending.turnId,
560
650
  pendingRequests: nextPendingRequests,
561
- state: {
562
- fileChangeMode: capabilities.output.fileChanges,
563
- observedItems: /* @__PURE__ */ new Map(),
564
- proposedPlans: /* @__PURE__ */ new Map(),
565
- started: true,
566
- terminal: false,
567
- waiting: false,
568
- finalDiffObserved: false
569
- },
651
+ state: waitingTurnState,
570
652
  inFlightOperations: /* @__PURE__ */ new Set()
571
653
  };
572
654
  let reachedStableBoundary = false;
@@ -580,16 +662,14 @@ function validateAgentProviderSession(input) {
580
662
  candidate,
581
663
  outputContext(pending.turnId)
582
664
  );
583
- if (output.kind === "event") {
584
- observeTurnEvent({
585
- providerKey,
586
- event: output.event,
587
- state: currentTurn.state,
588
- pendingRequests: currentTurn.pendingRequests,
589
- openedRequestIds,
590
- contextUsageState
591
- });
592
- }
665
+ observeTurnOutput({
666
+ providerKey,
667
+ output,
668
+ state: currentTurn.state,
669
+ pendingRequests: currentTurn.pendingRequests,
670
+ openedRequestIds,
671
+ contextUsageState
672
+ });
593
673
  yield output;
594
674
  }
595
675
  while (currentTurn.inFlightOperations.size > 0) {
@@ -601,6 +681,11 @@ function validateAgentProviderSession(input) {
601
681
  pendingRequests: currentTurn.pendingRequests
602
682
  });
603
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
+ }
604
689
  reachedStableBoundary = true;
605
690
  } finally {
606
691
  if (!reachedStableBoundary) unusable = true;
@@ -622,7 +707,7 @@ function validateAgentProviderSession(input) {
622
707
  const targetedWaitingTurn = targetedActiveTurn === null ? reconstructWaitingTurn({
623
708
  pendingRequests,
624
709
  turnId: parsed.turnId,
625
- fileChangeMode: capabilities.output.fileChanges
710
+ state: waitingTurnStates.get(parsed.turnId)
626
711
  }) : null;
627
712
  const targetedTurn = targetedActiveTurn ?? targetedWaitingTurn;
628
713
  if (targetedTurn === null) {
@@ -654,6 +739,7 @@ function validateAgentProviderSession(input) {
654
739
  parsed.turnId
655
740
  );
656
741
  targetedTurn.state.waiting = false;
742
+ waitingTurnStates.delete(parsed.turnId);
657
743
  }
658
744
  try {
659
745
  observeTurnOperationOutputs({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agen-ai/agent-runtime",
3
- "version": "0.2.1",
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.1"
30
+ "@agen-ai/agent-protocol": "^0.2.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "tsup": "^8.5.0"