@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 CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.4.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.4.1"
39
+ "@inixiative/json-rules": "^3.5.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@biomejs/biome": "2.5.2",
42
- "@inixiative/config": "^0.5.62",
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
@@ -86,6 +86,7 @@ export {
86
86
  } from './primed';
87
87
  export {
88
88
  assessCandidate,
89
+ type BillingMode,
89
90
  type CandidateRequest,
90
91
  DEFAULT_STRATEGY,
91
92
  type ExclusionReason,
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
- const preset = ROUTING_STRATEGIES[spec];
77
- if (!preset) throw Error(`Unknown routing strategy "${spec}"`);
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
- | 'not-subscription'
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 undefined with its utilization when eligible. */
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 (account.authentication !== 'subscription') return { excluded: 'not-subscription' };
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 unreadable = request.unknownUtilization === 'rank-last' ? null : undefined;
246
- if (account.windows.length === 0) {
247
- if (unreadable === undefined) return { excluded: 'no-observation' };
248
- return { utilizationPercent: null };
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 unreadable === undefined ? { excluded: 'invalid' } : { utilizationPercent: null };
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 unreadable === undefined
270
- ? { excluded: 'stale-observation' }
271
- : { utilizationPercent: null };
272
- fractions.push(window.usedPercent + window.reservedPercent);
282
+ ) {
283
+ if (!rankLast) return { excluded: 'stale-observation' };
284
+ unreadable = true;
285
+ continue;
286
+ }
287
+ readable.push(window.usedPercent + window.reservedPercent);
273
288
  }
274
- if (fractions.some((p) => p >= 100)) return { excluded: 'exhausted' };
275
- return { utilizationPercent: Math.max(...fractions) };
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
- const FIELD_VALUE: Record<RoutingField, (candidate: RankedCandidate) => number | null> = {
279
- priority: (c) => c.priority,
280
- utilization: (c) => c.utilizationPercent,
281
- quartile: (c) => c.quartile,
282
- headroom: (c) => c.headroom,
283
- preferred: (c) => Number(c.preferred),
284
- };
285
-
286
- /** Nulls last in either direction: an unknown reading never outranks a known one. */
287
- const compare = (a: number | null, b: number | null, dir: SortDir): number => {
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.sort((a, b) => {
333
- for (const { field, dir } of ordering) {
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
  );