@agen-ai/agent-runtime 0.1.0 → 0.2.0
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 +17 -3
- package/dist/outputValidation.js +35 -2
- package/dist/sessionValidation.js +100 -5
- package/dist/testing/fakeProvider.js +46 -5
- package/package.json +2 -2
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.
|
|
139
|
+
The package is at `0.2.0` 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
|
|
131
|
-
|
|
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
|
|
package/dist/outputValidation.js
CHANGED
|
@@ -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" && 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
|
|
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,63 @@ 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
|
+
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
|
+
if (input.usage.usedTokens < previous.usedTokens && !input.state.compactionReadyScopes.has(input.usage.measurementScope)) {
|
|
106
|
+
invalidTurnSequence(
|
|
107
|
+
input.providerKey,
|
|
108
|
+
"Provider context occupancy cannot decrease without completed compaction."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
input.state.latestByScope.set(input.usage.measurementScope, input.usage);
|
|
113
|
+
input.state.compactionReadyScopes.delete(input.usage.measurementScope);
|
|
114
|
+
}
|
|
67
115
|
function observeTurnEvent(input) {
|
|
68
|
-
const {
|
|
116
|
+
const {
|
|
117
|
+
contextUsageState,
|
|
118
|
+
event,
|
|
119
|
+
openedRequestIds,
|
|
120
|
+
pendingRequests,
|
|
121
|
+
providerKey,
|
|
122
|
+
state
|
|
123
|
+
} = input;
|
|
69
124
|
if (state.terminal) {
|
|
70
125
|
invalidTurnSequence(
|
|
71
126
|
providerKey,
|
|
@@ -108,6 +163,24 @@ function observeTurnEvent(input) {
|
|
|
108
163
|
);
|
|
109
164
|
}
|
|
110
165
|
}
|
|
166
|
+
if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
|
|
167
|
+
state.observedItems.set(event.payload.itemId, event.payload.status);
|
|
168
|
+
if (event.payload.itemKind === "context_compaction" && event.type === "item.completed" && event.payload.status === "completed") {
|
|
169
|
+
for (const scope of ["session", "materialization"]) {
|
|
170
|
+
contextUsageState.compactionReadyScopes.add(scope);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (event.type === "turn.plan.proposed") {
|
|
175
|
+
state.proposedPlans.set(event.payload.artifactId, event.payload.requestId);
|
|
176
|
+
}
|
|
177
|
+
if (event.type === "context.usage.updated") {
|
|
178
|
+
observeContextUsage({
|
|
179
|
+
providerKey,
|
|
180
|
+
usage: event.payload,
|
|
181
|
+
state: contextUsageState
|
|
182
|
+
});
|
|
183
|
+
}
|
|
111
184
|
if (event.type === "request.opened") {
|
|
112
185
|
if (pendingRequests.size > 0) {
|
|
113
186
|
invalidTurnSequence(
|
|
@@ -122,6 +195,7 @@ function observeTurnEvent(input) {
|
|
|
122
195
|
"Provider reused a request ID within a session."
|
|
123
196
|
);
|
|
124
197
|
}
|
|
198
|
+
assertApprovalSubjectCorrelation({ providerKey, event, state });
|
|
125
199
|
openedRequestIds.add(requestId);
|
|
126
200
|
pendingRequests.set(requestId, {
|
|
127
201
|
request: event.payload.request,
|
|
@@ -182,7 +256,8 @@ function observeTurnOperationOutputs(input) {
|
|
|
182
256
|
event: output.event,
|
|
183
257
|
state: input.state,
|
|
184
258
|
pendingRequests: input.pendingRequests,
|
|
185
|
-
openedRequestIds: input.openedRequestIds
|
|
259
|
+
openedRequestIds: input.openedRequestIds,
|
|
260
|
+
contextUsageState: input.contextUsageState
|
|
186
261
|
});
|
|
187
262
|
}
|
|
188
263
|
}
|
|
@@ -196,6 +271,8 @@ function reconstructWaitingTurn(input) {
|
|
|
196
271
|
return {
|
|
197
272
|
state: {
|
|
198
273
|
fileChangeMode: input.fileChangeMode,
|
|
274
|
+
observedItems: /* @__PURE__ */ new Map(),
|
|
275
|
+
proposedPlans: /* @__PURE__ */ new Map(),
|
|
199
276
|
started: true,
|
|
200
277
|
terminal: false,
|
|
201
278
|
waiting: true,
|
|
@@ -289,6 +366,10 @@ function validateAgentProviderSession(input) {
|
|
|
289
366
|
validateSessionPorts(capabilities, input.candidate);
|
|
290
367
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
291
368
|
const openedRequestIds = /* @__PURE__ */ new Set();
|
|
369
|
+
const contextUsageState = {
|
|
370
|
+
latestByScope: /* @__PURE__ */ new Map(),
|
|
371
|
+
compactionReadyScopes: /* @__PURE__ */ new Set()
|
|
372
|
+
};
|
|
292
373
|
let activeTurn = null;
|
|
293
374
|
let closePromise = null;
|
|
294
375
|
let closed = false;
|
|
@@ -370,6 +451,8 @@ function validateAgentProviderSession(input) {
|
|
|
370
451
|
pendingRequests: /* @__PURE__ */ new Map(),
|
|
371
452
|
state: {
|
|
372
453
|
fileChangeMode: capabilities.output.fileChanges,
|
|
454
|
+
observedItems: /* @__PURE__ */ new Map(),
|
|
455
|
+
proposedPlans: /* @__PURE__ */ new Map(),
|
|
373
456
|
started: false,
|
|
374
457
|
terminal: false,
|
|
375
458
|
waiting: false,
|
|
@@ -394,7 +477,8 @@ function validateAgentProviderSession(input) {
|
|
|
394
477
|
event: output.event,
|
|
395
478
|
state: currentTurn.state,
|
|
396
479
|
pendingRequests: currentTurn.pendingRequests,
|
|
397
|
-
openedRequestIds
|
|
480
|
+
openedRequestIds,
|
|
481
|
+
contextUsageState
|
|
398
482
|
});
|
|
399
483
|
}
|
|
400
484
|
yield output;
|
|
@@ -443,6 +527,13 @@ function validateAgentProviderSession(input) {
|
|
|
443
527
|
"Provider request resolution does not match the opened request."
|
|
444
528
|
);
|
|
445
529
|
}
|
|
530
|
+
if (pending.request.expiresAt !== void 0 && Date.parse(pending.request.expiresAt) <= Date.now()) {
|
|
531
|
+
throwAgentProviderContractError(
|
|
532
|
+
providerKey,
|
|
533
|
+
"request_resolution_mismatch",
|
|
534
|
+
"Provider request resolution identifies an expired request."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
446
537
|
const nextPendingRequests = new Map(pendingRequests);
|
|
447
538
|
nextPendingRequests.delete(resolution.requestId);
|
|
448
539
|
if (activeTurn !== null) {
|
|
@@ -457,6 +548,8 @@ function validateAgentProviderSession(input) {
|
|
|
457
548
|
pendingRequests: nextPendingRequests,
|
|
458
549
|
state: {
|
|
459
550
|
fileChangeMode: capabilities.output.fileChanges,
|
|
551
|
+
observedItems: /* @__PURE__ */ new Map(),
|
|
552
|
+
proposedPlans: /* @__PURE__ */ new Map(),
|
|
460
553
|
started: true,
|
|
461
554
|
terminal: false,
|
|
462
555
|
waiting: false,
|
|
@@ -481,7 +574,8 @@ function validateAgentProviderSession(input) {
|
|
|
481
574
|
event: output.event,
|
|
482
575
|
state: currentTurn.state,
|
|
483
576
|
pendingRequests: currentTurn.pendingRequests,
|
|
484
|
-
openedRequestIds
|
|
577
|
+
openedRequestIds,
|
|
578
|
+
contextUsageState
|
|
485
579
|
});
|
|
486
580
|
}
|
|
487
581
|
yield output;
|
|
@@ -555,7 +649,8 @@ function validateAgentProviderSession(input) {
|
|
|
555
649
|
outputs: result.outputs,
|
|
556
650
|
state: targetedTurn.state,
|
|
557
651
|
pendingRequests: targetedTurn.pendingRequests,
|
|
558
|
-
openedRequestIds
|
|
652
|
+
openedRequestIds,
|
|
653
|
+
contextUsageState
|
|
559
654
|
});
|
|
560
655
|
if (terminalized && !targetedTurn.state.terminal) {
|
|
561
656
|
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:
|
|
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: {
|
|
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:
|
|
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: {
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
30
|
+
"@agen-ai/agent-protocol": "^0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"tsup": "^8.5.0"
|