specrails-core 6.4.0 → 6.5.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/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +1 -1
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +30 -2
- package/dist/agent-runtime/compact/environment.js +120 -2
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +54 -21
- package/dist/agent-runtime/engine/pieces/verify.js +3 -5
- package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- package/dist/installer/util/registry.js +3 -3
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +42 -1
- package/package.json +11 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SessionPolicy } from '../../domain/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Names of MCP servers declared in the user's Codex config. `-c mcp_servers={}`
|
|
4
|
+
* does not remove them (overrides merge), so isolation disables each one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function declaredCodexMcpServers(env?: NodeJS.ProcessEnv): string[];
|
|
7
|
+
/**
|
|
8
|
+
* `codex app-server` argv for a policy. Sandbox and approvals travel in the
|
|
9
|
+
* JSON-RPC thread request; config-level switches go through `-c`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function codexArgs(policy: SessionPolicy, declaredServers: readonly string[]): string[];
|
|
12
|
+
export declare function codexSandbox(permissions: SessionPolicy['permissions']): 'danger-full-access' | 'workspace-write' | 'read-only';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { nativeSubagentsAllowed } from '../../domain/policy.js';
|
|
5
|
+
/** TOML string literal (basic string) for a `-c key=value` override. */
|
|
6
|
+
function tomlString(value) {
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
/** TOML key segment: bare when possible, quoted otherwise. */
|
|
10
|
+
function tomlKey(name) {
|
|
11
|
+
return /^[A-Za-z0-9_-]+$/.test(name) ? name : JSON.stringify(name);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Names of MCP servers declared in the user's Codex config. `-c mcp_servers={}`
|
|
15
|
+
* does not remove them (overrides merge), so isolation disables each one.
|
|
16
|
+
*/
|
|
17
|
+
export function declaredCodexMcpServers(env = process.env) {
|
|
18
|
+
const home = env.CODEX_HOME ?? path.join(env.HOME ?? os.homedir(), '.codex');
|
|
19
|
+
const file = path.join(home, 'config.toml');
|
|
20
|
+
if (!existsSync(file))
|
|
21
|
+
return [];
|
|
22
|
+
const names = new Set();
|
|
23
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
24
|
+
const match = /^\s*\[\s*mcp_servers\.(?:"((?:[^"\\]|\\.)+)"|([A-Za-z0-9_-]+))\s*\]\s*(?:#.*)?$/.exec(line);
|
|
25
|
+
if (match)
|
|
26
|
+
names.add(match[1] ? JSON.parse(`"${match[1]}"`) : match[2]);
|
|
27
|
+
}
|
|
28
|
+
return [...names];
|
|
29
|
+
}
|
|
30
|
+
function serverOverrides(server) {
|
|
31
|
+
const key = `mcp_servers.${tomlKey(server.name)}`;
|
|
32
|
+
const values = [`${key}.enabled=true`];
|
|
33
|
+
if (server.url) {
|
|
34
|
+
values.push(`${key}.url=${tomlString(server.url)}`);
|
|
35
|
+
for (const [header, value] of Object.entries(server.headers ?? {}))
|
|
36
|
+
values.push(`${key}.http_headers.${tomlKey(header)}=${tomlString(value)}`);
|
|
37
|
+
}
|
|
38
|
+
else if (server.command) {
|
|
39
|
+
values.push(`${key}.command=${tomlString(server.command)}`);
|
|
40
|
+
values.push(`${key}.args=[${(server.args ?? []).map(tomlString).join(',')}]`);
|
|
41
|
+
for (const [name, value] of Object.entries(server.env ?? {}))
|
|
42
|
+
values.push(`${key}.env.${tomlKey(name)}=${tomlString(value)}`);
|
|
43
|
+
}
|
|
44
|
+
// Codex gates MCP tools it cannot classify; under approvalPolicy=never it refuses them.
|
|
45
|
+
if (server.autoApprove)
|
|
46
|
+
values.push(`${key}.default_tools_approval_mode="approve"`);
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `codex app-server` argv for a policy. Sandbox and approvals travel in the
|
|
51
|
+
* JSON-RPC thread request; config-level switches go through `-c`.
|
|
52
|
+
*/
|
|
53
|
+
export function codexArgs(policy, declaredServers) {
|
|
54
|
+
const overrides = [];
|
|
55
|
+
if (!nativeSubagentsAllowed(policy))
|
|
56
|
+
overrides.push('features.multi_agent=false');
|
|
57
|
+
else if (policy.subagentRuntime.mode === 'native') {
|
|
58
|
+
// Verified live (codex-cli 0.160.1): a luna parent ran a terra/low child.
|
|
59
|
+
if (policy.subagentRuntime.model)
|
|
60
|
+
overrides.push(`agents.default_subagent_model=${tomlString(policy.subagentRuntime.model)}`);
|
|
61
|
+
if (policy.subagentRuntime.effort)
|
|
62
|
+
overrides.push(`agents.default_subagent_reasoning_effort=${tomlString(policy.subagentRuntime.effort)}`);
|
|
63
|
+
}
|
|
64
|
+
const wanted = new Set(policy.mcp.servers.map((server) => server.name));
|
|
65
|
+
if (!policy.mcp.inheritUserScope) {
|
|
66
|
+
for (const name of declaredServers)
|
|
67
|
+
if (!wanted.has(name))
|
|
68
|
+
overrides.push(`mcp_servers.${tomlKey(name)}.enabled=false`);
|
|
69
|
+
}
|
|
70
|
+
for (const server of policy.mcp.servers)
|
|
71
|
+
overrides.push(...serverOverrides(server));
|
|
72
|
+
return [...overrides.flatMap((value) => ['-c', value]), 'app-server', '--listen', 'stdio://'];
|
|
73
|
+
}
|
|
74
|
+
export function codexSandbox(permissions) {
|
|
75
|
+
return permissions === 'bypass' ? 'danger-full-access' : permissions === 'read-only' ? 'read-only' : 'workspace-write';
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DriverDescriptor } from '../../domain/types.js';
|
|
2
|
+
import type { DriverEventSink, DriverFactory, DriverOpenSpec, DriverSession } from '../../ports.js';
|
|
3
|
+
import { type ProcessSpawner } from '../common/process.js';
|
|
4
|
+
export declare const CODEX_DESCRIPTOR: DriverDescriptor;
|
|
5
|
+
export interface CodexDriverOptions {
|
|
6
|
+
binary?: string;
|
|
7
|
+
spawner?: ProcessSpawner;
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
terminateGraceMs?: number;
|
|
10
|
+
rpcTimeoutMs?: number;
|
|
11
|
+
/** Override discovery of the user's declared MCP servers (tests). */
|
|
12
|
+
declaredMcpServers?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare class CodexDriverFactory implements DriverFactory {
|
|
15
|
+
private readonly options;
|
|
16
|
+
readonly descriptor: DriverDescriptor;
|
|
17
|
+
constructor(options?: CodexDriverOptions);
|
|
18
|
+
open(spec: DriverOpenSpec, sink: DriverEventSink): Promise<DriverSession>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { spawnResidentProcess } from '../common/process.js';
|
|
2
|
+
import { codexArgs, codexSandbox, declaredCodexMcpServers } from './argv.js';
|
|
3
|
+
import { JsonRpcPeer, RpcError } from './rpc.js';
|
|
4
|
+
import { CodexTranslator } from './translator.js';
|
|
5
|
+
export const CODEX_DESCRIPTOR = Object.freeze({
|
|
6
|
+
id: 'codex',
|
|
7
|
+
displayName: 'Codex',
|
|
8
|
+
testedVersions: ['0.153.4', '0.160.1'],
|
|
9
|
+
capabilities: Object.freeze({
|
|
10
|
+
resident: true,
|
|
11
|
+
// Codex starts a turn per request; the application holds input while one runs.
|
|
12
|
+
nativeInputQueue: false,
|
|
13
|
+
subagents: 'supported',
|
|
14
|
+
subagentDisable: true,
|
|
15
|
+
subagentModel: true,
|
|
16
|
+
subagentEffort: true,
|
|
17
|
+
// The parent only reacts to finished sub-agents through `wait` or new input.
|
|
18
|
+
autonomousContinuation: false,
|
|
19
|
+
steer: true,
|
|
20
|
+
toolFiltering: false,
|
|
21
|
+
usage: Object.freeze({ costUsd: 'none', tokens: 'cumulative' }),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
function userInput(input) {
|
|
25
|
+
const images = (input.attachments ?? []).filter((item) => item.kind === 'image');
|
|
26
|
+
const files = (input.attachments ?? []).filter((item) => item.kind === 'file');
|
|
27
|
+
const text = files.length ? `${input.text}\n\nAttached files:\n${files.map((file) => `- ${file.path}`).join('\n')}` : input.text;
|
|
28
|
+
return [{ type: 'text', text, text_elements: [] }, ...images.map((image) => ({ type: 'localImage', path: image.path }))];
|
|
29
|
+
}
|
|
30
|
+
class CodexDriverSession {
|
|
31
|
+
child;
|
|
32
|
+
peer;
|
|
33
|
+
translator;
|
|
34
|
+
threadId;
|
|
35
|
+
spec;
|
|
36
|
+
sink;
|
|
37
|
+
graceMs;
|
|
38
|
+
markClosing;
|
|
39
|
+
pendingStops;
|
|
40
|
+
closing = null;
|
|
41
|
+
constructor(child, peer, translator, threadId, spec, sink, graceMs, markClosing,
|
|
42
|
+
/** Children asked to stop before their first turn started (0.160 announces them first). */
|
|
43
|
+
pendingStops) {
|
|
44
|
+
this.child = child;
|
|
45
|
+
this.peer = peer;
|
|
46
|
+
this.translator = translator;
|
|
47
|
+
this.threadId = threadId;
|
|
48
|
+
this.spec = spec;
|
|
49
|
+
this.sink = sink;
|
|
50
|
+
this.graceMs = graceMs;
|
|
51
|
+
this.markClosing = markClosing;
|
|
52
|
+
this.pendingStops = pendingStops;
|
|
53
|
+
}
|
|
54
|
+
async send(input) {
|
|
55
|
+
const activeTurn = this.translator.activeTurnId;
|
|
56
|
+
if (activeTurn && input.delivery === 'steer') {
|
|
57
|
+
await this.peer.request('turn/steer', { threadId: this.threadId, expectedTurnId: activeTurn, clientUserMessageId: input.inputId, input: userInput(input) });
|
|
58
|
+
this.sink({ kind: 'input.receipt', inputId: input.inputId, state: 'started' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.translator.noteTurnRequested(input.inputId);
|
|
62
|
+
await this.peer.request('turn/start', {
|
|
63
|
+
threadId: this.threadId,
|
|
64
|
+
clientUserMessageId: input.inputId,
|
|
65
|
+
input: userInput(input),
|
|
66
|
+
model: this.spec.model,
|
|
67
|
+
...(this.spec.effort ? { effort: this.spec.effort } : {}),
|
|
68
|
+
});
|
|
69
|
+
this.sink({ kind: 'input.receipt', inputId: input.inputId, state: 'started' });
|
|
70
|
+
}
|
|
71
|
+
async interrupt() {
|
|
72
|
+
const turnId = this.translator.activeTurnId;
|
|
73
|
+
if (!turnId)
|
|
74
|
+
return;
|
|
75
|
+
this.translator.noteInterrupt();
|
|
76
|
+
await this.peer.request('turn/interrupt', { threadId: this.threadId, turnId });
|
|
77
|
+
}
|
|
78
|
+
/** Children are threads: each live one is interrupted on its own thread. */
|
|
79
|
+
async stopSubagents(ids) {
|
|
80
|
+
const targets = this.translator.liveChildren().filter((child) => !ids || ids.includes(child.threadId));
|
|
81
|
+
const stopped = [];
|
|
82
|
+
// A child announced but not yet running is interrupted as soon as its turn starts.
|
|
83
|
+
for (const id of ids ?? []) {
|
|
84
|
+
if (this.translator.isChild(id) && !this.translator.childTurnOf(id)) {
|
|
85
|
+
this.pendingStops.add(id);
|
|
86
|
+
stopped.push(id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const target of targets) {
|
|
90
|
+
try {
|
|
91
|
+
await this.peer.request('turn/interrupt', { threadId: target.threadId, turnId: target.turnId });
|
|
92
|
+
stopped.push(target.threadId);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.sink({ kind: 'diagnostic', level: 'warning', code: 'provider.stop_failed', message: `${target.threadId}: ${error.message}` });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return stopped;
|
|
99
|
+
}
|
|
100
|
+
close(_reason) {
|
|
101
|
+
this.closing ??= (async () => {
|
|
102
|
+
this.markClosing();
|
|
103
|
+
this.peer.close('Codex session closed');
|
|
104
|
+
this.child.endInput();
|
|
105
|
+
await this.child.terminate(this.graceMs);
|
|
106
|
+
})();
|
|
107
|
+
return this.closing;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export class CodexDriverFactory {
|
|
111
|
+
options;
|
|
112
|
+
descriptor = CODEX_DESCRIPTOR;
|
|
113
|
+
constructor(options = {}) {
|
|
114
|
+
this.options = options;
|
|
115
|
+
}
|
|
116
|
+
async open(spec, sink) {
|
|
117
|
+
const env = { ...(this.options.env ?? process.env) };
|
|
118
|
+
const declared = this.options.declaredMcpServers ?? declaredCodexMcpServers(env);
|
|
119
|
+
const translator = new CodexTranslator();
|
|
120
|
+
const pendingStops = new Set();
|
|
121
|
+
let closeRequested = false;
|
|
122
|
+
let ended = false;
|
|
123
|
+
let peer = null;
|
|
124
|
+
const spawner = this.options.spawner ?? spawnResidentProcess;
|
|
125
|
+
const child = spawner({ command: this.options.binary ?? 'codex', args: codexArgs(spec.policy, declared), cwd: spec.cwd, env }, {
|
|
126
|
+
onLine: (line) => {
|
|
127
|
+
if (ended)
|
|
128
|
+
return;
|
|
129
|
+
let frame;
|
|
130
|
+
try {
|
|
131
|
+
frame = JSON.parse(line);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
sink({ kind: 'diagnostic', level: 'warning', code: 'provider.unparseable', message: line.slice(0, 500) });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
peer?.receive(frame);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
peer = new JsonRpcPeer((line) => child.write(line), {
|
|
141
|
+
onNotification: (method, params) => {
|
|
142
|
+
for (const event of translator.notification(method, params))
|
|
143
|
+
sink(event);
|
|
144
|
+
const threadId = typeof params.threadId === 'string' ? params.threadId : null;
|
|
145
|
+
const turnId = threadId ? translator.childTurnOf(threadId) : null;
|
|
146
|
+
if (method === 'turn/started' && threadId && turnId && pendingStops.delete(threadId)) {
|
|
147
|
+
void peer?.request('turn/interrupt', { threadId, turnId }).catch((error) => sink({ kind: 'diagnostic', level: 'warning', code: 'provider.stop_failed', message: `${threadId}: ${error.message}` }));
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
onRequest: (method) => {
|
|
151
|
+
// approvalPolicy is `never`; a request still arriving is declined (the sandbox is the boundary).
|
|
152
|
+
if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval')
|
|
153
|
+
return { decision: 'decline' };
|
|
154
|
+
if (method === 'item/tool/requestUserInput')
|
|
155
|
+
return { answers: {} };
|
|
156
|
+
throw new RpcError(`Unsupported client request ${method}`, -32601);
|
|
157
|
+
},
|
|
158
|
+
}, this.options.rpcTimeoutMs ?? 60_000);
|
|
159
|
+
void child.exited.then((exit) => {
|
|
160
|
+
peer?.close('Codex app-server exited');
|
|
161
|
+
if (!ended && !closeRequested)
|
|
162
|
+
sink({ kind: 'process.exited', exitCode: exit.exitCode, signal: exit.signal });
|
|
163
|
+
ended = true;
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
await peer.request('initialize', { clientInfo: { name: 'specrails_core', title: 'Specrails Core', version: '1' }, capabilities: { experimentalApi: true } });
|
|
167
|
+
peer.notify('initialized');
|
|
168
|
+
const thread = await peer.request(spec.providerSessionRef ? 'thread/resume' : 'thread/start', {
|
|
169
|
+
...(spec.providerSessionRef ? { threadId: spec.providerSessionRef } : {}),
|
|
170
|
+
model: spec.model,
|
|
171
|
+
cwd: spec.cwd,
|
|
172
|
+
approvalPolicy: 'never',
|
|
173
|
+
sandbox: codexSandbox(spec.policy.permissions),
|
|
174
|
+
...(spec.systemPrompt ? { developerInstructions: spec.systemPrompt } : {}),
|
|
175
|
+
});
|
|
176
|
+
const threadId = typeof thread.thread?.id === 'string' ? thread.thread.id : null;
|
|
177
|
+
if (!threadId)
|
|
178
|
+
throw new Error('Codex did not return a thread id');
|
|
179
|
+
if (spec.providerSessionRef && threadId !== spec.providerSessionRef)
|
|
180
|
+
throw new Error('Codex resumed an unexpected thread');
|
|
181
|
+
translator.setRoot(threadId, spec.model);
|
|
182
|
+
sink({ kind: 'process.started', ...(child.pid !== undefined ? { pid: child.pid } : {}) });
|
|
183
|
+
sink({ kind: 'provider.ref', providerSessionRef: threadId });
|
|
184
|
+
return new CodexDriverSession(child, peer, translator, threadId, spec, sink, this.options.terminateGraceMs ?? 5_000, () => { closeRequested = true; }, pendingStops);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
closeRequested = true;
|
|
188
|
+
peer.close('Codex session failed to start');
|
|
189
|
+
await child.terminate(this.options.terminateGraceMs ?? 5_000);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Minimal JSON-RPC 2.0 peer over newline-delimited frames (Codex app-server). */
|
|
2
|
+
type Json = Record<string, unknown>;
|
|
3
|
+
export declare class RpcError extends Error {
|
|
4
|
+
readonly code?: number | undefined;
|
|
5
|
+
constructor(message: string, code?: number | undefined);
|
|
6
|
+
}
|
|
7
|
+
export interface RpcHandlers {
|
|
8
|
+
/** Server notification. */
|
|
9
|
+
onNotification(method: string, params: Json): void;
|
|
10
|
+
/** Server request; return the result (or throw RpcError to answer with an error). */
|
|
11
|
+
onRequest(method: string, params: Json): Json;
|
|
12
|
+
}
|
|
13
|
+
export declare class JsonRpcPeer {
|
|
14
|
+
private readonly write;
|
|
15
|
+
private readonly handlers;
|
|
16
|
+
private readonly timeoutMs;
|
|
17
|
+
private nextId;
|
|
18
|
+
private readonly pending;
|
|
19
|
+
private closedError;
|
|
20
|
+
constructor(write: (line: string) => boolean, handlers: RpcHandlers, timeoutMs?: number);
|
|
21
|
+
request(method: string, params: Json): Promise<Json>;
|
|
22
|
+
notify(method: string, params?: Json): void;
|
|
23
|
+
/** Feed one stdout line. */
|
|
24
|
+
receive(frame: Json): void;
|
|
25
|
+
/** Reject everything in flight (process ended). */
|
|
26
|
+
close(reason: string): void;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class RpcError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'RpcError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class JsonRpcPeer {
|
|
10
|
+
write;
|
|
11
|
+
handlers;
|
|
12
|
+
timeoutMs;
|
|
13
|
+
nextId = 1;
|
|
14
|
+
pending = new Map();
|
|
15
|
+
closedError = null;
|
|
16
|
+
constructor(write, handlers, timeoutMs = 60_000) {
|
|
17
|
+
this.write = write;
|
|
18
|
+
this.handlers = handlers;
|
|
19
|
+
this.timeoutMs = timeoutMs;
|
|
20
|
+
}
|
|
21
|
+
request(method, params) {
|
|
22
|
+
if (this.closedError)
|
|
23
|
+
return Promise.reject(this.closedError);
|
|
24
|
+
const id = this.nextId++;
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
this.pending.delete(id);
|
|
28
|
+
reject(new RpcError(`Codex app-server did not answer ${method} in time`));
|
|
29
|
+
}, this.timeoutMs);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
this.pending.set(id, { method, resolve, reject, timer });
|
|
32
|
+
if (!this.write(JSON.stringify({ id, method, params }))) {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
this.pending.delete(id);
|
|
35
|
+
reject(new RpcError('Codex app-server input is closed'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
notify(method, params) {
|
|
40
|
+
this.write(JSON.stringify(params ? { method, params } : { method }));
|
|
41
|
+
}
|
|
42
|
+
/** Feed one stdout line. */
|
|
43
|
+
receive(frame) {
|
|
44
|
+
const id = frame.id;
|
|
45
|
+
if (typeof frame.method === 'string') {
|
|
46
|
+
if (typeof id === 'number' || typeof id === 'string') {
|
|
47
|
+
try {
|
|
48
|
+
this.write(JSON.stringify({ id, result: this.handlers.onRequest(frame.method, (frame.params ?? {})) }));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
this.write(JSON.stringify({ id, error: { code: error instanceof RpcError ? error.code ?? -32603 : -32603, message: error.message } }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
this.handlers.onNotification(frame.method, (frame.params ?? {}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof id !== 'number')
|
|
59
|
+
return;
|
|
60
|
+
const request = this.pending.get(id);
|
|
61
|
+
if (!request)
|
|
62
|
+
return;
|
|
63
|
+
this.pending.delete(id);
|
|
64
|
+
clearTimeout(request.timer);
|
|
65
|
+
if (frame.error && typeof frame.error === 'object') {
|
|
66
|
+
const error = frame.error;
|
|
67
|
+
request.reject(new RpcError(typeof error.message === 'string' ? error.message : `Codex rejected ${request.method}`, typeof error.code === 'number' ? error.code : undefined));
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
request.resolve((frame.result ?? {}));
|
|
71
|
+
}
|
|
72
|
+
/** Reject everything in flight (process ended). */
|
|
73
|
+
close(reason) {
|
|
74
|
+
this.closedError = new RpcError(reason);
|
|
75
|
+
for (const [id, request] of this.pending) {
|
|
76
|
+
clearTimeout(request.timer);
|
|
77
|
+
request.reject(this.closedError);
|
|
78
|
+
this.pending.delete(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DriverEvent } from '../../ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* Anti-corruption layer for `codex app-server` notifications (codex-cli 0.153.4
|
|
4
|
+
* and 0.160.1). Deterministic and free of I/O. Pinned by the recorded transcripts:
|
|
5
|
+
* - the session is one root thread; sub-agents are child threads. 0.153 announces
|
|
6
|
+
* them with a `collabAgentToolCall` (`spawnAgent`, `receiverThreadIds`); 0.160
|
|
7
|
+
* with a `subAgentActivity` item (`kind: started`, `agentThreadId`, `agentPath`)
|
|
8
|
+
* and no prompt, so the description comes from the agent's path name;
|
|
9
|
+
* - every thread streams its own turn/item/tokenUsage notifications, interleaved;
|
|
10
|
+
* - children keep running after the parent's turn completes; the parent never
|
|
11
|
+
* continues on its own (it collects them with `wait`).
|
|
12
|
+
*/
|
|
13
|
+
type Json = Record<string, unknown>;
|
|
14
|
+
export declare class CodexTranslator {
|
|
15
|
+
private rootThread;
|
|
16
|
+
private rootTurn;
|
|
17
|
+
/** Inputs whose `turn/start` was acknowledged, waiting for the turn notification. */
|
|
18
|
+
private pendingInputs;
|
|
19
|
+
private rootText;
|
|
20
|
+
private rootFinal;
|
|
21
|
+
private readonly streamed;
|
|
22
|
+
private readonly startedTools;
|
|
23
|
+
private readonly completedItems;
|
|
24
|
+
/** child thread → owner (null = root agent, else the owning child thread). */
|
|
25
|
+
private readonly children;
|
|
26
|
+
private readonly childTurn;
|
|
27
|
+
private readonly childPhase;
|
|
28
|
+
private readonly childLastText;
|
|
29
|
+
private rootUsage;
|
|
30
|
+
private model;
|
|
31
|
+
private interruptRequested;
|
|
32
|
+
setRoot(threadId: string, model: string | null): void;
|
|
33
|
+
get activeTurnId(): string | null;
|
|
34
|
+
noteTurnRequested(inputId: string): void;
|
|
35
|
+
noteInterrupt(): void;
|
|
36
|
+
/** Child threads with a turn in progress. */
|
|
37
|
+
liveChildren(): Array<{
|
|
38
|
+
threadId: string;
|
|
39
|
+
turnId: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** A registered child thread (live or not). */
|
|
42
|
+
isChild(threadId: string): boolean;
|
|
43
|
+
/** The turn a child is running, if any. */
|
|
44
|
+
childTurnOf(threadId: string): string | null;
|
|
45
|
+
notification(method: string, params: Json): DriverEvent[];
|
|
46
|
+
private root;
|
|
47
|
+
private child;
|
|
48
|
+
private item;
|
|
49
|
+
private collab;
|
|
50
|
+
private registerChild;
|
|
51
|
+
private roster;
|
|
52
|
+
}
|
|
53
|
+
export {};
|