@inixiative/agent-session 0.5.2 → 0.6.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 +45 -13
- package/package.json +3 -3
- package/src/claude-agent-sdk-session.ts +136 -3
- package/src/claude-code-session.ts +275 -14
- package/src/claude-primed.ts +153 -35
- package/src/codex-session.ts +339 -607
- package/src/harness-session.ts +52 -2
- package/src/index.ts +6 -4
- package/src/pool.ts +1 -1
- package/src/primed.ts +1 -1
- package/src/tool-gate.ts +61 -0
- package/src/transport.ts +11 -38
- package/src/transports.ts +1 -4
package/src/harness-session.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// HarnessSession is stateful: it owns a running process, tracks turns,
|
|
11
11
|
// captures every event for Oracle, and supports fork/interrupt.
|
|
12
12
|
//
|
|
13
|
-
// Implementations: ClaudeCodeSession,
|
|
13
|
+
// Implementations: ClaudeCodeSession, CodexAppServerSession,
|
|
14
14
|
// ClaudeAgentSdkSession. How a model is reached is the session's transport
|
|
15
15
|
// (see transport.ts); every transport implements this one contract.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -232,6 +232,47 @@ export interface SessionSendOptions {
|
|
|
232
232
|
onAdmission?: (attempt: SessionAttempt) => void | Promise<void>;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Tool gate — per-call decisions from the runtime's native approval channel
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
export interface ToolRequest {
|
|
240
|
+
/**
|
|
241
|
+
* `pre-use` (Claude only; Codex has no pre-use stage): before every tool call, in every
|
|
242
|
+
* permission mode (bypass included). Only a deny counts; an allow leaves the runtime's own mode
|
|
243
|
+
* to decide. `permission`: the runtime's own mode would ask the user; the decision is final.
|
|
244
|
+
*/
|
|
245
|
+
readonly stage: 'pre-use' | 'permission';
|
|
246
|
+
readonly toolName: string;
|
|
247
|
+
/** `mcp__<server>__<tool>` split, as tool_use events carry it. */
|
|
248
|
+
readonly toolServer?: string;
|
|
249
|
+
readonly toolMethod?: string;
|
|
250
|
+
readonly toolInput: Readonly<Record<string, unknown>>;
|
|
251
|
+
/** Native tool_use id (Claude) or item id (Codex). */
|
|
252
|
+
readonly callId?: string;
|
|
253
|
+
/** The in-flight admission, when one is attributable. */
|
|
254
|
+
readonly admissionId?: string;
|
|
255
|
+
/** The runtime's own description of, or reason for, the call. */
|
|
256
|
+
readonly description?: string;
|
|
257
|
+
/** Claude `can_use_tool` decision_reason: why the runtime's mode escalated to an ask. */
|
|
258
|
+
readonly decisionReason?: string;
|
|
259
|
+
/** Claude `can_use_tool` blocked_path. */
|
|
260
|
+
readonly blockedPath?: string;
|
|
261
|
+
/** Claude subagent id (`agent_id`) when a subagent, not the main thread, makes the call. */
|
|
262
|
+
readonly agentId?: string;
|
|
263
|
+
/**
|
|
264
|
+
* Aborted when the runtime withdraws the request or the session ends; a later decision is
|
|
265
|
+
* discarded. A turn timeout does not withdraw a pending Claude ask; interruptNative() does
|
|
266
|
+
* (the CLI sends control_cancel_request).
|
|
267
|
+
*/
|
|
268
|
+
readonly signal?: AbortSignal;
|
|
269
|
+
}
|
|
270
|
+
export type ToolDecision =
|
|
271
|
+
| { readonly behavior: 'allow' }
|
|
272
|
+
| { readonly behavior: 'deny'; readonly message: string };
|
|
273
|
+
/** Gates compose in registration order: the first deny wins; a thrown or rejected gate denies. */
|
|
274
|
+
export type ToolGate = (request: ToolRequest) => ToolDecision | Promise<ToolDecision>;
|
|
275
|
+
|
|
235
276
|
// ---------------------------------------------------------------------------
|
|
236
277
|
// HarnessSession interface
|
|
237
278
|
// ---------------------------------------------------------------------------
|
|
@@ -277,7 +318,8 @@ export interface HarnessSession {
|
|
|
277
318
|
|
|
278
319
|
/**
|
|
279
320
|
* Fork: create a new (unstarted) session branching from current state.
|
|
280
|
-
* Caller must call start() on the forked session.
|
|
321
|
+
* Caller must call start() on the forked session. A fork starts with a snapshot of the
|
|
322
|
+
* parent's tool gates; gates registered or unregistered afterwards on either side stay local.
|
|
281
323
|
*/
|
|
282
324
|
fork(opts?: { cwd?: string; baseContext?: string }): HarnessSession;
|
|
283
325
|
|
|
@@ -297,6 +339,14 @@ export interface HarnessSession {
|
|
|
297
339
|
*/
|
|
298
340
|
onBeforeSend(hook: BeforeSendHook): () => void;
|
|
299
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Register a tool gate. Present where the transport declares `approvals: "callback"`; the
|
|
344
|
+
* session must be launched for callbacks (Claude: `toolGate: true`; Codex app-server: an
|
|
345
|
+
* `approvalPolicy` other than `never`), otherwise this throws. Forks copy the gates registered
|
|
346
|
+
* at fork() time. Returns an unregister function.
|
|
347
|
+
*/
|
|
348
|
+
onToolRequest?(gate: ToolGate): () => void;
|
|
349
|
+
|
|
300
350
|
/**
|
|
301
351
|
* Mid-turn push: out-of-band signal injected into an in-flight turn.
|
|
302
352
|
* Used by guards and Herald to deliver urgent feedback. Best-effort —
|
package/src/index.ts
CHANGED
|
@@ -27,14 +27,16 @@ export {
|
|
|
27
27
|
codexTokens,
|
|
28
28
|
} from './codex-primed';
|
|
29
29
|
|
|
30
|
-
// Codex CLI adapter
|
|
31
|
-
// mcp-server variant; CodexAppServerSession is the app-server one.
|
|
30
|
+
// Codex CLI adapter: one persistent `codex app-server` JSON-RPC session.
|
|
32
31
|
export {
|
|
32
|
+
type CodexApprovalPolicy,
|
|
33
33
|
CodexAppServerSession,
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
type CodexFileChange,
|
|
35
|
+
type CodexSandbox,
|
|
36
36
|
type CodexSessionConfig,
|
|
37
37
|
type CodexSpawn,
|
|
38
|
+
codexPermissions,
|
|
39
|
+
type HostPermissionMode,
|
|
38
40
|
} from './codex-session';
|
|
39
41
|
// The provider-agnostic interface + event taxonomy.
|
|
40
42
|
export * from './harness-session';
|
package/src/pool.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { TRANSPORTS, type TransportKind } from './transport';
|
|
32
32
|
import { createSession } from './transports';
|
|
33
33
|
|
|
34
|
-
export type PooledTransport = 'claude-cli' | 'codex-
|
|
34
|
+
export type PooledTransport = 'claude-cli' | 'codex-app-server' | 'claude-agent-sdk';
|
|
35
35
|
|
|
36
36
|
export interface InstanceConfig {
|
|
37
37
|
readonly id: string;
|
package/src/primed.ts
CHANGED
|
@@ -262,7 +262,7 @@ export class Slots {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
/** Per-key serialization: a key's
|
|
265
|
+
/** Per-key serialization: a key's tasks run one at a time, in order. */
|
|
266
266
|
export class KeyedQueue {
|
|
267
267
|
private _tails = new Map<string, Promise<void>>();
|
|
268
268
|
run<T>(key: string, task: () => Promise<T>): Promise<T> {
|
package/src/tool-gate.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ToolDecision, ToolGate, ToolRequest } from './harness-session';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A session's registered tool gates. They compose in registration order and the first deny
|
|
5
|
+
* wins; a gate that throws or rejects denies. `pre-use` with no deny is no decision (the
|
|
6
|
+
* runtime's own mode decides); `permission` with no gate registered is denied.
|
|
7
|
+
*/
|
|
8
|
+
export class ToolGates {
|
|
9
|
+
private readonly _gates: ToolGate[] = [];
|
|
10
|
+
|
|
11
|
+
get size(): number {
|
|
12
|
+
return this._gates.length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
add(gate: ToolGate): () => void {
|
|
16
|
+
this._gates.push(gate);
|
|
17
|
+
return () => {
|
|
18
|
+
const index = this._gates.indexOf(gate);
|
|
19
|
+
if (index !== -1) this._gates.splice(index, 1);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The gates in order, for a sibling session (fork). */
|
|
24
|
+
list(): readonly ToolGate[] {
|
|
25
|
+
return [...this._gates];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Never rejects. Undefined means no decision: only a `pre-use` request without a deny. */
|
|
29
|
+
async decide(request: ToolRequest): Promise<ToolDecision | undefined> {
|
|
30
|
+
const gates = [...this._gates];
|
|
31
|
+
if (!gates.length)
|
|
32
|
+
return request.stage === 'permission'
|
|
33
|
+
? { behavior: 'deny', message: 'No approval handler' }
|
|
34
|
+
: undefined;
|
|
35
|
+
for (const gate of gates) {
|
|
36
|
+
let decision: ToolDecision;
|
|
37
|
+
try {
|
|
38
|
+
decision = await gate(request);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
behavior: 'deny',
|
|
42
|
+
message: `Tool gate failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (decision?.behavior === 'deny')
|
|
46
|
+
return { behavior: 'deny', message: String(decision.message ?? 'Denied') };
|
|
47
|
+
if (decision?.behavior !== 'allow')
|
|
48
|
+
return { behavior: 'deny', message: 'Tool gate failed: malformed decision' };
|
|
49
|
+
}
|
|
50
|
+
return request.stage === 'permission' ? { behavior: 'allow' } : undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `mcp__<server>__<tool>` split into its server and method. */
|
|
55
|
+
export function mcpToolParts(toolName: string): { toolServer?: string; toolMethod?: string } {
|
|
56
|
+
if (!toolName.startsWith('mcp__')) return {};
|
|
57
|
+
const rest = toolName.slice(5);
|
|
58
|
+
const split = rest.indexOf('__');
|
|
59
|
+
if (split <= 0 || split + 2 >= rest.length) return {};
|
|
60
|
+
return { toolServer: rest.slice(0, split), toolMethod: rest.slice(split + 2) };
|
|
61
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -13,28 +13,22 @@
|
|
|
13
13
|
// is declared absent, with a note.
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
export type TransportKind =
|
|
17
|
-
| 'claude-cli'
|
|
18
|
-
| 'codex-mcp'
|
|
19
|
-
| 'codex-app-server'
|
|
20
|
-
| 'claude-agent-sdk'
|
|
21
|
-
| 'acp'
|
|
22
|
-
| 'api';
|
|
16
|
+
export type TransportKind = 'claude-cli' | 'codex-app-server' | 'claude-agent-sdk' | 'acp' | 'api';
|
|
23
17
|
|
|
24
18
|
export type TransportRuntime = 'claude' | 'codex' | 'external';
|
|
25
19
|
|
|
26
20
|
export interface TransportCapabilities {
|
|
27
|
-
/** Continue a thread by native ID in a new process
|
|
28
|
-
readonly resume: 'native' | '
|
|
29
|
-
/** `native`: a new branch with its own ID
|
|
30
|
-
readonly fork: 'native' | '
|
|
21
|
+
/** Continue a thread by native ID in a new process. */
|
|
22
|
+
readonly resume: 'native' | 'none';
|
|
23
|
+
/** `native`: a new branch with its own ID. */
|
|
24
|
+
readonly fork: 'native' | 'none';
|
|
31
25
|
/** Drop the last N turns of native history. */
|
|
32
26
|
readonly rollback: 'native' | 'none';
|
|
33
27
|
/** `acknowledged`: interruptNative() confirms the stop through the turn's terminal; `local-only`: interrupt() releases the local waiter only. */
|
|
34
28
|
readonly interrupt: 'acknowledged' | 'local-only';
|
|
35
29
|
/** `steer`: push() adds input to the in-flight turn; `none`: push() records push_ignored. */
|
|
36
30
|
readonly push: 'steer' | 'none';
|
|
37
|
-
/** `callback`: per-call
|
|
31
|
+
/** `callback`: per-call decisions through onToolRequest(); `mode`: a fixed launch policy (bypass); `refuse`: runtime requests are refused. */
|
|
38
32
|
readonly approvals: 'callback' | 'mode' | 'refuse';
|
|
39
33
|
/** Token usage reported per turn. */
|
|
40
34
|
readonly usage: boolean;
|
|
@@ -72,7 +66,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
72
66
|
rollback: 'none',
|
|
73
67
|
interrupt: 'acknowledged',
|
|
74
68
|
push: 'none',
|
|
75
|
-
approvals: '
|
|
69
|
+
approvals: 'callback',
|
|
76
70
|
usage: true,
|
|
77
71
|
limits: { stream: true, poll: true },
|
|
78
72
|
textOnly: true,
|
|
@@ -84,30 +78,8 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
84
78
|
'textOnly launches with no tools, MCP servers, slash commands or user settings.',
|
|
85
79
|
'Primed decisions (ClaudePrimedSessions) fork a persisted primed session per cycle with a pre-spawned spare.',
|
|
86
80
|
"interruptNative() and readLimits() use the CLI's stdio control protocol (`interrupt`, `get_usage`).",
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
'codex-mcp': {
|
|
91
|
-
kind: 'codex-mcp',
|
|
92
|
-
runtime: 'codex',
|
|
93
|
-
status: 'implemented',
|
|
94
|
-
verified: 'live',
|
|
95
|
-
capabilities: {
|
|
96
|
-
resume: 'loaded-only',
|
|
97
|
-
fork: 'same-thread',
|
|
98
|
-
rollback: 'none',
|
|
99
|
-
interrupt: 'local-only',
|
|
100
|
-
push: 'none',
|
|
101
|
-
approvals: 'mode',
|
|
102
|
-
usage: true,
|
|
103
|
-
limits: none,
|
|
104
|
-
textOnly: false,
|
|
105
|
-
primed: 'none',
|
|
106
|
-
billing: 'subscription',
|
|
107
|
-
},
|
|
108
|
-
notes: [
|
|
109
|
-
'`codex mcp-server`; `codex-reply` continues only threads loaded in the same process.',
|
|
110
|
-
'fork() continues the parent thread; it is not a native fork.',
|
|
81
|
+
'toolGate: true launches with `--permission-prompt-tool stdio` and registers a PreToolUse callback over the control protocol: onToolRequest() gates see every call (`pre-use`, any mode, bypass included; only a deny counts) and the asks the permission mode raises (`permission`, final).',
|
|
82
|
+
'Rollback (`--resume-session-at`) exists natively but is not wired here.',
|
|
111
83
|
],
|
|
112
84
|
},
|
|
113
85
|
'codex-app-server': {
|
|
@@ -121,7 +93,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
121
93
|
rollback: 'none',
|
|
122
94
|
interrupt: 'acknowledged',
|
|
123
95
|
push: 'steer',
|
|
124
|
-
approvals: '
|
|
96
|
+
approvals: 'callback',
|
|
125
97
|
usage: true,
|
|
126
98
|
limits: { stream: true, poll: true },
|
|
127
99
|
textOnly: false,
|
|
@@ -132,6 +104,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
132
104
|
'`codex app-server --listen stdio://` with thread/start, thread/resume, turn/start.',
|
|
133
105
|
"CodexAppServerSession runs with the profile's tools; tool-free text sessions are CodexPrimedSessions (primed: fork).",
|
|
134
106
|
'interruptNative() sends turn/interrupt; push() sends turn/steer against the owned turn.',
|
|
107
|
+
'appServer.approvalPolicy/sandbox (default never / danger-full-access; `codexPermissions(mode)`) set the thread policy; command and file-change approvals for the owned turn go to onToolRequest() gates (`permission`) and are answered accept/decline. Other server requests are refused (-32601).',
|
|
135
108
|
'Primed decisions (CodexPrimedSessions) host every key as a thread of one process per account and fork the primed thread per cycle.',
|
|
136
109
|
'thread/rollback is refused by codex-cli 0.155 for both ephemeral and paginated threads; session fork() is not wired.',
|
|
137
110
|
],
|
package/src/transports.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ClaudeAgentSdkSessionConfig,
|
|
7
7
|
} from './claude-agent-sdk-session';
|
|
8
8
|
import { ClaudeCodeSession, type ClaudeCodeSessionConfig } from './claude-code-session';
|
|
9
|
-
import { CodexAppServerSession,
|
|
9
|
+
import { CodexAppServerSession, type CodexSessionConfig } from './codex-session';
|
|
10
10
|
import type { HarnessSession } from './harness-session';
|
|
11
11
|
import {
|
|
12
12
|
TRANSPORTS,
|
|
@@ -36,7 +36,6 @@ export interface ApiSessionConfig {
|
|
|
36
36
|
|
|
37
37
|
export interface TransportConfigs {
|
|
38
38
|
'claude-cli': ClaudeCodeSessionConfig;
|
|
39
|
-
'codex-mcp': CodexSessionConfig;
|
|
40
39
|
'codex-app-server': CodexSessionConfig;
|
|
41
40
|
'claude-agent-sdk': ClaudeAgentSdkSessionConfig;
|
|
42
41
|
acp: AcpSessionConfig;
|
|
@@ -81,8 +80,6 @@ export function createSession<K extends TransportKind>(
|
|
|
81
80
|
switch (kind) {
|
|
82
81
|
case 'claude-cli':
|
|
83
82
|
return new ClaudeCodeSession(config as ClaudeCodeSessionConfig);
|
|
84
|
-
case 'codex-mcp':
|
|
85
|
-
return new CodexMcpSession(config as CodexSessionConfig);
|
|
86
83
|
case 'codex-app-server':
|
|
87
84
|
return new CodexAppServerSession(config as CodexSessionConfig);
|
|
88
85
|
case 'claude-agent-sdk':
|