@inixiative/agent-session 0.10.0 → 0.11.1

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 CHANGED
@@ -1,17 +1,19 @@
1
1
  # @inixiative/agent-session
2
2
 
3
- Drive coding-agent CLIs as **persistent, streaming, event-captured sessions** —
4
- one interface, any agent, over interchangeable **transports**. Claude Code
5
- (CLI or Agent SDK) and Codex (app-server) today; Gemini and Grok planned.
6
- Also: warm **primed decision sessions**, **subscription pools** with
3
+ > Part of [inixiative](https://www.inixiative.com), technology for cooperation. See [all the projects](https://github.com/inixiative).
4
+
5
+ Drive Claude Code, Codex and Muse as persistent streaming sessions behind one
6
+ interface. Claude Code runs over its CLI or the Agent SDK, Codex over its
7
+ app-server and Muse over `muse serve`; Gemini and Grok are planned. The package
8
+ also holds warm **primed decision sessions**, **subscription pools** with
7
9
  deterministic routing, **limit polling** and **session continuity**.
8
10
 
9
- Most ways to script a coding agent are one-shot (`claude -p "…"`) and lose the
10
- process. `agent-session` keeps a single long-lived agent process, streams turns
11
- in over stdin, and classifies **every** event coming back — text, thinking,
12
- tool calls, tool results, usage — so you can drive multi-turn work and capture
13
- exactly what the agent did. Built for eval harnesses, agent comparisons, and
14
- multi-agent orchestration.
11
+ A one-shot call (`claude -p "…"`) ends the process after one answer.
12
+ `agent-session` keeps one long-lived agent process per session, sends each turn
13
+ in over stdin, and classifies each event that comes back (text, thinking, tool
14
+ calls, tool results, usage), so you can drive multi-turn work and record what
15
+ the agent did. Foundry, Oracle and Archive run their agent sessions on it, as do
16
+ our eval harnesses and agent comparisons.
15
17
 
16
18
  ```ts
17
19
  import { ClaudeCodeSession } from "@inixiative/agent-session";
@@ -33,23 +35,23 @@ for (const e of result.events) { // full classified event stream
33
35
  session.kill();
34
36
  ```
35
37
 
36
- ## Why it's different
38
+ ## What you get
37
39
 
38
40
  - **Persistent, multi-turn.** One process per session; `send()` per turn,
39
- resolves on the runtime's `result` event. Not one-shot `-p`.
41
+ resolves on the runtime's `result` event.
40
42
  - **Full event capture.** A normalized `SessionEvent` taxonomy
41
43
  (`text` / `thinking` / `tool_use` / `tool_result` / `result` / `error` /
42
- `session_compact`) — the same shape regardless of runtime.
44
+ `session_compact`), the same shape for every runtime.
43
45
  - **Subscription auth, no API billing.** The Claude Code adapter strips
44
46
  `ANTHROPIC_API_KEY` and authenticates via the CLI's subscription / an
45
- `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` — works headless and in
47
+ `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`; it works headless and in
46
48
  containers.
47
49
  - **Images with a turn.** `send(text, { images: [{ mediaType, data }] })` (png, jpeg, gif or
48
50
  webp, base64) where the transport declares `capabilities.images`: Claude gets base64 `image`
49
51
  blocks in the same user message, Codex `image` inputs with a `data:` URL. Anything else is
50
52
  refused before a byte is written.
51
53
  - **fork / resume / interrupt** and pre-send hooks (rewrite the outgoing
52
- message — e.g. inject just-in-time context per turn).
54
+ message, for example to add context for that turn).
53
55
  - **A resume whose native conversation is not found is typed.** Claude finding no transcript
54
56
  ("No conversation found with session ID": cleaned up, deleted, or under another project
55
57
  directory or config home) and Codex finding no rollout ("no rollout found for thread id") both
@@ -57,9 +59,10 @@ session.kill();
57
59
  with the `cwd` the lookup ran in). No turn ran, so the attempt settles as a no-send instead of
58
60
  uncertain native work, and the caller can start fresh from its own record.
59
61
  - **Transports with declared capabilities.** How a model is reached is a
