@inixiative/agent-session 0.6.5 → 0.6.7
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 +45 -0
- package/package.json +2 -2
- package/src/claude-code-session.ts +211 -4
- package/src/claude-in-session.ts +1369 -0
- package/src/codex-session.ts +17 -3
- package/src/harness-session.ts +37 -0
- package/src/index.ts +10 -0
- package/src/transport.ts +9 -0
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ session.kill();
|
|
|
44
44
|
`ANTHROPIC_API_KEY` and authenticates via the CLI's subscription / an
|
|
45
45
|
`CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` — works headless and in
|
|
46
46
|
containers.
|
|
47
|
+
- **Images with a turn.** `send(text, { images: [{ mediaType, data }] })` (png, jpeg, gif or
|
|
48
|
+
webp, base64) where the transport declares `capabilities.images`: Claude gets base64 `image`
|
|
49
|
+
blocks in the same user message, Codex `image` inputs with a `data:` URL. Anything else is
|
|
50
|
+
refused before a byte is written.
|
|
47
51
|
- **fork / resume / interrupt** and pre-send hooks (rewrite the outgoing
|
|
48
52
|
message — e.g. inject just-in-time context per turn).
|
|
49
53
|
- **Transports with declared capabilities.** How a model is reached is a
|
|
@@ -164,6 +168,47 @@ a pre-spawned spare. Measured on a ChatGPT login: warm Codex decisions 2.45 s
|
|
|
164
168
|
median vs 3.9 s for `codex exec` per decision, with 34% fewer input tokens; warm
|
|
165
169
|
Claude (haiku) decisions 1.95 s with the whole primed prefix read from cache.
|
|
166
170
|
|
|
171
|
+
### In the main session's own process (Claude)
|
|
172
|
+
|
|
173
|
+
`ClaudeInSessionDecisions` holds decision roles as subagents of a main
|
|
174
|
+
`ClaudeCodeSession`, so a thread and all its decision roles are one process:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const decisions = new ClaudeInSessionDecisions({ model: "claude-haiku-5-5", effort: "low" });
|
|
178
|
+
const session = new ClaudeCodeSession({ model: "claude-opus-5-5", cwd, toolGate: true });
|
|
179
|
+
session.extend(decisions); // before start(): agents, an SDK MCP server, a PreToolUse claim
|
|
180
|
+
await session.start(); // one internal setup turn starts the root agent
|
|
181
|
+
decisions.prestart(threadId, roles.map((spec) => ({ spec, standby })));
|
|
182
|
+
await decisions.ready();
|
|
183
|
+
const result = await decisions.decide(spec, { input, standby });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **Shape.** The setup turn asks the main model to start `foundry-root` in the
|
|
187
|
+
background. The root never ends: it starts dispatcher generations
|
|
188
|
+
(`foundry-dispatch`), which start members (`foundry-warden`), all in the
|
|
189
|
+
background, through a `next` tool the host holds open. A member holds the
|
|
190
|
+
host's `moment` tool until a decision answers it, and answers through
|
|
191
|
+
`answer`. After setup, no member activity needs a main-model turn.
|
|
192
|
+
- **Pinned input.** Every spawn passes the session's PreToolUse callback,
|
|
193
|
+
which replaces its prompt with the composed text, so agent definitions are
|
|
194
|
+
generic and an edited role reaches the next spare without a relaunch.
|
|
195
|
+
Spawns of these agent types from anywhere else are denied; the main agent's
|
|
196
|
+
own subagents go to the tool gate untouched.
|
|
197
|
+
- **Fresh per call.** Each member serves one decision. A used one is replaced
|
|
198
|
+
at once; a role with no held spare starts a member with its input inline.
|
|
199
|
+
- **Recycle.** A dispatcher's context grows ~700 tokens per member started;
|
|
200
|
+
past `recycleTokens` (100k) a new generation takes over and the old one
|
|
201
|
+
ends into the root once empty.
|
|
202
|
+
- **Orphans.** A nested agent whose parent ended reports its own end to the
|
|
203
|
+
main thread, which wakes the main model (Claude Code 2.1.300). Members of a
|
|
204
|
+
dispatcher that ended unasked are therefore never retired or stopped, and an
|
|
205
|
+
orphan's `answer` call is held for the session's life. Past `maxOrphans`
|
|
206
|
+
(40) the host stops serving and its caller falls back.
|
|
207
|
+
- **Limits.** Claude Code runs 20 subagents at once by default; the launch
|
|
208
|
+
raises `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` (`maxSubagents`, 96). The
|
|
209
|
+
session declares `perTaskStopAffordance`, so the user's Stop spares
|
|
210
|
+
background agents.
|
|
211
|
+
|
|
167
212
|
## Pools, routing and continuity
|
|
168
213
|
|
|
169
214
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@biomejs/biome": "2.5.2",
|
|
43
|
-
"@inixiative/config": "^0.5.
|
|
43
|
+
"@inixiative/config": "^0.5.105",
|
|
44
44
|
"@types/bun": "1.4.2",
|
|
45
45
|
"lefthook": "2.1.9",
|
|
46
46
|
"typescript": "6.0.3"
|
|
@@ -54,6 +54,7 @@ import type {
|
|
|
54
54
|
SessionArtifact,
|
|
55
55
|
SessionEvent,
|
|
56
56
|
SessionEventHandler,
|
|
57
|
+
SessionImage,
|
|
57
58
|
SessionResult,
|
|
58
59
|
SessionSendOptions,
|
|
59
60
|
SessionTokens,
|
|
@@ -62,6 +63,7 @@ import type {
|
|
|
62
63
|
ToolGate,
|
|
63
64
|
ToolRequest,
|
|
64
65
|
} from './harness-session';
|
|
66
|
+
import { sendableImages } from './harness-session';
|
|
65
67
|
|
|
66
68
|
// Re-export types so existing import paths keep working
|
|
67
69
|
export type {
|
|
@@ -143,6 +145,66 @@ export interface ClaudeCodeSessionConfig {
|
|
|
143
145
|
) => PipedSubprocess;
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Extension: work a host runs inside this session's own process (ClaudeInSessionDecisions)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** A custom agent definition (`--agents`), as Claude Code reads it. */
|
|
153
|
+
export interface ClaudeAgentDefinition {
|
|
154
|
+
readonly description: string;
|
|
155
|
+
readonly prompt: string;
|
|
156
|
+
readonly tools: readonly string[];
|
|
157
|
+
readonly model?: string;
|
|
158
|
+
readonly effort?: string;
|
|
159
|
+
readonly permissionMode?: string;
|
|
160
|
+
readonly omitClaudeMd?: boolean;
|
|
161
|
+
readonly background?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What the session offers an extension once its process is up. */
|
|
165
|
+
export interface ClaudeSessionHost {
|
|
166
|
+
/** A control request to the CLI (e.g. `stop_task`). */
|
|
167
|
+
control(request: Record<string, unknown>, timeoutMs: number): Promise<Record<string, unknown>>;
|
|
168
|
+
/** Answer an MCP message the extension was handed. */
|
|
169
|
+
mcpReply(requestId: string, message: unknown): void;
|
|
170
|
+
/**
|
|
171
|
+
* A turn of the extension's own, never a caller's: queued like any send, and nothing of it is
|
|
172
|
+
* attributed to a caller's admission. No deadline: its native outcome must settle.
|
|
173
|
+
*/
|
|
174
|
+
sendInternal(message: string): Promise<SessionResult>;
|
|
175
|
+
alive(): boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Work that runs inside this session's process (agents, an SDK MCP server, a PreToolUse claim on
|
|
180
|
+
* its own tool calls), never through vendor hook files. Attached before `start()`.
|
|
181
|
+
*/
|
|
182
|
+
export interface ClaudeSessionExtension {
|
|
183
|
+
/** Agent definitions added to the launch. */
|
|
184
|
+
readonly agents: Readonly<Record<string, ClaudeAgentDefinition>>;
|
|
185
|
+
/** SDK MCP servers it answers (declared in `initialize`). */
|
|
186
|
+
readonly mcpServers: readonly string[];
|
|
187
|
+
/** Environment its agents need on the launch (merged over the session's own). */
|
|
188
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
189
|
+
/** The process runs (before `initialize`): it may answer MCP messages from now on. */
|
|
190
|
+
attach(host: ClaudeSessionHost): void;
|
|
191
|
+
/** `initialize` was answered: it may write turns and control requests. */
|
|
192
|
+
started(): void;
|
|
193
|
+
/** An MCP message for one of its servers; answered through `host.mcpReply`. */
|
|
194
|
+
mcp(server: string, requestId: string, message: Record<string, unknown>): void;
|
|
195
|
+
/**
|
|
196
|
+
* A PreToolUse callback: the hook output when the call is its own (its agents' calls, a spawn of
|
|
197
|
+
* one of its agent types), else undefined and the tool gate decides.
|
|
198
|
+
*/
|
|
199
|
+
preToolUse(input: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
200
|
+
/** A permission ask (`can_use_tool`): its answer when the asking agent is its own, else undefined. */
|
|
201
|
+
permission(request: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
202
|
+
/** A native frame: true when it is its own (never emitted, never part of an admission). */
|
|
203
|
+
frame(raw: Record<string, unknown>): boolean;
|
|
204
|
+
/** The process ended or was killed. */
|
|
205
|
+
detach(reason: string): void;
|
|
206
|
+
}
|
|
207
|
+
|
|
146
208
|
// ---------------------------------------------------------------------------
|
|
147
209
|
// Internal turn queue entry
|
|
148
210
|
// ---------------------------------------------------------------------------
|
|
@@ -151,6 +213,7 @@ interface QueuedTurn {
|
|
|
151
213
|
onAdmission?: SessionSendOptions['onAdmission'];
|
|
152
214
|
evidence: TurnState;
|
|
153
215
|
message: string;
|
|
216
|
+
images: readonly SessionImage[];
|
|
154
217
|
timeout: number;
|
|
155
218
|
resolve: (result: SessionResult) => void;
|
|
156
219
|
reject: (error: Error) => void;
|
|
@@ -273,6 +336,9 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
273
336
|
private _persistSession: boolean;
|
|
274
337
|
private _toolGate: boolean;
|
|
275
338
|
private _gates = new ToolGates();
|
|
339
|
+
private _extension?: ClaudeSessionExtension;
|
|
340
|
+
/** The private directory holding the extension's agent definitions while a process runs. */
|
|
341
|
+
private _agentsDir?: string;
|
|
276
342
|
/** Inbound control requests awaiting a gate decision, by request id. */
|
|
277
343
|
private _inbound = new Map<string, AbortController>();
|
|
278
344
|
private readonly _hookCallbackId = `agent-session-pre-tool-${crypto.randomUUID()}`;
|
|
@@ -410,6 +476,17 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
410
476
|
return this._gates.add(gate);
|
|
411
477
|
}
|
|
412
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Run an extension inside this session's process (ClaudeInSessionDecisions). Before `start()`
|
|
481
|
+
* only, once, and never on a text-only session (it needs tools and an MCP server).
|
|
482
|
+
*/
|
|
483
|
+
extend(extension: ClaudeSessionExtension): void {
|
|
484
|
+
if (this._proc) throw new Error('Extensions attach before start()');
|
|
485
|
+
if (this._extension) throw new Error('Session already has an extension');
|
|
486
|
+
if (this._textOnly) throw new Error('A text-only session cannot host an extension');
|
|
487
|
+
this._extension = extension;
|
|
488
|
+
}
|
|
489
|
+
|
|
413
490
|
onBeforeSend(hook: BeforeSendHook): () => void {
|
|
414
491
|
this._beforeSendHooks.push(hook);
|
|
415
492
|
return () => {
|
|
@@ -451,6 +528,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
451
528
|
const env: Record<string, string | undefined> = {
|
|
452
529
|
...process.env,
|
|
453
530
|
...this._env,
|
|
531
|
+
...this._extension?.env,
|
|
454
532
|
DISABLE_AUTOUPDATER: '1',
|
|
455
533
|
};
|
|
456
534
|
delete env.ANTHROPIC_API_KEY;
|
|
@@ -471,6 +549,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
471
549
|
}
|
|
472
550
|
} catch (error) {
|
|
473
551
|
this._removeBaseContext();
|
|
552
|
+
this._removeAgents();
|
|
474
553
|
throw error;
|
|
475
554
|
}
|
|
476
555
|
|
|
@@ -486,9 +565,13 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
486
565
|
// Monitor process exit for cleanup
|
|
487
566
|
// The process read its base context file at startup; it goes when the process does.
|
|
488
567
|
const baseDir = this._baseContextDir;
|
|
568
|
+
const agentsDir = this._agentsDir;
|
|
489
569
|
proc.exited.then(async (code) => {
|
|
490
570
|
this._removeBaseContext(baseDir);
|
|
491
|
-
|
|
571
|
+
this._removeAgents(agentsDir);
|
|
572
|
+
if (this._proc !== proc) return;
|
|
573
|
+
this._detachExtension('process exited');
|
|
574
|
+
if (!this._alive) return;
|
|
492
575
|
this._alive = false;
|
|
493
576
|
await stdoutDone; // Drain buffered terminal evidence before interpreting process exit.
|
|
494
577
|
const errMsg = this._stderr.trim()
|
|
@@ -501,20 +584,58 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
501
584
|
});
|
|
502
585
|
|
|
503
586
|
// The PreToolUse callback is registered before any turn is written, so it covers the first call.
|
|
504
|
-
|
|
587
|
+
// An extension's agents run their own tools: it claims them through the same callback.
|
|
588
|
+
const extension = this._extension;
|
|
589
|
+
// The extension can answer from the moment the process runs: the CLI connects its SDK MCP
|
|
590
|
+
// server while `initialize` is still in flight, and an unanswered connect costs a 30 s retry.
|
|
591
|
+
if (extension)
|
|
592
|
+
extension.attach({
|
|
593
|
+
control: (request, timeoutMs) => this._controlRequest(request, timeoutMs),
|
|
594
|
+
mcpReply: (requestId, message) => {
|
|
595
|
+
if (this._proc === proc && this._alive)
|
|
596
|
+
this._writeControlResponse(proc, requestId, { mcp_response: message });
|
|
597
|
+
},
|
|
598
|
+
sendInternal: (message) => this.send(message, { timeout: 0 }),
|
|
599
|
+
alive: () => this._proc === proc && this._alive,
|
|
600
|
+
});
|
|
601
|
+
if (this._toolGate || extension) {
|
|
505
602
|
try {
|
|
506
603
|
await this._controlRequest(
|
|
507
604
|
{
|
|
508
605
|
subtype: 'initialize',
|
|
509
606
|
hooks: { PreToolUse: [{ matcher: '.*', hookCallbackIds: [this._hookCallbackId] }] },
|
|
607
|
+
...(extension
|
|
608
|
+
? {
|
|
609
|
+
sdkMcpServers: [...extension.mcpServers],
|
|
610
|
+
// The user's Stop aborts the turn only; background agents (the extension's) survive.
|
|
611
|
+
perTaskStopAffordance: true,
|
|
612
|
+
}
|
|
613
|
+
: {}),
|
|
510
614
|
},
|
|
511
615
|
30_000,
|
|
512
616
|
);
|
|
513
617
|
} catch (error) {
|
|
514
618
|
this.kill();
|
|
515
|
-
throw new Error(
|
|
619
|
+
throw new Error(
|
|
620
|
+
`${this._toolGate ? 'Tool gate' : 'Extension'} registration failed: ${(error as Error).message}`,
|
|
621
|
+
);
|
|
516
622
|
}
|
|
517
623
|
}
|
|
624
|
+
if (extension && this._proc === proc && this._alive) {
|
|
625
|
+
try {
|
|
626
|
+
extension.started();
|
|
627
|
+
} catch {
|
|
628
|
+
/* an extension's failure never touches the session */
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private _detachExtension(reason: string): void {
|
|
634
|
+
try {
|
|
635
|
+
this._extension?.detach(reason);
|
|
636
|
+
} catch {
|
|
637
|
+
/* an extension's failure never touches the session */
|
|
638
|
+
}
|
|
518
639
|
}
|
|
519
640
|
|
|
520
641
|
private _rejectControl(error: Error): void {
|
|
@@ -553,6 +674,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
553
674
|
if (!this._alive) throw new Error('Session ended');
|
|
554
675
|
|
|
555
676
|
const timeout = opts?.timeout ?? this._defaultTimeout;
|
|
677
|
+
const images = sendableImages(this.transport, opts?.images);
|
|
556
678
|
|
|
557
679
|
// Compose pre-send hooks in registration order. Each sees the previous
|
|
558
680
|
// hook's output. Errors in a hook reject the send() — callers should
|
|
@@ -568,6 +690,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
568
690
|
const turn: QueuedTurn = {
|
|
569
691
|
evidence,
|
|
570
692
|
message: transformed,
|
|
693
|
+
images,
|
|
571
694
|
timeout,
|
|
572
695
|
resolve,
|
|
573
696
|
reject,
|
|
@@ -752,6 +875,8 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
752
875
|
}
|
|
753
876
|
this._proc = null;
|
|
754
877
|
this._removeBaseContext();
|
|
878
|
+
this._removeAgents();
|
|
879
|
+
this._detachExtension('killed');
|
|
755
880
|
|
|
756
881
|
this._emit({ kind: 'session_end', timestamp: Date.now() });
|
|
757
882
|
}
|
|
@@ -817,11 +942,18 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
817
942
|
// Wire format validated empirically against claude 2.1.114:
|
|
818
943
|
// {type:"user", message:{role,content:[{type:"text",text}]}}
|
|
819
944
|
// Alternative shapes ({type:"user_message"}, {role,content}) are silently dropped.
|
|
945
|
+
// Images are Messages API base64 image blocks, ahead of the text they accompany.
|
|
820
946
|
const payload = `${JSON.stringify({
|
|
821
947
|
type: 'user',
|
|
822
948
|
message: {
|
|
823
949
|
role: 'user',
|
|
824
|
-
content: [
|
|
950
|
+
content: [
|
|
951
|
+
...turn.images.map((image) => ({
|
|
952
|
+
type: 'image',
|
|
953
|
+
source: { type: 'base64', media_type: image.mediaType, data: image.data },
|
|
954
|
+
})),
|
|
955
|
+
{ type: 'text', text: turn.message },
|
|
956
|
+
],
|
|
825
957
|
},
|
|
826
958
|
})}\n`;
|
|
827
959
|
try {
|
|
@@ -1007,6 +1139,8 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1007
1139
|
if (raw.type === 'control_request') {
|
|
1008
1140
|
const id = typeof raw.request_id === 'string' ? raw.request_id : undefined;
|
|
1009
1141
|
const request = raw.request as Record<string, unknown> | undefined;
|
|
1142
|
+
if (id && this._proc && this._extension && this._extensionRequest(this._proc, id, request))
|
|
1143
|
+
return;
|
|
1010
1144
|
const toolRequest =
|
|
1011
1145
|
id && this._toolGate && this._proc ? this._toolRequestFrom(request) : undefined;
|
|
1012
1146
|
if (id && toolRequest) {
|
|
@@ -1107,6 +1241,17 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1107
1241
|
return;
|
|
1108
1242
|
}
|
|
1109
1243
|
|
|
1244
|
+
// An extension's own agents (their frames and task lifecycle) are its alone.
|
|
1245
|
+
if (this._extension) {
|
|
1246
|
+
let own = false;
|
|
1247
|
+
try {
|
|
1248
|
+
own = this._extension.frame(raw);
|
|
1249
|
+
} catch {
|
|
1250
|
+
/* an extension's failure never touches the session */
|
|
1251
|
+
}
|
|
1252
|
+
if (own) return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1110
1255
|
// A subagent's frames and lifecycle are never part of an admission, whichever turn is in flight
|
|
1111
1256
|
// (a background subagent outlives the turn that started it): they reach handlers tagged
|
|
1112
1257
|
// `subagent`, with the spawning call in `parentToolUseId`.
|
|
@@ -1258,6 +1403,52 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1258
1403
|
};
|
|
1259
1404
|
}
|
|
1260
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* A control request the extension answers: an MCP message for its servers, or a PreToolUse
|
|
1408
|
+
* callback / permission ask that belongs to its agents or spawns one of its agent types. With no
|
|
1409
|
+
* tool gate, our callback's other calls get no opinion (the session's permission mode decides).
|
|
1410
|
+
*/
|
|
1411
|
+
private _extensionRequest(
|
|
1412
|
+
proc: PipedSubprocess,
|
|
1413
|
+
id: string,
|
|
1414
|
+
request: Record<string, unknown> | undefined,
|
|
1415
|
+
): boolean {
|
|
1416
|
+
const extension = this._extension!;
|
|
1417
|
+
try {
|
|
1418
|
+
if (
|
|
1419
|
+
request?.subtype === 'mcp_message' &&
|
|
1420
|
+
typeof request.server_name === 'string' &&
|
|
1421
|
+
extension.mcpServers.includes(request.server_name)
|
|
1422
|
+
) {
|
|
1423
|
+
extension.mcp(request.server_name, id, (request.message ?? {}) as Record<string, unknown>);
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
if (this._ownHookCallback(request)) {
|
|
1427
|
+
const input = request!.input as Record<string, unknown> | undefined;
|
|
1428
|
+
const output = input ? extension.preToolUse(input) : undefined;
|
|
1429
|
+
if (output) {
|
|
1430
|
+
this._writeControlResponse(proc, id, { hookSpecificOutput: output });
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
if (!this._toolGate) {
|
|
1434
|
+
this._writeControlResponse(proc, id, {});
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
if (request?.subtype === 'can_use_tool') {
|
|
1440
|
+
const answer = extension.permission(request);
|
|
1441
|
+
if (answer) {
|
|
1442
|
+
this._writeControlResponse(proc, id, answer);
|
|
1443
|
+
return true;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} catch {
|
|
1447
|
+
/* an extension's failure falls through to the session's own handling */
|
|
1448
|
+
}
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1261
1452
|
private _ownHookCallback(request: Record<string, unknown> | undefined): boolean {
|
|
1262
1453
|
return request?.subtype === 'hook_callback' && request.callback_id === this._hookCallbackId;
|
|
1263
1454
|
}
|
|
@@ -1376,10 +1567,26 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1376
1567
|
if (this._baseContext) {
|
|
1377
1568
|
args.push('--append-system-prompt-file', this._writeBaseContext(this._baseContext));
|
|
1378
1569
|
}
|
|
1570
|
+
// An extension's agents, from a private file; its MCP servers are declared in `initialize`.
|
|
1571
|
+
if (this._extension) args.push('--agents', this._writeAgents(this._extension.agents));
|
|
1379
1572
|
|
|
1380
1573
|
return args;
|
|
1381
1574
|
}
|
|
1382
1575
|
|
|
1576
|
+
private _writeAgents(agents: ClaudeSessionExtension['agents']): string {
|
|
1577
|
+
this._removeAgents();
|
|
1578
|
+
this._agentsDir = mkdtempSync(join(tmpdir(), `${BASE_CONTEXT_PREFIX}${process.pid}-agents-`));
|
|
1579
|
+
const file = join(this._agentsDir, 'agents.json');
|
|
1580
|
+
writeFileSync(file, JSON.stringify(agents), { mode: 0o600 });
|
|
1581
|
+
return file;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private _removeAgents(dir = this._agentsDir): void {
|
|
1585
|
+
if (!dir) return;
|
|
1586
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1587
|
+
if (dir === this._agentsDir) this._agentsDir = undefined;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1383
1590
|
/** The base context as a private file: argv carries its path, never its text. */
|
|
1384
1591
|
private _writeBaseContext(text: string): string {
|
|
1385
1592
|
this._removeBaseContext();
|