@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 CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.5.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"
@@ -39,7 +40,7 @@
39
40
  },
40
41
  "devDependencies": {
41
42
  "@biomejs/biome": "2.5.2",
42
- "@inixiative/config": "^0.5.63",
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
- const preset = ROUTING_STRATEGIES[spec];
78
- if (!preset) throw Error(`Unknown routing strategy "${spec}"`);
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 undefined with its utilization when eligible. */
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 unreadable = request.unknownUtilization === 'rank-last' ? null : undefined;
261
- if (account.windows.length === 0) {
262
- if (unreadable === undefined) return { excluded: 'no-observation' };
263
- return { utilizationPercent: null };
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 unreadable === undefined ? { excluded: 'invalid' } : { utilizationPercent: null };
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 unreadable === undefined
285
- ? { excluded: 'stale-observation' }
286
- : { utilizationPercent: null };
287
- 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);
288
288
  }
289
- if (fractions.some((p) => p >= 100)) return { excluded: 'exhausted' };
290
- 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) };
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);