agent-life-bridge 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/bin/agent-life-bridge.mjs +13 -0
- package/config.example.json +130 -0
- package/config.multi-agent.example.json +88 -0
- package/dist/agents/agent-life-session-map-store.d.ts +21 -0
- package/dist/agents/agent-life-session-map-store.d.ts.map +1 -0
- package/dist/agents/agent-life-session-map-store.js +78 -0
- package/dist/agents/agent-life-session-map-store.js.map +1 -0
- package/dist/agents/agent-resolver.d.ts +12 -0
- package/dist/agents/agent-resolver.d.ts.map +1 -0
- package/dist/agents/agent-resolver.js +114 -0
- package/dist/agents/agent-resolver.js.map +1 -0
- package/dist/agents/agent-router.d.ts +8 -0
- package/dist/agents/agent-router.d.ts.map +1 -0
- package/dist/agents/agent-router.js +72 -0
- package/dist/agents/agent-router.js.map +1 -0
- package/dist/agents/cli-backends.d.ts +25 -0
- package/dist/agents/cli-backends.d.ts.map +1 -0
- package/dist/agents/cli-backends.js +252 -0
- package/dist/agents/cli-backends.js.map +1 -0
- package/dist/agents/cli-output.d.ts +43 -0
- package/dist/agents/cli-output.d.ts.map +1 -0
- package/dist/agents/cli-output.js +352 -0
- package/dist/agents/cli-output.js.map +1 -0
- package/dist/agents/cli-runner.d.ts +32 -0
- package/dist/agents/cli-runner.d.ts.map +1 -0
- package/dist/agents/cli-runner.js +861 -0
- package/dist/agents/cli-runner.js.map +1 -0
- package/dist/agents/cli-session-store.d.ts +53 -0
- package/dist/agents/cli-session-store.d.ts.map +1 -0
- package/dist/agents/cli-session-store.js +263 -0
- package/dist/agents/cli-session-store.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.d.ts +80 -0
- package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.js +1049 -0
- package/dist/agents/codex-app-server-runtime.js.map +1 -0
- package/dist/agents/fch-runtime-context.d.ts +28 -0
- package/dist/agents/fch-runtime-context.d.ts.map +1 -0
- package/dist/agents/fch-runtime-context.js +65 -0
- package/dist/agents/fch-runtime-context.js.map +1 -0
- package/dist/agents/model-selection.d.ts +28 -0
- package/dist/agents/model-selection.d.ts.map +1 -0
- package/dist/agents/model-selection.js +40 -0
- package/dist/agents/model-selection.js.map +1 -0
- package/dist/agents/models-config.d.ts +28 -0
- package/dist/agents/models-config.d.ts.map +1 -0
- package/dist/agents/models-config.js +43 -0
- package/dist/agents/models-config.js.map +1 -0
- package/dist/channels/agent-life.d.ts +9 -0
- package/dist/channels/agent-life.d.ts.map +1 -0
- package/dist/channels/agent-life.js +407 -0
- package/dist/channels/agent-life.js.map +1 -0
- package/dist/channels/command-gating.d.ts +20 -0
- package/dist/channels/command-gating.d.ts.map +1 -0
- package/dist/channels/command-gating.js +43 -0
- package/dist/channels/command-gating.js.map +1 -0
- package/dist/channels/draft-stream-controls.d.ts +21 -0
- package/dist/channels/draft-stream-controls.d.ts.map +1 -0
- package/dist/channels/draft-stream-controls.js +43 -0
- package/dist/channels/draft-stream-controls.js.map +1 -0
- package/dist/channels/draft-stream-loop.d.ts +31 -0
- package/dist/channels/draft-stream-loop.d.ts.map +1 -0
- package/dist/channels/draft-stream-loop.js +60 -0
- package/dist/channels/draft-stream-loop.js.map +1 -0
- package/dist/channels/mention-gating.d.ts +18 -0
- package/dist/channels/mention-gating.d.ts.map +1 -0
- package/dist/channels/mention-gating.js +20 -0
- package/dist/channels/mention-gating.js.map +1 -0
- package/dist/channels/registry.d.ts +5 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +14 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/run-state-machine.d.ts +21 -0
- package/dist/channels/run-state-machine.d.ts.map +1 -0
- package/dist/channels/run-state-machine.js +41 -0
- package/dist/channels/run-state-machine.js.map +1 -0
- package/dist/channels/session-envelope.d.ts +5 -0
- package/dist/channels/session-envelope.d.ts.map +1 -0
- package/dist/channels/session-envelope.js +16 -0
- package/dist/channels/session-envelope.js.map +1 -0
- package/dist/channels/session-id.d.ts +3 -0
- package/dist/channels/session-id.d.ts.map +1 -0
- package/dist/channels/session-id.js +11 -0
- package/dist/channels/session-id.js.map +1 -0
- package/dist/channels/session.d.ts +18 -0
- package/dist/channels/session.d.ts.map +1 -0
- package/dist/channels/session.js +35 -0
- package/dist/channels/session.js.map +1 -0
- package/dist/channels/typing-lifecycle.d.ts +16 -0
- package/dist/channels/typing-lifecycle.d.ts.map +1 -0
- package/dist/channels/typing-lifecycle.js +31 -0
- package/dist/channels/typing-lifecycle.js.map +1 -0
- package/dist/cli/cmd-add-agent.d.ts +19 -0
- package/dist/cli/cmd-add-agent.d.ts.map +1 -0
- package/dist/cli/cmd-add-agent.js +362 -0
- package/dist/cli/cmd-add-agent.js.map +1 -0
- package/dist/cli/cmd-config.d.ts +3 -0
- package/dist/cli/cmd-config.d.ts.map +1 -0
- package/dist/cli/cmd-config.js +16 -0
- package/dist/cli/cmd-config.js.map +1 -0
- package/dist/cli/cmd-doctor.d.ts +3 -0
- package/dist/cli/cmd-doctor.d.ts.map +1 -0
- package/dist/cli/cmd-doctor.js +127 -0
- package/dist/cli/cmd-doctor.js.map +1 -0
- package/dist/cli/cmd-logs.d.ts +3 -0
- package/dist/cli/cmd-logs.d.ts.map +1 -0
- package/dist/cli/cmd-logs.js +26 -0
- package/dist/cli/cmd-logs.js.map +1 -0
- package/dist/cli/cmd-onboard.d.ts +5 -0
- package/dist/cli/cmd-onboard.d.ts.map +1 -0
- package/dist/cli/cmd-onboard.js +53 -0
- package/dist/cli/cmd-onboard.js.map +1 -0
- package/dist/cli/cmd-restart.d.ts +3 -0
- package/dist/cli/cmd-restart.d.ts.map +1 -0
- package/dist/cli/cmd-restart.js +22 -0
- package/dist/cli/cmd-restart.js.map +1 -0
- package/dist/cli/cmd-start.d.ts +19 -0
- package/dist/cli/cmd-start.d.ts.map +1 -0
- package/dist/cli/cmd-start.js +783 -0
- package/dist/cli/cmd-start.js.map +1 -0
- package/dist/cli/cmd-status.d.ts +3 -0
- package/dist/cli/cmd-status.d.ts.map +1 -0
- package/dist/cli/cmd-status.js +16 -0
- package/dist/cli/cmd-status.js.map +1 -0
- package/dist/cli/cmd-stop.d.ts +9 -0
- package/dist/cli/cmd-stop.d.ts.map +1 -0
- package/dist/cli/cmd-stop.js +59 -0
- package/dist/cli/cmd-stop.js.map +1 -0
- package/dist/cli/cmd-update.d.ts +3 -0
- package/dist/cli/cmd-update.d.ts.map +1 -0
- package/dist/cli/cmd-update.js +127 -0
- package/dist/cli/cmd-update.js.map +1 -0
- 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/pid-file.d.ts +23 -0
- package/dist/cli/pid-file.d.ts.map +1 -0
- package/dist/cli/pid-file.js +136 -0
- package/dist/cli/pid-file.js.map +1 -0
- package/dist/cli/run-main.d.ts +16 -0
- package/dist/cli/run-main.d.ts.map +1 -0
- package/dist/cli/run-main.js +114 -0
- package/dist/cli/run-main.js.map +1 -0
- package/dist/cli/update-check.d.ts +15 -0
- package/dist/cli/update-check.d.ts.map +1 -0
- package/dist/cli/update-check.js +187 -0
- package/dist/cli/update-check.js.map +1 -0
- package/dist/commands/prefix-router.d.ts +17 -0
- package/dist/commands/prefix-router.d.ts.map +1 -0
- package/dist/commands/prefix-router.js +79 -0
- package/dist/commands/prefix-router.js.map +1 -0
- package/dist/config/agent-dirs.d.ts +24 -0
- package/dist/config/agent-dirs.d.ts.map +1 -0
- package/dist/config/agent-dirs.js +71 -0
- package/dist/config/agent-dirs.js.map +1 -0
- package/dist/config/config.d.ts +4 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +42 -0
- package/dist/config/config.js.map +1 -0
- 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 +94 -0
- package/dist/config/default-config.js.map +1 -0
- package/dist/config/env-vars.d.ts +15 -0
- package/dist/config/env-vars.d.ts.map +1 -0
- package/dist/config/env-vars.js +16 -0
- package/dist/config/env-vars.js.map +1 -0
- package/dist/config/merge-config.d.ts +7 -0
- package/dist/config/merge-config.d.ts.map +1 -0
- package/dist/config/merge-config.js +45 -0
- package/dist/config/merge-config.js.map +1 -0
- package/dist/config/paths.d.ts +25 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +31 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/zod-schema.d.ts +3114 -0
- package/dist/config/zod-schema.d.ts.map +1 -0
- package/dist/config/zod-schema.js +217 -0
- package/dist/config/zod-schema.js.map +1 -0
- package/dist/entry.d.ts +3 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +70 -0
- package/dist/entry.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +42 -0
- package/dist/logger.js.map +1 -0
- package/dist/types/index.d.ts +215 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { buildCodexFchConfigArgs, buildFchEnv, } from './fch-runtime-context.js';
|
|
4
|
+
import { isSessionExpiredError } from './cli-output.js';
|
|
5
|
+
class CodexAppServerProcessError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CodexAppServerProcessError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class CodexAppServerRpcError extends Error {
|
|
12
|
+
method;
|
|
13
|
+
code;
|
|
14
|
+
constructor(method, code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.method = method;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.name = 'CodexAppServerRpcError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class CodexAppServerTurnTimeoutError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'CodexAppServerTurnTimeoutError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class CodexAppServerRuntime {
|
|
28
|
+
options;
|
|
29
|
+
connection;
|
|
30
|
+
loadedThreads = new Map();
|
|
31
|
+
activeTurnsByThread = new Map();
|
|
32
|
+
activeTurnsByTurn = new Map();
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.connection = options.connection ?? new DefaultCodexAppServerConnection({
|
|
36
|
+
backend: options.backend,
|
|
37
|
+
logger: options.logger,
|
|
38
|
+
fchContext: options.fchContext,
|
|
39
|
+
});
|
|
40
|
+
this.connection.onNotification((notification) => {
|
|
41
|
+
this.handleNotification(notification);
|
|
42
|
+
});
|
|
43
|
+
this.connection.onFatalError((error) => {
|
|
44
|
+
this.handleFatalError(error);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async run(params) {
|
|
48
|
+
const restartCountBefore = this.connection.getRestartCount();
|
|
49
|
+
await this.connection.ensureReady();
|
|
50
|
+
try {
|
|
51
|
+
const prepared = await this.ensureThread(params);
|
|
52
|
+
const completed = await this.runTurn({
|
|
53
|
+
sessionId: params.sessionId,
|
|
54
|
+
threadId: prepared.threadId,
|
|
55
|
+
model: params.model,
|
|
56
|
+
prompt: params.prompt,
|
|
57
|
+
imagePaths: params.imagePaths,
|
|
58
|
+
workspaceDir: params.workspaceDir,
|
|
59
|
+
timeoutMs: params.timeoutMs,
|
|
60
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs,
|
|
61
|
+
onProgress: params.onProgress,
|
|
62
|
+
onAssistantMessage: params.onAssistantMessage,
|
|
63
|
+
});
|
|
64
|
+
const runtimeRestartCount = this.connection.getRestartCount() - restartCountBefore;
|
|
65
|
+
return {
|
|
66
|
+
text: completed.text,
|
|
67
|
+
messages: completed.messages,
|
|
68
|
+
cliSessionId: prepared.threadId,
|
|
69
|
+
isResume: prepared.isResume,
|
|
70
|
+
usage: completed.usage,
|
|
71
|
+
diagnostics: {
|
|
72
|
+
queueWaitMs: 0,
|
|
73
|
+
commandElapsedMs: completed.turnCompletedMs ?? Date.now() - completed.startedAt,
|
|
74
|
+
totalElapsedMs: prepared.threadOperationElapsedMs + (completed.turnCompletedMs ?? Date.now() - completed.startedAt),
|
|
75
|
+
outputMode: 'app-server',
|
|
76
|
+
runtimeMode: 'app-server',
|
|
77
|
+
hadStdout: false,
|
|
78
|
+
hadStderr: false,
|
|
79
|
+
firstAgentMessageMs: completed.firstAgentMessageMs,
|
|
80
|
+
threadStartedMs: prepared.threadOperation === 'start' ? prepared.threadOperationElapsedMs : undefined,
|
|
81
|
+
turnCompletedMs: completed.turnCompletedMs,
|
|
82
|
+
reconnectCount: completed.reconnectCount,
|
|
83
|
+
fellBackToHttps: completed.fellBackToHttps,
|
|
84
|
+
runtimeRestartCount: runtimeRestartCount > 0 ? runtimeRestartCount : undefined,
|
|
85
|
+
threadOperation: prepared.threadOperation,
|
|
86
|
+
threadOperationElapsedMs: prepared.threadOperationElapsedMs,
|
|
87
|
+
cliSessionIdSource: prepared.threadOperation === 'start' ? 'parsed' : 'existing',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (isSessionExpiredError(error)) {
|
|
93
|
+
this.loadedThreads.delete(params.sessionId);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async teardown() {
|
|
99
|
+
this.loadedThreads.clear();
|
|
100
|
+
this.activeTurnsByThread.clear();
|
|
101
|
+
this.activeTurnsByTurn.clear();
|
|
102
|
+
await this.connection.dispose();
|
|
103
|
+
}
|
|
104
|
+
async ensureThread(params) {
|
|
105
|
+
const existingThreadId = params.existingThreadId?.trim() || undefined;
|
|
106
|
+
const loaded = this.loadedThreads.get(params.sessionId);
|
|
107
|
+
if (!existingThreadId && loaded) {
|
|
108
|
+
this.loadedThreads.delete(params.sessionId);
|
|
109
|
+
}
|
|
110
|
+
if (loaded && existingThreadId && loaded.threadId === existingThreadId) {
|
|
111
|
+
return {
|
|
112
|
+
threadId: existingThreadId,
|
|
113
|
+
isResume: true,
|
|
114
|
+
threadOperation: 'reuse',
|
|
115
|
+
threadOperationElapsedMs: 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const requestTimeoutMs = this.resolveRequestTimeout(params.timeoutMs);
|
|
119
|
+
if (existingThreadId) {
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
const response = await this.connection.request('thread/resume', buildThreadResumeParams({
|
|
122
|
+
backend: this.options.backend,
|
|
123
|
+
threadId: existingThreadId,
|
|
124
|
+
model: params.model,
|
|
125
|
+
workspaceDir: params.workspaceDir,
|
|
126
|
+
}), requestTimeoutMs);
|
|
127
|
+
const threadId = getThreadIdFromResponse(response) ?? existingThreadId;
|
|
128
|
+
this.loadedThreads.set(params.sessionId, { threadId });
|
|
129
|
+
return {
|
|
130
|
+
threadId,
|
|
131
|
+
isResume: true,
|
|
132
|
+
threadOperation: 'resume',
|
|
133
|
+
threadOperationElapsedMs: Date.now() - startedAt,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const startedAt = Date.now();
|
|
137
|
+
const response = await this.connection.request('thread/start', buildThreadStartParams({
|
|
138
|
+
backend: this.options.backend,
|
|
139
|
+
model: params.model,
|
|
140
|
+
workspaceDir: params.workspaceDir,
|
|
141
|
+
systemPrompt: params.systemPrompt,
|
|
142
|
+
}), requestTimeoutMs);
|
|
143
|
+
const threadId = getThreadIdFromResponse(response);
|
|
144
|
+
if (!threadId) {
|
|
145
|
+
throw new Error('thread/start succeeded but did not return a thread id');
|
|
146
|
+
}
|
|
147
|
+
this.loadedThreads.set(params.sessionId, { threadId });
|
|
148
|
+
return {
|
|
149
|
+
threadId,
|
|
150
|
+
isResume: false,
|
|
151
|
+
threadOperation: 'start',
|
|
152
|
+
threadOperationElapsedMs: Date.now() - startedAt,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async runTurn(params) {
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
const state = this.createActiveTurnState({
|
|
158
|
+
sessionId: params.sessionId,
|
|
159
|
+
threadId: params.threadId,
|
|
160
|
+
timeoutMs: params.timeoutMs,
|
|
161
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs,
|
|
162
|
+
startedAt,
|
|
163
|
+
onProgress: params.onProgress,
|
|
164
|
+
onAssistantMessage: params.onAssistantMessage,
|
|
165
|
+
});
|
|
166
|
+
this.activeTurnsByThread.set(params.threadId, state);
|
|
167
|
+
try {
|
|
168
|
+
const response = await this.connection.request('turn/start', buildTurnStartParams({
|
|
169
|
+
backend: this.options.backend,
|
|
170
|
+
threadId: params.threadId,
|
|
171
|
+
model: params.model,
|
|
172
|
+
prompt: params.prompt,
|
|
173
|
+
imagePaths: params.imagePaths,
|
|
174
|
+
workspaceDir: params.workspaceDir,
|
|
175
|
+
}), this.resolveRequestTimeout(params.timeoutMs));
|
|
176
|
+
const turnId = getTurnIdFromResponse(response);
|
|
177
|
+
if (!state.settled && turnId) {
|
|
178
|
+
this.bindTurnId(state, turnId);
|
|
179
|
+
}
|
|
180
|
+
if (!state.settled && params.noProgressTimeoutMs > 0) {
|
|
181
|
+
this.armNoProgressTimer(state, params.noProgressTimeoutMs);
|
|
182
|
+
}
|
|
183
|
+
const completed = await state.completion;
|
|
184
|
+
return {
|
|
185
|
+
...completed,
|
|
186
|
+
startedAt,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.cleanupTurnState(state);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
createActiveTurnState(params) {
|
|
195
|
+
let state;
|
|
196
|
+
let resolve;
|
|
197
|
+
let reject;
|
|
198
|
+
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
199
|
+
resolve = resolvePromise;
|
|
200
|
+
reject = rejectPromise;
|
|
201
|
+
});
|
|
202
|
+
void completion.catch(() => undefined);
|
|
203
|
+
state = {
|
|
204
|
+
sessionId: params.sessionId,
|
|
205
|
+
threadId: params.threadId,
|
|
206
|
+
startedAt: params.startedAt,
|
|
207
|
+
reconnectCount: 0,
|
|
208
|
+
fellBackToHttps: false,
|
|
209
|
+
messages: [],
|
|
210
|
+
settled: false,
|
|
211
|
+
timeoutMs: params.timeoutMs,
|
|
212
|
+
noProgressTimeoutMs: params.noProgressTimeoutMs > 0 ? params.noProgressTimeoutMs : undefined,
|
|
213
|
+
onProgress: params.onProgress,
|
|
214
|
+
onAssistantMessage: params.onAssistantMessage,
|
|
215
|
+
overallTimer: setTimeout(() => undefined, params.timeoutMs),
|
|
216
|
+
completion,
|
|
217
|
+
resolve,
|
|
218
|
+
reject,
|
|
219
|
+
};
|
|
220
|
+
this.armOverallTimer(state, params.timeoutMs);
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
armOverallTimer(state, timeoutMs) {
|
|
224
|
+
clearTimeout(state.overallTimer);
|
|
225
|
+
state.overallTimer = setTimeout(() => {
|
|
226
|
+
this.rejectTurn(state, new CodexAppServerTurnTimeoutError(`Codex turn exceeded timeout (${Math.round(timeoutMs / 1000)}s).`));
|
|
227
|
+
if (state.turnId) {
|
|
228
|
+
this.interruptTurn(state.turnId).catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
}, timeoutMs);
|
|
231
|
+
}
|
|
232
|
+
armNoProgressTimer(state, noProgressTimeoutMs) {
|
|
233
|
+
clearTimeout(state.noProgressTimer);
|
|
234
|
+
state.noProgressTimer = setTimeout(() => {
|
|
235
|
+
this.rejectTurn(state, new CodexAppServerTurnTimeoutError(`Codex turn produced no progress for ${Math.round(noProgressTimeoutMs / 1000)}s.`));
|
|
236
|
+
if (state.turnId) {
|
|
237
|
+
this.interruptTurn(state.turnId).catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
}, noProgressTimeoutMs);
|
|
240
|
+
}
|
|
241
|
+
bindTurnId(state, turnId) {
|
|
242
|
+
if (state.turnId === turnId) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (state.turnId) {
|
|
246
|
+
this.activeTurnsByTurn.delete(state.turnId);
|
|
247
|
+
}
|
|
248
|
+
state.turnId = turnId;
|
|
249
|
+
this.activeTurnsByTurn.set(turnId, state);
|
|
250
|
+
}
|
|
251
|
+
handleNotification(notification) {
|
|
252
|
+
switch (notification.method) {
|
|
253
|
+
case 'turn/started':
|
|
254
|
+
this.handleTurnStarted(notification.params);
|
|
255
|
+
break;
|
|
256
|
+
case 'item/completed':
|
|
257
|
+
this.handleItemCompleted(notification.params);
|
|
258
|
+
break;
|
|
259
|
+
case 'thread/tokenUsage/updated':
|
|
260
|
+
this.handleTokenUsageUpdated(notification.params);
|
|
261
|
+
break;
|
|
262
|
+
case 'turn/completed':
|
|
263
|
+
this.handleTurnCompleted(notification.params);
|
|
264
|
+
break;
|
|
265
|
+
case 'error':
|
|
266
|
+
this.handleTurnError(notification.params);
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
handleTurnStarted(params) {
|
|
273
|
+
const record = asRecord(params);
|
|
274
|
+
const threadId = getString(record?.threadId);
|
|
275
|
+
const turn = asRecord(record?.turn);
|
|
276
|
+
const turnId = getString(turn?.id);
|
|
277
|
+
const state = this.findTurnState(threadId, turnId);
|
|
278
|
+
if (!state) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (turnId) {
|
|
282
|
+
this.bindTurnId(state, turnId);
|
|
283
|
+
}
|
|
284
|
+
this.noteTurnProgress(state);
|
|
285
|
+
this.emitProgress(state, '正在处理你的消息...');
|
|
286
|
+
}
|
|
287
|
+
handleItemCompleted(params) {
|
|
288
|
+
const record = asRecord(params);
|
|
289
|
+
const threadId = getString(record?.threadId);
|
|
290
|
+
const turnId = getString(record?.turnId);
|
|
291
|
+
const state = this.findTurnState(threadId, turnId);
|
|
292
|
+
if (!state) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (turnId) {
|
|
296
|
+
this.bindTurnId(state, turnId);
|
|
297
|
+
}
|
|
298
|
+
this.noteTurnProgress(state);
|
|
299
|
+
const item = asRecord(record?.item);
|
|
300
|
+
const itemType = getString(item?.type);
|
|
301
|
+
if (itemType === 'agentMessage') {
|
|
302
|
+
const text = getString(item?.text);
|
|
303
|
+
if (!text) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
state.lastText = text;
|
|
307
|
+
state.messages.push(text);
|
|
308
|
+
if (state.firstAgentMessageMs === undefined) {
|
|
309
|
+
state.firstAgentMessageMs = Date.now() - state.startedAt;
|
|
310
|
+
}
|
|
311
|
+
this.options.logger.info({
|
|
312
|
+
sessionId: state.sessionId,
|
|
313
|
+
threadId: state.threadId,
|
|
314
|
+
turnId: state.turnId,
|
|
315
|
+
messageIndex: state.messages.length,
|
|
316
|
+
textPreview: text.length > 200 ? `${text.slice(0, 200)}...` : text,
|
|
317
|
+
}, 'Codex app-server emitted agent message');
|
|
318
|
+
this.emitAssistantMessage(state, text);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const progressText = summarizeProgressItem(item);
|
|
322
|
+
if (progressText) {
|
|
323
|
+
this.emitProgress(state, progressText);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
handleTokenUsageUpdated(params) {
|
|
327
|
+
const record = asRecord(params);
|
|
328
|
+
const threadId = getString(record?.threadId);
|
|
329
|
+
const turnId = getString(record?.turnId);
|
|
330
|
+
const state = this.findTurnState(threadId, turnId);
|
|
331
|
+
if (!state) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (turnId) {
|
|
335
|
+
this.bindTurnId(state, turnId);
|
|
336
|
+
}
|
|
337
|
+
state.usage = toCliUsage(asRecord(record?.tokenUsage));
|
|
338
|
+
}
|
|
339
|
+
handleTurnCompleted(params) {
|
|
340
|
+
const record = asRecord(params);
|
|
341
|
+
const threadId = getString(record?.threadId);
|
|
342
|
+
const turn = asRecord(record?.turn);
|
|
343
|
+
const turnId = getString(turn?.id);
|
|
344
|
+
const state = this.findTurnState(threadId, turnId);
|
|
345
|
+
if (!state) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (turnId) {
|
|
349
|
+
this.bindTurnId(state, turnId);
|
|
350
|
+
}
|
|
351
|
+
this.noteTurnProgress(state);
|
|
352
|
+
state.turnCompletedMs = Date.now() - state.startedAt;
|
|
353
|
+
const failureMessage = getTurnFailureMessage(turn);
|
|
354
|
+
if (failureMessage) {
|
|
355
|
+
this.rejectTurn(state, new Error(failureMessage));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.resolveTurn(state, {
|
|
359
|
+
text: state.messages.at(-1) ?? state.lastText ?? '',
|
|
360
|
+
messages: [...state.messages],
|
|
361
|
+
usage: state.usage,
|
|
362
|
+
reconnectCount: state.reconnectCount,
|
|
363
|
+
fellBackToHttps: state.fellBackToHttps,
|
|
364
|
+
firstAgentMessageMs: state.firstAgentMessageMs,
|
|
365
|
+
turnCompletedMs: state.turnCompletedMs,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
handleTurnError(params) {
|
|
369
|
+
const record = asRecord(params);
|
|
370
|
+
const threadId = getString(record?.threadId);
|
|
371
|
+
const turnId = getString(record?.turnId);
|
|
372
|
+
const state = this.findTurnState(threadId, turnId);
|
|
373
|
+
if (!state) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (turnId) {
|
|
377
|
+
this.bindTurnId(state, turnId);
|
|
378
|
+
}
|
|
379
|
+
this.noteTurnProgress(state);
|
|
380
|
+
const error = asRecord(record?.error);
|
|
381
|
+
const message = `${getString(error?.message) ?? ''}\n${getString(error?.additionalDetails) ?? ''}`.trim();
|
|
382
|
+
if (message) {
|
|
383
|
+
this.emitProgress(state, message);
|
|
384
|
+
}
|
|
385
|
+
if (RECONNECT_PATTERN.test(message)) {
|
|
386
|
+
state.reconnectCount += 1;
|
|
387
|
+
}
|
|
388
|
+
if (HTTPS_FALLBACK_PATTERN.test(message)) {
|
|
389
|
+
state.fellBackToHttps = true;
|
|
390
|
+
}
|
|
391
|
+
const willRetry = Boolean(record?.willRetry);
|
|
392
|
+
if (!willRetry) {
|
|
393
|
+
this.rejectTurn(state, new Error(getString(error?.message) ?? 'Codex turn failed'));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
handleFatalError(error) {
|
|
397
|
+
this.loadedThreads.clear();
|
|
398
|
+
const states = new Set(this.activeTurnsByThread.values());
|
|
399
|
+
for (const state of states) {
|
|
400
|
+
this.rejectTurn(state, error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
noteTurnProgress(state) {
|
|
404
|
+
if (state.noProgressTimeoutMs) {
|
|
405
|
+
this.armNoProgressTimer(state, state.noProgressTimeoutMs);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
emitProgress(state, text) {
|
|
409
|
+
const normalized = text.trim();
|
|
410
|
+
if (!normalized) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.armOverallTimer(state, state.timeoutMs);
|
|
414
|
+
try {
|
|
415
|
+
void Promise.resolve(state.onProgress?.(normalized)).catch((error) => {
|
|
416
|
+
this.options.logger.warn({
|
|
417
|
+
sessionId: state.sessionId,
|
|
418
|
+
threadId: state.threadId,
|
|
419
|
+
turnId: state.turnId,
|
|
420
|
+
err: error instanceof Error ? error.message : String(error),
|
|
421
|
+
}, 'Codex progress callback failed');
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
emitAssistantMessage(state, text) {
|
|
429
|
+
const normalized = text.trim();
|
|
430
|
+
if (!normalized) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.armOverallTimer(state, state.timeoutMs);
|
|
434
|
+
try {
|
|
435
|
+
void Promise.resolve(state.onAssistantMessage?.(normalized)).catch((error) => {
|
|
436
|
+
this.options.logger.warn({
|
|
437
|
+
sessionId: state.sessionId,
|
|
438
|
+
threadId: state.threadId,
|
|
439
|
+
turnId: state.turnId,
|
|
440
|
+
err: error instanceof Error ? error.message : String(error),
|
|
441
|
+
}, 'Codex assistant message callback failed');
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
findTurnState(threadId, turnId) {
|
|
449
|
+
if (turnId) {
|
|
450
|
+
const byTurn = this.activeTurnsByTurn.get(turnId);
|
|
451
|
+
if (byTurn) {
|
|
452
|
+
return byTurn;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (threadId) {
|
|
456
|
+
return this.activeTurnsByThread.get(threadId);
|
|
457
|
+
}
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
resolveTurn(state, value) {
|
|
461
|
+
if (state.settled) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
state.settled = true;
|
|
465
|
+
this.cleanupTurnState(state);
|
|
466
|
+
state.resolve(value);
|
|
467
|
+
}
|
|
468
|
+
rejectTurn(state, error) {
|
|
469
|
+
if (state.settled) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
state.settled = true;
|
|
473
|
+
this.cleanupTurnState(state);
|
|
474
|
+
state.reject(error);
|
|
475
|
+
}
|
|
476
|
+
cleanupTurnState(state) {
|
|
477
|
+
clearTimeout(state.overallTimer);
|
|
478
|
+
clearTimeout(state.noProgressTimer);
|
|
479
|
+
this.activeTurnsByThread.delete(state.threadId);
|
|
480
|
+
if (state.turnId) {
|
|
481
|
+
this.activeTurnsByTurn.delete(state.turnId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async interruptTurn(turnId) {
|
|
485
|
+
await this.connection.request('turn/interrupt', { turnId }, Math.max(5_000, Math.min(this.resolveRequestTimeout(15_000), 15_000))).catch(() => undefined);
|
|
486
|
+
}
|
|
487
|
+
resolveRequestTimeout(timeoutMs) {
|
|
488
|
+
const configured = this.options.backend.appServer?.requestTimeoutMs ?? 30_000;
|
|
489
|
+
return Math.max(1_000, Math.min(timeoutMs, configured));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
class DefaultCodexAppServerConnection {
|
|
493
|
+
options;
|
|
494
|
+
child = null;
|
|
495
|
+
stdoutReader = null;
|
|
496
|
+
stderrReader = null;
|
|
497
|
+
readyPromise = null;
|
|
498
|
+
pending = new Map();
|
|
499
|
+
nextId = 1;
|
|
500
|
+
initialized = false;
|
|
501
|
+
disposed = false;
|
|
502
|
+
notificationListeners = new Set();
|
|
503
|
+
fatalListeners = new Set();
|
|
504
|
+
restartCount = 0;
|
|
505
|
+
constructor(options) {
|
|
506
|
+
this.options = options;
|
|
507
|
+
}
|
|
508
|
+
async ensureReady() {
|
|
509
|
+
if (this.disposed) {
|
|
510
|
+
throw new CodexAppServerProcessError('Codex app-server runtime is already shut down');
|
|
511
|
+
}
|
|
512
|
+
if (this.child && this.initialized) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (this.readyPromise) {
|
|
516
|
+
return this.readyPromise;
|
|
517
|
+
}
|
|
518
|
+
this.readyPromise = this.startAndInitialize().finally(() => {
|
|
519
|
+
this.readyPromise = null;
|
|
520
|
+
});
|
|
521
|
+
return this.readyPromise;
|
|
522
|
+
}
|
|
523
|
+
async request(method, params, timeoutMs) {
|
|
524
|
+
return this.sendRequest(method, params, timeoutMs, true);
|
|
525
|
+
}
|
|
526
|
+
async sendRequest(method, params, timeoutMs, ensureReady) {
|
|
527
|
+
if (ensureReady) {
|
|
528
|
+
await this.ensureReady();
|
|
529
|
+
}
|
|
530
|
+
if (!this.child?.stdin.writable) {
|
|
531
|
+
throw new CodexAppServerProcessError('Codex app-server stdin is not writable');
|
|
532
|
+
}
|
|
533
|
+
const id = this.nextId++;
|
|
534
|
+
const payload = JSON.stringify({
|
|
535
|
+
jsonrpc: '2.0',
|
|
536
|
+
id,
|
|
537
|
+
method,
|
|
538
|
+
params,
|
|
539
|
+
});
|
|
540
|
+
return new Promise((resolve, reject) => {
|
|
541
|
+
const timer = setTimeout(() => {
|
|
542
|
+
this.pending.delete(id);
|
|
543
|
+
reject(new CodexAppServerRpcError(method, undefined, `${method} timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
544
|
+
}, timeoutMs);
|
|
545
|
+
this.pending.set(id, {
|
|
546
|
+
method,
|
|
547
|
+
timer,
|
|
548
|
+
resolve: resolve,
|
|
549
|
+
reject,
|
|
550
|
+
});
|
|
551
|
+
this.child?.stdin.write(`${payload}\n`);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
notify(method, params) {
|
|
555
|
+
if (!this.child?.stdin.writable) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const message = {
|
|
559
|
+
jsonrpc: '2.0',
|
|
560
|
+
method,
|
|
561
|
+
};
|
|
562
|
+
if (params !== undefined) {
|
|
563
|
+
message.params = params;
|
|
564
|
+
}
|
|
565
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
566
|
+
}
|
|
567
|
+
getRestartCount() {
|
|
568
|
+
return Math.max(0, this.restartCount - 1);
|
|
569
|
+
}
|
|
570
|
+
onNotification(listener) {
|
|
571
|
+
this.notificationListeners.add(listener);
|
|
572
|
+
}
|
|
573
|
+
onFatalError(listener) {
|
|
574
|
+
this.fatalListeners.add(listener);
|
|
575
|
+
}
|
|
576
|
+
async dispose() {
|
|
577
|
+
this.disposed = true;
|
|
578
|
+
this.initialized = false;
|
|
579
|
+
const child = this.child;
|
|
580
|
+
if (!child) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
await new Promise((resolve) => {
|
|
584
|
+
const timer = setTimeout(() => {
|
|
585
|
+
child.kill();
|
|
586
|
+
resolve();
|
|
587
|
+
}, 2_000);
|
|
588
|
+
child.once('exit', () => {
|
|
589
|
+
clearTimeout(timer);
|
|
590
|
+
resolve();
|
|
591
|
+
});
|
|
592
|
+
child.kill();
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
async startAndInitialize() {
|
|
596
|
+
const startupTimeoutMs = this.options.backend.appServer?.startupTimeoutMs ?? 15_000;
|
|
597
|
+
await this.spawnProcess();
|
|
598
|
+
const initTimeoutMs = Math.max(1_000, startupTimeoutMs);
|
|
599
|
+
await this.sendRequest('initialize', {
|
|
600
|
+
clientInfo: {
|
|
601
|
+
name: 'agent-life-bridge',
|
|
602
|
+
title: 'agent-life-bridge',
|
|
603
|
+
version: '0.1.0',
|
|
604
|
+
},
|
|
605
|
+
capabilities: {
|
|
606
|
+
experimentalApi: this.options.backend.appServer?.experimentalApi ?? false,
|
|
607
|
+
optOutNotificationMethods: this.options.backend.appServer?.optOutNotificationMethods ?? [],
|
|
608
|
+
},
|
|
609
|
+
}, initTimeoutMs, false);
|
|
610
|
+
this.notify('initialized');
|
|
611
|
+
this.initialized = true;
|
|
612
|
+
}
|
|
613
|
+
async spawnProcess() {
|
|
614
|
+
const backend = this.options.backend;
|
|
615
|
+
const appServerArgs = backend.appServer?.args ?? [];
|
|
616
|
+
const listen = backend.appServer?.listen ?? 'stdio://';
|
|
617
|
+
const env = {
|
|
618
|
+
...process.env,
|
|
619
|
+
...backend.env,
|
|
620
|
+
...(this.options.fchContext ? buildFchEnv(this.options.fchContext) : {}),
|
|
621
|
+
};
|
|
622
|
+
for (const key of backend.clearEnv ?? []) {
|
|
623
|
+
delete env[key];
|
|
624
|
+
}
|
|
625
|
+
this.child = spawn(backend.command, [
|
|
626
|
+
...buildCodexFchConfigArgs(this.options.fchContext),
|
|
627
|
+
'app-server',
|
|
628
|
+
...appServerArgs,
|
|
629
|
+
'--listen',
|
|
630
|
+
listen,
|
|
631
|
+
], {
|
|
632
|
+
env,
|
|
633
|
+
stdio: 'pipe',
|
|
634
|
+
shell: process.platform === 'win32',
|
|
635
|
+
windowsHide: true,
|
|
636
|
+
});
|
|
637
|
+
this.stdoutReader = createInterface({
|
|
638
|
+
input: this.child.stdout,
|
|
639
|
+
crlfDelay: Infinity,
|
|
640
|
+
});
|
|
641
|
+
this.stderrReader = createInterface({
|
|
642
|
+
input: this.child.stderr,
|
|
643
|
+
crlfDelay: Infinity,
|
|
644
|
+
});
|
|
645
|
+
this.stdoutReader.on('line', (line) => {
|
|
646
|
+
this.handleStdoutLine(line);
|
|
647
|
+
});
|
|
648
|
+
this.stderrReader.on('line', (line) => {
|
|
649
|
+
this.options.logger.debug({ line }, 'Codex app-server stderr');
|
|
650
|
+
});
|
|
651
|
+
this.child.on('error', (error) => {
|
|
652
|
+
this.failAll(new CodexAppServerProcessError(error.message));
|
|
653
|
+
});
|
|
654
|
+
this.child.on('exit', (code, signal) => {
|
|
655
|
+
this.initialized = false;
|
|
656
|
+
this.stdoutReader?.close();
|
|
657
|
+
this.stderrReader?.close();
|
|
658
|
+
this.stdoutReader = null;
|
|
659
|
+
this.stderrReader = null;
|
|
660
|
+
this.child = null;
|
|
661
|
+
if (this.disposed) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.failAll(new CodexAppServerProcessError(`Codex app-server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'})`));
|
|
665
|
+
});
|
|
666
|
+
this.restartCount += 1;
|
|
667
|
+
}
|
|
668
|
+
handleStdoutLine(line) {
|
|
669
|
+
const trimmed = line.trim();
|
|
670
|
+
if (!trimmed) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
let parsed;
|
|
674
|
+
try {
|
|
675
|
+
parsed = JSON.parse(trimmed);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
this.options.logger.warn({ line: trimmed }, 'Codex app-server emitted non-JSON stdout line');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const id = typeof parsed.id === 'number' ? parsed.id : undefined;
|
|
682
|
+
const hasResult = Object.prototype.hasOwnProperty.call(parsed, 'result');
|
|
683
|
+
const hasError = Object.prototype.hasOwnProperty.call(parsed, 'error');
|
|
684
|
+
const method = getString(parsed.method);
|
|
685
|
+
if (id !== undefined && (hasResult || hasError)) {
|
|
686
|
+
const pending = this.pending.get(id);
|
|
687
|
+
if (!pending) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
clearTimeout(pending.timer);
|
|
691
|
+
this.pending.delete(id);
|
|
692
|
+
if (parsed.error) {
|
|
693
|
+
const error = asRecord(parsed.error);
|
|
694
|
+
pending.reject(new CodexAppServerRpcError(pending.method, typeof error?.code === 'number' ? error.code : undefined, getString(error?.message) ?? `${pending.method} failed`));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
pending.resolve(parsed.result);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (id !== undefined && method) {
|
|
701
|
+
this.replyUnsupportedServerRequest(parsed);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (!method) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const notification = {
|
|
708
|
+
method,
|
|
709
|
+
params: parsed.params,
|
|
710
|
+
};
|
|
711
|
+
for (const listener of this.notificationListeners) {
|
|
712
|
+
listener(notification);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
replyUnsupportedServerRequest(parsed) {
|
|
716
|
+
if (!this.child?.stdin.writable || typeof parsed.id !== 'number') {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
this.child.stdin.write(`${JSON.stringify({
|
|
720
|
+
jsonrpc: '2.0',
|
|
721
|
+
id: parsed.id,
|
|
722
|
+
error: {
|
|
723
|
+
code: -32601,
|
|
724
|
+
message: 'agent-life-bridge does not handle app-server initiated requests',
|
|
725
|
+
},
|
|
726
|
+
})}\n`);
|
|
727
|
+
}
|
|
728
|
+
failAll(error) {
|
|
729
|
+
const pending = Array.from(this.pending.values());
|
|
730
|
+
this.pending.clear();
|
|
731
|
+
for (const request of pending) {
|
|
732
|
+
clearTimeout(request.timer);
|
|
733
|
+
request.reject(error);
|
|
734
|
+
}
|
|
735
|
+
for (const listener of this.fatalListeners) {
|
|
736
|
+
listener(error);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const RECONNECT_PATTERN = /reconnecting\.\.\./i;
|
|
741
|
+
const HTTPS_FALLBACK_PATTERN = /falling back from websockets to https transport/i;
|
|
742
|
+
function buildThreadStartParams(params) {
|
|
743
|
+
const appServer = params.backend.appServer ?? {};
|
|
744
|
+
return {
|
|
745
|
+
model: params.model,
|
|
746
|
+
cwd: params.workspaceDir,
|
|
747
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
748
|
+
sandbox: appServer.sandbox ?? 'danger-full-access',
|
|
749
|
+
serviceName: appServer.serviceName ?? 'agent-life-bridge',
|
|
750
|
+
developerInstructions: params.systemPrompt?.trim() || undefined,
|
|
751
|
+
experimentalRawEvents: appServer.experimentalRawEvents ?? false,
|
|
752
|
+
persistExtendedHistory: appServer.persistExtendedHistory ?? false,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function buildThreadResumeParams(params) {
|
|
756
|
+
const appServer = params.backend.appServer ?? {};
|
|
757
|
+
return {
|
|
758
|
+
threadId: params.threadId,
|
|
759
|
+
model: params.model,
|
|
760
|
+
cwd: params.workspaceDir,
|
|
761
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
762
|
+
sandbox: appServer.sandbox ?? 'danger-full-access',
|
|
763
|
+
persistExtendedHistory: appServer.persistExtendedHistory ?? false,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function buildTurnStartParams(params) {
|
|
767
|
+
const appServer = params.backend.appServer ?? {};
|
|
768
|
+
return {
|
|
769
|
+
threadId: params.threadId,
|
|
770
|
+
input: buildTurnInput(params.prompt, params.imagePaths),
|
|
771
|
+
cwd: params.workspaceDir,
|
|
772
|
+
approvalPolicy: appServer.approvalPolicy ?? 'never',
|
|
773
|
+
model: params.model,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function buildTurnInput(prompt, imagePaths) {
|
|
777
|
+
const input = [];
|
|
778
|
+
if (prompt.trim()) {
|
|
779
|
+
input.push({
|
|
780
|
+
type: 'text',
|
|
781
|
+
text: prompt.trim(),
|
|
782
|
+
text_elements: [],
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
for (const imagePath of imagePaths ?? []) {
|
|
786
|
+
input.push({
|
|
787
|
+
type: 'localImage',
|
|
788
|
+
path: imagePath,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return input;
|
|
792
|
+
}
|
|
793
|
+
function getThreadIdFromResponse(response) {
|
|
794
|
+
const record = asRecord(response);
|
|
795
|
+
const thread = asRecord(record?.thread);
|
|
796
|
+
return getString(thread?.id);
|
|
797
|
+
}
|
|
798
|
+
function getTurnIdFromResponse(response) {
|
|
799
|
+
const record = asRecord(response);
|
|
800
|
+
const turn = asRecord(record?.turn);
|
|
801
|
+
return getString(turn?.id);
|
|
802
|
+
}
|
|
803
|
+
function getTurnFailureMessage(turn) {
|
|
804
|
+
if (!turn) {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
const status = getString(turn.status);
|
|
808
|
+
const error = asRecord(turn.error);
|
|
809
|
+
const message = getString(error?.message);
|
|
810
|
+
if (message) {
|
|
811
|
+
return message;
|
|
812
|
+
}
|
|
813
|
+
if (status && status !== 'completed') {
|
|
814
|
+
return `Codex turn finished with status ${status}`;
|
|
815
|
+
}
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
function summarizeProgressItem(item) {
|
|
819
|
+
if (!item) {
|
|
820
|
+
return undefined;
|
|
821
|
+
}
|
|
822
|
+
const itemType = getString(item.type);
|
|
823
|
+
const command = getNestedString(item, [
|
|
824
|
+
['command'],
|
|
825
|
+
['cmd'],
|
|
826
|
+
['input', 'command'],
|
|
827
|
+
['input', 'cmd'],
|
|
828
|
+
['payload', 'command'],
|
|
829
|
+
['payload', 'cmd'],
|
|
830
|
+
['data', 'command'],
|
|
831
|
+
['data', 'cmd'],
|
|
832
|
+
]);
|
|
833
|
+
if (command) {
|
|
834
|
+
return `执行命令: ${command}`;
|
|
835
|
+
}
|
|
836
|
+
const message = getNestedString(item, [
|
|
837
|
+
['message'],
|
|
838
|
+
['title'],
|
|
839
|
+
['summary'],
|
|
840
|
+
['text'],
|
|
841
|
+
['input', 'description'],
|
|
842
|
+
['payload', 'description'],
|
|
843
|
+
['data', 'description'],
|
|
844
|
+
]);
|
|
845
|
+
const query = findStringByKeysDeep(item, ['query', 'searchQuery', 'q', 'term', 'keyword', 'keywords']);
|
|
846
|
+
const url = findStringByKeysDeep(item, ['url', 'uri', 'link']);
|
|
847
|
+
const title = findStringByKeysDeep(item, ['title', 'name', 'label']);
|
|
848
|
+
const text = findStringByKeysDeep(item, ['text', 'description', 'summary', 'reason', 'status']);
|
|
849
|
+
switch (itemType) {
|
|
850
|
+
case 'userMessage':
|
|
851
|
+
if (message || text) {
|
|
852
|
+
return `收到问题: ${truncateProgressText(message ?? text ?? '')}`;
|
|
853
|
+
}
|
|
854
|
+
return '收到你的问题,开始处理...';
|
|
855
|
+
case 'reasoning':
|
|
856
|
+
if (message || text) {
|
|
857
|
+
return `思考中: ${truncateProgressText(message ?? text ?? '')}`;
|
|
858
|
+
}
|
|
859
|
+
return undefined;
|
|
860
|
+
case 'webSearch':
|
|
861
|
+
if (query) {
|
|
862
|
+
return `搜索网页: ${truncateProgressText(query)}`;
|
|
863
|
+
}
|
|
864
|
+
if (title || url) {
|
|
865
|
+
return `查看网页: ${truncateProgressText(title ?? url ?? '')}`;
|
|
866
|
+
}
|
|
867
|
+
break;
|
|
868
|
+
case 'open':
|
|
869
|
+
case 'click':
|
|
870
|
+
case 'find':
|
|
871
|
+
if (title || url || text) {
|
|
872
|
+
return `查看内容: ${truncateProgressText(title ?? url ?? text ?? '')}`;
|
|
873
|
+
}
|
|
874
|
+
break;
|
|
875
|
+
default:
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
if (message) {
|
|
879
|
+
return truncateProgressText(message);
|
|
880
|
+
}
|
|
881
|
+
if (query) {
|
|
882
|
+
return `处理中: ${truncateProgressText(query)}`;
|
|
883
|
+
}
|
|
884
|
+
if (title || url || text) {
|
|
885
|
+
return truncateProgressText(title ?? url ?? text ?? '');
|
|
886
|
+
}
|
|
887
|
+
const deepString = findDescriptiveStringDeep(item);
|
|
888
|
+
if (deepString) {
|
|
889
|
+
return truncateProgressText(deepString);
|
|
890
|
+
}
|
|
891
|
+
if (itemType) {
|
|
892
|
+
return `步骤完成: ${itemType}`;
|
|
893
|
+
}
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
function toCliUsage(tokenUsage) {
|
|
897
|
+
const breakdown = asRecord(tokenUsage?.last);
|
|
898
|
+
if (!breakdown) {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
const input = getNumber(breakdown.inputTokens);
|
|
902
|
+
const output = getNumber(breakdown.outputTokens);
|
|
903
|
+
const cacheRead = getNumber(breakdown.cachedInputTokens);
|
|
904
|
+
const total = getNumber(breakdown.totalTokens);
|
|
905
|
+
if ([input, output, cacheRead, total].every((value) => value === undefined)) {
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
input,
|
|
910
|
+
output,
|
|
911
|
+
cacheRead,
|
|
912
|
+
total,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function asRecord(value) {
|
|
916
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
function getString(value) {
|
|
922
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
923
|
+
}
|
|
924
|
+
function getNestedString(record, paths) {
|
|
925
|
+
for (const path of paths) {
|
|
926
|
+
let current = record;
|
|
927
|
+
for (const key of path) {
|
|
928
|
+
current = asRecord(current)?.[key];
|
|
929
|
+
}
|
|
930
|
+
const value = getString(current);
|
|
931
|
+
if (value) {
|
|
932
|
+
return value;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
function findStringByKeysDeep(value, preferredKeys, maxDepth = 5) {
|
|
938
|
+
const lowered = new Set(preferredKeys.map((key) => key.toLowerCase()));
|
|
939
|
+
const queue = [{ value, depth: 0 }];
|
|
940
|
+
const visited = new Set();
|
|
941
|
+
while (queue.length > 0) {
|
|
942
|
+
const current = queue.shift();
|
|
943
|
+
if (!current) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const { value: currentValue, depth } = current;
|
|
947
|
+
if (!currentValue || typeof currentValue !== 'object') {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (visited.has(currentValue)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
visited.add(currentValue);
|
|
954
|
+
if (depth > maxDepth) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (Array.isArray(currentValue)) {
|
|
958
|
+
for (const entry of currentValue) {
|
|
959
|
+
queue.push({ value: entry, depth: depth + 1 });
|
|
960
|
+
}
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
for (const [key, nested] of Object.entries(currentValue)) {
|
|
964
|
+
if (lowered.has(key.toLowerCase())) {
|
|
965
|
+
const direct = getString(nested);
|
|
966
|
+
if (direct && isDescriptiveProgressText(direct)) {
|
|
967
|
+
return direct;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (const nested of Object.values(currentValue)) {
|
|
972
|
+
queue.push({ value: nested, depth: depth + 1 });
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
function findDescriptiveStringDeep(value, maxDepth = 5) {
|
|
978
|
+
const queue = [{ value, depth: 0 }];
|
|
979
|
+
const visited = new Set();
|
|
980
|
+
while (queue.length > 0) {
|
|
981
|
+
const current = queue.shift();
|
|
982
|
+
if (!current) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const { value: currentValue, depth } = current;
|
|
986
|
+
const direct = getString(currentValue);
|
|
987
|
+
if (direct && isDescriptiveProgressText(direct)) {
|
|
988
|
+
return direct;
|
|
989
|
+
}
|
|
990
|
+
if (!currentValue || typeof currentValue !== 'object') {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (visited.has(currentValue)) {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
visited.add(currentValue);
|
|
997
|
+
if (depth > maxDepth) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (Array.isArray(currentValue)) {
|
|
1001
|
+
for (const entry of currentValue) {
|
|
1002
|
+
queue.push({ value: entry, depth: depth + 1 });
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
for (const [key, nested] of Object.entries(currentValue)) {
|
|
1007
|
+
if (IGNORED_PROGRESS_KEYS.has(key.toLowerCase())) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
queue.push({ value: nested, depth: depth + 1 });
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
const IGNORED_PROGRESS_KEYS = new Set([
|
|
1016
|
+
'id',
|
|
1017
|
+
'type',
|
|
1018
|
+
'kind',
|
|
1019
|
+
'status',
|
|
1020
|
+
'role',
|
|
1021
|
+
'index',
|
|
1022
|
+
]);
|
|
1023
|
+
function isDescriptiveProgressText(value) {
|
|
1024
|
+
const normalized = value.trim();
|
|
1025
|
+
if (!normalized) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (normalized.length < 4) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (/^(ok|done|true|false|success|completed?)$/i.test(normalized)) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (/^[a-z0-9_-]{1,24}$/i.test(normalized)) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
function truncateProgressText(value, maxLength = 140) {
|
|
1040
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
1041
|
+
if (normalized.length <= maxLength) {
|
|
1042
|
+
return normalized;
|
|
1043
|
+
}
|
|
1044
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
1045
|
+
}
|
|
1046
|
+
function getNumber(value) {
|
|
1047
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1048
|
+
}
|
|
1049
|
+
//# sourceMappingURL=codex-app-server-runtime.js.map
|