@inixiative/agent-session 0.4.0 → 0.5.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/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/pool.ts +11 -0
- package/src/routing.ts +52 -42
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/index.ts"
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./routing": "./src/routing.ts"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"src"
|
|
@@ -35,11 +36,11 @@
|
|
|
35
36
|
],
|
|
36
37
|
"license": "MIT",
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@inixiative/json-rules": "^3.
|
|
39
|
+
"@inixiative/json-rules": "^3.5.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@biomejs/biome": "2.5.2",
|
|
42
|
-
"@inixiative/config": "^0.5.
|
|
43
|
+
"@inixiative/config": "^0.5.64",
|
|
43
44
|
"@types/bun": "1.4.2",
|
|
44
45
|
"lefthook": "2.1.9",
|
|
45
46
|
"typescript": "6.0.3"
|
package/src/index.ts
CHANGED
package/src/pool.ts
CHANGED
|
@@ -19,12 +19,14 @@ import type { HarnessSession } from './harness-session';
|
|
|
19
19
|
import { type LimitSnapshot, mergeLimits } from './limits';
|
|
20
20
|
import { probeClaudeLimits, probeCodexLimits } from './limits-probe';
|
|
21
21
|
import {
|
|
22
|
+
type BillingMode,
|
|
22
23
|
type ExclusionReason,
|
|
23
24
|
type OrderingSpec,
|
|
24
25
|
type RankedCandidate,
|
|
25
26
|
type ResolvedOrdering,
|
|
26
27
|
rankCandidates,
|
|
27
28
|
type SubscriptionCandidate,
|
|
29
|
+
type UnknownUtilizationPolicy,
|
|
28
30
|
} from './routing';
|
|
29
31
|
import { TRANSPORTS, type TransportKind } from './transport';
|
|
30
32
|
import { createSession } from './transports';
|
|
@@ -66,6 +68,9 @@ export interface PoolRequest {
|
|
|
66
68
|
readonly ordering?: OrderingSpec;
|
|
67
69
|
/** Only the preferred instance may serve. Requires preferredInstanceId. */
|
|
68
70
|
readonly pinned?: boolean;
|
|
71
|
+
/** Billing modes this request may use. Default: subscription only. */
|
|
72
|
+
readonly allowedBilling?: readonly BillingMode[];
|
|
73
|
+
readonly unknownUtilization?: UnknownUtilizationPolicy;
|
|
69
74
|
/** Restrict to these transports (e.g. only resumable ones). */
|
|
70
75
|
readonly transports?: readonly TransportKind[];
|
|
71
76
|
readonly excludeInstanceIds?: readonly string[];
|
|
@@ -228,6 +233,8 @@ export class SubscriptionPool {
|
|
|
228
233
|
const limit = config.concurrencyLimit ?? 1;
|
|
229
234
|
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
230
235
|
throw Error(`Instance ${config.id}: concurrencyLimit must be a positive integer`);
|
|
236
|
+
if (config.priority !== undefined && !Number.isSafeInteger(config.priority))
|
|
237
|
+
throw Error(`Instance ${config.id}: priority must be a safe integer`);
|
|
231
238
|
this._instances.set(config.id, {
|
|
232
239
|
config,
|
|
233
240
|
runtime,
|
|
@@ -332,6 +339,10 @@ export class SubscriptionPool {
|
|
|
332
339
|
maximumObservationAgeMs: this._maxAge,
|
|
333
340
|
ordering: request.ordering,
|
|
334
341
|
...(request.pinned !== undefined ? { pinned: request.pinned } : {}),
|
|
342
|
+
...(request.allowedBilling !== undefined ? { allowedBilling: request.allowedBilling } : {}),
|
|
343
|
+
...(request.unknownUtilization !== undefined
|
|
344
|
+
? { unknownUtilization: request.unknownUtilization }
|
|
345
|
+
: {}),
|
|
335
346
|
organizationId: request.organizationId,
|
|
336
347
|
preferredId: request.preferredInstanceId,
|
|
337
348
|
},
|
package/src/routing.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
22
|
import type { OrderBy, SortDir } from '@inixiative/json-rules';
|
|
23
|
+
import { orderRecords } from '@inixiative/json-rules';
|
|
23
24
|
|
|
24
25
|
export type { OrderBy, SortDir };
|
|
25
26
|
|
|
@@ -73,9 +74,8 @@ const ROUTING_FIELDS = Object.keys(NATURAL_DIRECTION) as RoutingField[];
|
|
|
73
74
|
/** Resolve a strategy name or explicit term list into terms, each with a direction. */
|
|
74
75
|
export const resolveOrdering = (spec: OrderingSpec = DEFAULT_STRATEGY): ResolvedOrdering => {
|
|
75
76
|
if (typeof spec === 'string') {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return resolveOrdering(preset);
|
|
77
|
+
if (!Object.hasOwn(ROUTING_STRATEGIES, spec)) throw Error(`Unknown routing strategy "${spec}"`);
|
|
78
|
+
return resolveOrdering(ROUTING_STRATEGIES[spec]);
|
|
79
79
|
}
|
|
80
80
|
if (!spec.length) throw Error('Ordering requires at least one term');
|
|
81
81
|
return spec.map((term) => {
|
|
@@ -127,6 +127,16 @@ export interface SubscriptionCandidate {
|
|
|
127
127
|
*/
|
|
128
128
|
export type UnknownUtilizationPolicy = 'exclude' | 'rank-last';
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* How an instance is paid for. Routing serves only `subscription` unless the
|
|
132
|
+
* caller widens it: spending real money is the caller's decision, never a
|
|
133
|
+
* routing default. Kingdom widens to `api-key`, where a run's spend is bounded
|
|
134
|
+
* by its allocation policies.
|
|
135
|
+
*/
|
|
136
|
+
export type BillingMode = 'subscription' | 'api-key';
|
|
137
|
+
|
|
138
|
+
const DEFAULT_BILLING: readonly BillingMode[] = ['subscription'];
|
|
139
|
+
|
|
130
140
|
export interface RoutingRequest {
|
|
131
141
|
repository: string;
|
|
132
142
|
organizationId?: string;
|
|
@@ -138,6 +148,7 @@ export interface RoutingRequest {
|
|
|
138
148
|
ordering?: OrderingSpec;
|
|
139
149
|
pinned?: boolean;
|
|
140
150
|
unknownUtilization?: UnknownUtilizationPolicy;
|
|
151
|
+
allowedBilling?: readonly BillingMode[];
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
/** Repository resolution already done by the caller (or not needed). */
|
|
@@ -153,6 +164,8 @@ export interface CandidateRequest {
|
|
|
153
164
|
/** Only the preferred instance may serve; others are excluded as `not-pinned`. */
|
|
154
165
|
pinned?: boolean;
|
|
155
166
|
unknownUtilization?: UnknownUtilizationPolicy;
|
|
167
|
+
/** Billing modes this request may use. Default: subscription only. */
|
|
168
|
+
allowedBilling?: readonly BillingMode[];
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
export interface RankedCandidate {
|
|
@@ -167,7 +180,7 @@ export interface RankedCandidate {
|
|
|
167
180
|
|
|
168
181
|
export type ExclusionReason =
|
|
169
182
|
| 'disabled'
|
|
170
|
-
| '
|
|
183
|
+
| 'billing'
|
|
171
184
|
| 'organization'
|
|
172
185
|
| 'runtime'
|
|
173
186
|
| 'model'
|
|
@@ -214,7 +227,7 @@ export function repositoryIdentity(remote: string): string {
|
|
|
214
227
|
return `${url.hostname.toLowerCase()}/${normalized}`;
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
/** Why one candidate is ineligible, or
|
|
230
|
+
/** Why one candidate is ineligible, or its utilization when eligible. */
|
|
218
231
|
export function assessCandidate(
|
|
219
232
|
request: CandidateRequest,
|
|
220
233
|
account: SubscriptionCandidate,
|
|
@@ -229,7 +242,8 @@ export function assessCandidate(
|
|
|
229
242
|
)
|
|
230
243
|
return { excluded: 'invalid' };
|
|
231
244
|
if (!account.enabled) return { excluded: 'disabled' };
|
|
232
|
-
if (
|
|
245
|
+
if (!(request.allowedBilling ?? DEFAULT_BILLING).includes(account.authentication))
|
|
246
|
+
return { excluded: 'billing' };
|
|
233
247
|
if (
|
|
234
248
|
request.organizationId !== undefined &&
|
|
235
249
|
!account.organizationIds.includes(request.organizationId)
|
|
@@ -242,13 +256,13 @@ export function assessCandidate(
|
|
|
242
256
|
return { excluded: 'unavailable' };
|
|
243
257
|
if (account.activeRuns >= account.concurrencyLimit) return { excluded: 'occupied' };
|
|
244
258
|
if (account.blocked) return { excluded: 'blocked' };
|
|
245
|
-
const
|
|
246
|
-
if (account.windows.length === 0)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const fractions: number[] = [];
|
|
259
|
+
const rankLast = request.unknownUtilization === 'rank-last';
|
|
260
|
+
if (account.windows.length === 0)
|
|
261
|
+
return rankLast ? { utilizationPercent: null } : { excluded: 'no-observation' };
|
|
262
|
+
const readable: number[] = [];
|
|
263
|
+
let unreadable = false;
|
|
251
264
|
for (const window of account.windows) {
|
|
265
|
+
// Corrupt is not unknown: a structurally impossible reading is never routable under any policy.
|
|
252
266
|
if (
|
|
253
267
|
window.usedPercent === null ||
|
|
254
268
|
!Number.isFinite(window.usedPercent) ||
|
|
@@ -257,7 +271,7 @@ export function assessCandidate(
|
|
|
257
271
|
!Number.isFinite(window.reservedPercent) ||
|
|
258
272
|
window.reservedPercent < 0
|
|
259
273
|
)
|
|
260
|
-
return
|
|
274
|
+
return { excluded: 'invalid' };
|
|
261
275
|
// resetsAt === Infinity: the provider reported no reset for this window, so no reset can have crossed the observation.
|
|
262
276
|
if (
|
|
263
277
|
!Number.isFinite(window.observedAt) ||
|
|
@@ -265,30 +279,29 @@ export function assessCandidate(
|
|
|
265
279
|
request.now - window.observedAt > request.maximumObservationAgeMs ||
|
|
266
280
|
(window.resetsAt !== Number.POSITIVE_INFINITY &&
|
|
267
281
|
(!Number.isFinite(window.resetsAt) || window.resetsAt <= request.now))
|
|
268
|
-
)
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
) {
|
|
283
|
+
if (!rankLast) return { excluded: 'stale-observation' };
|
|
284
|
+
unreadable = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
readable.push(window.usedPercent + window.reservedPercent);
|
|
273
288
|
}
|
|
274
|
-
|
|
275
|
-
return {
|
|
289
|
+
// A window that is readable and full exhausts the instance even when another is not.
|
|
290
|
+
if (readable.some((p) => p >= 100)) return { excluded: 'exhausted' };
|
|
291
|
+
if (unreadable || !readable.length) return { utilizationPercent: null };
|
|
292
|
+
return { utilizationPercent: Math.max(...readable) };
|
|
276
293
|
}
|
|
277
294
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (a === null) return b === null ? 0 : 1;
|
|
289
|
-
if (b === null) return -1;
|
|
290
|
-
return dir === 'desc' ? b - a : a - b;
|
|
291
|
-
};
|
|
295
|
+
/** Term fields as own properties, so json-rules orders them by name. */
|
|
296
|
+
const sortKey = (candidate: RankedCandidate, index: number) => ({
|
|
297
|
+
index,
|
|
298
|
+
priority: candidate.priority,
|
|
299
|
+
utilization: candidate.utilizationPercent,
|
|
300
|
+
quartile: candidate.quartile,
|
|
301
|
+
headroom: candidate.headroom,
|
|
302
|
+
preferred: Number(candidate.preferred),
|
|
303
|
+
id: candidate.accountId,
|
|
304
|
+
});
|
|
292
305
|
|
|
293
306
|
/** Rank eligible candidates deterministically (ties broken by id). */
|
|
294
307
|
export function rankCandidates(
|
|
@@ -307,6 +320,8 @@ export function rankCandidates(
|
|
|
307
320
|
throw Error('Invalid observation policy');
|
|
308
321
|
if (request.pinned && !request.preferredId)
|
|
309
322
|
throw Error('Pinned routing requires a preferred instance');
|
|
323
|
+
if (request.allowedBilling?.length === 0)
|
|
324
|
+
throw Error('Ordering requires at least one billing mode');
|
|
310
325
|
if (new Set(accounts.map((a) => a.id)).size !== accounts.length)
|
|
311
326
|
throw Error('Duplicate account identity');
|
|
312
327
|
const ordering = resolveOrdering(request.ordering);
|
|
@@ -329,14 +344,8 @@ export function rankCandidates(
|
|
|
329
344
|
},
|
|
330
345
|
];
|
|
331
346
|
});
|
|
332
|
-
ranked.
|
|
333
|
-
|
|
334
|
-
const order = compare(FIELD_VALUE[field](a), FIELD_VALUE[field](b), dir);
|
|
335
|
-
if (order !== 0) return order;
|
|
336
|
-
}
|
|
337
|
-
return a.accountId.localeCompare(b.accountId);
|
|
338
|
-
});
|
|
339
|
-
return { candidates: ranked, excluded, ordering };
|
|
347
|
+
const ordered = orderRecords(ranked.map(sortKey), [...ordering, { field: 'id', dir: 'asc' }]);
|
|
348
|
+
return { candidates: ordered.map((key) => ranked[key.index]!), excluded, ordering };
|
|
340
349
|
}
|
|
341
350
|
|
|
342
351
|
/** The draft's repository-scoped entry point: resolve ownership, then rank. */
|
|
@@ -373,6 +382,7 @@ export function rankSubscriptionAccounts(
|
|
|
373
382
|
...(request.unknownUtilization !== undefined
|
|
374
383
|
? { unknownUtilization: request.unknownUtilization }
|
|
375
384
|
: {}),
|
|
385
|
+
...(request.allowedBilling !== undefined ? { allowedBilling: request.allowedBilling } : {}),
|
|
376
386
|
},
|
|
377
387
|
accounts,
|
|
378
388
|
);
|