@inixiative/agent-session 0.1.0 → 0.3.0
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/LICENSE +21 -0
- package/README.md +98 -6
- package/package.json +39 -8
- package/src/claude-agent-sdk-session.ts +316 -0
- package/src/claude-code-session.ts +715 -195
- package/src/claude-primed.ts +655 -0
- package/src/claude-usage.ts +29 -0
- package/src/codex-primed.ts +1482 -0
- package/src/codex-session.ts +1317 -241
- package/src/harness-session.ts +182 -24
- package/src/index.ts +106 -10
- package/src/json-rpc.ts +240 -0
- package/src/limits-probe.ts +78 -0
- package/src/limits.ts +216 -0
- package/src/pool.ts +652 -0
- package/src/primed.ts +284 -0
- package/src/retained-evidence.ts +20 -0
- package/src/routing.ts +273 -0
- package/src/transport.ts +215 -0
- package/src/transports.ts +97 -0
- package/src/turn-state.ts +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Inixiative
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# @inixiative/agent-session
|
|
2
2
|
|
|
3
3
|
Drive coding-agent CLIs as **persistent, streaming, event-captured sessions** —
|
|
4
|
-
one interface, any agent
|
|
4
|
+
one interface, any agent, over interchangeable **transports**. Claude Code
|
|
5
|
+
(CLI or Agent SDK) and Codex (MCP or app-server) today; Gemini and Grok planned.
|
|
6
|
+
Also: warm **primed decision sessions**, **subscription pools** with
|
|
7
|
+
deterministic routing, **limit polling** and **session continuity**.
|
|
5
8
|
|
|
6
9
|
Most ways to script a coding agent are one-shot (`claude -p "…"`) and lose the
|
|
7
10
|
process. `agent-session` keeps a single long-lived agent process, streams turns
|
|
@@ -22,7 +25,7 @@ await session.start();
|
|
|
22
25
|
|
|
23
26
|
const result = await session.send("Build a CLI that …");
|
|
24
27
|
console.log(result.content); // final text
|
|
25
|
-
console.log(result.tokens); //
|
|
28
|
+
console.log(result.tokens); // input/output + reported cache/thinking counters
|
|
26
29
|
for (const e of result.events) { // full classified event stream
|
|
27
30
|
if (e.kind === "tool_use") console.log("tool:", e.toolName);
|
|
28
31
|
}
|
|
@@ -43,25 +46,110 @@ session.kill();
|
|
|
43
46
|
containers.
|
|
44
47
|
- **fork / resume / interrupt** and pre-send hooks (rewrite the outgoing
|
|
45
48
|
message — e.g. inject just-in-time context per turn).
|
|
46
|
-
- **
|
|
49
|
+
- **Transports with declared capabilities.** How a model is reached is a
|
|
50
|
+
pluggable transport; switching it is a routing change, not a rewrite.
|
|
51
|
+
- **Zero dependencies.** Just Bun + the agent CLI you're driving. The Agent SDK
|
|
52
|
+
transport takes the SDK's `query` from the caller.
|
|
47
53
|
|
|
48
54
|
## Interface
|
|
49
55
|
|
|
50
56
|
`HarnessSession` is the provider-agnostic contract (`start` / `send` / `kill` /
|
|
51
|
-
`fork` / `interrupt` + an event handler).
|
|
52
|
-
`
|
|
57
|
+
`fork` / `interrupt` + an event handler). Optional members where a transport
|
|
58
|
+
supports them: `transport` (its descriptor), `interruptNative()` (acknowledged
|
|
59
|
+
stop), `readLimits()` (account limits without a model turn). Account limits also
|
|
60
|
+
arrive as `rate_limit` events.
|
|
61
|
+
|
|
62
|
+
## Transports
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createSession, TRANSPORTS } from "@inixiative/agent-session";
|
|
66
|
+
|
|
67
|
+
const kind = "codex-app-server"; // or claude-cli, claude-agent-sdk, codex-mcp
|
|
68
|
+
if (TRANSPORTS[kind].capabilities.interrupt === "acknowledged") { /* … */ }
|
|
69
|
+
const session = createSession(kind, { cwd, model: "gpt-6-luna", env: { CODEX_HOME: profile } });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| | claude-cli | codex-mcp | codex-app-server | claude-agent-sdk | acp | api |
|
|
73
|
+
|---|---|---|---|---|---|---|
|
|
74
|
+
| Status | ✅ live | ✅ live | ✅ live | ✅ live | stub | stub |
|
|
75
|
+
| Resume / fork | native / native | loaded-only / same-thread | native / – | native / native | – | – |
|
|
76
|
+
| Interrupt | acknowledged | local-only | acknowledged | acknowledged | – | – |
|
|
77
|
+
| Push | – | – | steer | – | – | – |
|
|
78
|
+
| Approvals | mode | mode | refuse | callback | – | – |
|
|
79
|
+
| Limits stream / poll | ✅ / ✅ | – / – | ✅ / ✅ | ✅ / – | – | – |
|
|
80
|
+
| Primed reset | fork | – | fork | – | – | – |
|
|
81
|
+
|
|
82
|
+
Full matrix, notes and what is stubbed: [docs/transports-pools-routing.md](docs/transports-pools-routing.md).
|
|
83
|
+
|
|
84
|
+
## Primed decision sessions
|
|
85
|
+
|
|
86
|
+
One live, warm session per middleware role, primed once with its role
|
|
87
|
+
instructions and stable context, reset to that primed state every cycle:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { CodexPrimedSessions } from "@inixiative/agent-session";
|
|
91
|
+
|
|
92
|
+
const decisions = new CodexPrimedSessions({ model: "gpt-6-luna", effort: "low", cwd: privateDir });
|
|
93
|
+
const result = await decisions.decide(
|
|
94
|
+
{ key: `${threadId}:aux:domain:api`, instructions: rolePrompt, context: layerContent, hash: layerHash },
|
|
95
|
+
{ input: "## Message\n…", onAdmission: register },
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Codex hosts every key as a thread of one `app-server` process per account and
|
|
100
|
+
forks the primed thread per cycle; Claude forks a persisted primed session with
|
|
101
|
+
a pre-spawned spare. Measured on a ChatGPT login: warm Codex decisions 2.45 s
|
|
102
|
+
median vs 3.9 s for `codex exec` per decision, with 34% fewer input tokens; warm
|
|
103
|
+
Claude (haiku) decisions 1.95 s with the whole primed prefix read from cache.
|
|
104
|
+
|
|
105
|
+
## Pools, routing and continuity
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { SubscriptionPool } from "@inixiative/agent-session";
|
|
109
|
+
|
|
110
|
+
const pool = new SubscriptionPool({ instances: [
|
|
111
|
+
{ id: "work", transport: "claude-cli", profileDirectory: "/profiles/work", organizationIds: ["org"] },
|
|
112
|
+
{ id: "spare", transport: "claude-cli", profileDirectory: "/profiles/spare", organizationIds: ["org"] },
|
|
113
|
+
] });
|
|
114
|
+
const { session, lease } = await pool.open({ runtime: "claude", model: "sonnet", organizationId: "org", preferredInstanceId: "work" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Routing is deterministic (quartile-balanced, owner-first or pinned) over
|
|
118
|
+
observed limits, leases and health; stale or unknown limits exclude an instance
|
|
119
|
+
and every exclusion has a reason. `continueOn` moves a thread only between
|
|
120
|
+
instances that share native history, and says why when it cannot.
|
|
121
|
+
|
|
122
|
+
## Usage telemetry
|
|
123
|
+
|
|
124
|
+
Claude results preserve `cacheRead`, `cacheWrite`, `cacheWrite5m`, `cacheWrite1h`,
|
|
125
|
+
and `thinking` alongside `input`/`output`. Input excludes the disjoint cache
|
|
126
|
+
counters; thinking is already included in output and TTL counts are included in
|
|
127
|
+
cacheWrite. Unreported optional counters remain absent. `providerUsage` retains
|
|
128
|
+
the entire original usage object, including service tier and future tags.
|
|
129
|
+
|
|
130
|
+
`result` events and `send().tokens` carry the native turn aggregate. Session
|
|
131
|
+
`totalTokens` and artifact totals sum these completed results once. `usage`
|
|
132
|
+
events carry request snapshots with the original envelope (request/message IDs,
|
|
133
|
+
model and tags) in `raw`; they may repeat across content blocks. Do not sum
|
|
134
|
+
request snapshots together with result totals. Interrupted turns retain request
|
|
135
|
+
snapshots in the artifact but do not fabricate a completed-turn total.
|
|
53
136
|
|
|
54
137
|
## Roadmap
|
|
55
138
|
|
|
56
139
|
| Runtime | Status |
|
|
57
140
|
|---|---|
|
|
58
141
|
| Claude Code | ✅ shipped |
|
|
59
|
-
| Codex CLI (`codex mcp-server`, JSON-RPC) | ✅ shipped (`CodexSession
|
|
142
|
+
| Codex CLI (`codex mcp-server`, JSON-RPC) | ✅ shipped (`CodexSession`) |
|
|
143
|
+
| Codex app-server | ✅ shipped (`CodexAppServerSession`, `CodexPrimedSessions`) |
|
|
144
|
+
| Claude Agent SDK | ✅ shipped (`ClaudeAgentSdkSession`; caller supplies `query`) |
|
|
145
|
+
| ACP agents / direct API | typed stubs |
|
|
60
146
|
| Gemini CLI | planned |
|
|
61
147
|
| Grok CLI | planned (when its headless/stream mode matures) |
|
|
62
148
|
|
|
63
149
|
Opaque runtimes degrade gracefully — fewer event types, never a hard failure.
|
|
64
150
|
|
|
151
|
+
[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.
|
|
152
|
+
|
|
65
153
|
## Provenance
|
|
66
154
|
|
|
67
155
|
Extracted from [`inixiative/foundry`](https://github.com/inixiative/foundry),
|
|
@@ -71,3 +159,7 @@ and the inixiative bench.
|
|
|
71
159
|
## License
|
|
72
160
|
|
|
73
161
|
MIT
|
|
162
|
+
|
|
163
|
+
## Runtime
|
|
164
|
+
|
|
165
|
+
Development and new continuation captures use Bun 1.4.2, pinned in `.bun-version` and `package.json`. Historical S0 recordings retain their original Bun 1.3.14 evidence; they are not new 1.4.2 captures.
|
package/package.json
CHANGED
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions
|
|
3
|
+
"version": "0.3.0",
|
|
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
9
|
".": "./src/index.ts"
|
|
10
10
|
},
|
|
11
|
-
"files": [
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
12
14
|
"scripts": {
|
|
13
|
-
"typecheck": "tsc --noEmit"
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"check": "bun run typecheck && bun run lint && bun run test",
|
|
18
|
+
"prepublishOnly": "bun run check",
|
|
19
|
+
"measure:primed": "bun scripts/measure-primed.ts",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"prepare": "lefthook install"
|
|
14
22
|
},
|
|
15
23
|
"keywords": [
|
|
16
|
-
"claude-code",
|
|
17
|
-
"
|
|
24
|
+
"claude-code",
|
|
25
|
+
"codex",
|
|
26
|
+
"agent",
|
|
27
|
+
"session",
|
|
28
|
+
"stream",
|
|
29
|
+
"stream-json",
|
|
30
|
+
"ai",
|
|
31
|
+
"cli",
|
|
32
|
+
"sandbox",
|
|
33
|
+
"harness",
|
|
34
|
+
"eval"
|
|
18
35
|
],
|
|
19
36
|
"license": "MIT",
|
|
20
37
|
"devDependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"
|
|
38
|
+
"@biomejs/biome": "2.5.2",
|
|
39
|
+
"@inixiative/config": "^0.5.40",
|
|
40
|
+
"@types/bun": "1.4.2",
|
|
41
|
+
"lefthook": "2.1.9",
|
|
42
|
+
"typescript": "6.0.3"
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "bun@1.4.2",
|
|
45
|
+
"engines": {
|
|
46
|
+
"bun": ">=1.4.2"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/inixiative/agent-session.git"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
23
54
|
}
|
|
24
55
|
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// ClaudeAgentSdkSession — Claude through @anthropic-ai/claude-agent-sdk
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// The Agent SDK drives the same Claude Code runtime and yields the same
|
|
6
|
+
// stream-json messages. This transport feeds SDK `query()` through a
|
|
7
|
+
// process-shaped bridge, so ClaudeCodeSession's classification, admission and
|
|
8
|
+
// ownership evidence apply unchanged. The caller supplies `query` (this
|
|
9
|
+
// package keeps zero dependencies):
|
|
10
|
+
//
|
|
11
|
+
// import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
// const session = new ClaudeAgentSdkSession({ query, model: "sonnet", cwd });
|
|
13
|
+
//
|
|
14
|
+
// Launch options come from the same argv the CLI transport would use, mapped
|
|
15
|
+
// to SDK options; unmapped flags pass through `extraArgs`. Adds per-call
|
|
16
|
+
// approvals (`canUseTool`) and SDK-native interrupt. Limit polling
|
|
17
|
+
// (`get_usage`) is not exposed by the SDK Query API and is refused.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
ClaudeCodeSession,
|
|
22
|
+
type ClaudeCodeSessionConfig,
|
|
23
|
+
type PipedSubprocess,
|
|
24
|
+
} from './claude-code-session';
|
|
25
|
+
import { TRANSPORTS } from './transport';
|
|
26
|
+
|
|
27
|
+
/** The subset of the SDK's Query this transport uses. */
|
|
28
|
+
export interface ClaudeAgentSdkQuery extends AsyncIterable<unknown> {
|
|
29
|
+
interrupt(): Promise<unknown>;
|
|
30
|
+
/** Stops the underlying CLI. The bridge reports exit only after the message stream ends. */
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
export type ClaudeAgentSdkQueryFunction = (params: {
|
|
34
|
+
prompt: AsyncIterable<unknown>;
|
|
35
|
+
options: Record<string, unknown>;
|
|
36
|
+
}) => ClaudeAgentSdkQuery;
|
|
37
|
+
export type ClaudeAgentSdkCanUseTool = (
|
|
38
|
+
toolName: string,
|
|
39
|
+
input: Record<string, unknown>,
|
|
40
|
+
context: unknown,
|
|
41
|
+
) => Promise<unknown>;
|
|
42
|
+
|
|
43
|
+
export interface ClaudeAgentSdkSessionConfig extends Omit<ClaudeCodeSessionConfig, 'spawn'> {
|
|
44
|
+
/** `query` from @anthropic-ai/claude-agent-sdk. */
|
|
45
|
+
query: ClaudeAgentSdkQueryFunction;
|
|
46
|
+
/** Per-call tool approval. Defaults permissionMode to "default"; combining it with "bypassPermissions" throws. */
|
|
47
|
+
canUseTool?: ClaudeAgentSdkCanUseTool;
|
|
48
|
+
/** Use the SDK's bundled CLI instead of resolving `bin` on PATH. Default false. */
|
|
49
|
+
bundledCli?: boolean;
|
|
50
|
+
/** Additional SDK options, passed verbatim (e.g. hooks). Mapped options take precedence. */
|
|
51
|
+
sdkOptions?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SDK_OWNED = new Set(['--print', '--verbose']);
|
|
55
|
+
const SDK_OWNED_VALUE = new Set(['--input-format', '--output-format']);
|
|
56
|
+
|
|
57
|
+
/** Map a ClaudeCodeSession argv (without the binary) to SDK options. Exported for tests. */
|
|
58
|
+
export function sdkOptionsFromArgv(argv: readonly string[]): Record<string, unknown> {
|
|
59
|
+
const options: Record<string, unknown> = {};
|
|
60
|
+
const extraArgs: Record<string, string | null> = {};
|
|
61
|
+
for (let i = 0; i < argv.length; i++) {
|
|
62
|
+
const flag = argv[i]!;
|
|
63
|
+
const hasValue = i + 1 < argv.length && !argv[i + 1]!.startsWith('--');
|
|
64
|
+
const value = () => argv[++i]!;
|
|
65
|
+
if (SDK_OWNED.has(flag)) continue;
|
|
66
|
+
if (SDK_OWNED_VALUE.has(flag)) {
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
switch (flag) {
|
|
71
|
+
case '--model':
|
|
72
|
+
options.model = value();
|
|
73
|
+
break;
|
|
74
|
+
case '--max-turns':
|
|
75
|
+
options.maxTurns = Number(value());
|
|
76
|
+
break;
|
|
77
|
+
case '--permission-mode': {
|
|
78
|
+
options.permissionMode = value();
|
|
79
|
+
if (options.permissionMode === 'bypassPermissions')
|
|
80
|
+
options.allowDangerouslySkipPermissions = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case '--resume':
|
|
84
|
+
options.resume = value();
|
|
85
|
+
break;
|
|
86
|
+
case '--fork-session':
|
|
87
|
+
options.forkSession = true;
|
|
88
|
+
break;
|
|
89
|
+
case '--append-system-prompt':
|
|
90
|
+
options.systemPrompt = { type: 'preset', preset: 'claude_code', append: value() };
|
|
91
|
+
break;
|
|
92
|
+
case '--no-session-persistence':
|
|
93
|
+
options.persistSession = false;
|
|
94
|
+
break;
|
|
95
|
+
case '--strict-mcp-config':
|
|
96
|
+
options.strictMcpConfig = true;
|
|
97
|
+
break;
|
|
98
|
+
case '--mcp-config': {
|
|
99
|
+
const parsed = JSON.parse(value()) as { mcpServers?: Record<string, unknown> };
|
|
100
|
+
options.mcpServers = parsed.mcpServers ?? {};
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case '--tools': {
|
|
104
|
+
// `--tools ""` (text-only) is an empty list; the SDK spells that `tools: []`.
|
|
105
|
+
const list = argv[i + 1] !== undefined && !argv[i + 1]!.startsWith('--') ? value() : '';
|
|
106
|
+
options.tools = list ? list.split(',') : [];
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
extraArgs[flag.replace(/^--/, '')] = hasValue ? value() : null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(extraArgs).length) options.extraArgs = extraArgs;
|
|
114
|
+
return options;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function bridge(
|
|
118
|
+
config: ClaudeAgentSdkSessionConfig,
|
|
119
|
+
): NonNullable<ClaudeCodeSessionConfig['spawn']> {
|
|
120
|
+
return (cmd, { cwd, env }) => {
|
|
121
|
+
const encoder = new TextEncoder();
|
|
122
|
+
let out!: ReadableStreamDefaultController<Uint8Array>,
|
|
123
|
+
err!: ReadableStreamDefaultController<Uint8Array>;
|
|
124
|
+
let finish!: (code: number) => void,
|
|
125
|
+
finished = false;
|
|
126
|
+
const exited = new Promise<number>((resolve) => {
|
|
127
|
+
finish = resolve;
|
|
128
|
+
});
|
|
129
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
130
|
+
start(c) {
|
|
131
|
+
out = c;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const stderr = new ReadableStream<Uint8Array>({
|
|
135
|
+
start(c) {
|
|
136
|
+
err = c;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const end = (code: number) => {
|
|
140
|
+
if (finished) return;
|
|
141
|
+
finished = true;
|
|
142
|
+
try {
|
|
143
|
+
out.close();
|
|
144
|
+
} catch {
|
|
145
|
+
/* closed */
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
err.close();
|
|
149
|
+
} catch {
|
|
150
|
+
/* closed */
|
|
151
|
+
}
|
|
152
|
+
finish(code);
|
|
153
|
+
};
|
|
154
|
+
const emit = (message: unknown) => {
|
|
155
|
+
if (!finished) out.enqueue(encoder.encode(`${JSON.stringify(message)}\n`));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const inbox: unknown[] = [];
|
|
159
|
+
let wake: (() => void) | undefined,
|
|
160
|
+
inputEnded = false;
|
|
161
|
+
const prompt: AsyncIterable<unknown> = {
|
|
162
|
+
async *[Symbol.asyncIterator]() {
|
|
163
|
+
while (true) {
|
|
164
|
+
while (inbox.length) yield inbox.shift();
|
|
165
|
+
if (inputEnded) return;
|
|
166
|
+
await new Promise<void>((resolve) => {
|
|
167
|
+
wake = resolve;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const stringEnv = Object.fromEntries(
|
|
174
|
+
Object.entries(env).filter(
|
|
175
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
const options: Record<string, unknown> = {
|
|
179
|
+
...config.sdkOptions,
|
|
180
|
+
...sdkOptionsFromArgv(cmd.slice(1)),
|
|
181
|
+
cwd,
|
|
182
|
+
env: stringEnv,
|
|
183
|
+
...(config.canUseTool ? { canUseTool: config.canUseTool } : {}),
|
|
184
|
+
...(config.bundledCli ? {} : { pathToClaudeCodeExecutable: Bun.which(cmd[0]!) ?? cmd[0] }),
|
|
185
|
+
};
|
|
186
|
+
let query: ClaudeAgentSdkQuery,
|
|
187
|
+
killed = false;
|
|
188
|
+
try {
|
|
189
|
+
query = config.query({ prompt, options });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
queueMicrotask(() => {
|
|
192
|
+
err.enqueue(encoder.encode(String((error as Error).message ?? error)));
|
|
193
|
+
end(1);
|
|
194
|
+
});
|
|
195
|
+
query = { interrupt: async () => {}, close() {}, async *[Symbol.asyncIterator]() {} };
|
|
196
|
+
}
|
|
197
|
+
// `exited` resolves only when the SDK's message stream has ended, so it is evidence that the CLI stopped.
|
|
198
|
+
void (async () => {
|
|
199
|
+
try {
|
|
200
|
+
for await (const message of query) if (!killed) emit(message);
|
|
201
|
+
end(killed ? 143 : 0);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (!finished && !killed)
|
|
204
|
+
err.enqueue(encoder.encode(String((error as Error)?.message ?? error)));
|
|
205
|
+
end(killed ? 143 : 1);
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
|
|
209
|
+
const respond = (requestId: string, error?: string) =>
|
|
210
|
+
emit({
|
|
211
|
+
type: 'control_response',
|
|
212
|
+
response: error
|
|
213
|
+
? { subtype: 'error', request_id: requestId, error }
|
|
214
|
+
: { subtype: 'success', request_id: requestId, response: {} },
|
|
215
|
+
});
|
|
216
|
+
const process: PipedSubprocess = {
|
|
217
|
+
stdin: {
|
|
218
|
+
write(data: string) {
|
|
219
|
+
for (const line of data.split('\n')) {
|
|
220
|
+
if (!line.trim()) continue;
|
|
221
|
+
const message = JSON.parse(line) as Record<string, unknown>;
|
|
222
|
+
if (message.type === 'user') {
|
|
223
|
+
inbox.push({ ...message, parent_tool_use_id: null, session_id: '' });
|
|
224
|
+
wake?.();
|
|
225
|
+
wake = undefined;
|
|
226
|
+
} else if (message.type === 'control_request') {
|
|
227
|
+
const id = String(message.request_id);
|
|
228
|
+
const subtype = (message.request as Record<string, unknown> | undefined)?.subtype;
|
|
229
|
+
if (subtype === 'interrupt')
|
|
230
|
+
query.interrupt().then(
|
|
231
|
+
() => respond(id),
|
|
232
|
+
(e) => respond(id, String((e as Error)?.message ?? e)),
|
|
233
|
+
);
|
|
234
|
+
else
|
|
235
|
+
queueMicrotask(() =>
|
|
236
|
+
respond(
|
|
237
|
+
id,
|
|
238
|
+
`Control request ${String(subtype)} is not exposed by the Agent SDK transport`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
flush() {},
|
|
245
|
+
end() {
|
|
246
|
+
inputEnded = true;
|
|
247
|
+
wake?.();
|
|
248
|
+
wake = undefined;
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
stdout,
|
|
252
|
+
stderr,
|
|
253
|
+
exited,
|
|
254
|
+
kill() {
|
|
255
|
+
if (killed) return;
|
|
256
|
+
killed = true;
|
|
257
|
+
inputEnded = true;
|
|
258
|
+
wake?.();
|
|
259
|
+
wake = undefined;
|
|
260
|
+
try {
|
|
261
|
+
query.close();
|
|
262
|
+
} catch {
|
|
263
|
+
/* already closed */
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
return process;
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class ClaudeAgentSdkSession extends ClaudeCodeSession {
|
|
272
|
+
override get transport() {
|
|
273
|
+
return TRANSPORTS['claude-agent-sdk'];
|
|
274
|
+
}
|
|
275
|
+
private readonly _sdk: ClaudeAgentSdkSessionConfig;
|
|
276
|
+
|
|
277
|
+
constructor(config: ClaudeAgentSdkSessionConfig) {
|
|
278
|
+
if (typeof config?.query !== 'function')
|
|
279
|
+
throw Error('ClaudeAgentSdkSession requires `query` from @anthropic-ai/claude-agent-sdk');
|
|
280
|
+
if (config.canUseTool && config.permissionMode === 'bypassPermissions')
|
|
281
|
+
throw Error(
|
|
282
|
+
'ClaudeAgentSdkSession: canUseTool is never consulted under permissionMode "bypassPermissions"; pass another mode or drop canUseTool',
|
|
283
|
+
);
|
|
284
|
+
const {
|
|
285
|
+
query: _query,
|
|
286
|
+
canUseTool: _canUseTool,
|
|
287
|
+
bundledCli: _bundled,
|
|
288
|
+
sdkOptions: _options,
|
|
289
|
+
...session
|
|
290
|
+
} = config;
|
|
291
|
+
super({
|
|
292
|
+
...session,
|
|
293
|
+
...(config.canUseTool ? { permissionMode: config.permissionMode ?? 'default' } : {}),
|
|
294
|
+
spawn: bridge(config),
|
|
295
|
+
});
|
|
296
|
+
this._sdk = config;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** get_usage is not reachable through the SDK Query API. */
|
|
300
|
+
override async readLimits(): Promise<never> {
|
|
301
|
+
throw Error(
|
|
302
|
+
'Limit polling is not exposed by the Agent SDK transport; use the claude-cli transport or rate_limit events',
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected override _construct(config: ClaudeCodeSessionConfig): ClaudeCodeSession {
|
|
307
|
+
const { spawn: _spawn, ...rest } = config;
|
|
308
|
+
return new ClaudeAgentSdkSession({
|
|
309
|
+
...rest,
|
|
310
|
+
query: this._sdk.query,
|
|
311
|
+
canUseTool: this._sdk.canUseTool,
|
|
312
|
+
bundledCli: this._sdk.bundledCli,
|
|
313
|
+
sdkOptions: this._sdk.sdkOptions,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|