@peakflo/agent-harness 0.0.0-stage → 0.2.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/adapters/acp-adapter.d.ts +177 -0
- package/dist/adapters/acp-adapter.js +1732 -0
- package/dist/adapters/claude-code-adapter.d.ts +171 -0
- package/dist/adapters/claude-code-adapter.js +1918 -0
- package/dist/adapters/codex-adapter.d.ts +67 -0
- package/dist/adapters/codex-adapter.js +681 -0
- package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
- package/dist/adapters/codex-app-server-adapter.js +1309 -0
- package/dist/adapters/coding-agent-adapter.d.ts +289 -0
- package/dist/adapters/coding-agent-adapter.js +31 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +35 -0
- package/dist/adapters/model-gateway.d.ts +84 -0
- package/dist/adapters/model-gateway.js +270 -0
- package/dist/adapters/opencode-adapter.d.ts +403 -0
- package/dist/adapters/opencode-adapter.js +2710 -0
- package/dist/adapters/opencode-config.d.ts +41 -0
- package/dist/adapters/opencode-config.js +147 -0
- package/dist/adapters/opencode-server-policy.d.ts +82 -0
- package/dist/adapters/opencode-server-policy.js +148 -0
- package/dist/adapters/pi-adapter.d.ts +168 -0
- package/dist/adapters/pi-adapter.js +1468 -0
- package/dist/capabilities.d.ts +58 -0
- package/dist/capabilities.js +161 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +231 -0
- package/dist/contract.d.ts +273 -0
- package/dist/contract.js +357 -0
- package/dist/control-plane.d.ts +178 -0
- package/dist/control-plane.js +431 -0
- package/dist/credential-store.d.ts +110 -0
- package/dist/credential-store.js +181 -0
- package/dist/enrol/browser-enrol.d.ts +85 -0
- package/dist/enrol/browser-enrol.js +387 -0
- package/dist/github-command-guards.d.ts +20 -0
- package/dist/github-command-guards.js +330 -0
- package/dist/github-proxy-certificates.d.ts +25 -0
- package/dist/github-proxy-certificates.js +145 -0
- package/dist/github-proxy.d.ts +37 -0
- package/dist/github-proxy.js +476 -0
- package/dist/github-workspace.d.ts +55 -0
- package/dist/github-workspace.js +537 -0
- package/dist/harness.d.ts +333 -0
- package/dist/harness.js +1414 -0
- package/dist/health.d.ts +45 -0
- package/dist/health.js +116 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +123 -0
- package/dist/model-probe.d.ts +96 -0
- package/dist/model-probe.js +195 -0
- package/dist/options.d.ts +15 -0
- package/dist/options.js +212 -0
- package/dist/ports-default.d.ts +212 -0
- package/dist/ports-default.js +333 -0
- package/dist/ports.d.ts +436 -0
- package/dist/ports.js +27 -0
- package/dist/runtime/attachments.d.ts +92 -0
- package/dist/runtime/attachments.js +377 -0
- package/dist/runtime/child-stream-guards.d.ts +45 -0
- package/dist/runtime/child-stream-guards.js +111 -0
- package/dist/runtime/conversation.d.ts +87 -0
- package/dist/runtime/conversation.js +156 -0
- package/dist/runtime/lease-renewal.d.ts +169 -0
- package/dist/runtime/lease-renewal.js +433 -0
- package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
- package/dist/runtime/mcp-auth-proxy.js +300 -0
- package/dist/runtime/permission-mode.d.ts +116 -0
- package/dist/runtime/permission-mode.js +146 -0
- package/dist/runtime/projection.d.ts +71 -0
- package/dist/runtime/projection.js +211 -0
- package/dist/runtime/session-runner.d.ts +520 -0
- package/dist/runtime/session-runner.js +2122 -0
- package/dist/shared/reasoning-effort.d.ts +11 -0
- package/dist/shared/reasoning-effort.js +20 -0
- package/dist/store/conversation-store.d.ts +100 -0
- package/dist/store/conversation-store.js +305 -0
- package/dist/store/sqlite-store.d.ts +120 -0
- package/dist/store/sqlite-store.js +346 -0
- package/dist/types.d.ts +952 -0
- package/dist/types.js +55 -0
- package/dist/version.d.ts +32 -0
- package/dist/version.js +77 -0
- package/package.json +53 -4
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex Adapter
|
|
4
|
+
*
|
|
5
|
+
* Implements CodingAgentAdapter for OpenAI Codex via direct CLI integration.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from Claude Code:
|
|
8
|
+
* - Uses JSON-RPC over stdio (like MCP protocol)
|
|
9
|
+
* - Spawns `codex` CLI process per session
|
|
10
|
+
* - Thread-based session model (thread_id instead of session_id)
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.CodexAdapter = exports.DEFAULT_CODEX_MODEL = void 0;
|
|
47
|
+
/**
|
|
48
|
+
* Extracted verbatim from 20x `src/main/adapters/codex-adapter.ts`.
|
|
49
|
+
* CHANGED: imports only — `../child-stream-guards` ->
|
|
50
|
+
* `../runtime/child-stream-guards`. Source formatting (no semicolons)
|
|
51
|
+
* preserved.
|
|
52
|
+
*/
|
|
53
|
+
const child_process_1 = require("child_process");
|
|
54
|
+
const fs_1 = require("fs");
|
|
55
|
+
const child_stream_guards_1 = require("../runtime/child-stream-guards");
|
|
56
|
+
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
57
|
+
exports.DEFAULT_CODEX_MODEL = 'gpt-5.6-sol';
|
|
58
|
+
class CodexAdapter {
|
|
59
|
+
sessions = new Map();
|
|
60
|
+
codexExecutablePath = null;
|
|
61
|
+
/** Callback set by agent-manager to trigger an immediate poll cycle */
|
|
62
|
+
onDataAvailable;
|
|
63
|
+
async initialize() {
|
|
64
|
+
// Find codex CLI executable
|
|
65
|
+
this.codexExecutablePath = await this.findCodexExecutable();
|
|
66
|
+
console.log(`[CodexAdapter] Initialized with executable: ${this.codexExecutablePath}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Find the Codex CLI executable path
|
|
70
|
+
*/
|
|
71
|
+
async findCodexExecutable() {
|
|
72
|
+
if (this.codexExecutablePath) {
|
|
73
|
+
return this.codexExecutablePath;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
77
|
+
const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
|
|
78
|
+
const execFileAsync = promisify(execFile);
|
|
79
|
+
// Try to find codex in PATH
|
|
80
|
+
const isWin = process.platform === 'win32';
|
|
81
|
+
const whichCmd = isWin ? 'where' : 'which';
|
|
82
|
+
const binaryName = isWin ? 'codex.cmd' : 'codex';
|
|
83
|
+
const { stdout } = await execFileAsync(whichCmd, [binaryName]);
|
|
84
|
+
this.codexExecutablePath = stdout.trim().split(/\r?\n/)[0];
|
|
85
|
+
console.log(`[CodexAdapter] Found codex executable at: ${this.codexExecutablePath}`);
|
|
86
|
+
return this.codexExecutablePath;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Common installation locations — must cover Homebrew, npm globals,
|
|
90
|
+
// pnpm globals, Volta, and NVM paths so the binary is found even when
|
|
91
|
+
// the shell PATH wasn't fully resolved (packaged macOS GUI app).
|
|
92
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
93
|
+
const commonPaths = process.platform === 'win32'
|
|
94
|
+
? [
|
|
95
|
+
`${home}\\AppData\\Roaming\\npm\\codex.cmd`,
|
|
96
|
+
`${home}\\.local\\bin\\codex.cmd`
|
|
97
|
+
]
|
|
98
|
+
: [
|
|
99
|
+
'/usr/local/bin/codex',
|
|
100
|
+
'/opt/homebrew/bin/codex',
|
|
101
|
+
`${home}/.local/bin/codex`,
|
|
102
|
+
`${home}/.npm-global/bin/codex`,
|
|
103
|
+
`${home}/Library/pnpm/codex`,
|
|
104
|
+
`${home}/.volta/bin/codex`,
|
|
105
|
+
];
|
|
106
|
+
// Dynamically detect NVM-managed npm global bin (avoids hardcoding a Node version)
|
|
107
|
+
if (process.platform !== 'win32' && home) {
|
|
108
|
+
try {
|
|
109
|
+
const { join } = await Promise.resolve().then(() => __importStar(require('path')));
|
|
110
|
+
const { readdirSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
111
|
+
const nvmDir = join(home, '.nvm', 'versions', 'node');
|
|
112
|
+
const versions = readdirSync(nvmDir);
|
|
113
|
+
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
114
|
+
for (const v of versions) {
|
|
115
|
+
commonPaths.push(join(nvmDir, v, 'bin', 'codex'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// NVM not installed — skip
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const path of commonPaths) {
|
|
123
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
124
|
+
this.codexExecutablePath = path;
|
|
125
|
+
console.log(`[CodexAdapter] Found codex executable at: ${this.codexExecutablePath}`);
|
|
126
|
+
return this.codexExecutablePath;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error('Codex CLI not found. Install it from OpenAI: https://platform.openai.com/docs/codex');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build environment variables for Codex process
|
|
134
|
+
*/
|
|
135
|
+
buildCodexEnvironment() {
|
|
136
|
+
const env = { ...process.env };
|
|
137
|
+
// Ensure API key is available
|
|
138
|
+
if (!env.OPENAI_API_KEY && !env.CODEX_API_KEY) {
|
|
139
|
+
console.warn('[CodexAdapter] No OPENAI_API_KEY or CODEX_API_KEY found in environment');
|
|
140
|
+
}
|
|
141
|
+
return env;
|
|
142
|
+
}
|
|
143
|
+
buildCodexArgs(config) {
|
|
144
|
+
const args = [
|
|
145
|
+
'--model',
|
|
146
|
+
config.model || exports.DEFAULT_CODEX_MODEL,
|
|
147
|
+
];
|
|
148
|
+
if (config.reasoningEffort && config.reasoningEffort !== 'max') {
|
|
149
|
+
args.push('-c', `model_reasoning_effort="${config.reasoningEffort}"`);
|
|
150
|
+
}
|
|
151
|
+
args.push('--json-rpc');
|
|
152
|
+
return args;
|
|
153
|
+
}
|
|
154
|
+
async createSession(config) {
|
|
155
|
+
if (!this.codexExecutablePath) {
|
|
156
|
+
throw new Error('Codex executable not found');
|
|
157
|
+
}
|
|
158
|
+
const abortController = new AbortController();
|
|
159
|
+
// Spawn codex process
|
|
160
|
+
// On Windows, .cmd wrappers need shell:true to resolve
|
|
161
|
+
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(this.codexExecutablePath);
|
|
162
|
+
const codexProcess = (0, child_process_1.spawn)(this.codexExecutablePath, this.buildCodexArgs(config), {
|
|
163
|
+
cwd: config.workspaceDir,
|
|
164
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
+
env: this.buildCodexEnvironment(),
|
|
166
|
+
...(needsShell ? { shell: true } : {}),
|
|
167
|
+
});
|
|
168
|
+
// Every pipe needs an error listener before the first write. Codex can exit
|
|
169
|
+
// at any moment, and an unhandled EPIPE on its stdin crashes the main
|
|
170
|
+
// process.
|
|
171
|
+
(0, child_stream_guards_1.guardChildStreams)(codexProcess, 'CodexAdapter');
|
|
172
|
+
// Create session state
|
|
173
|
+
const session = {
|
|
174
|
+
sessionId: '', // Will be set to threadId after thread.create
|
|
175
|
+
threadId: '', // Will be set after thread.create
|
|
176
|
+
process: codexProcess,
|
|
177
|
+
workspaceDir: config.workspaceDir,
|
|
178
|
+
messageBuffer: [],
|
|
179
|
+
status: 'idle',
|
|
180
|
+
abortController,
|
|
181
|
+
config,
|
|
182
|
+
lastError: null,
|
|
183
|
+
nextRpcId: 1,
|
|
184
|
+
pendingRpcCalls: new Map(),
|
|
185
|
+
stdoutBuffer: '',
|
|
186
|
+
};
|
|
187
|
+
// Set up stdout parser
|
|
188
|
+
this.setupStdoutParser(codexProcess, session);
|
|
189
|
+
// Set up stderr logging
|
|
190
|
+
codexProcess.stderr?.on('data', (chunk) => {
|
|
191
|
+
console.error('[CodexAdapter] Codex stderr:', chunk.toString());
|
|
192
|
+
});
|
|
193
|
+
// Handle process exit
|
|
194
|
+
codexProcess.on('exit', (code, signal) => {
|
|
195
|
+
console.log(`[CodexAdapter] Codex process exited: code=${code}, signal=${signal}`);
|
|
196
|
+
if (code !== 0 && code !== null) {
|
|
197
|
+
session.status = 'error';
|
|
198
|
+
session.lastError = `Codex process exited with code ${code}`;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// Initialize JSON-RPC session
|
|
202
|
+
try {
|
|
203
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY;
|
|
204
|
+
await this.sendRpcRequest(session, 'initialize', {
|
|
205
|
+
api_key: apiKey,
|
|
206
|
+
model: config.model || exports.DEFAULT_CODEX_MODEL,
|
|
207
|
+
});
|
|
208
|
+
// Create thread (Codex's session concept)
|
|
209
|
+
const threadResponse = (await this.sendRpcRequest(session, 'thread.create', {}));
|
|
210
|
+
session.threadId = threadResponse.thread_id;
|
|
211
|
+
session.sessionId = session.threadId;
|
|
212
|
+
console.log(`[CodexAdapter] Created Codex thread: ${session.threadId}`);
|
|
213
|
+
// Store session
|
|
214
|
+
this.sessions.set(session.threadId, session);
|
|
215
|
+
return session.threadId;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// Cleanup on error
|
|
219
|
+
codexProcess.kill();
|
|
220
|
+
throw new Error(`Failed to create Codex session: ${error instanceof Error ? error.message : String(error)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async resumeSession(sessionId, config) {
|
|
224
|
+
if (!this.codexExecutablePath) {
|
|
225
|
+
throw new Error('Codex executable not found');
|
|
226
|
+
}
|
|
227
|
+
// Validate session ID format (Codex uses thread_xxx format)
|
|
228
|
+
if (!sessionId.startsWith('thread_')) {
|
|
229
|
+
throw new Error('INCOMPATIBLE_SESSION_ID: This session was created with a different coding agent and cannot be resumed with Codex.');
|
|
230
|
+
}
|
|
231
|
+
const abortController = new AbortController();
|
|
232
|
+
// Spawn new codex process
|
|
233
|
+
// On Windows, .cmd wrappers need shell:true to resolve
|
|
234
|
+
const needsShellResume = process.platform === 'win32' && /\.(cmd|bat)$/i.test(this.codexExecutablePath);
|
|
235
|
+
const codexProcess = (0, child_process_1.spawn)(this.codexExecutablePath, this.buildCodexArgs(config), {
|
|
236
|
+
cwd: config.workspaceDir,
|
|
237
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
238
|
+
env: this.buildCodexEnvironment(),
|
|
239
|
+
...(needsShellResume ? { shell: true } : {}),
|
|
240
|
+
});
|
|
241
|
+
// Every pipe needs an error listener before the first write. Codex can exit
|
|
242
|
+
// at any moment, and an unhandled EPIPE on its stdin crashes the main
|
|
243
|
+
// process.
|
|
244
|
+
(0, child_stream_guards_1.guardChildStreams)(codexProcess, 'CodexAdapter');
|
|
245
|
+
// Create session state
|
|
246
|
+
const session = {
|
|
247
|
+
sessionId,
|
|
248
|
+
threadId: sessionId,
|
|
249
|
+
process: codexProcess,
|
|
250
|
+
workspaceDir: config.workspaceDir,
|
|
251
|
+
messageBuffer: [],
|
|
252
|
+
status: 'idle',
|
|
253
|
+
abortController,
|
|
254
|
+
config,
|
|
255
|
+
lastError: null,
|
|
256
|
+
nextRpcId: 1,
|
|
257
|
+
pendingRpcCalls: new Map(),
|
|
258
|
+
stdoutBuffer: '',
|
|
259
|
+
};
|
|
260
|
+
// Set up stdout parser
|
|
261
|
+
this.setupStdoutParser(codexProcess, session);
|
|
262
|
+
// Set up stderr logging
|
|
263
|
+
codexProcess.stderr?.on('data', (chunk) => {
|
|
264
|
+
console.error('[CodexAdapter] Codex stderr:', chunk.toString());
|
|
265
|
+
});
|
|
266
|
+
// Handle process exit
|
|
267
|
+
codexProcess.on('exit', (code, signal) => {
|
|
268
|
+
console.log(`[CodexAdapter] Codex process exited: code=${code}, signal=${signal}`);
|
|
269
|
+
if (code !== 0 && code !== null) {
|
|
270
|
+
session.status = 'error';
|
|
271
|
+
session.lastError = `Codex process exited with code ${code}`;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
try {
|
|
275
|
+
// Initialize JSON-RPC session
|
|
276
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY;
|
|
277
|
+
await this.sendRpcRequest(session, 'initialize', {
|
|
278
|
+
api_key: apiKey,
|
|
279
|
+
model: config.model || exports.DEFAULT_CODEX_MODEL,
|
|
280
|
+
});
|
|
281
|
+
// Fetch thread history
|
|
282
|
+
const historyResponse = (await this.sendRpcRequest(session, 'thread.messages', {
|
|
283
|
+
thread_id: sessionId,
|
|
284
|
+
}));
|
|
285
|
+
// Store session
|
|
286
|
+
this.sessions.set(sessionId, session);
|
|
287
|
+
// Convert history to SessionMessage format
|
|
288
|
+
const messages = historyResponse.messages.map((msg) => ({
|
|
289
|
+
id: msg.id,
|
|
290
|
+
role: msg.role === 'user' ? coding_agent_adapter_1.MessageRole.USER : coding_agent_adapter_1.MessageRole.ASSISTANT,
|
|
291
|
+
parts: [
|
|
292
|
+
{
|
|
293
|
+
id: `${msg.id}-text`,
|
|
294
|
+
type: coding_agent_adapter_1.MessagePartType.TEXT,
|
|
295
|
+
text: msg.content,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
}));
|
|
299
|
+
console.log(`[CodexAdapter] Resumed session ${sessionId} with ${messages.length} messages`);
|
|
300
|
+
return messages;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
// Check if thread not found
|
|
304
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
305
|
+
if (errMsg.includes('thread not found') || errMsg.includes('Thread not found')) {
|
|
306
|
+
codexProcess.kill();
|
|
307
|
+
throw new Error('INCOMPATIBLE_SESSION_ID: This session does not exist on Codex servers. It may have been created with a different coding agent or has expired.');
|
|
308
|
+
}
|
|
309
|
+
// Cleanup on error
|
|
310
|
+
codexProcess.kill();
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async sendPrompt(sessionId, parts, _config) {
|
|
315
|
+
const session = this.sessions.get(sessionId);
|
|
316
|
+
if (!session) {
|
|
317
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
318
|
+
}
|
|
319
|
+
// Extract text from parts
|
|
320
|
+
const promptText = parts
|
|
321
|
+
.filter((p) => p.type === coding_agent_adapter_1.MessagePartType.TEXT && p.text)
|
|
322
|
+
.map((p) => p.text)
|
|
323
|
+
.join('\n');
|
|
324
|
+
if (!promptText) {
|
|
325
|
+
throw new Error('No text content in prompt parts');
|
|
326
|
+
}
|
|
327
|
+
// Add user message to buffer
|
|
328
|
+
const userMessage = {
|
|
329
|
+
type: 'text',
|
|
330
|
+
id: `user-${Date.now()}`,
|
|
331
|
+
role: 'user',
|
|
332
|
+
content: promptText,
|
|
333
|
+
};
|
|
334
|
+
session.messageBuffer.push(userMessage);
|
|
335
|
+
session.status = 'busy';
|
|
336
|
+
try {
|
|
337
|
+
// Send message via JSON-RPC
|
|
338
|
+
await this.sendRpcRequest(session, 'message.create', {
|
|
339
|
+
thread_id: session.threadId,
|
|
340
|
+
role: 'user',
|
|
341
|
+
content: promptText,
|
|
342
|
+
});
|
|
343
|
+
console.log(`[CodexAdapter] Sent prompt to thread ${session.threadId}`);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
session.status = 'error';
|
|
347
|
+
session.lastError = error instanceof Error ? error.message : String(error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async getStatus(sessionId, _config) {
|
|
352
|
+
const session = this.sessions.get(sessionId);
|
|
353
|
+
if (!session) {
|
|
354
|
+
return { type: coding_agent_adapter_1.SessionStatusType.ERROR, message: 'Session not found' };
|
|
355
|
+
}
|
|
356
|
+
if (session.lastError) {
|
|
357
|
+
return { type: coding_agent_adapter_1.SessionStatusType.ERROR, message: session.lastError };
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
type: session.status === 'busy' ? coding_agent_adapter_1.SessionStatusType.BUSY : coding_agent_adapter_1.SessionStatusType.IDLE,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async pollMessages(sessionId, seenMessageIds, seenPartIds, partContentLengths, _config) {
|
|
364
|
+
const session = this.sessions.get(sessionId);
|
|
365
|
+
if (!session) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
const newParts = [];
|
|
369
|
+
// Process buffered messages
|
|
370
|
+
for (const codexMsg of session.messageBuffer) {
|
|
371
|
+
if (seenMessageIds.has(codexMsg.id))
|
|
372
|
+
continue;
|
|
373
|
+
seenMessageIds.add(codexMsg.id);
|
|
374
|
+
// Convert CodexMessage to MessagePart
|
|
375
|
+
const parts = this.convertCodexMessageToParts(codexMsg, seenPartIds, partContentLengths);
|
|
376
|
+
newParts.push(...parts);
|
|
377
|
+
}
|
|
378
|
+
// Clear processed messages from buffer
|
|
379
|
+
session.messageBuffer = [];
|
|
380
|
+
return newParts;
|
|
381
|
+
}
|
|
382
|
+
async abortPrompt(sessionId, _config) {
|
|
383
|
+
const session = this.sessions.get(sessionId);
|
|
384
|
+
if (!session) {
|
|
385
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
386
|
+
}
|
|
387
|
+
// Send abort request
|
|
388
|
+
try {
|
|
389
|
+
await this.sendRpcRequest(session, 'message.abort', {
|
|
390
|
+
thread_id: session.threadId,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.warn(`[CodexAdapter] Failed to abort: ${error instanceof Error ? error.message : String(error)}`);
|
|
395
|
+
}
|
|
396
|
+
session.status = 'idle';
|
|
397
|
+
}
|
|
398
|
+
async destroySession(sessionId, _config) {
|
|
399
|
+
const session = this.sessions.get(sessionId);
|
|
400
|
+
if (!session) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// Kill process
|
|
404
|
+
session.process.kill('SIGTERM');
|
|
405
|
+
// Wait a bit, then force kill if needed
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
if (!session.process.killed) {
|
|
408
|
+
session.process.kill('SIGKILL');
|
|
409
|
+
}
|
|
410
|
+
}, 1000);
|
|
411
|
+
// Remove session
|
|
412
|
+
this.sessions.delete(sessionId);
|
|
413
|
+
}
|
|
414
|
+
async registerMcpServer(_serverName, _mcpConfig, _workspaceDir) {
|
|
415
|
+
// Codex doesn't support MCP servers yet
|
|
416
|
+
// This is a no-op for now
|
|
417
|
+
console.warn('[CodexAdapter] MCP servers are not yet supported by Codex');
|
|
418
|
+
}
|
|
419
|
+
async checkHealth() {
|
|
420
|
+
try {
|
|
421
|
+
const execPath = await this.findCodexExecutable();
|
|
422
|
+
if (!execPath) {
|
|
423
|
+
return { available: false, reason: 'Codex CLI not found. Install from: https://openai.com/codex' };
|
|
424
|
+
}
|
|
425
|
+
// Check if API key is available
|
|
426
|
+
if (!process.env.OPENAI_API_KEY && !process.env.CODEX_API_KEY) {
|
|
427
|
+
return { available: false, reason: 'OPENAI_API_KEY not set in environment' };
|
|
428
|
+
}
|
|
429
|
+
// Verify Codex CLI is executable by checking version
|
|
430
|
+
try {
|
|
431
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
432
|
+
const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
|
|
433
|
+
const execFileAsync = promisify(execFile);
|
|
434
|
+
await execFileAsync(execPath, ['--version'], { timeout: 5000 });
|
|
435
|
+
return { available: true };
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return { available: false, reason: `Codex CLI found but not executable: ${error instanceof Error ? error.message : String(error)}` };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return { available: false, reason: error instanceof Error ? error.message : String(error) };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// ========================================================================
|
|
446
|
+
// Private Helper Methods
|
|
447
|
+
// ========================================================================
|
|
448
|
+
/**
|
|
449
|
+
* Set up stdout parser for JSON-RPC messages
|
|
450
|
+
*/
|
|
451
|
+
setupStdoutParser(process, session) {
|
|
452
|
+
process.stdout?.on('data', (chunk) => {
|
|
453
|
+
session.stdoutBuffer += chunk.toString();
|
|
454
|
+
// Parse newline-delimited JSON messages
|
|
455
|
+
let newlineIndex;
|
|
456
|
+
while ((newlineIndex = session.stdoutBuffer.indexOf('\n')) !== -1) {
|
|
457
|
+
const line = session.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
458
|
+
session.stdoutBuffer = session.stdoutBuffer.slice(newlineIndex + 1);
|
|
459
|
+
if (!line)
|
|
460
|
+
continue;
|
|
461
|
+
try {
|
|
462
|
+
const message = JSON.parse(line);
|
|
463
|
+
this.handleRpcMessage(session, message);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
console.error('[CodexAdapter] Failed to parse JSON-RPC message:', line);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Handle incoming JSON-RPC message
|
|
473
|
+
*/
|
|
474
|
+
handleRpcMessage(session, message) {
|
|
475
|
+
// Response to our RPC request
|
|
476
|
+
if ('id' in message && message.id !== undefined) {
|
|
477
|
+
const pending = session.pendingRpcCalls.get(message.id);
|
|
478
|
+
if (pending) {
|
|
479
|
+
session.pendingRpcCalls.delete(message.id);
|
|
480
|
+
// Type guard for JsonRpcResponse
|
|
481
|
+
const response = message;
|
|
482
|
+
if ('error' in response && response.error) {
|
|
483
|
+
pending.reject(new Error(response.error.message));
|
|
484
|
+
}
|
|
485
|
+
else if ('result' in response) {
|
|
486
|
+
pending.resolve(response.result);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
pending.reject(new Error('Invalid RPC response'));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Notification (streaming event)
|
|
495
|
+
if ('method' in message) {
|
|
496
|
+
this.handleNotification(session, message);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Handle JSON-RPC notification (streaming events)
|
|
501
|
+
*/
|
|
502
|
+
handleNotification(session, notification) {
|
|
503
|
+
const params = notification.params;
|
|
504
|
+
const delta = params.delta;
|
|
505
|
+
switch (notification.method) {
|
|
506
|
+
case 'message.delta':
|
|
507
|
+
// Text content streaming
|
|
508
|
+
session.messageBuffer.push({
|
|
509
|
+
type: 'text',
|
|
510
|
+
id: params.message_id || `msg-${Date.now()}`,
|
|
511
|
+
role: params.role || 'assistant',
|
|
512
|
+
delta: delta?.content || '',
|
|
513
|
+
content: delta?.content || '',
|
|
514
|
+
});
|
|
515
|
+
break;
|
|
516
|
+
case 'message.completed':
|
|
517
|
+
// Message finished
|
|
518
|
+
session.status = 'idle';
|
|
519
|
+
session.messageBuffer.push({
|
|
520
|
+
type: 'text',
|
|
521
|
+
id: params.message_id || `msg-${Date.now()}`,
|
|
522
|
+
role: 'assistant',
|
|
523
|
+
finished: true,
|
|
524
|
+
});
|
|
525
|
+
break;
|
|
526
|
+
case 'tool.call':
|
|
527
|
+
// Tool call started
|
|
528
|
+
session.messageBuffer.push({
|
|
529
|
+
type: 'tool_call',
|
|
530
|
+
id: params.tool_call_id || `tool-${Date.now()}`,
|
|
531
|
+
tool_name: params.tool_name,
|
|
532
|
+
tool_call_id: params.tool_call_id,
|
|
533
|
+
arguments: params.arguments,
|
|
534
|
+
});
|
|
535
|
+
break;
|
|
536
|
+
case 'tool.result':
|
|
537
|
+
// Tool call completed
|
|
538
|
+
session.messageBuffer.push({
|
|
539
|
+
type: 'tool_result',
|
|
540
|
+
id: params.tool_call_id || `tool-${Date.now()}`,
|
|
541
|
+
tool_call_id: params.tool_call_id,
|
|
542
|
+
result: params.result,
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
case 'error':
|
|
546
|
+
// Error occurred
|
|
547
|
+
session.status = 'error';
|
|
548
|
+
session.lastError = params.message || 'Unknown error';
|
|
549
|
+
session.messageBuffer.push({
|
|
550
|
+
type: 'error',
|
|
551
|
+
id: `error-${Date.now()}`,
|
|
552
|
+
error: params.message,
|
|
553
|
+
});
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
console.log(`[CodexAdapter] Unknown notification: ${notification.method}`);
|
|
557
|
+
}
|
|
558
|
+
// Notify the polling coordinator that new data is available
|
|
559
|
+
this.onDataAvailable?.(session.sessionId);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Send JSON-RPC request and wait for response
|
|
563
|
+
*/
|
|
564
|
+
sendRpcRequest(session, method, params) {
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
const id = session.nextRpcId++;
|
|
567
|
+
const request = {
|
|
568
|
+
jsonrpc: '2.0',
|
|
569
|
+
id,
|
|
570
|
+
method,
|
|
571
|
+
params,
|
|
572
|
+
};
|
|
573
|
+
// Store pending call
|
|
574
|
+
session.pendingRpcCalls.set(id, { resolve, reject });
|
|
575
|
+
// Send request
|
|
576
|
+
const requestJson = JSON.stringify(request) + '\n';
|
|
577
|
+
session.process.stdin?.write(requestJson, (error) => {
|
|
578
|
+
if (error) {
|
|
579
|
+
session.pendingRpcCalls.delete(id);
|
|
580
|
+
reject(error);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
// Timeout after 30 seconds
|
|
584
|
+
setTimeout(() => {
|
|
585
|
+
if (session.pendingRpcCalls.has(id)) {
|
|
586
|
+
session.pendingRpcCalls.delete(id);
|
|
587
|
+
reject(new Error(`RPC request timed out: ${method}`));
|
|
588
|
+
}
|
|
589
|
+
}, 30000);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Convert CodexMessage to MessagePart format
|
|
594
|
+
*/
|
|
595
|
+
convertCodexMessageToParts(msg, seenPartIds, partContentLengths) {
|
|
596
|
+
const parts = [];
|
|
597
|
+
switch (msg.type) {
|
|
598
|
+
case 'text':
|
|
599
|
+
const textPartId = `${msg.id}-text`;
|
|
600
|
+
if (!seenPartIds.has(textPartId)) {
|
|
601
|
+
seenPartIds.add(textPartId);
|
|
602
|
+
const content = msg.content || msg.delta || '';
|
|
603
|
+
partContentLengths.set(textPartId, String(content.length));
|
|
604
|
+
parts.push({
|
|
605
|
+
id: textPartId,
|
|
606
|
+
type: coding_agent_adapter_1.MessagePartType.TEXT,
|
|
607
|
+
text: content,
|
|
608
|
+
role: msg.role,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
case 'tool_call':
|
|
613
|
+
const toolCallPartId = `tool-${msg.tool_call_id}`;
|
|
614
|
+
if (!seenPartIds.has(toolCallPartId)) {
|
|
615
|
+
seenPartIds.add(toolCallPartId);
|
|
616
|
+
const input = msg.arguments ? JSON.stringify(msg.arguments, null, 2) : undefined;
|
|
617
|
+
partContentLengths.set(toolCallPartId, `pending:${msg.tool_name}`);
|
|
618
|
+
parts.push({
|
|
619
|
+
id: toolCallPartId,
|
|
620
|
+
type: coding_agent_adapter_1.MessagePartType.TOOL,
|
|
621
|
+
content: `Tool: ${msg.tool_name}`,
|
|
622
|
+
tool: {
|
|
623
|
+
name: msg.tool_name || 'unknown',
|
|
624
|
+
status: 'pending',
|
|
625
|
+
input,
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
case 'tool_result':
|
|
631
|
+
const toolResultPartId = `tool-${msg.tool_call_id}`;
|
|
632
|
+
const previousContent = partContentLengths.get(toolResultPartId);
|
|
633
|
+
if (previousContent) {
|
|
634
|
+
// Update existing tool part
|
|
635
|
+
partContentLengths.set(toolResultPartId, `success:${msg.result?.length || 0}`);
|
|
636
|
+
parts.push({
|
|
637
|
+
id: toolResultPartId,
|
|
638
|
+
type: coding_agent_adapter_1.MessagePartType.TOOL,
|
|
639
|
+
content: 'Tool completed',
|
|
640
|
+
tool: {
|
|
641
|
+
name: previousContent.split(':')[1] || 'tool',
|
|
642
|
+
status: 'success',
|
|
643
|
+
output: msg.result?.slice(0, 2000),
|
|
644
|
+
},
|
|
645
|
+
update: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
// First time seeing this tool (result before call)
|
|
650
|
+
if (!seenPartIds.has(toolResultPartId)) {
|
|
651
|
+
seenPartIds.add(toolResultPartId);
|
|
652
|
+
partContentLengths.set(toolResultPartId, `success:${msg.result?.length || 0}`);
|
|
653
|
+
parts.push({
|
|
654
|
+
id: toolResultPartId,
|
|
655
|
+
type: coding_agent_adapter_1.MessagePartType.TOOL,
|
|
656
|
+
content: 'Tool result',
|
|
657
|
+
tool: {
|
|
658
|
+
name: 'tool',
|
|
659
|
+
status: 'success',
|
|
660
|
+
output: msg.result?.slice(0, 2000),
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
case 'error':
|
|
667
|
+
const errorPartId = `${msg.id}-error`;
|
|
668
|
+
if (!seenPartIds.has(errorPartId)) {
|
|
669
|
+
seenPartIds.add(errorPartId);
|
|
670
|
+
parts.push({
|
|
671
|
+
id: errorPartId,
|
|
672
|
+
type: coding_agent_adapter_1.MessagePartType.ERROR,
|
|
673
|
+
content: msg.error || 'Unknown error',
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
return parts;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
exports.CodexAdapter = CodexAdapter;
|