agents-bridge 0.1.1 → 0.1.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/README.md +12 -12
- package/config.example.json +116 -51
- package/dist/agents/agent-resolver.d.ts +12 -0
- package/dist/agents/agent-resolver.js +1 -0
- package/dist/agents/agent-resolver.js.map +1 -1
- package/dist/agents/agent-router.d.ts +8 -0
- package/dist/agents/cli-output.d.ts +35 -0
- package/dist/agents/cli-output.d.ts.map +1 -0
- package/dist/agents/cli-output.js +277 -0
- package/dist/agents/cli-output.js.map +1 -0
- package/dist/agents/cli-output.test.d.ts +2 -0
- package/dist/agents/cli-output.test.d.ts.map +1 -0
- package/dist/agents/cli-output.test.js +56 -0
- package/dist/agents/cli-output.test.js.map +1 -0
- package/dist/agents/cli-runner.d.ts +23 -0
- package/dist/agents/cli-runner.d.ts.map +1 -1
- package/dist/agents/cli-runner.js +196 -157
- package/dist/agents/cli-runner.js.map +1 -1
- package/dist/agents/cli-runner.test.d.ts +2 -0
- package/dist/agents/cli-runner.test.d.ts.map +1 -0
- package/dist/agents/cli-runner.test.js +119 -0
- package/dist/agents/cli-runner.test.js.map +1 -0
- package/dist/agents/cli-session-store.d.ts +47 -0
- package/dist/agents/cli-session-store.d.ts.map +1 -1
- package/dist/agents/cli-session-store.js +29 -0
- package/dist/agents/cli-session-store.js.map +1 -1
- package/dist/agents/cli-session-store.test.d.ts +2 -0
- package/dist/agents/cli-session-store.test.d.ts.map +1 -0
- package/dist/agents/cli-session-store.test.js +30 -0
- package/dist/agents/cli-session-store.test.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.d.ts +72 -0
- package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.js +772 -0
- package/dist/agents/codex-app-server-runtime.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.test.d.ts +2 -0
- package/dist/agents/codex-app-server-runtime.test.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.test.js +164 -0
- package/dist/agents/codex-app-server-runtime.test.js.map +1 -0
- package/dist/agents/model-selection.d.ts +3 -2
- package/dist/agents/model-selection.js +1 -1
- package/dist/channels/botlink.d.ts +2 -0
- package/dist/cli/cmd-onboard.d.ts.map +1 -1
- package/dist/cli/cmd-onboard.js +9 -78
- package/dist/cli/cmd-onboard.js.map +1 -1
- package/dist/cli/cmd-start.d.ts.map +1 -1
- package/dist/cli/cmd-start.js +154 -16
- package/dist/cli/cmd-start.js.map +1 -1
- package/dist/cli/cmd-version.d.ts +4 -0
- package/dist/cli/cmd-version.d.ts.map +1 -0
- package/dist/cli/cmd-version.js +12 -0
- package/dist/cli/cmd-version.js.map +1 -0
- package/dist/cli/run-main.d.ts +2 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +42 -21
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/run-main.test.d.ts +2 -0
- package/dist/cli/run-main.test.d.ts.map +1 -0
- package/dist/cli/run-main.test.js +17 -0
- package/dist/cli/run-main.test.js.map +1 -0
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/default-config.d.ts +9 -0
- package/dist/config/default-config.d.ts.map +1 -0
- package/dist/config/default-config.js +83 -0
- package/dist/config/default-config.js.map +1 -0
- package/dist/config/default-config.test.d.ts +2 -0
- package/dist/config/default-config.test.d.ts.map +1 -0
- package/dist/config/default-config.test.js +19 -0
- package/dist/config/default-config.test.js.map +1 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +7 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/zod-schema.d.ts +656 -51
- package/dist/config/zod-schema.d.ts.map +1 -1
- package/dist/config/zod-schema.js +16 -0
- package/dist/config/zod-schema.js.map +1 -1
- package/dist/entry.d.ts +1 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +12 -1
- package/dist/entry.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +28 -5
- package/dist/logger.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/console-encoding.d.ts +0 -6
- package/dist/console-encoding.d.ts.map +0 -1
- package/dist/console-encoding.js +0 -35
- package/dist/console-encoding.js.map +0 -1
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { isSessionExpiredError } from './cli-output.js';
|
|
4
|
+
class CodexAppServerProcessError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'CodexAppServerProcessError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class CodexAppServerRpcError extends Error {
|
|
11
|
+
method;
|
|
12
|
+
code;
|
|
13
|
+
constructor(method, code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.method = method;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = 'CodexAppServerRpcError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class CodexAppServerTurnTimeoutError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'CodexAppServerTurnTimeoutError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class CodexAppServerRuntime {
|
|
27
|
+
options;
|
|
28
|
+
connection;
|
|
29
|
+
loadedThreads = new Map();
|
|
30
|
+
activeTurnsByThread = new Map();
|
|
31
|
+
activeTurnsByTurn = new Map();
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.connection = options.connection ?? new DefaultCodexAppServerConnection({
|
|
35
|
+
backend: options.backend,
|
|
36
|
+
logger: options.logger,
|
|
37
|
+
});
|
|
38
|
+
this.connection.onNotification((notification) => {
|
|
39
|
+
this.handleNotification(notification);
|
|
40
|
+
});
|
|
41
|
+
this.connection.onFatalError((error) => {
|
|
42
|
+
this.handleFatalError(error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async run(params) {
|
|
46
|
+
const restartCountBefore = this.connection.getRestartCount();
|
|
47
|
+
await this.connection.ensureReady();
|
|
48
|
+
try {
|
|
49
|
+
const prepared = await this.ensureThread(params);
|
|
50
|
+
const completed = await this.runTurn({
|
|
51
|
+
sessionId: params.sessionId,
|
|
52
|
+
threadId: prepared.threadId,
|
|
53
|
+
model: params.model,
|
|
54
|
+
prompt: params.prompt,
|
|
55
|
+
imagePaths: params.imagePaths,
|
|
56
|
+
workspaceDir: params.workspaceDir,
|
|
57
|
+
timeoutMs: params.timeoutMs,
|
|
58
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs,
|
|
59
|
+
});
|
|
60
|
+
const runtimeRestartCount = this.connection.getRestartCount() - restartCountBefore;
|
|
61
|
+
return {
|
|
62
|
+
text: completed.text,
|
|
63
|
+
cliSessionId: prepared.threadId,
|
|
64
|
+
isResume: prepared.isResume,
|
|
65
|
+
usage: completed.usage,
|
|
66
|
+
diagnostics: {
|
|
67
|
+
queueWaitMs: 0,
|
|
68
|
+
commandElapsedMs: completed.turnCompletedMs ?? Date.now() - completed.startedAt,
|
|
69
|
+
totalElapsedMs: prepared.threadOperationElapsedMs + (completed.turnCompletedMs ?? Date.now() - completed.startedAt),
|
|
70
|
+
outputMode: 'app-server',
|
|
71
|
+
runtimeMode: 'app-server',
|
|
72
|
+
hadStdout: false,
|
|
73
|
+
hadStderr: false,
|
|
74
|
+
firstAgentMessageMs: completed.firstAgentMessageMs,
|
|
75
|
+
threadStartedMs: prepared.threadOperation === 'start' ? prepared.threadOperationElapsedMs : undefined,
|
|
76
|
+
turnCompletedMs: completed.turnCompletedMs,
|
|
77
|
+
reconnectCount: completed.reconnectCount,
|
|
78
|
+
fellBackToHttps: completed.fellBackToHttps,
|
|
79
|
+
runtimeRestartCount: runtimeRestartCount > 0 ? runtimeRestartCount : undefined,
|
|
80
|
+
threadOperation: prepared.threadOperation,
|
|
81
|
+
threadOperationElapsedMs: prepared.threadOperationElapsedMs,
|
|
82
|
+
cliSessionIdSource: prepared.threadOperation === 'start' ? 'parsed' : 'existing',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isSessionExpiredError(error)) {
|
|
88
|
+
this.loadedThreads.delete(params.sessionId);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async teardown() {
|
|
94
|
+
this.loadedThreads.clear();
|
|
95
|
+
this.activeTurnsByThread.clear();
|
|
96
|
+
this.activeTurnsByTurn.clear();
|
|
97
|
+
await this.connection.dispose();
|
|
98
|
+
}
|
|
99
|
+
async ensureThread(params) {
|
|
100
|
+
const existingThreadId = params.existingThreadId?.trim() || undefined;
|
|
101
|
+
const loaded = this.loadedThreads.get(params.sessionId);
|
|
102
|
+
if (!existingThreadId && loaded) {
|
|
103
|
+
this.loadedThreads.delete(params.sessionId);
|
|
104
|
+
}
|
|
105
|
+
if (loaded && existingThreadId && loaded.threadId === existingThreadId) {
|
|
106
|
+
return {
|
|
107
|
+
threadId: existingThreadId,
|
|
108
|
+
isResume: true,
|
|
109
|
+
threadOperation: 'reuse',
|
|
110
|
+
threadOperationElapsedMs: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const requestTimeoutMs = this.resolveRequestTimeout(params.timeoutMs);
|
|
114
|
+
if (existingThreadId) {
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
const response = await this.connection.request('thread/resume', buildThreadResumeParams({
|
|
117
|
+
backend: this.options.backend,
|
|
118
|
+
threadId: existingThreadId,
|
|
119
|
+
model: params.model,
|
|
120
|
+
workspaceDir: params.workspaceDir,
|
|
121
|
+
}), requestTimeoutMs);
|
|
122
|
+
const threadId = getThreadIdFromResponse(response) ?? existingThreadId;
|
|
123
|
+
this.loadedThreads.set(params.sessionId, { threadId });
|
|
124
|
+
return {
|
|
125
|
+
threadId,
|
|
126
|
+
isResume: true,
|
|
127
|
+
threadOperation: 'resume',
|
|
128
|
+
threadOperationElapsedMs: Date.now() - startedAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const startedAt = Date.now();
|
|
132
|
+
const response = await this.connection.request('thread/start', buildThreadStartParams({
|
|
133
|
+
backend: this.options.backend,
|
|
134
|
+
model: params.model,
|
|
135
|
+
workspaceDir: params.workspaceDir,
|
|
136
|
+
systemPrompt: params.systemPrompt,
|
|
137
|
+
}), requestTimeoutMs);
|
|
138
|
+
const threadId = getThreadIdFromResponse(response);
|
|
139
|
+
if (!threadId) {
|
|
140
|
+
throw new Error('thread/start succeeded but did not return a thread id');
|
|
141
|
+
}
|
|
142
|
+
this.loadedThreads.set(params.sessionId, { threadId });
|
|
143
|
+
return {
|
|
144
|
+
threadId,
|
|
145
|
+
isResume: false,
|
|
146
|
+
threadOperation: 'start',
|
|
147
|
+
threadOperationElapsedMs: Date.now() - startedAt,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async runTurn(params) {
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
const state = this.createActiveTurnState({
|
|
153
|
+
sessionId: params.sessionId,
|
|
154
|
+
threadId: params.threadId,
|
|
155
|
+
timeoutMs: params.timeoutMs,
|
|
156
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs,
|
|
157
|
+
startedAt,
|
|
158
|
+
});
|
|
159
|
+
this.activeTurnsByThread.set(params.threadId, state);
|
|
160
|
+
try {
|
|
161
|
+
const response = await this.connection.request('turn/start', buildTurnStartParams({
|
|
162
|
+
backend: this.options.backend,
|
|
163
|
+
threadId: params.threadId,
|
|
164
|
+
model: params.model,
|
|
165
|
+
prompt: params.prompt,
|
|
166
|
+
imagePaths: params.imagePaths,
|
|
167
|
+
workspaceDir: params.workspaceDir,
|
|
168
|
+
}), this.resolveRequestTimeout(params.timeoutMs));
|
|
169
|
+
const turnId = getTurnIdFromResponse(response);
|
|
170
|
+
if (!state.settled && turnId) {
|
|
171
|
+
this.bindTurnId(state, turnId);
|
|
172
|
+
}
|
|
173
|
+
if (!state.settled && params.noProgressTimeoutMs > 0) {
|
|
174
|
+
this.armNoProgressTimer(state, params.noProgressTimeoutMs);
|
|
175
|
+
}
|
|
176
|
+
const completed = await state.completion;
|
|
177
|
+
return {
|
|
178
|
+
...completed,
|
|
179
|
+
startedAt,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.cleanupTurnState(state);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
createActiveTurnState(params) {
|
|
188
|
+
let state;
|
|
189
|
+
let resolve;
|
|
190
|
+
let reject;
|
|
191
|
+
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
192
|
+
resolve = resolvePromise;
|
|
193
|
+
reject = rejectPromise;
|
|
194
|
+
});
|
|
195
|
+
void completion.catch(() => undefined);
|
|
196
|
+
state = {
|
|
197
|
+
sessionId: params.sessionId,
|
|
198
|
+
threadId: params.threadId,
|
|
199
|
+
startedAt: params.startedAt,
|
|
200
|
+
reconnectCount: 0,
|
|
201
|
+
fellBackToHttps: false,
|
|
202
|
+
settled: false,
|
|
203
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs > 0 ? params.noProgressTimeoutMs : undefined,
|
|
204
|
+
overallTimer: setTimeout(() => {
|
|
205
|
+
this.rejectTurn(state, new CodexAppServerTurnTimeoutError(`Codex turn exceeded timeout (${Math.round(params.timeoutMs / 1000)}s).`));
|
|
206
|
+
if (state.turnId) {
|
|
207
|
+
this.interruptTurn(state.turnId).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
}, params.timeoutMs),
|
|
210
|
+
completion,
|
|
211
|
+
resolve,
|
|
212
|
+
reject,
|
|
213
|
+
};
|
|
214
|
+
return state;
|
|
215
|
+
}
|
|
216
|
+
armNoProgressTimer(state, noProgressTimeoutMs) {
|
|
217
|
+
clearTimeout(state.noProgressTimer);
|
|
218
|
+
state.noProgressTimer = setTimeout(() => {
|
|
219
|
+
this.rejectTurn(state, new CodexAppServerTurnTimeoutError(`Codex turn produced no progress for ${Math.round(noProgressTimeoutMs / 1000)}s.`));
|
|
220
|
+
if (state.turnId) {
|
|
221
|
+
this.interruptTurn(state.turnId).catch(() => undefined);
|
|
222
|
+
}
|
|
223
|
+
}, noProgressTimeoutMs);
|
|
224
|
+
}
|
|
225
|
+
bindTurnId(state, turnId) {
|
|
226
|
+
if (state.turnId === turnId) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (state.turnId) {
|
|
230
|
+
this.activeTurnsByTurn.delete(state.turnId);
|
|
231
|
+
}
|
|
232
|
+
state.turnId = turnId;
|
|
233
|
+
this.activeTurnsByTurn.set(turnId, state);
|
|
234
|
+
}
|
|
235
|
+
handleNotification(notification) {
|
|
236
|
+
switch (notification.method) {
|
|
237
|
+
case 'turn/started':
|
|
238
|
+
this.handleTurnStarted(notification.params);
|
|
239
|
+
break;
|
|
240
|
+
case 'item/completed':
|
|
241
|
+
this.handleItemCompleted(notification.params);
|
|
242
|
+
break;
|
|
243
|
+
case 'thread/tokenUsage/updated':
|
|
244
|
+
this.handleTokenUsageUpdated(notification.params);
|
|
245
|
+
break;
|
|
246
|
+
case 'turn/completed':
|
|
247
|
+
this.handleTurnCompleted(notification.params);
|
|
248
|
+
break;
|
|
249
|
+
case 'error':
|
|
250
|
+
this.handleTurnError(notification.params);
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
handleTurnStarted(params) {
|
|
257
|
+
const record = asRecord(params);
|
|
258
|
+
const threadId = getString(record?.threadId);
|
|
259
|
+
const turn = asRecord(record?.turn);
|
|
260
|
+
const turnId = getString(turn?.id);
|
|
261
|
+
const state = this.findTurnState(threadId, turnId);
|
|
262
|
+
if (!state) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (turnId) {
|
|
266
|
+
this.bindTurnId(state, turnId);
|
|
267
|
+
}
|
|
268
|
+
this.noteTurnProgress(state);
|
|
269
|
+
}
|
|
270
|
+
handleItemCompleted(params) {
|
|
271
|
+
const record = asRecord(params);
|
|
272
|
+
const threadId = getString(record?.threadId);
|
|
273
|
+
const turnId = getString(record?.turnId);
|
|
274
|
+
const state = this.findTurnState(threadId, turnId);
|
|
275
|
+
if (!state) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (turnId) {
|
|
279
|
+
this.bindTurnId(state, turnId);
|
|
280
|
+
}
|
|
281
|
+
this.noteTurnProgress(state);
|
|
282
|
+
const item = asRecord(record?.item);
|
|
283
|
+
if (getString(item?.type) !== 'agentMessage') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const text = getString(item?.text);
|
|
287
|
+
if (!text) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
state.lastText = text;
|
|
291
|
+
if (state.firstAgentMessageMs === undefined) {
|
|
292
|
+
state.firstAgentMessageMs = Date.now() - state.startedAt;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
handleTokenUsageUpdated(params) {
|
|
296
|
+
const record = asRecord(params);
|
|
297
|
+
const threadId = getString(record?.threadId);
|
|
298
|
+
const turnId = getString(record?.turnId);
|
|
299
|
+
const state = this.findTurnState(threadId, turnId);
|
|
300
|
+
if (!state) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (turnId) {
|
|
304
|
+
this.bindTurnId(state, turnId);
|
|
305
|
+
}
|
|
306
|
+
state.usage = toCliUsage(asRecord(record?.tokenUsage));
|
|
307
|
+
}
|
|
308
|
+
handleTurnCompleted(params) {
|
|
309
|
+
const record = asRecord(params);
|
|
310
|
+
const threadId = getString(record?.threadId);
|
|
311
|
+
const turn = asRecord(record?.turn);
|
|
312
|
+
const turnId = getString(turn?.id);
|
|
313
|
+
const state = this.findTurnState(threadId, turnId);
|
|
314
|
+
if (!state) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (turnId) {
|
|
318
|
+
this.bindTurnId(state, turnId);
|
|
319
|
+
}
|
|
320
|
+
this.noteTurnProgress(state);
|
|
321
|
+
state.turnCompletedMs = Date.now() - state.startedAt;
|
|
322
|
+
const failureMessage = getTurnFailureMessage(turn);
|
|
323
|
+
if (failureMessage) {
|
|
324
|
+
this.rejectTurn(state, new Error(failureMessage));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.resolveTurn(state, {
|
|
328
|
+
text: state.lastText ?? '',
|
|
329
|
+
usage: state.usage,
|
|
330
|
+
reconnectCount: state.reconnectCount,
|
|
331
|
+
fellBackToHttps: state.fellBackToHttps,
|
|
332
|
+
firstAgentMessageMs: state.firstAgentMessageMs,
|
|
333
|
+
turnCompletedMs: state.turnCompletedMs,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
handleTurnError(params) {
|
|
337
|
+
const record = asRecord(params);
|
|
338
|
+
const threadId = getString(record?.threadId);
|
|
339
|
+
const turnId = getString(record?.turnId);
|
|
340
|
+
const state = this.findTurnState(threadId, turnId);
|
|
341
|
+
if (!state) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (turnId) {
|
|
345
|
+
this.bindTurnId(state, turnId);
|
|
346
|
+
}
|
|
347
|
+
this.noteTurnProgress(state);
|
|
348
|
+
const error = asRecord(record?.error);
|
|
349
|
+
const message = `${getString(error?.message) ?? ''}\n${getString(error?.additionalDetails) ?? ''}`.trim();
|
|
350
|
+
if (RECONNECT_PATTERN.test(message)) {
|
|
351
|
+
state.reconnectCount += 1;
|
|
352
|
+
}
|
|
353
|
+
if (HTTPS_FALLBACK_PATTERN.test(message)) {
|
|
354
|
+
state.fellBackToHttps = true;
|
|
355
|
+
}
|
|
356
|
+
const willRetry = Boolean(record?.willRetry);
|
|
357
|
+
if (!willRetry) {
|
|
358
|
+
this.rejectTurn(state, new Error(getString(error?.message) ?? 'Codex turn failed'));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
handleFatalError(error) {
|
|
362
|
+
this.loadedThreads.clear();
|
|
363
|
+
const states = new Set(this.activeTurnsByThread.values());
|
|
364
|
+
for (const state of states) {
|
|
365
|
+
this.rejectTurn(state, error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
noteTurnProgress(state) {
|
|
369
|
+
if (state.noProgressTimeoutMs) {
|
|
370
|
+
this.armNoProgressTimer(state, state.noProgressTimeoutMs);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
findTurnState(threadId, turnId) {
|
|
374
|
+
if (turnId) {
|
|
375
|
+
const byTurn = this.activeTurnsByTurn.get(turnId);
|
|
376
|
+
if (byTurn) {
|
|
377
|
+
return byTurn;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (threadId) {
|
|
381
|
+
return this.activeTurnsByThread.get(threadId);
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
resolveTurn(state, value) {
|
|
386
|
+
if (state.settled) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
state.settled = true;
|
|
390
|
+
this.cleanupTurnState(state);
|
|
391
|
+
state.resolve(value);
|
|
392
|
+
}
|
|
393
|
+
rejectTurn(state, error) {
|
|
394
|
+
if (state.settled) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
state.settled = true;
|
|
398
|
+
this.cleanupTurnState(state);
|
|
399
|
+
state.reject(error);
|
|
400
|
+
}
|
|
401
|
+
cleanupTurnState(state) {
|
|
402
|
+
clearTimeout(state.overallTimer);
|
|
403
|
+
clearTimeout(state.noProgressTimer);
|
|
404
|
+
this.activeTurnsByThread.delete(state.threadId);
|
|
405
|
+
if (state.turnId) {
|
|
406
|
+
this.activeTurnsByTurn.delete(state.turnId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async interruptTurn(turnId) {
|
|
410
|
+
await this.connection.request('turn/interrupt', { turnId }, Math.max(5_000, Math.min(this.resolveRequestTimeout(15_000), 15_000))).catch(() => undefined);
|
|
411
|
+
}
|
|
412
|
+
resolveRequestTimeout(timeoutMs) {
|
|
413
|
+
const configured = this.options.backend.appServer?.requestTimeoutMs ?? 30_000;
|
|
414
|
+
return Math.max(1_000, Math.min(timeoutMs, configured));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
class DefaultCodexAppServerConnection {
|
|
418
|
+
options;
|
|
419
|
+
child = null;
|
|
420
|
+
stdoutReader = null;
|
|
421
|
+
stderrReader = null;
|
|
422
|
+
readyPromise = null;
|
|
423
|
+
pending = new Map();
|
|
424
|
+
nextId = 1;
|
|
425
|
+
initialized = false;
|
|
426
|
+
disposed = false;
|
|
427
|
+
notificationListeners = new Set();
|
|
428
|
+
fatalListeners = new Set();
|
|
429
|
+
restartCount = 0;
|
|
430
|
+
constructor(options) {
|
|
431
|
+
this.options = options;
|
|
432
|
+
}
|
|
433
|
+
async ensureReady() {
|
|
434
|
+
if (this.disposed) {
|
|
435
|
+
throw new CodexAppServerProcessError('Codex app-server runtime is already shut down');
|
|
436
|
+
}
|
|
437
|
+
if (this.child && this.initialized) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (this.readyPromise) {
|
|
441
|
+
return this.readyPromise;
|
|
442
|
+
}
|
|
443
|
+
this.readyPromise = this.startAndInitialize().finally(() => {
|
|
444
|
+
this.readyPromise = null;
|
|
445
|
+
});
|
|
446
|
+
return this.readyPromise;
|
|
447
|
+
}
|
|
448
|
+
async request(method, params, timeoutMs) {
|
|
449
|
+
return this.sendRequest(method, params, timeoutMs, true);
|
|
450
|
+
}
|
|
451
|
+
async sendRequest(method, params, timeoutMs, ensureReady) {
|
|
452
|
+
if (ensureReady) {
|
|
453
|
+
await this.ensureReady();
|
|
454
|
+
}
|
|
455
|
+
if (!this.child?.stdin.writable) {
|
|
456
|
+
throw new CodexAppServerProcessError('Codex app-server stdin is not writable');
|
|
457
|
+
}
|
|
458
|
+
const id = this.nextId++;
|
|
459
|
+
const payload = JSON.stringify({
|
|
460
|
+
jsonrpc: '2.0',
|
|
461
|
+
id,
|
|
462
|
+
method,
|
|
463
|
+
params,
|
|
464
|
+
});
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
const timer = setTimeout(() => {
|
|
467
|
+
this.pending.delete(id);
|
|
468
|
+
reject(new CodexAppServerRpcError(method, undefined, `${method} timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
469
|
+
}, timeoutMs);
|
|
470
|
+
this.pending.set(id, {
|
|
471
|
+
method,
|
|
472
|
+
timer,
|
|
473
|
+
resolve: resolve,
|
|
474
|
+
reject,
|
|
475
|
+
});
|
|
476
|
+
this.child?.stdin.write(`${payload}\n`);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
notify(method, params) {
|
|
480
|
+
if (!this.child?.stdin.writable) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const message = {
|
|
484
|
+
jsonrpc: '2.0',
|
|
485
|
+
method,
|
|
486
|
+
};
|
|
487
|
+
if (params !== undefined) {
|
|
488
|
+
message.params = params;
|
|
489
|
+
}
|
|
490
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
491
|
+
}
|
|
492
|
+
getRestartCount() {
|
|
493
|
+
return Math.max(0, this.restartCount - 1);
|
|
494
|
+
}
|
|
495
|
+
onNotification(listener) {
|
|
496
|
+
this.notificationListeners.add(listener);
|
|
497
|
+
}
|
|
498
|
+
onFatalError(listener) {
|
|
499
|
+
this.fatalListeners.add(listener);
|
|
500
|
+
}
|
|
501
|
+
async dispose() {
|
|
502
|
+
this.disposed = true;
|
|
503
|
+
this.initialized = false;
|
|
504
|
+
const child = this.child;
|
|
505
|
+
if (!child) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
await new Promise((resolve) => {
|
|
509
|
+
const timer = setTimeout(() => {
|
|
510
|
+
child.kill();
|
|
511
|
+
resolve();
|
|
512
|
+
}, 2_000);
|
|
513
|
+
child.once('exit', () => {
|
|
514
|
+
clearTimeout(timer);
|
|
515
|
+
resolve();
|
|
516
|
+
});
|
|
517
|
+
child.kill();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async startAndInitialize() {
|
|
521
|
+
const startupTimeoutMs = this.options.backend.appServer?.startupTimeoutMs ?? 15_000;
|
|
522
|
+
await this.spawnProcess();
|
|
523
|
+
const initTimeoutMs = Math.max(1_000, startupTimeoutMs);
|
|
524
|
+
await this.sendRequest('initialize', {
|
|
525
|
+
clientInfo: {
|
|
526
|
+
name: 'agent-bridge',
|
|
527
|
+
title: 'agent-bridge',
|
|
528
|
+
version: '0.1.0',
|
|
529
|
+
},
|
|
530
|
+
capabilities: {
|
|
531
|
+
experimentalApi: this.options.backend.appServer?.experimentalApi ?? false,
|
|
532
|
+
optOutNotificationMethods: this.options.backend.appServer?.optOutNotificationMethods ?? [],
|
|
533
|
+
},
|
|
534
|
+
}, initTimeoutMs, false);
|
|
535
|
+
this.notify('initialized');
|
|
536
|
+
this.initialized = true;
|
|
537
|
+
}
|
|
538
|
+
async spawnProcess() {
|
|
539
|
+
const backend = this.options.backend;
|
|
540
|
+
const appServerArgs = backend.appServer?.args ?? [];
|
|
541
|
+
const listen = backend.appServer?.listen ?? 'stdio://';
|
|
542
|
+
const sessionSource = backend.appServer?.sessionSource ?? 'agent-bridge';
|
|
543
|
+
const env = { ...process.env, ...backend.env };
|
|
544
|
+
for (const key of backend.clearEnv ?? []) {
|
|
545
|
+
delete env[key];
|
|
546
|
+
}
|
|
547
|
+
this.child = spawn(backend.command, [
|
|
548
|
+
'app-server',
|
|
549
|
+
...appServerArgs,
|
|
550
|
+
'--listen',
|
|
551
|
+
listen,
|
|
552
|
+
'--session-source',
|
|
553
|
+
sessionSource,
|
|
554
|
+
], {
|
|
555
|
+
env,
|
|
556
|
+
stdio: 'pipe',
|
|
557
|
+
shell: process.platform === 'win32',
|
|
558
|
+
windowsHide: true,
|
|
559
|
+
});
|
|
560
|
+
this.stdoutReader = createInterface({
|
|
561
|
+
input: this.child.stdout,
|
|
562
|
+
crlfDelay: Infinity,
|
|
563
|
+
});
|
|
564
|
+
this.stderrReader = createInterface({
|
|
565
|
+
input: this.child.stderr,
|
|
566
|
+
crlfDelay: Infinity,
|
|
567
|
+
});
|
|
568
|
+
this.stdoutReader.on('line', (line) => {
|
|
569
|
+
this.handleStdoutLine(line);
|
|
570
|
+
});
|
|
571
|
+
this.stderrReader.on('line', (line) => {
|
|
572
|
+
this.options.logger.debug({ line }, 'Codex app-server stderr');
|
|
573
|
+
});
|
|
574
|
+
this.child.on('error', (error) => {
|
|
575
|
+
this.failAll(new CodexAppServerProcessError(error.message));
|
|
576
|
+
});
|
|
577
|
+
this.child.on('exit', (code, signal) => {
|
|
578
|
+
this.initialized = false;
|
|
579
|
+
this.stdoutReader?.close();
|
|
580
|
+
this.stderrReader?.close();
|
|
581
|
+
this.stdoutReader = null;
|
|
582
|
+
this.stderrReader = null;
|
|
583
|
+
this.child = null;
|
|
584
|
+
if (this.disposed) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.failAll(new CodexAppServerProcessError(`Codex app-server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'})`));
|
|
588
|
+
});
|
|
589
|
+
this.restartCount += 1;
|
|
590
|
+
}
|
|
591
|
+
handleStdoutLine(line) {
|
|
592
|
+
const trimmed = line.trim();
|
|
593
|
+
if (!trimmed) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let parsed;
|
|
597
|
+
try {
|
|
598
|
+
parsed = JSON.parse(trimmed);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
this.options.logger.warn({ line: trimmed }, 'Codex app-server emitted non-JSON stdout line');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const id = typeof parsed.id === 'number' ? parsed.id : undefined;
|
|
605
|
+
const hasResult = Object.prototype.hasOwnProperty.call(parsed, 'result');
|
|
606
|
+
const hasError = Object.prototype.hasOwnProperty.call(parsed, 'error');
|
|
607
|
+
const method = getString(parsed.method);
|
|
608
|
+
if (id !== undefined && (hasResult || hasError)) {
|
|
609
|
+
const pending = this.pending.get(id);
|
|
610
|
+
if (!pending) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
clearTimeout(pending.timer);
|
|
614
|
+
this.pending.delete(id);
|
|
615
|
+
if (parsed.error) {
|
|
616
|
+
const error = asRecord(parsed.error);
|
|
617
|
+
pending.reject(new CodexAppServerRpcError(pending.method, typeof error?.code === 'number' ? error.code : undefined, getString(error?.message) ?? `${pending.method} failed`));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
pending.resolve(parsed.result);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (id !== undefined && method) {
|
|
624
|
+
this.replyUnsupportedServerRequest(parsed);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (!method) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const notification = {
|
|
631
|
+
method,
|
|
632
|
+
params: parsed.params,
|
|
633
|
+
};
|
|
634
|
+
for (const listener of this.notificationListeners) {
|
|
635
|
+
listener(notification);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
replyUnsupportedServerRequest(parsed) {
|
|
639
|
+
if (!this.child?.stdin.writable || typeof parsed.id !== 'number') {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
this.child.stdin.write(`${JSON.stringify({
|
|
643
|
+
jsonrpc: '2.0',
|
|
644
|
+
id: parsed.id,
|
|
645
|
+
error: {
|
|
646
|
+
code: -32601,
|
|
647
|
+
message: 'agent-bridge does not handle app-server initiated requests',
|
|
648
|
+
},
|
|
649
|
+
})}\n`);
|
|
650
|
+
}
|
|
651
|
+
failAll(error) {
|
|
652
|
+
const pending = Array.from(this.pending.values());
|
|
653
|
+
this.pending.clear();
|
|
654
|
+
for (const request of pending) {
|
|
655
|
+
clearTimeout(request.timer);
|
|
656
|
+
request.reject(error);
|
|
657
|
+
}
|
|
658
|
+
for (const listener of this.fatalListeners) {
|
|
659
|
+
listener(error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const RECONNECT_PATTERN = /reconnecting\.\.\./i;
|
|
664
|
+
const HTTPS_FALLBACK_PATTERN = /falling back from websockets to https transport/i;
|
|
665
|
+
function buildThreadStartParams(params) {
|
|
666
|
+
const appServer = params.backend.appServer ?? {};
|
|
667
|
+
return {
|
|
668
|
+
model: params.model,
|
|
669
|
+
cwd: params.workspaceDir,
|
|
670
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
671
|
+
sandbox: appServer.sandbox ?? 'read-only',
|
|
672
|
+
serviceName: appServer.serviceName ?? 'agent-bridge',
|
|
673
|
+
developerInstructions: params.systemPrompt?.trim() || undefined,
|
|
674
|
+
experimentalRawEvents: appServer.experimentalRawEvents ?? false,
|
|
675
|
+
persistExtendedHistory: appServer.persistExtendedHistory ?? false,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function buildThreadResumeParams(params) {
|
|
679
|
+
const appServer = params.backend.appServer ?? {};
|
|
680
|
+
return {
|
|
681
|
+
threadId: params.threadId,
|
|
682
|
+
model: params.model,
|
|
683
|
+
cwd: params.workspaceDir,
|
|
684
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
685
|
+
sandbox: appServer.sandbox ?? 'read-only',
|
|
686
|
+
persistExtendedHistory: appServer.persistExtendedHistory ?? false,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function buildTurnStartParams(params) {
|
|
690
|
+
const appServer = params.backend.appServer ?? {};
|
|
691
|
+
return {
|
|
692
|
+
threadId: params.threadId,
|
|
693
|
+
input: buildTurnInput(params.prompt, params.imagePaths),
|
|
694
|
+
cwd: params.workspaceDir,
|
|
695
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
696
|
+
model: params.model,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function buildTurnInput(prompt, imagePaths) {
|
|
700
|
+
const input = [];
|
|
701
|
+
if (prompt.trim()) {
|
|
702
|
+
input.push({
|
|
703
|
+
type: 'text',
|
|
704
|
+
text: prompt.trim(),
|
|
705
|
+
text_elements: [],
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
for (const imagePath of imagePaths ?? []) {
|
|
709
|
+
input.push({
|
|
710
|
+
type: 'localImage',
|
|
711
|
+
path: imagePath,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return input;
|
|
715
|
+
}
|
|
716
|
+
function getThreadIdFromResponse(response) {
|
|
717
|
+
const record = asRecord(response);
|
|
718
|
+
const thread = asRecord(record?.thread);
|
|
719
|
+
return getString(thread?.id);
|
|
720
|
+
}
|
|
721
|
+
function getTurnIdFromResponse(response) {
|
|
722
|
+
const record = asRecord(response);
|
|
723
|
+
const turn = asRecord(record?.turn);
|
|
724
|
+
return getString(turn?.id);
|
|
725
|
+
}
|
|
726
|
+
function getTurnFailureMessage(turn) {
|
|
727
|
+
if (!turn) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
const status = getString(turn.status);
|
|
731
|
+
const error = asRecord(turn.error);
|
|
732
|
+
const message = getString(error?.message);
|
|
733
|
+
if (message) {
|
|
734
|
+
return message;
|
|
735
|
+
}
|
|
736
|
+
if (status && status !== 'completed') {
|
|
737
|
+
return `Codex turn finished with status ${status}`;
|
|
738
|
+
}
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
function toCliUsage(tokenUsage) {
|
|
742
|
+
const breakdown = asRecord(tokenUsage?.last);
|
|
743
|
+
if (!breakdown) {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
const input = getNumber(breakdown.inputTokens);
|
|
747
|
+
const output = getNumber(breakdown.outputTokens);
|
|
748
|
+
const cacheRead = getNumber(breakdown.cachedInputTokens);
|
|
749
|
+
const total = getNumber(breakdown.totalTokens);
|
|
750
|
+
if ([input, output, cacheRead, total].every((value) => value === undefined)) {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
input,
|
|
755
|
+
output,
|
|
756
|
+
cacheRead,
|
|
757
|
+
total,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function asRecord(value) {
|
|
761
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
function getString(value) {
|
|
767
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
768
|
+
}
|
|
769
|
+
function getNumber(value) {
|
|
770
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
771
|
+
}
|
|
772
|
+
//# sourceMappingURL=codex-app-server-runtime.js.map
|