@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 +12 -2
- package/dist/outputValidation.js +1 -1
- package/dist/sessionValidation.js +156 -58
- 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
|
|
package/dist/outputValidation.js
CHANGED
|
@@ -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
|
|
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) {
|
|
@@ -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
|
-
|
|
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({
|
|
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
|
-
|
|
254
|
-
observeTurnEvent({
|
|
352
|
+
observeTurnOutput({
|
|
255
353
|
providerKey: input.providerKey,
|
|
256
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|