@agen-ai/agent-runtime 0.2.3 → 0.2.5
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 +28 -10
- package/dist/accountQuota/index.d.ts +3 -0
- package/dist/accountQuota/index.js +2 -0
- package/dist/accountQuota/types.d.ts +26 -0
- package/dist/accountQuota/types.js +0 -0
- package/dist/accountQuota/validation.d.ts +5 -0
- package/dist/accountQuota/validation.js +278 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providerDriver.d.ts +2 -0
- package/dist/providerInstanceRegistry.js +8 -0
- package/dist/sessionValidation.js +85 -2
- package/dist/sessions.d.ts +3 -0
- package/dist/testing/conformance.js +25 -1
- package/dist/testing/fakeProvider.d.ts +2 -0
- package/dist/testing/fakeProvider.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
`@agen-ai/agent-runtime` is the process-local service-provider interface for coding-agent
|
|
4
4
|
implementations. It preserves a small ownership chain: a driver parses host configuration and
|
|
5
5
|
materializes an instance; the instance owns one opaque ID, technical capabilities, an adapter,
|
|
6
|
-
readiness, and disposal; the adapter opens provider-native
|
|
7
|
-
its binding and conversation-local operations.
|
|
6
|
+
an account-quota observation port, readiness, and disposal; the adapter opens provider-native
|
|
7
|
+
sessions; each returned session owns its binding and conversation-local operations.
|
|
8
8
|
|
|
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
|
-
This source package implements Agent Protocol V8. Agent Protocol V8, private host
|
|
12
|
+
This source package implements Agent Protocol V8. Agent Protocol V8, private host V18/catalog V11, and Workspaces event V11/member response V12.
|
|
13
13
|
This is intentionally one coordinated tuple rather than a deployable mixed-version graph.
|
|
14
14
|
|
|
15
15
|
## Entrypoints
|
|
16
16
|
|
|
17
17
|
- `@agen-ai/agent-runtime` exports the public driver, instance, adapter, session, output,
|
|
18
|
-
readiness, bounded-evidence, artifact-candidate,
|
|
19
|
-
and registry APIs.
|
|
18
|
+
readiness, account-quota port validation, bounded-evidence, artifact-candidate,
|
|
19
|
+
capability-bound interaction validation, and registry APIs.
|
|
20
20
|
- `@agen-ai/agent-runtime/testing` exports the deterministic fake provider and reusable
|
|
21
21
|
conformance runner.
|
|
22
22
|
|
|
@@ -48,6 +48,17 @@ An unsupported declaration exposes exactly `{ kind: "unsupported" }`; a supporte
|
|
|
48
48
|
expose exactly its typed handlers. Catalogs and results are parsed again at the runtime boundary,
|
|
49
49
|
bounded by the declaration, and correlated to the offered revision and caller-owned identity.
|
|
50
50
|
|
|
51
|
+
Account-quota observation is an instance port beside the session adapter. Its four exact variants
|
|
52
|
+
are `unsupported`, `query`, `observe`, and `query_and_observe`; supported variants must expose all
|
|
53
|
+
methods named by their kind. Every operation receives an `AbortSignal`. The registry combines that
|
|
54
|
+
caller signal with the materialized-instance lifetime, parses every returned snapshot, closes an
|
|
55
|
+
observation iterator on cancellation or disposal, and preserves provider errors. Quota reads are
|
|
56
|
+
replaceable account state and never become model prompts, conversation events, turn output, or
|
|
57
|
+
execution authorization. A malformed or unavailable quota result cannot invalidate an otherwise
|
|
58
|
+
usable session.
|
|
59
|
+
|
|
60
|
+
Explicit iterator return and normal end join native cleanup for at most 1,000 ms; cleanup failure or timeout is observable. Cancellation or an existing provider failure keeps its original error while cleanup is still attempted. Iterator closure aborts its producer and removes listeners, and late native rejections remain observed.
|
|
61
|
+
|
|
51
62
|
Approval continuations are refusal-first. Before delegating to the candidate adapter, the
|
|
52
63
|
validated session proves that the request is still pending and unexpired and that a selected
|
|
53
64
|
`optionId` was offered by that exact request. Provider-emitted approval requests must correlate to
|
|
@@ -101,8 +112,13 @@ receipt rule: stale or malformed input fails before provider delegation, while a
|
|
|
101
112
|
the candidate reports execution start is wrapped as `AgentProviderDelegatedOperationError` and
|
|
102
113
|
makes the session unusable. The start boundary commits only after the host observer returns
|
|
103
114
|
successfully; an observer failure remains a pre-delegation error and leaves the session reusable.
|
|
104
|
-
Operation
|
|
105
|
-
|
|
115
|
+
Operation invocation also carries a caller-owned `observationTurnId` and an optional `onOutput`
|
|
116
|
+
sink. An adapter must await that sink for every operation observation. The validated runtime
|
|
117
|
+
admits only operation-scoped progress, context, compaction, operation, resource, warning, error,
|
|
118
|
+
and diagnostic events; it enforces the exact session/observation-turn correlation, immutable
|
|
119
|
+
identity, declared bounds, forward-only lifecycle transitions, and per-output backpressure before
|
|
120
|
+
the operation result may settle. Collaboration and generated-resource observations retain the
|
|
121
|
+
same correlation and lifecycle validation at their respective ports.
|
|
106
122
|
|
|
107
123
|
## Minimal driver
|
|
108
124
|
|
|
@@ -127,7 +143,7 @@ export const driver = defineAgentProviderDriver({
|
|
|
127
143
|
return input;
|
|
128
144
|
},
|
|
129
145
|
createInstance({ instanceId }): MaterializedAgentProviderInstance {
|
|
130
|
-
// Construct capabilities, adapter, readiness, and disposal here.
|
|
146
|
+
// Construct capabilities, adapter, accountQuota, readiness, and disposal here.
|
|
131
147
|
throw new Error(`Implement ${parseAgentInstanceId(instanceId)}.`);
|
|
132
148
|
},
|
|
133
149
|
});
|
|
@@ -163,9 +179,11 @@ turn/request ordering, request resolution, steering, interruption, configuration
|
|
|
163
179
|
close, and idempotent disposal. Unsupported operations must remain explicit discriminants and
|
|
164
180
|
must not expose handlers.
|
|
165
181
|
|
|
166
|
-
The package is at `0.2.
|
|
182
|
+
The package is at `0.2.5` while the public SPI is being proven with external adapters. Pre-1.0
|
|
167
183
|
releases may include breaking changes during this beta period, and those changes are called out in
|
|
168
|
-
the release notes.
|
|
184
|
+
the release notes. Requiring `accountQuota` on every materialized instance is a source-level
|
|
185
|
+
implementer change in the coordinated `0.2.5` release. Upgrade all three SDK packages together;
|
|
186
|
+
there is no compatibility shim or optional-field fallback.
|
|
169
187
|
|
|
170
188
|
The public runtime implements Agent Protocol V8. Its package version remains independent of private
|
|
171
189
|
host, catalog, persistence, and member-projection versions. V8 directly replaces V7; there is no
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentAccountQuotaSnapshot, AgentProviderKey } from "@agen-ai/agent-protocol";
|
|
2
|
+
import type { MaybePromise } from "../foundation.js";
|
|
3
|
+
export interface AgentAccountQuotaQueryInput {
|
|
4
|
+
readonly signal: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export type AgentAccountQuotaQuery = (input: AgentAccountQuotaQueryInput) => MaybePromise<AgentAccountQuotaSnapshot>;
|
|
7
|
+
export type AgentAccountQuotaObserve = (input: AgentAccountQuotaQueryInput) => AsyncIterable<AgentAccountQuotaSnapshot>;
|
|
8
|
+
export type AgentAccountQuotaPort = Readonly<{
|
|
9
|
+
kind: "unsupported";
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
kind: "query";
|
|
12
|
+
query: AgentAccountQuotaQuery;
|
|
13
|
+
}> | Readonly<{
|
|
14
|
+
kind: "observe";
|
|
15
|
+
observe: AgentAccountQuotaObserve;
|
|
16
|
+
}> | Readonly<{
|
|
17
|
+
kind: "query_and_observe";
|
|
18
|
+
query: AgentAccountQuotaQuery;
|
|
19
|
+
observe: AgentAccountQuotaObserve;
|
|
20
|
+
}>;
|
|
21
|
+
export interface ValidateAgentAccountQuotaPortInput {
|
|
22
|
+
readonly providerKey: AgentProviderKey;
|
|
23
|
+
readonly port: unknown;
|
|
24
|
+
readonly signal: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.d.ts.map
|
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentAccountQuotaPort, ValidateAgentAccountQuotaPortInput } from "./types.js";
|
|
2
|
+
/** Maximum time the runtime waits for a provider's native iterator cleanup. */
|
|
3
|
+
export declare const AGENT_ACCOUNT_QUOTA_ITERATOR_CLEANUP_TIMEOUT_MS = 1000;
|
|
4
|
+
export declare function validateAgentAccountQuotaPort(input: ValidateAgentAccountQuotaPortInput): AgentAccountQuotaPort;
|
|
5
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseAgentAccountQuotaSnapshot
|
|
3
|
+
} from "@agen-ai/agent-protocol";
|
|
4
|
+
import { throwAgentProviderContractError } from "../contractErrors.js";
|
|
5
|
+
import { throwIfAgentOperationAborted } from "../foundation.js";
|
|
6
|
+
const AGENT_ACCOUNT_QUOTA_ITERATOR_CLEANUP_TIMEOUT_MS = 1e3;
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function assertSignal(value, field) {
|
|
11
|
+
if (!(value instanceof AbortSignal)) {
|
|
12
|
+
throw new TypeError(`Account quota ${field} must be an AbortSignal.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function exactKeys(value, expected) {
|
|
16
|
+
const actual = Reflect.ownKeys(value);
|
|
17
|
+
return actual.length === expected.length && actual.every((key) => typeof key === "string" && expected.includes(key)) && expected.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
18
|
+
}
|
|
19
|
+
function contractError(providerKey, detail) {
|
|
20
|
+
return throwAgentProviderContractError(
|
|
21
|
+
providerKey,
|
|
22
|
+
"capability_port_mismatch",
|
|
23
|
+
`Provider ${providerKey} returned an invalid account quota ${detail}.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function abortReason(signal) {
|
|
27
|
+
return signal.reason instanceof Error ? signal.reason : new DOMException("The agent operation was aborted.", "AbortError");
|
|
28
|
+
}
|
|
29
|
+
function awaitCancellable(candidate, signal) {
|
|
30
|
+
const providerResult = Promise.resolve(candidate);
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let settled = false;
|
|
33
|
+
const remove = () => signal.removeEventListener("abort", onAbort);
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
settled = true;
|
|
37
|
+
remove();
|
|
38
|
+
reject(abortReason(signal));
|
|
39
|
+
};
|
|
40
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
41
|
+
providerResult.then(
|
|
42
|
+
(value) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
remove();
|
|
46
|
+
resolve(value);
|
|
47
|
+
},
|
|
48
|
+
(error) => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
remove();
|
|
52
|
+
reject(error);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
if (signal.aborted) onAbort();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function operationSignal(input) {
|
|
59
|
+
if (!isObject(input)) {
|
|
60
|
+
throw new TypeError("Account quota operation input must be an object.");
|
|
61
|
+
}
|
|
62
|
+
assertSignal(input.signal, "operation signal");
|
|
63
|
+
return input.signal;
|
|
64
|
+
}
|
|
65
|
+
function providerIterator(providerKey, iterable) {
|
|
66
|
+
if (!isObject(iterable)) return contractError(providerKey, "observation");
|
|
67
|
+
const factory = iterable[Symbol.asyncIterator];
|
|
68
|
+
if (typeof factory !== "function") return contractError(providerKey, "observation");
|
|
69
|
+
const iterator = factory.call(iterable);
|
|
70
|
+
if (!isObject(iterator)) return contractError(providerKey, "observation iterator");
|
|
71
|
+
const next = iterator.next;
|
|
72
|
+
if (typeof next !== "function") return contractError(providerKey, "observation iterator");
|
|
73
|
+
const returnMethod = iterator.return;
|
|
74
|
+
if (returnMethod !== void 0 && typeof returnMethod !== "function") {
|
|
75
|
+
return contractError(providerKey, "observation iterator");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
iterator,
|
|
79
|
+
next,
|
|
80
|
+
returnMethod
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function iteratorResult(value) {
|
|
84
|
+
if (!isObject(value)) return false;
|
|
85
|
+
const done = value.done;
|
|
86
|
+
return done === void 0 || typeof done === "boolean";
|
|
87
|
+
}
|
|
88
|
+
function boundedCleanup(result, providerKey) {
|
|
89
|
+
let timer;
|
|
90
|
+
const timeout = new Promise((_, reject) => {
|
|
91
|
+
timer = setTimeout(() => reject(new Error(
|
|
92
|
+
`Provider ${providerKey} account quota iterator cleanup exceeded ${AGENT_ACCOUNT_QUOTA_ITERATOR_CLEANUP_TIMEOUT_MS}ms.`
|
|
93
|
+
)), AGENT_ACCOUNT_QUOTA_ITERATOR_CLEANUP_TIMEOUT_MS);
|
|
94
|
+
});
|
|
95
|
+
return Promise.race([Promise.resolve(result), timeout]).then(() => void 0).finally(() => {
|
|
96
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function beginCleanup(state) {
|
|
100
|
+
if (state.cleanupPromise !== void 0) return state.cleanupPromise;
|
|
101
|
+
const source = state.source;
|
|
102
|
+
if (source === void 0) return Promise.resolve();
|
|
103
|
+
if (source.returnMethod === void 0) {
|
|
104
|
+
state.cleanupPromise = Promise.resolve();
|
|
105
|
+
} else {
|
|
106
|
+
try {
|
|
107
|
+
state.cleanupPromise = boundedCleanup(
|
|
108
|
+
source.returnMethod.call(source.iterator),
|
|
109
|
+
state.providerKey
|
|
110
|
+
);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
state.cleanupPromise = Promise.reject(error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
state.cleanupPromise.catch(() => void 0);
|
|
116
|
+
return state.cleanupPromise;
|
|
117
|
+
}
|
|
118
|
+
function closeState(state) {
|
|
119
|
+
if (state.closed) {
|
|
120
|
+
void beginCleanup(state);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
state.closed = true;
|
|
124
|
+
state.signal.removeEventListener("abort", state.onAbort);
|
|
125
|
+
if (!state.controller.signal.aborted) state.controller.abort();
|
|
126
|
+
void beginCleanup(state);
|
|
127
|
+
}
|
|
128
|
+
function createIterator(providerKey, declaredObserve, receiver, lifetimeSignal, operationSignalValue) {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const signal = AbortSignal.any([
|
|
131
|
+
lifetimeSignal,
|
|
132
|
+
operationSignalValue,
|
|
133
|
+
controller.signal
|
|
134
|
+
]);
|
|
135
|
+
const state = {
|
|
136
|
+
providerKey,
|
|
137
|
+
controller,
|
|
138
|
+
signal,
|
|
139
|
+
onAbort: () => closeState(state),
|
|
140
|
+
closed: false,
|
|
141
|
+
closedNormally: false,
|
|
142
|
+
pendingNext: false
|
|
143
|
+
};
|
|
144
|
+
if (signal.aborted) closeState(state);
|
|
145
|
+
else signal.addEventListener("abort", state.onAbort, { once: true });
|
|
146
|
+
const source = () => {
|
|
147
|
+
if (state.source !== void 0) return state.source;
|
|
148
|
+
throwIfAgentOperationAborted(signal);
|
|
149
|
+
const iterable = declaredObserve.call(receiver, { signal });
|
|
150
|
+
state.source = providerIterator(providerKey, iterable);
|
|
151
|
+
if (signal.aborted) closeState(state);
|
|
152
|
+
return state.source;
|
|
153
|
+
};
|
|
154
|
+
const next = async () => {
|
|
155
|
+
if (state.pendingNext) {
|
|
156
|
+
throw new TypeError("Concurrent account quota observation next calls are not supported.");
|
|
157
|
+
}
|
|
158
|
+
if (state.closed) {
|
|
159
|
+
if (state.closedNormally || !signal.aborted) return { done: true, value: void 0 };
|
|
160
|
+
throwIfAgentOperationAborted(signal);
|
|
161
|
+
}
|
|
162
|
+
throwIfAgentOperationAborted(signal);
|
|
163
|
+
state.pendingNext = true;
|
|
164
|
+
try {
|
|
165
|
+
const iterator = source();
|
|
166
|
+
throwIfAgentOperationAborted(signal);
|
|
167
|
+
const result = await awaitCancellable(iterator.next.call(iterator.iterator), signal);
|
|
168
|
+
throwIfAgentOperationAborted(signal);
|
|
169
|
+
if (!iteratorResult(result)) return contractError(providerKey, "observation");
|
|
170
|
+
if (result.done === true) {
|
|
171
|
+
state.closedNormally = true;
|
|
172
|
+
closeState(state);
|
|
173
|
+
await beginCleanup(state);
|
|
174
|
+
return { done: true, value: void 0 };
|
|
175
|
+
}
|
|
176
|
+
return { done: false, value: parseAgentAccountQuotaSnapshot(result.value) };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
closeState(state);
|
|
179
|
+
throw error;
|
|
180
|
+
} finally {
|
|
181
|
+
state.pendingNext = false;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const returnIterator = async (value) => {
|
|
185
|
+
state.closedNormally = true;
|
|
186
|
+
state.controller.abort();
|
|
187
|
+
closeState(state);
|
|
188
|
+
await beginCleanup(state);
|
|
189
|
+
return { done: true, value };
|
|
190
|
+
};
|
|
191
|
+
return { next, return: returnIterator };
|
|
192
|
+
}
|
|
193
|
+
function wrapQuery(declaredQuery, receiver, lifetimeSignal) {
|
|
194
|
+
return async (input) => {
|
|
195
|
+
const signal = AbortSignal.any([lifetimeSignal, operationSignal(input)]);
|
|
196
|
+
throwIfAgentOperationAborted(signal);
|
|
197
|
+
const candidate = declaredQuery.call(receiver, { signal });
|
|
198
|
+
const snapshot = await awaitCancellable(candidate, signal);
|
|
199
|
+
throwIfAgentOperationAborted(signal);
|
|
200
|
+
return parseAgentAccountQuotaSnapshot(snapshot);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function wrapObserve(providerKey, declaredObserve, receiver, lifetimeSignal) {
|
|
204
|
+
return (input) => {
|
|
205
|
+
const operationSignalValue = operationSignal(input);
|
|
206
|
+
return {
|
|
207
|
+
[Symbol.asyncIterator]() {
|
|
208
|
+
return createIterator(
|
|
209
|
+
providerKey,
|
|
210
|
+
declaredObserve,
|
|
211
|
+
receiver,
|
|
212
|
+
lifetimeSignal,
|
|
213
|
+
operationSignalValue
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function validateShape(providerKey, candidate) {
|
|
220
|
+
if (!isObject(candidate)) return contractError(providerKey, "port");
|
|
221
|
+
const kind = candidate.kind;
|
|
222
|
+
const query = candidate.query;
|
|
223
|
+
const observe = candidate.observe;
|
|
224
|
+
if (kind === "unsupported") {
|
|
225
|
+
if (!exactKeys(candidate, ["kind"]) || "query" in candidate || "observe" in candidate) {
|
|
226
|
+
return contractError(providerKey, "port");
|
|
227
|
+
}
|
|
228
|
+
return Object.freeze({ kind: "unsupported" });
|
|
229
|
+
}
|
|
230
|
+
if (kind === "query") {
|
|
231
|
+
if (!exactKeys(candidate, ["kind", "query"]) || typeof query !== "function" || "observe" in candidate) {
|
|
232
|
+
return contractError(providerKey, "port");
|
|
233
|
+
}
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
if (kind === "observe") {
|
|
237
|
+
if (!exactKeys(candidate, ["kind", "observe"]) || typeof observe !== "function" || "query" in candidate) {
|
|
238
|
+
return contractError(providerKey, "port");
|
|
239
|
+
}
|
|
240
|
+
return candidate;
|
|
241
|
+
}
|
|
242
|
+
if (kind === "query_and_observe") {
|
|
243
|
+
if (!exactKeys(candidate, ["kind", "query", "observe"]) || typeof query !== "function" || typeof observe !== "function") {
|
|
244
|
+
return contractError(providerKey, "port");
|
|
245
|
+
}
|
|
246
|
+
return candidate;
|
|
247
|
+
}
|
|
248
|
+
return contractError(providerKey, "port");
|
|
249
|
+
}
|
|
250
|
+
function validateAgentAccountQuotaPort(input) {
|
|
251
|
+
if (!isObject(input)) {
|
|
252
|
+
throw new TypeError("Account quota port validation input must be an object.");
|
|
253
|
+
}
|
|
254
|
+
assertSignal(input.signal, "instance signal");
|
|
255
|
+
const declared = validateShape(input.providerKey, input.port);
|
|
256
|
+
if (declared.kind === "unsupported") return declared;
|
|
257
|
+
if (declared.kind === "query") {
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
kind: "query",
|
|
260
|
+
query: wrapQuery(declared.query, declared, input.signal)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (declared.kind === "observe") {
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
kind: "observe",
|
|
266
|
+
observe: wrapObserve(input.providerKey, declared.observe, declared, input.signal)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
kind: "query_and_observe",
|
|
271
|
+
query: wrapQuery(declared.query, declared, input.signal),
|
|
272
|
+
observe: wrapObserve(input.providerKey, declared.observe, declared, input.signal)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
AGENT_ACCOUNT_QUOTA_ITERATOR_CLEANUP_TIMEOUT_MS,
|
|
277
|
+
validateAgentAccountQuotaPort
|
|
278
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/providerDriver.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentCapabilities, type AgentInstanceId, type AgentProviderKey } from "@agen-ai/agent-protocol";
|
|
2
2
|
import type { MaybePromise } from "./foundation.js";
|
|
3
|
+
import type { AgentAccountQuotaPort } from "./accountQuota/types.js";
|
|
3
4
|
import type { AgentProviderReadiness } from "./readiness.js";
|
|
4
5
|
import type { AgentProviderAdapter } from "./sessions.js";
|
|
5
6
|
export interface AgentProviderInstanceDefinition {
|
|
@@ -14,6 +15,7 @@ export interface MaterializedAgentProviderInstance {
|
|
|
14
15
|
readonly instanceId: AgentInstanceId;
|
|
15
16
|
readonly capabilities: AgentCapabilities;
|
|
16
17
|
readonly adapter: AgentProviderAdapter;
|
|
18
|
+
readonly accountQuota: AgentAccountQuotaPort;
|
|
17
19
|
readonly checkReadiness: (input?: AgentProviderReadinessCheckInput) => MaybePromise<AgentProviderReadiness>;
|
|
18
20
|
readonly dispose: () => MaybePromise<void>;
|
|
19
21
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
parseAgentProviderKey
|
|
5
5
|
} from "@agen-ai/agent-protocol";
|
|
6
6
|
import { validateAgentProviderAdapter } from "./adapterValidation.js";
|
|
7
|
+
import { validateAgentAccountQuotaPort } from "./accountQuota/validation.js";
|
|
7
8
|
import {
|
|
8
9
|
createAgentProviderCatalogEntries,
|
|
9
10
|
createAgentProviderInstanceCatalogEntries
|
|
@@ -138,10 +139,16 @@ function managedInstance(rawInstance, providerKey) {
|
|
|
138
139
|
status: "active",
|
|
139
140
|
disposePromise: null
|
|
140
141
|
};
|
|
142
|
+
const quotaLifetime = new AbortController();
|
|
141
143
|
const instance = Object.freeze({
|
|
142
144
|
instanceId: parseAgentInstanceId(rawInstance.instanceId),
|
|
143
145
|
capabilities,
|
|
144
146
|
adapter: validateAgentProviderAdapter(capabilities, rawInstance.adapter),
|
|
147
|
+
accountQuota: validateAgentAccountQuotaPort({
|
|
148
|
+
providerKey,
|
|
149
|
+
port: rawInstance.accountQuota,
|
|
150
|
+
signal: quotaLifetime.signal
|
|
151
|
+
}),
|
|
145
152
|
checkReadiness: async (input) => {
|
|
146
153
|
const readiness = await checkReadiness(input);
|
|
147
154
|
return validateAgentProviderReadiness(readiness);
|
|
@@ -150,6 +157,7 @@ function managedInstance(rawInstance, providerKey) {
|
|
|
150
157
|
if (lifecycle.status === "disposed") return Promise.resolve();
|
|
151
158
|
if (lifecycle.disposePromise) return lifecycle.disposePromise;
|
|
152
159
|
lifecycle.status = "disposing";
|
|
160
|
+
quotaLifetime.abort(new DOMException("The provider instance was disposed.", "AbortError"));
|
|
153
161
|
lifecycle.disposePromise = Promise.resolve().then(() => dispose()).then(() => {
|
|
154
162
|
lifecycle.status = "disposed";
|
|
155
163
|
}).catch((error) => {
|
|
@@ -507,6 +507,45 @@ function observeTurnOutput(input) {
|
|
|
507
507
|
contextUsageState: input.contextUsageState
|
|
508
508
|
});
|
|
509
509
|
}
|
|
510
|
+
const SESSION_OPERATION_OBSERVATION_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
511
|
+
"item.started",
|
|
512
|
+
"item.updated",
|
|
513
|
+
"item.completed",
|
|
514
|
+
"progress.updated",
|
|
515
|
+
"context.usage.updated",
|
|
516
|
+
"operation.updated",
|
|
517
|
+
"resource.updated",
|
|
518
|
+
"runtime.warning",
|
|
519
|
+
"runtime.error",
|
|
520
|
+
"provider.diagnostic"
|
|
521
|
+
]);
|
|
522
|
+
function observeSessionOperationOutput(input) {
|
|
523
|
+
if (input.output.kind === "event") {
|
|
524
|
+
const event = input.output.event;
|
|
525
|
+
if (!SESSION_OPERATION_OBSERVATION_EVENT_TYPES.has(event.type)) {
|
|
526
|
+
invalidTurnSequence(
|
|
527
|
+
input.providerKey,
|
|
528
|
+
"Provider emitted turn-owned output from a session operation."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if ((event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") && event.payload.itemKind !== "context_compaction") {
|
|
532
|
+
invalidTurnSequence(
|
|
533
|
+
input.providerKey,
|
|
534
|
+
"Provider emitted a non-compaction item from a session operation."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
observeTurnOutput({
|
|
539
|
+
capabilities: input.capabilities,
|
|
540
|
+
providerKey: input.providerKey,
|
|
541
|
+
output: input.output,
|
|
542
|
+
state: input.state,
|
|
543
|
+
pendingRequests: /* @__PURE__ */ new Map(),
|
|
544
|
+
openedRequestIds: /* @__PURE__ */ new Set(),
|
|
545
|
+
contextUsageState: input.contextUsageState,
|
|
546
|
+
interactionState: input.interactionState
|
|
547
|
+
});
|
|
548
|
+
}
|
|
510
549
|
function assertStableTurnBoundary(input) {
|
|
511
550
|
const terminal = input.state.terminal && !input.state.waiting;
|
|
512
551
|
const waiting = !input.state.terminal && input.state.waiting && input.pendingRequests.size === 1;
|
|
@@ -718,8 +757,18 @@ function validateAgentProviderSession(input) {
|
|
|
718
757
|
input2.observer?.();
|
|
719
758
|
executionStarted = true;
|
|
720
759
|
};
|
|
760
|
+
const onOutput = async (candidate) => {
|
|
761
|
+
if (!executionStarted) {
|
|
762
|
+
throwAgentProviderContractError(
|
|
763
|
+
providerKey,
|
|
764
|
+
"invalid_operation_result",
|
|
765
|
+
"Provider emitted operation output before reporting execution start."
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
await input2.observeOutput?.(candidate);
|
|
769
|
+
};
|
|
721
770
|
try {
|
|
722
|
-
const candidate = await input2.invoke(onProviderExecutionStarted);
|
|
771
|
+
const candidate = await input2.invoke(onProviderExecutionStarted, onOutput);
|
|
723
772
|
if (!executionStarted) {
|
|
724
773
|
throwAgentProviderContractError(
|
|
725
774
|
providerKey,
|
|
@@ -1129,8 +1178,12 @@ function validateAgentProviderSession(input) {
|
|
|
1129
1178
|
requireUsable();
|
|
1130
1179
|
throwIfAgentOperationAborted(operationInput.signal);
|
|
1131
1180
|
let invocation;
|
|
1181
|
+
let observationTurnId;
|
|
1132
1182
|
try {
|
|
1133
1183
|
invocation = parseAgentOperationInvocation(operationInput.invocation);
|
|
1184
|
+
observationTurnId = parseAgentTurnId(
|
|
1185
|
+
operationInput.observationTurnId
|
|
1186
|
+
);
|
|
1134
1187
|
} catch {
|
|
1135
1188
|
return throwAgentProviderContractError(
|
|
1136
1189
|
providerKey,
|
|
@@ -1189,6 +1242,16 @@ function validateAgentProviderSession(input) {
|
|
|
1189
1242
|
const trackedInvocation = {
|
|
1190
1243
|
invocation
|
|
1191
1244
|
};
|
|
1245
|
+
const observationState = {
|
|
1246
|
+
fileChangeMode: capabilities.output.fileChanges,
|
|
1247
|
+
observedItems: /* @__PURE__ */ new Map(),
|
|
1248
|
+
proposedPlans: /* @__PURE__ */ new Map(),
|
|
1249
|
+
generatedResourceIds: /* @__PURE__ */ new Set(),
|
|
1250
|
+
started: true,
|
|
1251
|
+
terminal: false,
|
|
1252
|
+
waiting: false,
|
|
1253
|
+
finalDiffObserved: false
|
|
1254
|
+
};
|
|
1192
1255
|
interactionState.operationInvocations.set(
|
|
1193
1256
|
invocation.invocationId,
|
|
1194
1257
|
trackedInvocation
|
|
@@ -1197,11 +1260,31 @@ function validateAgentProviderSession(input) {
|
|
|
1197
1260
|
const result = await delegateProviderMutation({
|
|
1198
1261
|
operation: "invoke_operation",
|
|
1199
1262
|
observer: operationInput.onProviderExecutionStarted,
|
|
1200
|
-
invoke: (onProviderExecutionStarted) => declaredOperations.invokeOperation({
|
|
1263
|
+
invoke: (onProviderExecutionStarted, onOutput) => declaredOperations.invokeOperation({
|
|
1201
1264
|
invocation,
|
|
1265
|
+
observationTurnId,
|
|
1202
1266
|
onProviderExecutionStarted,
|
|
1267
|
+
onOutput,
|
|
1203
1268
|
...operationInput.signal === void 0 ? {} : { signal: operationInput.signal }
|
|
1204
1269
|
}),
|
|
1270
|
+
observeOutput: async (candidate) => {
|
|
1271
|
+
const output = validateAgentProviderOutputForContext(
|
|
1272
|
+
candidate,
|
|
1273
|
+
{
|
|
1274
|
+
...outputContext(observationTurnId),
|
|
1275
|
+
operationInvocationId: invocation.invocationId
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
observeSessionOperationOutput({
|
|
1279
|
+
capabilities,
|
|
1280
|
+
providerKey,
|
|
1281
|
+
output,
|
|
1282
|
+
state: observationState,
|
|
1283
|
+
contextUsageState,
|
|
1284
|
+
interactionState
|
|
1285
|
+
});
|
|
1286
|
+
await operationInput.onOutput?.(output);
|
|
1287
|
+
},
|
|
1205
1288
|
validate: (candidate) => {
|
|
1206
1289
|
const result2 = validateAgentOperationResultTransition({
|
|
1207
1290
|
providerKey,
|
package/dist/sessions.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface AgentProviderBranchSessionInput extends AgentProviderSessionCon
|
|
|
20
20
|
readonly onBindingCreated: AgentSessionBindingCreatedObserver;
|
|
21
21
|
}
|
|
22
22
|
export type AgentProviderExecutionStartedObserver = () => void;
|
|
23
|
+
export type AgentProviderOutputObserver = (output: AgentProviderOutput) => MaybePromise<void>;
|
|
23
24
|
export interface AgentProviderRunTurnInput {
|
|
24
25
|
readonly turnId: AgentTurnId;
|
|
25
26
|
readonly interactionMode: AgentTurnInteractionMode;
|
|
@@ -49,8 +50,10 @@ export interface AgentProviderApplyConfigurationSelectionInput {
|
|
|
49
50
|
}
|
|
50
51
|
export interface AgentProviderInvokeOperationInput {
|
|
51
52
|
readonly invocation: AgentOperationInvocation;
|
|
53
|
+
readonly observationTurnId: AgentTurnId;
|
|
52
54
|
readonly signal?: AbortSignal;
|
|
53
55
|
readonly onProviderExecutionStarted?: AgentProviderExecutionStartedObserver;
|
|
56
|
+
readonly onOutput?: AgentProviderOutputObserver;
|
|
54
57
|
}
|
|
55
58
|
export interface AgentProviderSpawnCollaborationInput {
|
|
56
59
|
readonly spawn: AgentCollaborationSpawnInput;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
matchesAgentSessionBinding,
|
|
3
|
-
parseAgentRequestResolutionFor
|
|
3
|
+
parseAgentRequestResolutionFor,
|
|
4
|
+
parseAgentTurnId
|
|
4
5
|
} from "@agen-ai/agent-protocol";
|
|
5
6
|
import { isAgentOperationAbortError } from "../foundation.js";
|
|
6
7
|
import {
|
|
@@ -121,6 +122,26 @@ async function runAgentProviderConformance(scenario) {
|
|
|
121
122
|
"Version-reporting capability did not return a runtime version."
|
|
122
123
|
);
|
|
123
124
|
checks.push("readiness", "version_reporting_capability");
|
|
125
|
+
const quota = instance.accountQuota;
|
|
126
|
+
const quotaAbort = new AbortController();
|
|
127
|
+
quotaAbort.abort();
|
|
128
|
+
const quotaOperations = [];
|
|
129
|
+
if (quota.kind === "query" || quota.kind === "query_and_observe") {
|
|
130
|
+
quotaOperations.push(async () => quota.query({ signal: quotaAbort.signal }));
|
|
131
|
+
}
|
|
132
|
+
if (quota.kind === "observe" || quota.kind === "query_and_observe") {
|
|
133
|
+
quotaOperations.push(async () => quota.observe({ signal: quotaAbort.signal })[Symbol.asyncIterator]().next());
|
|
134
|
+
}
|
|
135
|
+
for (const operation of quotaOperations) {
|
|
136
|
+
let aborted2 = false;
|
|
137
|
+
try {
|
|
138
|
+
await operation();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
aborted2 = isAgentOperationAbortError(error);
|
|
141
|
+
}
|
|
142
|
+
requireCheck(aborted2, "account_quota", "Pre-aborted quota work must fail with AbortError.");
|
|
143
|
+
}
|
|
144
|
+
checks.push("account_quota");
|
|
124
145
|
let createdBindingCount = 0;
|
|
125
146
|
const created = await instance.adapter.createSession({
|
|
126
147
|
sessionId: scenario.createSessionId,
|
|
@@ -330,6 +351,9 @@ async function runAgentProviderConformance(scenario) {
|
|
|
330
351
|
let executionStarted = 0;
|
|
331
352
|
const result = await created.operations.invokeOperation({
|
|
332
353
|
invocation: scenario.operationInvocation,
|
|
354
|
+
observationTurnId: parseAgentTurnId(
|
|
355
|
+
scenario.operationInvocation.invocationId
|
|
356
|
+
),
|
|
333
357
|
onProviderExecutionStarted: () => {
|
|
334
358
|
executionStarted += 1;
|
|
335
359
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AgentCapabilities, type AgentInstanceId, type AgentRequestResolution, type AgentSessionId, type AgentTurnId } from "@agen-ai/agent-protocol";
|
|
2
|
+
import type { AgentAccountQuotaPort } from "../accountQuota/types.js";
|
|
2
3
|
import { type AgentProviderDriver, type AgentProviderInstanceDefinition } from "../providerDriver.js";
|
|
3
4
|
import type { AgentProviderSteerTurnInput, AgentTurnSteeringResult } from "../sessions.js";
|
|
4
5
|
export interface FakeAgentProviderOptions {
|
|
@@ -7,6 +8,7 @@ export interface FakeAgentProviderOptions {
|
|
|
7
8
|
readonly capabilities?: AgentCapabilities;
|
|
8
9
|
readonly now?: () => string;
|
|
9
10
|
readonly steeringResult?: AgentTurnSteeringResult;
|
|
11
|
+
readonly accountQuota?: AgentAccountQuotaPort;
|
|
10
12
|
}
|
|
11
13
|
export type FakeAgentSteeringInput = Readonly<Omit<AgentProviderSteerTurnInput, "signal">>;
|
|
12
14
|
export interface FakeAgentProviderSnapshot {
|
|
@@ -555,6 +555,7 @@ function createFakeAgentProvider(options = {}) {
|
|
|
555
555
|
instanceId: createdInstanceId,
|
|
556
556
|
capabilities,
|
|
557
557
|
adapter: fakeAdapter({ state, capabilities, now, steeringResult }),
|
|
558
|
+
accountQuota: options.accountQuota ?? { kind: "unsupported" },
|
|
558
559
|
checkReadiness: () => createAgentProviderReadiness({
|
|
559
560
|
status: "ready",
|
|
560
561
|
checkedAt: now(),
|
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.5",
|
|
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.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"tsup": "^8.5.0"
|