@inixiative/agent-session 0.5.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 +4 -3
- package/src/pool.ts +11 -0
- package/src/routing.ts +22 -18
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.5.
|
|
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"
|
|
@@ -39,7 +40,7 @@
|
|
|
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/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
|
@@ -74,9 +74,8 @@ const ROUTING_FIELDS = Object.keys(NATURAL_DIRECTION) as RoutingField[];
|
|
|
74
74
|
/** Resolve a strategy name or explicit term list into terms, each with a direction. */
|
|
75
75
|
export const resolveOrdering = (spec: OrderingSpec = DEFAULT_STRATEGY): ResolvedOrdering => {
|
|
76
76
|
if (typeof spec === 'string') {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return resolveOrdering(preset);
|
|
77
|
+
if (!Object.hasOwn(ROUTING_STRATEGIES, spec)) throw Error(`Unknown routing strategy "${spec}"`);
|
|
78
|
+
return resolveOrdering(ROUTING_STRATEGIES[spec]);
|
|
80
79
|
}
|
|
81
80
|
if (!spec.length) throw Error('Ordering requires at least one term');
|
|
82
81
|
return spec.map((term) => {
|
|
@@ -228,7 +227,7 @@ export function repositoryIdentity(remote: string): string {
|
|
|
228
227
|
return `${url.hostname.toLowerCase()}/${normalized}`;
|
|
229
228
|
}
|
|
230
229
|
|
|
231
|
-
/** Why one candidate is ineligible, or
|
|
230
|
+
/** Why one candidate is ineligible, or its utilization when eligible. */
|
|
232
231
|
export function assessCandidate(
|
|
233
232
|
request: CandidateRequest,
|
|
234
233
|
account: SubscriptionCandidate,
|
|
@@ -257,13 +256,13 @@ export function assessCandidate(
|
|
|
257
256
|
return { excluded: 'unavailable' };
|
|
258
257
|
if (account.activeRuns >= account.concurrencyLimit) return { excluded: 'occupied' };
|
|
259
258
|
if (account.blocked) return { excluded: 'blocked' };
|
|
260
|
-
const
|
|
261
|
-
if (account.windows.length === 0)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
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;
|
|
266
264
|
for (const window of account.windows) {
|
|
265
|
+
// Corrupt is not unknown: a structurally impossible reading is never routable under any policy.
|
|
267
266
|
if (
|
|
268
267
|
window.usedPercent === null ||
|
|
269
268
|
!Number.isFinite(window.usedPercent) ||
|
|
@@ -272,7 +271,7 @@ export function assessCandidate(
|
|
|
272
271
|
!Number.isFinite(window.reservedPercent) ||
|
|
273
272
|
window.reservedPercent < 0
|
|
274
273
|
)
|
|
275
|
-
return
|
|
274
|
+
return { excluded: 'invalid' };
|
|
276
275
|
// resetsAt === Infinity: the provider reported no reset for this window, so no reset can have crossed the observation.
|
|
277
276
|
if (
|
|
278
277
|
!Number.isFinite(window.observedAt) ||
|
|
@@ -280,14 +279,17 @@ export function assessCandidate(
|
|
|
280
279
|
request.now - window.observedAt > request.maximumObservationAgeMs ||
|
|
281
280
|
(window.resetsAt !== Number.POSITIVE_INFINITY &&
|
|
282
281
|
(!Number.isFinite(window.resetsAt) || window.resetsAt <= request.now))
|
|
283
|
-
)
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
) {
|
|
283
|
+
if (!rankLast) return { excluded: 'stale-observation' };
|
|
284
|
+
unreadable = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
readable.push(window.usedPercent + window.reservedPercent);
|
|
288
288
|
}
|
|
289
|
-
|
|
290
|
-
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) };
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
/** Term fields as own properties, so json-rules orders them by name. */
|
|
@@ -318,6 +320,8 @@ export function rankCandidates(
|
|
|
318
320
|
throw Error('Invalid observation policy');
|
|
319
321
|
if (request.pinned && !request.preferredId)
|
|
320
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');
|
|
321
325
|
if (new Set(accounts.map((a) => a.id)).size !== accounts.length)
|
|
322
326
|
throw Error('Duplicate account identity');
|
|
323
327
|
const ordering = resolveOrdering(request.ordering);
|