60
- pluggable transport; switching it is a routing change, not a rewrite.
61
- - **Zero dependencies.** Just Bun + the agent CLI you're driving. The Agent SDK
62
- transport takes the SDK's `query` from the caller.
62
+ pluggable transport; switching it is a routing change.
63
+ - **Two dependencies.** `@inixiative/json-rules` and Meta's `@muse-code/sdk`
64
+ (for the Muse transport), plus Bun and the agent CLI you're driving. The Agent
65
+ SDK transport takes the SDK's `query` from the caller.
63
66
 
64
67
  ## Interface
65
68
 
@@ -272,15 +275,15 @@ snapshots in the artifact but do not fabricate a completed-turn total.
272
275
  | Gemini CLI | planned |
273
276
  | Grok CLI | planned (when its headless/stream mode matures) |
274
277
 
275
- Opaque runtimes degrade gracefully — fewer event types, never a hard failure.
278
+ A runtime that reports less produces fewer event types; it never fails a turn for that.
276
279
 
277
280
  [Subscription pooling goals](tickets/README.md) (AS-001…005) are implemented as described in [docs/transports-pools-routing.md](docs/transports-pools-routing.md); each ticket lists what remains.
278
281
 
279
282
  ## Provenance
280
283
 
281
284
  Extracted from [`inixiative/foundry`](https://github.com/inixiative/foundry),
282
- where this drives the Artificer session. Consumed by foundry, oracle,
283
- and the inixiative bench.
285
+ where it runs Foundry's agent sessions. Foundry, Oracle, Archive and the
286
+ inixiative bench use it.
284
287
 
285
288
  ## License
286
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@biomejs/biome": "2.5.2",
44
- "@inixiative/config": "^0.5.183",
44
+ "@inixiative/config": "^0.5.186",
45
45
  "@types/bun": "1.4.4",
46
46
  "lefthook": "2.1.9",
47
47
  "typescript": "6.0.3"
@@ -187,7 +187,7 @@ function bridge(
187
187
  const awaiting = new Map<string, (response: unknown) => void>();
188
188
  const ask = (request: Record<string, unknown>, signal?: AbortSignal) =>
189
189
  new Promise<unknown>((resolve) => {
190
- const id = `sdk-bridge-${crypto.randomUUID()}`;
190
+ const id = `sdk-bridge-${Bun.randomUUIDv7()}`;
191
191
  awaiting.set(id, resolve);
192
192
  signal?.addEventListener(
193
193
  'abort',
@@ -379,7 +379,7 @@ export class ClaudeCodeSession implements HarnessSession {
379
379
  private _steerAcks = new Map<string, (state: SteerState) => void>();
380
380
  /** Steers written and not yet ended natively: a late `completed` after their turn settled is theirs. */
381
381
  private _steerUuids = new Set<string>();
382
- private readonly _hookCallbackId = `agent-session-pre-tool-${crypto.randomUUID()}`;
382
+ private readonly _hookCallbackId = `agent-session-pre-tool-${Bun.randomUUIDv7()}`;
383
383
  private _limits?: LimitSnapshot;
384
384
  private _controlSeq = 0;
385
385
  private _control = new Map<
@@ -555,7 +555,7 @@ export class ClaudeCodeSession implements HarnessSession {
555
555
  if (!turn || !a || a.dispatch !== 'attempted' || a.nativeOutcome !== 'unknown')
556
556
  return ignored('no owned in-flight turn');
557
557
  if (!this._proc || !this._alive) return ignored('session not running');
558
- const uuid = crypto.randomUUID();
558
+ const uuid = Bun.randomUUIDv7();
559
559
  turn.steers ??= new Map();
560
560
  const steers = turn.steers;
561
561
  steers.set(uuid, 'written');
@@ -999,7 +999,7 @@ export class ClaudeCodeSession implements HarnessSession {
999
999
  timeoutMs: number,
1000
1000
  ): Promise<Record<string, unknown>> {
1001
1001
  if (!this._proc || !this._alive) return Promise.reject(new Error('Session not running'));
1002
- const requestId = `agent-session-${++this._controlSeq}-${crypto.randomUUID()}`;
1002
+ const requestId = `agent-session-${++this._controlSeq}-${Bun.randomUUIDv7()}`;
1003
1003
  return new Promise((resolve, reject) => {
1004
1004
  const timer = setTimeout(() => {
1005
1005
  this._control.delete(requestId);
@@ -1068,7 +1068,7 @@ export class ClaudeInSessionDecisions implements PrimedSessions, ClaudeSessionEx
1068
1068
  }
1069
1069
 
1070
1070
  private async _admit(member: Member): Promise<boolean> {
1071
- member.admissionId ??= crypto.randomUUID();
1071
+ member.admissionId ??= Bun.randomUUIDv7();
1072
1072
  if (member.admitted) return true;
1073
1073
  try {
1074
1074
  await Promise.race([
@@ -472,7 +472,7 @@ export class ClaudeSubagentSessions implements PrimedSessions {
472
472
 
473
473
  /** Admit a decision onto its spare; the input goes out when the spare's moment call is held. */
474
474
  private async _reserve(spare: Spare, member: Member): Promise<void> {
475
- member.admissionId = crypto.randomUUID();
475
+ member.admissionId = Bun.randomUUIDv7();
476
476
  const ours = () => spare.member === member && member.spare === spare;
477
477
  try {
478
478
  await Promise.race([
@@ -789,7 +789,7 @@ export class ClaudeSubagentSessions implements PrimedSessions {
789
789
  return;
790
790
  }
791
791
  const spares = [...side.spares.values()];
792
- const uuid = crypto.randomUUID();
792
+ const uuid = Bun.randomUUIDv7();
793
793
  const turnEnded = new Promise<void>((resolve) => {
794
794
  side.onResult = resolve;
795
795
  });
@@ -1041,7 +1041,7 @@ export class ClaudeSubagentSessions implements PrimedSessions {
1041
1041
  }
1042
1042
 
1043
1043
  private _control(side: Side, request: Record<string, unknown>, timeoutMs = 30_000) {
1044
- const id = `c-${crypto.randomUUID()}`;
1044
+ const id = `c-${Bun.randomUUIDv7()}`;
1045
1045
  return new Promise<Record<string, unknown>>((resolve, reject) => {
1046
1046
  const timer = setTimeout(() => {
1047
1047
  side.control.delete(id);
@@ -1417,7 +1417,7 @@ export class ClaudeSubagentSessions implements PrimedSessions {
1417
1417
  await Promise.all(
1418
1418
  chunk.map(async (m) => {
1419
1419
  if (m.settled) return;
1420
- m.admissionId ??= crypto.randomUUID();
1420
+ m.admissionId ??= Bun.randomUUIDv7();
1421
1421
  try {
1422
1422
  if (!retry && !m.admitted)
1423
1423
  await Promise.race([
@@ -1463,7 +1463,7 @@ export class ClaudeSubagentSessions implements PrimedSessions {
1463
1463
  m.token = `m${i + 1}`;
1464
1464
  });
1465
1465
  side.members = new Map(admitted.map((m) => [m.token, m]));
1466
- const uuid = crypto.randomUUID();
1466
+ const uuid = Bun.randomUUIDv7();
1467
1467
  const turnEnded = new Promise<void>((resolve) => {
1468
1468
  side.onResult = resolve;
1469
1469
  });
@@ -1335,7 +1335,7 @@ export class CodexPrimedSessions implements PrimedSessions {
1335
1335
  if (session) session.model ??= model;
1336
1336
  const branchMs = Math.round(performance.now() - t1);
1337
1337
  let branch = opened.branch;
1338
- admissionId = crypto.randomUUID();
1338
+ admissionId = Bun.randomUUIDv7();
1339
1339
  try {
1340
1340
  await request.onAdmission?.({ admissionId, key: spec.key, runtime: 'codex', threadId });
1341
1341
  } catch (cause) {
package/src/pool.ts CHANGED
@@ -584,7 +584,7 @@ export class SubscriptionPool {
584
584
  utilizationPercent: number | null,
585
585
  rank: number,
586
586
  ): Lease {
587
- const leaseId = crypto.randomUUID();
587
+ const leaseId = Bun.randomUUIDv7();
588
588
  state.leases.add(leaseId);
589
589
  let released = false;
590
590
  const lease: Lease = {
@@ -208,27 +208,66 @@ export class SecretScratchPad {
208
208
  return this._dir;
209
209
  }
210
210
 
211
- /** Lift every `$$word` out of `text` into `thread`'s pad, under the dagger for `message`. */
211
+ /**
212
+ * Lift every `$$word` out of `text` into `thread`'s pad, under the dagger for `message`. With
213
+ * `patterns` (global regexes, e.g. a caller's known token shapes), every match is lifted too, the
214
+ * person having forgotten the marker; a match may span lines (a private key). Positions follow the
215
+ * text's order; a match inside another lifted span is left to it. `recognised` counts the matches.
216
+ */
212
217
  lift(
213
218
  text: string,
214
219
  at: { thread: string; message: string; now?: Date },
215
- ): { text: string; refs: string[] } {
216
- const values = [...text.matchAll(MARKED)].map((match) => match[2] ?? '');
217
- if (!values.length) return { text, refs: [] };
220
+ options: { patterns?: readonly RegExp[] } = {},
221
+ ): { text: string; refs: string[]; recognised: number } {
222
+ const spans: Array<{
223
+ start: number;
224
+ end: number;
225
+ value: string;
226
+ marked: boolean;
227
+ }> = [];
228
+ for (const match of text.matchAll(MARKED)) {
229
+ const lead = match[1] ?? '';
230
+ spans.push({
231
+ start: (match.index ?? 0) + lead.length,
232
+ end: (match.index ?? 0) + match[0].length,
233
+ value: match[2] ?? '',
234
+ marked: true,
235
+ });
236
+ }
237
+ for (const pattern of options.patterns ?? []) {
238
+ if (!pattern.global) throw new SecretScratchError('A lift pattern must be global (/g).');
239
+ for (const match of text.matchAll(pattern)) {
240
+ if (!match[0]) continue;
241
+ const start = match.index ?? 0;
242
+ spans.push({
243
+ start,
244
+ end: start + match[0].length,
245
+ value: match[0],
246
+ marked: false,
247
+ });
248
+ }
249
+ }
250
+ if (!spans.length) return { text, refs: [], recognised: 0 };
251
+ // Earliest first, the longer of two at one place; a span overlapping a kept one is dropped.
252
+ spans.sort((a, b) => a.start - b.start || b.end - a.end);
253
+ const kept: typeof spans = [];
254
+ for (const span of spans)
255
+ if (!kept.length || span.start >= kept[kept.length - 1]!.end) kept.push(span);
218
256
  checkId(at.thread, 'thread');
219
257
  checkId(at.message, 'message');
220
258
  const daggers = this._read(at.thread);
221
259
  let dagger = daggers.find((d) => d.message === at.message);
222
260
  const first = dagger ? Math.max(0, ...dagger.blocks.keys()) + 1 : 1;
223
- const refs: string[] = [];
224
- const lifted = text.replace(MARKED, (_whole, lead: string) => {
225
- const ref = formatScratchRef(
226
- { thread: at.thread, message: at.message, position: first + refs.length },
227
- at.thread,
228
- );
229
- refs.push(ref);
230
- return `${lead}${ref}`;
261
+ const refs = kept.map((_, i) =>
262
+ formatScratchRef({ thread: at.thread, message: at.message, position: first + i }, at.thread),
263
+ );
264
+ let lifted = '';
265
+ let cursor = 0;
266
+ kept.forEach((span, i) => {
267
+ lifted += text.slice(cursor, span.start) + refs[i];
268
+ cursor = span.end;
231
269
  });
270
+ lifted += text.slice(cursor);
232
271
  if (!dagger) {
233
272
  dagger = {
234
273
  message: at.message,
@@ -238,9 +277,9 @@ export class SecretScratchPad {
238
277
  };
239
278
  daggers.push(dagger);
240
279
  }
241
- for (const [i, value] of values.entries()) dagger.blocks.set(first + i, value);
280
+ for (const [i, span] of kept.entries()) dagger.blocks.set(first + i, span.value);
242
281
  this._write(at.thread, daggers);
243
- return { text: lifted, refs };
282
+ return { text: lifted, refs, recognised: kept.filter((span) => !span.marked).length };
244
283
  }
245
284
 
246
285
  /** Every pad here (or one thread's) with what it holds pending, never the values. */
package/src/turn-state.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import { retainEvidence } from './retained-evidence';
10
10
  /** Per-admission evidence. Caller settlement never transfers native ownership. */
11
11
  export class TurnState {
12
- readonly admissionId = crypto.randomUUID();
12
+ readonly admissionId = Bun.randomUUIDv7();
13
13
  identity: SessionIdentity = {};
14
14
  nativeOutcome: SessionAttempt['nativeOutcome'] = 'unknown';
15
15
  localOutcome: SessionAttempt['localOutcome'] = 'pending';