@inixiative/agent-session 0.6.0 → 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 +15 -13
- package/package.json +3 -3
- package/src/claude-agent-sdk-session.ts +8 -0
- package/src/claude-code-session.ts +79 -12
- package/src/claude-primed.ts +153 -35
- package/src/codex-session.ts +103 -609
- package/src/harness-session.ts +1 -1
- package/src/index.ts +1 -4
- package/src/pool.ts +1 -1
- package/src/primed.ts +1 -1
- package/src/transport.ts +5 -34
- package/src/transports.ts +1 -4
package/src/codex-session.ts
CHANGED
|
@@ -5,32 +5,15 @@ import { ToolGates } from './tool-gate';
|
|
|
5
5
|
import { TRANSPORTS } from './transport';
|
|
6
6
|
import { TurnState } from './turn-state';
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// CodexMcpSession (default) — `codex mcp-server` (stdio MCP JSON-RPC)
|
|
17
|
-
// CodexAppServerSession (experimental) — `codex app-server` (stdio JSON-RPC)
|
|
18
|
-
//
|
|
19
|
-
// Both DISABLE codex's own approvals + sandbox (the union of
|
|
20
|
-
// `--dangerously-bypass-approvals-and-sandbox`) so OUR container is the only
|
|
21
|
-
// jail — identical intent to ClaudeCodeSession's `bypassPermissions`.
|
|
8
|
+
// CodexAppServerSession — one persistent `codex app-server --listen stdio://`
|
|
9
|
+
// process per session, driven over JSON-RPC (thread/start, thread/resume,
|
|
10
|
+
// turn/start). Every notification is classified into the shared SessionEvent
|
|
11
|
+
// taxonomy; a turn resolves once its native terminal and RPC have both settled.
|
|
12
|
+
// Approvals and sandbox default to never / danger-full-access (the container
|
|
13
|
+
// is the jail); appServer.approvalPolicy routes approvals to onToolRequest().
|
|
14
|
+
// The native thread ID is the externalSessionId used for cold resume.
|
|
22
15
|
//
|
|
23
|
-
//
|
|
24
|
-
// start() → spawn one process, start background stdout reader, MCP handshake
|
|
25
|
-
// send() → JSON-RPC tools/call (codex / codex-reply), resolve on completion
|
|
26
|
-
// fork() → new (unstarted) session resuming from the captured threadId
|
|
27
|
-
// kill() → close stdin, kill process
|
|
28
|
-
//
|
|
29
|
-
// codex's native "session ID" is the threadId returned by the `codex` tool's
|
|
30
|
-
// structuredContent — captured into externalSessionId and used for multi-turn
|
|
31
|
-
// (`codex-reply`) and fork.
|
|
32
|
-
//
|
|
33
|
-
// Verified against codex 0.140.
|
|
16
|
+
// Wire contract: codex-cli 0.155.1 v2 JSON schema.
|
|
34
17
|
// ---------------------------------------------------------------------------
|
|
35
18
|
|
|
36
19
|
import type {
|
|
@@ -109,8 +92,7 @@ export type CodexSpawn = (
|
|
|
109
92
|
// ---------------------------------------------------------------------------
|
|
110
93
|
|
|
111
94
|
export interface CodexSessionConfig {
|
|
112
|
-
/**
|
|
113
|
-
* Config is native launch data, not event/recording data. */
|
|
95
|
+
/** Thread launch options. Config is native launch data, not event/recording data. */
|
|
114
96
|
appServer?: {
|
|
115
97
|
/** Persist the actual returned binding before model admission; failure blocks work. */
|
|
116
98
|
onThreadReady?: (binding: string) => void | Promise<void>;
|
|
@@ -144,14 +126,13 @@ export interface CodexSessionConfig {
|
|
|
144
126
|
timeout?: number;
|
|
145
127
|
/**
|
|
146
128
|
* Base context to pre-load. Codex has no `--append-system-prompt`; we prepend
|
|
147
|
-
* it to the first turn's prompt
|
|
148
|
-
*
|
|
129
|
+
* it to the first turn's prompt. Persists logically for the session via the
|
|
130
|
+
* continued thread.
|
|
149
131
|
*/
|
|
150
132
|
baseContext?: string;
|
|
151
133
|
/**
|
|
152
|
-
* codex's native thread ID
|
|
153
|
-
*
|
|
154
|
-
* `thread/resume` on app-server. Only app-server supports cold resume by ID.
|
|
134
|
+
* codex's native thread ID. When set, the first send() resumes it with
|
|
135
|
+
* `thread/resume` (cold resume by ID).
|
|
155
136
|
*/
|
|
156
137
|
externalSessionId?: string;
|
|
157
138
|
/** Environment merged over process.env (e.g. CODEX_HOME for a profile). */
|
|
@@ -180,64 +161,58 @@ interface QueuedTurn {
|
|
|
180
161
|
// codex reasoning-effort levels (model_reasoning_effort), low→high.
|
|
181
162
|
const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
182
163
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// stdout read loop, event emission, fork, artifact — is shared here, exactly as
|
|
191
|
-
// ClaudeCodeSession structures it.
|
|
164
|
+
class NativeValidationError extends Error {}
|
|
165
|
+
|
|
166
|
+
export class CodexAppServerSession implements HarnessSession {
|
|
167
|
+
get transport() {
|
|
168
|
+
return TRANSPORTS['codex-app-server'];
|
|
169
|
+
}
|
|
170
|
+
readonly appServerProtocol = 'owned-thread-v1';
|
|
192
171
|
|
|
193
|
-
abstract class BaseCodexSession implements HarnessSession {
|
|
194
172
|
// -- Config --
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
173
|
+
private _bin: string;
|
|
174
|
+
private _model: string;
|
|
175
|
+
private _effort?: string;
|
|
176
|
+
private _cwd: string;
|
|
177
|
+
private _defaultTimeout: number;
|
|
178
|
+
private _baseContext?: string;
|
|
179
|
+
private _spawn?: CodexSpawn;
|
|
180
|
+
private _env?: Record<string, string | undefined>;
|
|
203
181
|
|
|
204
182
|
// -- Process --
|
|
205
|
-
|
|
206
|
-
|
|
183
|
+
private _proc: PipedSubprocess | null = null;
|
|
184
|
+
private _stderr = '';
|
|
207
185
|
|
|
208
186
|
// -- Session state --
|
|
209
|
-
|
|
210
|
-
|
|
187
|
+
private _externalSessionId?: string;
|
|
188
|
+
private _alive = false;
|
|
211
189
|
private _endEmitted = false;
|
|
212
|
-
|
|
213
|
-
|
|
190
|
+
private _eventLog: SessionEvent[] = [];
|
|
191
|
+
private _handlers: SessionEventHandler[] = [];
|
|
214
192
|
private _observerFailures = { synchronous: 0, asynchronous: 0 };
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
private _beforeSendHooks: BeforeSendHook[] = [];
|
|
194
|
+
private _turns = 0;
|
|
195
|
+
private _totalTokens = { input: 0, output: 0 };
|
|
196
|
+
private _startedAt: number;
|
|
219
197
|
/** Whether baseContext has been prepended onto a sent turn yet. */
|
|
220
|
-
|
|
221
|
-
/** Whether this session was created via fork() (continues a thread). */
|
|
222
|
-
protected _forking = false;
|
|
198
|
+
private _injectedBaseContext = false;
|
|
223
199
|
|
|
224
200
|
// -- JSON-RPC --
|
|
225
201
|
/** Monotonic JSON-RPC request id. */
|
|
226
|
-
|
|
202
|
+
private _rpcId = 0;
|
|
227
203
|
/** Pending JSON-RPC responses, keyed by request id. */
|
|
228
|
-
|
|
204
|
+
private _pending = new Map<
|
|
229
205
|
number,
|
|
230
206
|
{ resolve: (result: unknown) => void; reject: (e: Error) => void }
|
|
231
207
|
>();
|
|
232
208
|
|
|
233
209
|
// -- Turn queue (identical to ClaudeCodeSession) --
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
protected _turnTimer: ReturnType<typeof setTimeout> | null = null;
|
|
210
|
+
private _queue: QueuedTurn[] = [];
|
|
211
|
+
private _attempts: TurnState[] = [];
|
|
212
|
+
private _seenTerminals = new Set<string>();
|
|
213
|
+
private _inflight: QueuedTurn | null = null;
|
|
214
|
+
private _resultText = '';
|
|
215
|
+
private _turnTimer: ReturnType<typeof setTimeout> | null = null;
|
|
241
216
|
|
|
242
217
|
constructor(config?: CodexSessionConfig) {
|
|
243
218
|
const bin = config?.bin ?? 'codex';
|
|
@@ -259,6 +234,17 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
259
234
|
this._spawn = config?.spawn;
|
|
260
235
|
this._env = config?.env;
|
|
261
236
|
this._startedAt = Date.now();
|
|
237
|
+
const { onThreadReady, ...options } = config?.appServer ?? {};
|
|
238
|
+
this._onThreadReady = onThreadReady;
|
|
239
|
+
this._options = retainEvidence(options);
|
|
240
|
+
const required = this._options.requiredMcpServer;
|
|
241
|
+
if (
|
|
242
|
+
required &&
|
|
243
|
+
(!/^[a-zA-Z0-9_-]+$/.test(required.name) ||
|
|
244
|
+
!required.tools.length ||
|
|
245
|
+
required.tools.some((t) => !/^[a-zA-Z0-9_-]+$/.test(t)))
|
|
246
|
+
)
|
|
247
|
+
throw new NativeValidationError('Invalid required MCP inventory');
|
|
262
248
|
}
|
|
263
249
|
|
|
264
250
|
// ---------------------------------------------------------------------------
|
|
@@ -311,21 +297,6 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
311
297
|
};
|
|
312
298
|
}
|
|
313
299
|
|
|
314
|
-
/**
|
|
315
|
-
* Mid-turn push. Like Claude Code's stream-json stdin, codex's tools/call has
|
|
316
|
-
* no out-of-band signal channel mid-turn; we emit a "push_ignored" error event
|
|
317
|
-
* so callers observe the attempt without the model seeing the payload until the
|
|
318
|
-
* next turn.
|
|
319
|
-
*/
|
|
320
|
-
async push(payload: { kind: string; text: string }): Promise<void> {
|
|
321
|
-
this._emit({
|
|
322
|
-
kind: 'error',
|
|
323
|
-
timestamp: Date.now(),
|
|
324
|
-
text: `push_ignored: kind=${payload.kind} — codex turn has no OOB channel`,
|
|
325
|
-
raw: payload,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
300
|
// ---------------------------------------------------------------------------
|
|
330
301
|
// interrupt / kill / artifact (identical to ClaudeCodeSession)
|
|
331
302
|
// ---------------------------------------------------------------------------
|
|
@@ -386,10 +357,24 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
386
357
|
}
|
|
387
358
|
|
|
388
359
|
// ---------------------------------------------------------------------------
|
|
389
|
-
// start() — spawn the persistent process + handshake
|
|
360
|
+
// start() — spawn the persistent process + handshake
|
|
390
361
|
// ---------------------------------------------------------------------------
|
|
391
362
|
|
|
392
363
|
async start(): Promise<void> {
|
|
364
|
+
if (this._inflight)
|
|
365
|
+
throw Error('Native call ownership unresolved; automatic resume is blocked');
|
|
366
|
+
if (this._startedOnce && !this._alive)
|
|
367
|
+
throw Error('App-server restart requires a new owned instance and preflight');
|
|
368
|
+
this._startedOnce = true;
|
|
369
|
+
try {
|
|
370
|
+
await this._launch();
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.kill();
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async _launch(): Promise<void> {
|
|
393
378
|
if (this._attempts.some((a) => a.dispatch === 'attempted' && a.nativeOutcome === 'unknown'))
|
|
394
379
|
throw new Error('Native outcome unresolved; automatic resume is blocked');
|
|
395
380
|
if (this._proc) throw new Error('Session already started');
|
|
@@ -551,11 +536,9 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
551
536
|
|
|
552
537
|
private _writeTurn(turn: QueuedTurn): void {
|
|
553
538
|
turn.evidence.dispatch = 'attempted';
|
|
554
|
-
this._turnEvents = turn.evidence.events;
|
|
555
539
|
this._resultText = '';
|
|
556
540
|
|
|
557
|
-
//
|
|
558
|
-
// the tools/call (or turn) completes; that resolves the turn.
|
|
541
|
+
// Resolves once the native terminal and the turn/start RPC have both settled.
|
|
559
542
|
this._sendTurn(turn.message)
|
|
560
543
|
.then((tokens) => {
|
|
561
544
|
if (this._inflight !== turn) return;
|
|
@@ -610,10 +593,6 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
610
593
|
this._processNextTurn();
|
|
611
594
|
}
|
|
612
595
|
|
|
613
|
-
protected _canReleaseAttempt(attempt: TurnState): boolean {
|
|
614
|
-
return attempt.rpcSettled;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
596
|
private _processNextTurn(): void {
|
|
618
597
|
if (!this._inflight && this._queue.length > 0 && this._alive) {
|
|
619
598
|
const next = this._queue.shift()!;
|
|
@@ -621,7 +600,7 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
621
600
|
}
|
|
622
601
|
}
|
|
623
602
|
|
|
624
|
-
|
|
603
|
+
private _rejectInflight(
|
|
625
604
|
err: Error,
|
|
626
605
|
reason: import('./harness-session').SessionAttempt['localFailure'] = 'transport',
|
|
627
606
|
): void {
|
|
@@ -647,7 +626,7 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
647
626
|
this._withdrawRequests();
|
|
648
627
|
}
|
|
649
628
|
|
|
650
|
-
|
|
629
|
+
private _rejectQueue(err: Error): void {
|
|
651
630
|
for (const turn of this._queue) turn.reject(turn.evidence.fail(err, 'blocked'));
|
|
652
631
|
this._queue = [];
|
|
653
632
|
}
|
|
@@ -701,7 +680,7 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
701
680
|
}
|
|
702
681
|
|
|
703
682
|
// ---------------------------------------------------------------------------
|
|
704
|
-
// Private — JSON-RPC line processor
|
|
683
|
+
// Private — JSON-RPC line processor
|
|
705
684
|
// ---------------------------------------------------------------------------
|
|
706
685
|
|
|
707
686
|
private _processLine(line: string): void {
|
|
@@ -770,7 +749,7 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
770
749
|
// Private — emit (identical to ClaudeCodeSession)
|
|
771
750
|
// ---------------------------------------------------------------------------
|
|
772
751
|
|
|
773
|
-
|
|
752
|
+
private _emit(event: SessionEvent): void {
|
|
774
753
|
event = retainEvidence(event);
|
|
775
754
|
if (event.kind === 'session_end') {
|
|
776
755
|
if (this._endEmitted) return;
|
|
@@ -795,9 +774,9 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
795
774
|
// Private — JSON-RPC request helper (resolves on the matching response)
|
|
796
775
|
// ---------------------------------------------------------------------------
|
|
797
776
|
|
|
798
|
-
|
|
777
|
+
private _rpcRequest(method: string, params?: unknown): Promise<unknown> {
|
|
799
778
|
const id = ++this._rpcId;
|
|
800
|
-
if (
|
|
779
|
+
if (method === 'turn/start' && this._inflight) {
|
|
801
780
|
this._inflight.evidence.identity = { ...this._inflight.evidence.identity, rpcRequestId: id };
|
|
802
781
|
this._inflight.evidence.rpcOutcome = 'pending';
|
|
803
782
|
}
|
|
@@ -814,346 +793,16 @@ abstract class BaseCodexSession implements HarnessSession {
|
|
|
814
793
|
});
|
|
815
794
|
}
|
|
816
795
|
|
|
817
|
-
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
protected _rpcNotify(method: string, params?: unknown): void {
|
|
796
|
+
private _rpcNotify(method: string, params?: unknown): void {
|
|
822
797
|
const payload = `${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`;
|
|
823
798
|
this._proc!.stdin.write(payload);
|
|
824
799
|
this._proc!.stdin.flush();
|
|
825
800
|
}
|
|
826
801
|
|
|
827
802
|
// ---------------------------------------------------------------------------
|
|
828
|
-
//
|
|
803
|
+
// Account limits, native interrupt, steer
|
|
829
804
|
// ---------------------------------------------------------------------------
|
|
830
805
|
|
|
831
|
-
/** CLI args for the persistent process. */
|
|
832
|
-
protected abstract _buildSpawnArgs(): string[];
|
|
833
|
-
/** Protocol handshake after spawn (initialize / initialized / etc.). */
|
|
834
|
-
protected abstract _handshake(): Promise<void>;
|
|
835
|
-
/** Send one turn; resolve with token usage when the turn completes. */
|
|
836
|
-
protected abstract _sendTurn(
|
|
837
|
-
message: string,
|
|
838
|
-
): Promise<{ input: number; output: number } | undefined>;
|
|
839
|
-
/** Map a streamed notification to SessionEvents. */
|
|
840
|
-
protected abstract _classify(msg: Record<string, unknown>): SessionEvent[];
|
|
841
|
-
|
|
842
|
-
protected _observeRpcResult(_id: number, _result: unknown): void {}
|
|
843
|
-
|
|
844
|
-
/** The process is gone (kill, exit, transport failure): withdraw server requests still pending. */
|
|
845
|
-
protected _withdrawRequests(): void {}
|
|
846
|
-
|
|
847
|
-
/** Shared fork constructor — subclass passes its own ctor. */
|
|
848
|
-
fork(opts?: { cwd?: string; baseContext?: string }): HarnessSession {
|
|
849
|
-
if (this._inflight) throw new Error('Cannot fork while native ownership is unresolved');
|
|
850
|
-
if (!this._externalSessionId) {
|
|
851
|
-
throw new Error('Cannot fork — no thread ID yet (send at least one message first)');
|
|
852
|
-
}
|
|
853
|
-
const Ctor = this.constructor as new (c?: CodexSessionConfig) => BaseCodexSession;
|
|
854
|
-
const forked = new Ctor({
|
|
855
|
-
bin: this._bin,
|
|
856
|
-
model: this._model,
|
|
857
|
-
effort: this._effort,
|
|
858
|
-
cwd: opts?.cwd ?? this._cwd,
|
|
859
|
-
timeout: this._defaultTimeout,
|
|
860
|
-
baseContext: opts?.baseContext ?? this._baseContext,
|
|
861
|
-
externalSessionId: this._externalSessionId,
|
|
862
|
-
spawn: this._spawn,
|
|
863
|
-
env: this._env,
|
|
864
|
-
});
|
|
865
|
-
forked._forking = true;
|
|
866
|
-
return forked;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ---------------------------------------------------------------------------
|
|
871
|
-
// CodexMcpSession (DEFAULT) — `codex mcp-server` over stdio MCP JSON-RPC
|
|
872
|
-
// ---------------------------------------------------------------------------
|
|
873
|
-
//
|
|
874
|
-
// Handshake: initialize → notifications/initialized → tools/list.
|
|
875
|
-
// A turn is an MCP `tools/call`:
|
|
876
|
-
// - first turn → tool "codex" (params: prompt, model, cwd, ...)
|
|
877
|
-
// - subsequent turns → tool "codex-reply" (params: prompt, threadId)
|
|
878
|
-
// During the call, codex streams `codex/event` notifications (agent message,
|
|
879
|
-
// reasoning, command execution, token usage) — mapped to SessionEvents. The
|
|
880
|
-
// threadId comes back in the call result's structuredContent (captured for
|
|
881
|
-
// multi-turn + fork).
|
|
882
|
-
|
|
883
|
-
export class CodexMcpSession extends BaseCodexSession {
|
|
884
|
-
get transport() {
|
|
885
|
-
return TRANSPORTS['codex-mcp'];
|
|
886
|
-
}
|
|
887
|
-
private _mcpAdmission?: string;
|
|
888
|
-
private _mcpCalls = new Map<string, { invocation: Record<string, unknown>; ended: boolean }>();
|
|
889
|
-
protected _buildSpawnArgs(): string[] {
|
|
890
|
-
// Disable codex's own approvals + sandbox (the union of
|
|
891
|
-
// --dangerously-bypass-approvals-and-sandbox) so OUR container is the jail.
|
|
892
|
-
// model_reasoning_effort sets the effort level; passed as TOML-ish `-c` values.
|
|
893
|
-
const args = [
|
|
894
|
-
'mcp-server',
|
|
895
|
-
'-c',
|
|
896
|
-
`sandbox_mode="danger-full-access"`,
|
|
897
|
-
'-c',
|
|
898
|
-
`approval_policy="never"`,
|
|
899
|
-
];
|
|
900
|
-
if (this._effort) {
|
|
901
|
-
args.push('-c', `model_reasoning_effort="${this._effort}"`);
|
|
902
|
-
}
|
|
903
|
-
return args;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
protected async _handshake(): Promise<void> {
|
|
907
|
-
await this._rpcRequest('initialize', {
|
|
908
|
-
protocolVersion: '2025-06-18',
|
|
909
|
-
capabilities: {},
|
|
910
|
-
clientInfo: { name: 'inixiative-bench', version: '0.1.0' },
|
|
911
|
-
});
|
|
912
|
-
this._rpcNotify('notifications/initialized');
|
|
913
|
-
// tools/list confirms the `codex` + `codex-reply` tools are present.
|
|
914
|
-
await this._rpcRequest('tools/list', {});
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
protected async _sendTurn(
|
|
918
|
-
message: string,
|
|
919
|
-
): Promise<{ input: number; output: number } | undefined> {
|
|
920
|
-
const isReply = this._externalSessionId !== undefined;
|
|
921
|
-
const name = isReply ? 'codex-reply' : 'codex';
|
|
922
|
-
const args: Record<string, unknown> = isReply
|
|
923
|
-
? { prompt: message, threadId: this._externalSessionId }
|
|
924
|
-
: {
|
|
925
|
-
prompt: message,
|
|
926
|
-
model: this._model,
|
|
927
|
-
cwd: this._cwd,
|
|
928
|
-
// Belt-and-suspenders: also disable per-call (matches spawn `-c` flags).
|
|
929
|
-
sandbox: 'danger-full-access',
|
|
930
|
-
'approval-policy': 'never',
|
|
931
|
-
...(this._effort ? { config: { model_reasoning_effort: this._effort } } : {}),
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const result = (await this._rpcRequest('tools/call', {
|
|
935
|
-
name,
|
|
936
|
-
arguments: args,
|
|
937
|
-
})) as Record<string, unknown> | undefined;
|
|
938
|
-
|
|
939
|
-
// Capture the threadId for multi-turn (codex-reply) + fork.
|
|
940
|
-
const structured = result?.structuredContent as Record<string, unknown> | undefined;
|
|
941
|
-
const threadId = structured?.threadId as string | undefined;
|
|
942
|
-
if (threadId && this._externalSessionId && threadId !== this._externalSessionId)
|
|
943
|
-
throw new Error('MCP response changed the native resume binding');
|
|
944
|
-
if (threadId && !this._externalSessionId) {
|
|
945
|
-
this._externalSessionId = threadId;
|
|
946
|
-
}
|
|
947
|
-
if (threadId && this._inflight)
|
|
948
|
-
this._inflight.evidence.identity = { ...this._inflight.evidence.identity, threadId };
|
|
949
|
-
// Final text: prefer structuredContent.content, else the tool result content.
|
|
950
|
-
const finalText =
|
|
951
|
-
(structured?.content as string | undefined) ?? this._extractToolText(result?.content);
|
|
952
|
-
if (result?.isError === true) throw new Error(finalText ?? 'MCP tools/call failed');
|
|
953
|
-
if (finalText) {
|
|
954
|
-
this._resultText = finalText;
|
|
955
|
-
if (this._inflight) this._inflight.evidence.content = finalText;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Usage, when the call result reports it. Streamed token_count events are
|
|
959
|
-
// already accounted by the base loop; the call result is the authoritative
|
|
960
|
-
// turn total, returned to _dispatchTurn for the SessionResult. To avoid
|
|
961
|
-
// double-counting against streamed events, we only return it if no streamed
|
|
962
|
-
// token event was seen this turn.
|
|
963
|
-
const sawStreamedTokens = this._turnEvents.some((e) => e.tokens);
|
|
964
|
-
const usage = structured?.usage as Record<string, number> | undefined;
|
|
965
|
-
if (
|
|
966
|
-
usage &&
|
|
967
|
-
!sawStreamedTokens &&
|
|
968
|
-
typeof (usage.input_tokens ?? usage.inputTokens) === 'number' &&
|
|
969
|
-
typeof (usage.output_tokens ?? usage.outputTokens) === 'number'
|
|
970
|
-
) {
|
|
971
|
-
return {
|
|
972
|
-
input: (usage.input_tokens ?? usage.inputTokens ?? 0) as number,
|
|
973
|
-
output: (usage.output_tokens ?? usage.outputTokens ?? 0) as number,
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
return undefined;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
private _extractToolText(content: unknown): string | undefined {
|
|
980
|
-
if (!Array.isArray(content)) return undefined;
|
|
981
|
-
const parts: string[] = [];
|
|
982
|
-
for (const block of content) {
|
|
983
|
-
const b = block as Record<string, unknown>;
|
|
984
|
-
if (b.type === 'text' && typeof b.text === 'string') parts.push(b.text);
|
|
985
|
-
}
|
|
986
|
-
return parts.length ? parts.join('\n') : undefined;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
protected _classify(msg: Record<string, unknown>): SessionEvent[] {
|
|
990
|
-
// Streamed events arrive as JSON-RPC notifications: method "codex/event"
|
|
991
|
-
// with params carrying a `msg` of a tagged type.
|
|
992
|
-
if (msg.method !== 'codex/event') return [];
|
|
993
|
-
const params = msg.params as Record<string, unknown> | undefined;
|
|
994
|
-
const ev = (params?.msg ?? params) as Record<string, unknown> | undefined;
|
|
995
|
-
if (!ev) return [];
|
|
996
|
-
const a = this._inflight?.evidence;
|
|
997
|
-
const str = (v: unknown) => (typeof v === 'string' ? v : undefined);
|
|
998
|
-
const nativeSessionId = str(ev.session_id),
|
|
999
|
-
threadId = str(ev.thread_id);
|
|
1000
|
-
const turnId = str(ev.turn_id),
|
|
1001
|
-
envelopeId = str(params?.id);
|
|
1002
|
-
const item = ev.item as Record<string, unknown> | undefined;
|
|
1003
|
-
const unowned = (reason: NonNullable<SessionEvent['unattributedReason']>): SessionEvent[] => [
|
|
1004
|
-
{
|
|
1005
|
-
kind: 'native_status',
|
|
1006
|
-
timestamp: Date.now(),
|
|
1007
|
-
correlation: 'unknown',
|
|
1008
|
-
unattributedReason: reason,
|
|
1009
|
-
nativeSessionId,
|
|
1010
|
-
threadId,
|
|
1011
|
-
turnId,
|
|
1012
|
-
callId: str(ev.call_id),
|
|
1013
|
-
itemId: str(item?.id) ?? str(ev.item_id),
|
|
1014
|
-
// Keep the envelope too: params.id may explain a rejected correlation.
|
|
1015
|
-
// Unowned MCP calls retain identity/type diagnostics, never unrelated tool payloads.
|
|
1016
|
-
raw:
|
|
1017
|
-
ev.type === 'mcp_tool_call_begin' || ev.type === 'mcp_tool_call_end'
|
|
1018
|
-
? { type: ev.type }
|
|
1019
|
-
: msg,
|
|
1020
|
-
},
|
|
1021
|
-
];
|
|
1022
|
-
if (threadId && this._externalSessionId && threadId !== this._externalSessionId)
|
|
1023
|
-
return unowned('foreign-session');
|
|
1024
|
-
if (
|
|
1025
|
-
nativeSessionId &&
|
|
1026
|
-
a?.identity.nativeSessionId &&
|
|
1027
|
-
nativeSessionId !== a.identity.nativeSessionId
|
|
1028
|
-
)
|
|
1029
|
-
return unowned('foreign-session');
|
|
1030
|
-
if (
|
|
1031
|
-
(envelopeId && this._seenTerminals.has(envelopeId)) ||
|
|
1032
|
-
(turnId && this._seenTerminals.has(turnId))
|
|
1033
|
-
) {
|
|
1034
|
-
return unowned(ev.type === 'task_complete' ? 'duplicate-terminal' : 'after-terminal');
|
|
1035
|
-
}
|
|
1036
|
-
if (!a) {
|
|
1037
|
-
if (ev.type === 'task_complete' && turnId) this._seenTerminals.add(turnId);
|
|
1038
|
-
return unowned('no-admission');
|
|
1039
|
-
}
|
|
1040
|
-
if (ev.type === 'session_configured') {
|
|
1041
|
-
if (!this._externalSessionId && threadId) this._externalSessionId = threadId;
|
|
1042
|
-
a.identity = { ...a.identity, nativeSessionId, threadId };
|
|
1043
|
-
}
|
|
1044
|
-
if (
|
|
1045
|
-
ev.type !== 'session_configured' &&
|
|
1046
|
-
a.identity.turnId &&
|
|
1047
|
-
envelopeId &&
|
|
1048
|
-
envelopeId !== a.identity.turnId
|
|
1049
|
-
)
|
|
1050
|
-
return unowned('foreign-turn');
|
|
1051
|
-
if (threadId && a.identity.threadId && threadId !== a.identity.threadId)
|
|
1052
|
-
return unowned('foreign-session');
|
|
1053
|
-
if (ev.type === 'task_started' && turnId && !a.identity.turnId)
|
|
1054
|
-
a.identity = { ...a.identity, turnId, correlation: 'native-turn' };
|
|
1055
|
-
if (turnId && turnId !== a.identity.turnId) return unowned('foreign-turn');
|
|
1056
|
-
if (ev.type === 'task_complete') {
|
|
1057
|
-
if (!turnId || turnId !== a.identity.turnId) return unowned('unrecognized-event');
|
|
1058
|
-
this._seenTerminals.add(turnId);
|
|
1059
|
-
return [
|
|
1060
|
-
{
|
|
1061
|
-
...a.identity,
|
|
1062
|
-
kind: 'result',
|
|
1063
|
-
timestamp: Date.now(),
|
|
1064
|
-
nativeOutcome: 'completed',
|
|
1065
|
-
terminal: { type: 'task_complete', turnId },
|
|
1066
|
-
raw: ev,
|
|
1067
|
-
},
|
|
1068
|
-
];
|
|
1069
|
-
}
|
|
1070
|
-
const identity = {
|
|
1071
|
-
...a.identity,
|
|
1072
|
-
threadId: threadId ?? a.identity.threadId,
|
|
1073
|
-
turnId: turnId ?? a.identity.turnId,
|
|
1074
|
-
callId: str(ev.call_id),
|
|
1075
|
-
itemId: str(item?.id) ?? str(ev.item_id),
|
|
1076
|
-
};
|
|
1077
|
-
if (ev.type === 'mcp_tool_call_begin' || ev.type === 'mcp_tool_call_end') {
|
|
1078
|
-
const invocation = object(ev.invocation);
|
|
1079
|
-
if (
|
|
1080
|
-
!identity.callId ||
|
|
1081
|
-
!invocation ||
|
|
1082
|
-
typeof invocation.server !== 'string' ||
|
|
1083
|
-
!invocation.server ||
|
|
1084
|
-
typeof invocation.tool !== 'string' ||
|
|
1085
|
-
!invocation.tool
|
|
1086
|
-
)
|
|
1087
|
-
return unowned('malformed-tool');
|
|
1088
|
-
if (this._mcpAdmission !== a.identity.admissionId) {
|
|
1089
|
-
this._mcpAdmission = a.identity.admissionId;
|
|
1090
|
-
this._mcpCalls.clear();
|
|
1091
|
-
}
|
|
1092
|
-
// Compare the original argument value, including absence. Never join a result
|
|
1093
|
-
// for another server/tool or silently substitute arguments from its envelope.
|
|
1094
|
-
const join = {
|
|
1095
|
-
server: invocation.server,
|
|
1096
|
-
tool: invocation.tool,
|
|
1097
|
-
arguments: invocation.arguments,
|
|
1098
|
-
};
|
|
1099
|
-
const prior = this._mcpCalls.get(identity.callId);
|
|
1100
|
-
const tool = {
|
|
1101
|
-
toolName: `mcp__${invocation.server}__${invocation.tool}`,
|
|
1102
|
-
toolServer: invocation.server,
|
|
1103
|
-
toolMethod: invocation.tool,
|
|
1104
|
-
};
|
|
1105
|
-
if (ev.type === 'mcp_tool_call_begin') {
|
|
1106
|
-
if (prior) return unowned('duplicate-tool');
|
|
1107
|
-
this._mcpCalls.set(identity.callId, { invocation: structuredClone(join), ended: false });
|
|
1108
|
-
const args = publicMcpArguments(invocation.arguments);
|
|
1109
|
-
return [{ ...identity, ...tool, kind: 'tool_use', timestamp: Date.now(), ...args }];
|
|
1110
|
-
}
|
|
1111
|
-
if (!prior || !isDeepStrictEqual(prior.invocation, join)) return unowned('unmatched-tool');
|
|
1112
|
-
if (prior.ended) return unowned('duplicate-tool');
|
|
1113
|
-
const result = publicMcpResult(ev.result);
|
|
1114
|
-
if (!result) return unowned('malformed-tool');
|
|
1115
|
-
prior.ended = true;
|
|
1116
|
-
return [{ ...identity, ...tool, kind: 'tool_result', timestamp: Date.now(), ...result }];
|
|
1117
|
-
}
|
|
1118
|
-
if (
|
|
1119
|
-
['session_configured', 'task_started', 'item_started', 'item_completed'].includes(
|
|
1120
|
-
String(ev.type),
|
|
1121
|
-
)
|
|
1122
|
-
) {
|
|
1123
|
-
return [
|
|
1124
|
-
{
|
|
1125
|
-
...identity,
|
|
1126
|
-
kind: 'native_status',
|
|
1127
|
-
timestamp: Date.now(),
|
|
1128
|
-
nativeOutcome: a.nativeOutcome,
|
|
1129
|
-
raw: ev,
|
|
1130
|
-
},
|
|
1131
|
-
];
|
|
1132
|
-
}
|
|
1133
|
-
// Notifications without a turn ID are associated only with the single owned RPC.
|
|
1134
|
-
const classified = classifyCodexEvent(ev, 'type');
|
|
1135
|
-
return classified.length
|
|
1136
|
-
? classified.map((event) => ({ ...identity, ...event }))
|
|
1137
|
-
: unowned('unrecognized-event');
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// ---------------------------------------------------------------------------
|
|
1142
|
-
// CodexAppServerSession (EXPERIMENTAL) — `codex app-server` over stdio
|
|
1143
|
-
// ---------------------------------------------------------------------------
|
|
1144
|
-
//
|
|
1145
|
-
// Wire contract: installed codex-cli 0.153.4 v2 JSON schema. Controlled tests
|
|
1146
|
-
// establish source behavior, not a real create/resume/native-completion capture.
|
|
1147
|
-
// No default switch. Full history hydration, native fork and cancellation remain
|
|
1148
|
-
// separate integration work; a local interrupt never acknowledges native stop.
|
|
1149
|
-
|
|
1150
|
-
class NativeValidationError extends Error {}
|
|
1151
|
-
|
|
1152
|
-
export class CodexAppServerSession extends BaseCodexSession {
|
|
1153
|
-
get transport() {
|
|
1154
|
-
return TRANSPORTS['codex-app-server'];
|
|
1155
|
-
}
|
|
1156
|
-
readonly appServerProtocol = 'owned-thread-v1';
|
|
1157
806
|
private _limits?: LimitSnapshot;
|
|
1158
807
|
|
|
1159
808
|
/** Latest account limits observed on this session (stream or poll). */
|
|
@@ -1229,7 +878,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1229
878
|
}
|
|
1230
879
|
|
|
1231
880
|
/** Mid-turn steer (turn/steer) into the owned in-flight turn; otherwise records push_ignored. */
|
|
1232
|
-
|
|
881
|
+
async push(payload: { kind: string; text: string }): Promise<void> {
|
|
1233
882
|
const a = this._inflight?.evidence;
|
|
1234
883
|
const ignored = (reason: string) =>
|
|
1235
884
|
this._emit({
|
|
@@ -1307,41 +956,12 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1307
956
|
}
|
|
1308
957
|
>();
|
|
1309
958
|
|
|
1310
|
-
|
|
1311
|
-
super(config);
|
|
1312
|
-
const { onThreadReady, ...options } = config?.appServer ?? {};
|
|
1313
|
-
this._onThreadReady = onThreadReady;
|
|
1314
|
-
this._options = retainEvidence(options);
|
|
1315
|
-
const required = this._options.requiredMcpServer;
|
|
1316
|
-
if (
|
|
1317
|
-
required &&
|
|
1318
|
-
(!/^[a-zA-Z0-9_-]+$/.test(required.name) ||
|
|
1319
|
-
!required.tools.length ||
|
|
1320
|
-
required.tools.some((t) => !/^[a-zA-Z0-9_-]+$/.test(t)))
|
|
1321
|
-
)
|
|
1322
|
-
throw new NativeValidationError('Invalid required MCP inventory');
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
override fork(): HarnessSession {
|
|
959
|
+
fork(): HarnessSession {
|
|
1326
960
|
throw Error(
|
|
1327
961
|
'Native app-server fork is not implemented; a same-thread wrapper cannot create a native fork or inherit a process capability',
|
|
1328
962
|
);
|
|
1329
963
|
}
|
|
1330
964
|
|
|
1331
|
-
override async start(): Promise<void> {
|
|
1332
|
-
if (this._inflight)
|
|
1333
|
-
throw Error('Native call ownership unresolved; automatic resume is blocked');
|
|
1334
|
-
if (this._startedOnce && !this._alive)
|
|
1335
|
-
throw Error('App-server restart requires a new owned instance and preflight');
|
|
1336
|
-
this._startedOnce = true;
|
|
1337
|
-
try {
|
|
1338
|
-
await super.start();
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
this.kill();
|
|
1341
|
-
throw error;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
965
|
/** Needs an `approvalPolicy` other than `never`. Gates registered here are not copied (no fork). */
|
|
1346
966
|
onToolRequest(gate: ToolGate): () => void {
|
|
1347
967
|
if (this._approvalPolicy === 'never')
|
|
@@ -1351,7 +971,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1351
971
|
return this._gates.add(gate);
|
|
1352
972
|
}
|
|
1353
973
|
|
|
1354
|
-
|
|
974
|
+
private _withdrawRequests(): void {
|
|
1355
975
|
for (const approval of this._approvals.values()) approval.abort();
|
|
1356
976
|
this._approvals.clear();
|
|
1357
977
|
}
|
|
@@ -1447,7 +1067,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1447
1067
|
return true;
|
|
1448
1068
|
}
|
|
1449
1069
|
|
|
1450
|
-
|
|
1070
|
+
private _serverRequest(request: Record<string, unknown>): boolean {
|
|
1451
1071
|
if (this._approval(request)) return true;
|
|
1452
1072
|
// JSON-RPC's explicit method-not-supported refusal also covers unknown future
|
|
1453
1073
|
// requests. Never approve native work or echo private request parameters.
|
|
@@ -1557,17 +1177,17 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1557
1177
|
this._refuse('mcp-inventory-page-limit');
|
|
1558
1178
|
}
|
|
1559
1179
|
|
|
1560
|
-
|
|
1180
|
+
private _canReleaseAttempt(a: TurnState): boolean {
|
|
1561
1181
|
// An observer/write failure may occur after delivery. A terminal alone does
|
|
1562
1182
|
// not settle the unanswered turn/start call or authorize another admission.
|
|
1563
1183
|
return a.rpcSettled && (a.rpcOutcome === 'resolved' || a.rpcOutcome === 'failed');
|
|
1564
1184
|
}
|
|
1565
1185
|
|
|
1566
|
-
|
|
1186
|
+
private _buildSpawnArgs(): string[] {
|
|
1567
1187
|
return ['app-server', '--listen', 'stdio://'];
|
|
1568
1188
|
}
|
|
1569
1189
|
|
|
1570
|
-
|
|
1190
|
+
private async _handshake(): Promise<void> {
|
|
1571
1191
|
this._threadReady = false;
|
|
1572
1192
|
const initialized = object(
|
|
1573
1193
|
await this._rpcRequest('initialize', {
|
|
@@ -1587,9 +1207,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1587
1207
|
this._rpcNotify('initialized');
|
|
1588
1208
|
}
|
|
1589
1209
|
|
|
1590
|
-
|
|
1591
|
-
message: string,
|
|
1592
|
-
): Promise<{ input: number; output: number } | undefined> {
|
|
1210
|
+
private async _sendTurn(message: string): Promise<{ input: number; output: number } | undefined> {
|
|
1593
1211
|
const a = this._inflight!.evidence;
|
|
1594
1212
|
if (!this._threadReady) {
|
|
1595
1213
|
const binding = this._externalSessionId;
|
|
@@ -1673,6 +1291,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1673
1291
|
timestamp: Date.now(),
|
|
1674
1292
|
threadId: id,
|
|
1675
1293
|
nativeSessionId: this._nativeSessionId,
|
|
1294
|
+
correlation: 'unknown',
|
|
1676
1295
|
unattributedReason: 'session-configuration',
|
|
1677
1296
|
raw: {
|
|
1678
1297
|
type: 'thread-configured',
|
|
@@ -1728,7 +1347,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1728
1347
|
return a.tokens;
|
|
1729
1348
|
}
|
|
1730
1349
|
|
|
1731
|
-
|
|
1350
|
+
private _observeRpcResult(id: number, result: unknown): void {
|
|
1732
1351
|
const a = this._inflight?.evidence;
|
|
1733
1352
|
if (!a || this._appAdmission !== a.admissionId || a.identity.rpcRequestId !== id) return;
|
|
1734
1353
|
const turn = this._turn(object(result)?.turn);
|
|
@@ -1756,7 +1375,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1756
1375
|
return turn as { id: string; status: string; error?: unknown };
|
|
1757
1376
|
}
|
|
1758
1377
|
|
|
1759
|
-
|
|
1378
|
+
private _classify(msg: Record<string, unknown>): SessionEvent[] {
|
|
1760
1379
|
const method = typeof msg.method === 'string' ? msg.method : undefined;
|
|
1761
1380
|
if (!method) return [];
|
|
1762
1381
|
if (method === 'account/rateLimits/updated') {
|
|
@@ -1995,7 +1614,7 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
1995
1614
|
return unowned('malformed-tool');
|
|
1996
1615
|
const error = object(item.error),
|
|
1997
1616
|
result = object(item.result);
|
|
1998
|
-
const output = result ? publicMcpResult(
|
|
1617
|
+
const output = result ? publicMcpResult(result.content) : undefined;
|
|
1999
1618
|
if (item.error != null && typeof error?.message !== 'string')
|
|
2000
1619
|
return unowned('malformed-tool');
|
|
2001
1620
|
prior.completed = true;
|
|
@@ -2022,17 +1641,8 @@ export class CodexAppServerSession extends BaseCodexSession {
|
|
|
2022
1641
|
}
|
|
2023
1642
|
|
|
2024
1643
|
// ---------------------------------------------------------------------------
|
|
2025
|
-
//
|
|
1644
|
+
// Helpers
|
|
2026
1645
|
// ---------------------------------------------------------------------------
|
|
2027
|
-
//
|
|
2028
|
-
// Both protocols carry the same ThreadItem / event shapes (agent message,
|
|
2029
|
-
// reasoning, command execution, token usage). `tag` is the discriminant field
|
|
2030
|
-
// ("type" for both mcp `codex/event.msg.type` and app-server `item.type`).
|
|
2031
|
-
//
|
|
2032
|
-
// agent_message / agentMessage → text
|
|
2033
|
-
// reasoning / agent_reasoning → thinking
|
|
2034
|
-
// command_execution / commandExecution → tool_use (+ tool_result when done)
|
|
2035
|
-
// token_count / usage → tokens (attached to a result event)
|
|
2036
1646
|
|
|
2037
1647
|
const object = (value: unknown): Record<string, unknown> | undefined =>
|
|
2038
1648
|
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -2117,25 +1727,15 @@ function publicMcpArguments(value: unknown): Pick<SessionEvent, 'toolInput' | 't
|
|
|
2117
1727
|
};
|
|
2118
1728
|
}
|
|
2119
1729
|
|
|
2120
|
-
/**
|
|
2121
|
-
*
|
|
2122
|
-
* blocks (including the bridge's JSON receipt), not arbitrary envelope fields. */
|
|
1730
|
+
/** An mcpToolCall result's content: public text blocks (including the bridge's JSON receipt)
|
|
1731
|
+
* are kept; any other block keeps only a known type label. */
|
|
2123
1732
|
function publicMcpResult(
|
|
2124
1733
|
value: unknown,
|
|
2125
|
-
): Pick<SessionEvent, 'toolOutput' | '
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
if (
|
|
2131
|
-
!ok ||
|
|
2132
|
-
!Array.isArray(ok.content) ||
|
|
2133
|
-
(ok.isError !== undefined && typeof ok.isError !== 'boolean')
|
|
2134
|
-
)
|
|
2135
|
-
return;
|
|
2136
|
-
let omitted = Object.keys(ok).some((k) => k !== 'content' && k !== 'isError');
|
|
2137
|
-
const content = ok.content.map((value) => {
|
|
2138
|
-
const block = object(value);
|
|
1734
|
+
): Pick<SessionEvent, 'toolOutput' | 'toolOutputOmitted'> | undefined {
|
|
1735
|
+
if (!Array.isArray(value)) return;
|
|
1736
|
+
let omitted = false;
|
|
1737
|
+
const content = value.map((entry) => {
|
|
1738
|
+
const block = object(entry);
|
|
2139
1739
|
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
2140
1740
|
if (Object.keys(block).some((k) => k !== 'type' && k !== 'text')) omitted = true;
|
|
2141
1741
|
return { type: 'text', text: block.text };
|
|
@@ -2149,113 +1749,7 @@ function publicMcpResult(
|
|
|
2149
1749
|
};
|
|
2150
1750
|
});
|
|
2151
1751
|
return {
|
|
2152
|
-
toolOutput: JSON.stringify({
|
|
2153
|
-
content,
|
|
2154
|
-
...(ok.isError !== undefined ? { isError: ok.isError } : {}),
|
|
2155
|
-
}),
|
|
2156
|
-
...(ok.isError !== undefined ? { toolError: ok.isError as boolean } : {}),
|
|
1752
|
+
toolOutput: JSON.stringify({ content }),
|
|
2157
1753
|
...(omitted ? { toolOutputOmitted: true } : {}),
|
|
2158
1754
|
};
|
|
2159
1755
|
}
|
|
2160
|
-
|
|
2161
|
-
function classifyCodexEvent(ev: Record<string, unknown>, tag: string): SessionEvent[] {
|
|
2162
|
-
const ts = Date.now();
|
|
2163
|
-
const type = String(ev[tag] ?? '').toLowerCase();
|
|
2164
|
-
const events: SessionEvent[] = [];
|
|
2165
|
-
|
|
2166
|
-
// Agent message → text.
|
|
2167
|
-
if (type === 'agent_message' || type === 'agentmessage' || type === 'agent_message_delta') {
|
|
2168
|
-
const text = (ev.message ?? ev.text ?? ev.delta) as string | undefined;
|
|
2169
|
-
if (text) events.push({ kind: 'text', timestamp: ts, text, raw: ev });
|
|
2170
|
-
return events;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// Reasoning → thinking.
|
|
2174
|
-
if (
|
|
2175
|
-
type === 'reasoning' ||
|
|
2176
|
-
type === 'agent_reasoning' ||
|
|
2177
|
-
type === 'agentreasoning' ||
|
|
2178
|
-
type === 'agent_reasoning_delta'
|
|
2179
|
-
) {
|
|
2180
|
-
const text = (ev.text ?? ev.reasoning ?? ev.delta ?? ev.summary) as string | undefined;
|
|
2181
|
-
if (text) events.push({ kind: 'thinking', timestamp: ts, text, raw: ev });
|
|
2182
|
-
return events;
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
// Command execution → tool_use, plus tool_result if output is present.
|
|
2186
|
-
if (
|
|
2187
|
-
type === 'command_execution' ||
|
|
2188
|
-
type === 'commandexecution' ||
|
|
2189
|
-
type === 'exec_command_begin' ||
|
|
2190
|
-
type === 'exec_command_end'
|
|
2191
|
-
) {
|
|
2192
|
-
const command = (ev.command ?? ev.cmd) as string | string[] | undefined;
|
|
2193
|
-
const cmdStr = Array.isArray(command) ? command.join(' ') : command;
|
|
2194
|
-
if (type !== 'exec_command_end')
|
|
2195
|
-
events.push({
|
|
2196
|
-
kind: 'tool_use',
|
|
2197
|
-
timestamp: ts,
|
|
2198
|
-
toolName: 'shell',
|
|
2199
|
-
toolInput: cmdStr ? { command: cmdStr } : (ev as Record<string, unknown>),
|
|
2200
|
-
raw: ev,
|
|
2201
|
-
});
|
|
2202
|
-
const output = (ev.output ?? ev.stdout ?? ev.aggregated_output) as string | undefined;
|
|
2203
|
-
const exitCode = (ev.exit_code ?? ev.exitCode) as number | undefined;
|
|
2204
|
-
if (output !== undefined || exitCode !== undefined) {
|
|
2205
|
-
events.push({
|
|
2206
|
-
kind: 'tool_result',
|
|
2207
|
-
timestamp: ts,
|
|
2208
|
-
toolOutput: output ?? '',
|
|
2209
|
-
toolError: exitCode !== undefined && exitCode !== 0,
|
|
2210
|
-
raw: ev,
|
|
2211
|
-
});
|
|
2212
|
-
}
|
|
2213
|
-
return events;
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
// Token usage → carried on a result-less event so accounting picks it up.
|
|
2217
|
-
if (
|
|
2218
|
-
type === 'token_count' ||
|
|
2219
|
-
type === 'token_usage' ||
|
|
2220
|
-
type === 'usage' ||
|
|
2221
|
-
type === 'tokenusage'
|
|
2222
|
-
) {
|
|
2223
|
-
const u = (ev.info ?? ev.usage ?? ev) as Record<string, unknown>;
|
|
2224
|
-
const input = (u.input_tokens ?? u.inputTokens ?? u.total_input_tokens ?? 0) as number;
|
|
2225
|
-
const output = (u.output_tokens ?? u.outputTokens ?? u.total_output_tokens ?? 0) as number;
|
|
2226
|
-
if (input || output) {
|
|
2227
|
-
events.push({
|
|
2228
|
-
kind: 'result',
|
|
2229
|
-
timestamp: ts,
|
|
2230
|
-
tokens: { input, output },
|
|
2231
|
-
raw: ev,
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
return events;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
// Errors.
|
|
2238
|
-
if (type === 'error' || type === 'stream_error') {
|
|
2239
|
-
events.push({
|
|
2240
|
-
kind: 'error',
|
|
2241
|
-
timestamp: ts,
|
|
2242
|
-
text: (ev.message ?? ev.error ?? JSON.stringify(ev)) as string,
|
|
2243
|
-
raw: ev,
|
|
2244
|
-
});
|
|
2245
|
-
return events;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// Unclassified events are preserved via `raw` on nothing — but we keep them
|
|
2249
|
-
// out of turn content. Oracle can still introspect via the live stream.
|
|
2250
|
-
return events;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
// ---------------------------------------------------------------------------
|
|
2254
|
-
// Default export selection
|
|
2255
|
-
// ---------------------------------------------------------------------------
|
|
2256
|
-
|
|
2257
|
-
/**
|
|
2258
|
-
* The default CodexSession is the MCP variant (stdio mcp-server). Import
|
|
2259
|
-
* CodexAppServerSession explicitly for the experimental app-server variant.
|
|
2260
|
-
*/
|
|
2261
|
-
export const CodexSession = CodexMcpSession;
|