@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 +12 -2
- package/dist/sessionValidation.js +134 -48
- package/package.json +2 -2
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.
|
|
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
|
|
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
|
|
80
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
265
|
-
observeTurnEvent({
|
|
352
|
+
observeTurnOutput({
|
|
266
353
|
providerKey: input.providerKey,
|
|
267
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
+
"@agen-ai/agent-protocol": "^0.2.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"tsup": "^8.5.0"
|