@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 +24 -21
- package/package.json +2 -2
- package/src/claude-agent-sdk-session.ts +1 -1
- package/src/claude-code-session.ts +3 -3
- package/src/claude-in-session.ts +1 -1
- package/src/claude-subagents.ts +5 -5
- package/src/codex-primed.ts +1 -1
- package/src/pool.ts +1 -1
- package/src/secret-scratch.ts +53 -14
- package/src/turn-state.ts +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# @inixiative/agent-session
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
in over stdin, and classifies
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
##
|
|
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.
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
61
|
-
- **
|
|
62
|
-
transport
|
|
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
|
-
|
|
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
|
|
283
|
-
|
|
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.
|
|
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.
|
|
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-${
|
|
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-${
|
|
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 =
|
|
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}-${
|
|
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);
|
package/src/claude-in-session.ts
CHANGED
|
@@ -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 ??=
|
|
1071
|
+
member.admissionId ??= Bun.randomUUIDv7();
|
|
1072
1072
|
if (member.admitted) return true;
|
|
1073
1073
|
try {
|
|
1074
1074
|
await Promise.race([
|
package/src/claude-subagents.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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-${
|
|
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 ??=
|
|
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 =
|
|
1466
|
+
const uuid = Bun.randomUUIDv7();
|
|
1467
1467
|
const turnEnded = new Promise<void>((resolve) => {
|
|
1468
1468
|
side.onResult = resolve;
|
|
1469
1469
|
});
|
package/src/codex-primed.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
587
|
+
const leaseId = Bun.randomUUIDv7();
|
|
588
588
|
state.leases.add(leaseId);
|
|
589
589
|
let released = false;
|
|
590
590
|
const lease: Lease = {
|
package/src/secret-scratch.ts
CHANGED
|
@@ -208,27 +208,66 @@ export class SecretScratchPad {
|
|
|
208
208
|
return this._dir;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/**
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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,
|
|
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 =
|
|
12
|
+
readonly admissionId = Bun.randomUUIDv7();
|
|
13
13
|
identity: SessionIdentity = {};
|
|
14
14
|
nativeOutcome: SessionAttempt['nativeOutcome'] = 'unknown';
|
|
15
15
|
localOutcome: SessionAttempt['localOutcome'] = 'pending';
|