@northflare/runner 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/claude-sdk-manager.d.ts +1 -1
- package/dist/components/claude-sdk-manager.d.ts.map +1 -1
- package/dist/components/claude-sdk-manager.js +25 -12
- package/dist/components/claude-sdk-manager.js.map +1 -1
- package/dist/components/codex-sdk-manager.d.ts +58 -0
- package/dist/components/codex-sdk-manager.d.ts.map +1 -0
- package/dist/components/codex-sdk-manager.js +907 -0
- package/dist/components/codex-sdk-manager.js.map +1 -0
- package/dist/components/message-handler-sse.d.ts +3 -0
- package/dist/components/message-handler-sse.d.ts.map +1 -1
- package/dist/components/message-handler-sse.js +51 -7
- package/dist/components/message-handler-sse.js.map +1 -1
- package/dist/runner-sse.d.ts +4 -0
- package/dist/runner-sse.d.ts.map +1 -1
- package/dist/runner-sse.js +145 -15
- package/dist/runner-sse.js.map +1 -1
- package/dist/types/claude.d.ts +11 -1
- package/dist/types/claude.d.ts.map +1 -1
- package/dist/types/runner-interface.d.ts +2 -0
- package/dist/types/runner-interface.d.ts.map +1 -1
- package/dist/utils/model.d.ts +6 -0
- package/dist/utils/model.d.ts.map +1 -0
- package/dist/utils/model.js +23 -0
- package/dist/utils/model.js.map +1 -0
- package/dist/utils/status-line.d.ts +2 -2
- package/dist/utils/status-line.js +5 -5
- package/dist/utils/status-line.js.map +1 -1
- package/lib/codex-sdk/.prettierignore +3 -0
- package/lib/codex-sdk/.prettierrc +5 -0
- package/lib/codex-sdk/README.md +133 -0
- package/lib/codex-sdk/dist/index.d.ts +260 -0
- package/lib/codex-sdk/dist/index.js +426 -0
- package/lib/codex-sdk/eslint.config.js +21 -0
- package/lib/codex-sdk/jest.config.cjs +31 -0
- package/lib/codex-sdk/package.json +65 -0
- package/lib/codex-sdk/samples/basic_streaming.ts +90 -0
- package/lib/codex-sdk/samples/helpers.ts +8 -0
- package/lib/codex-sdk/samples/structured_output.ts +22 -0
- package/lib/codex-sdk/samples/structured_output_zod.ts +19 -0
- package/lib/codex-sdk/src/codex.ts +38 -0
- package/lib/codex-sdk/src/codexOptions.ts +10 -0
- package/lib/codex-sdk/src/events.ts +80 -0
- package/lib/codex-sdk/src/exec.ts +336 -0
- package/lib/codex-sdk/src/index.ts +39 -0
- package/lib/codex-sdk/src/items.ts +127 -0
- package/lib/codex-sdk/src/outputSchemaFile.ts +40 -0
- package/lib/codex-sdk/src/thread.ts +155 -0
- package/lib/codex-sdk/src/threadOptions.ts +18 -0
- package/lib/codex-sdk/src/turnOptions.ts +6 -0
- package/lib/codex-sdk/tests/abort.test.ts +165 -0
- package/lib/codex-sdk/tests/codexExecSpy.ts +37 -0
- package/lib/codex-sdk/tests/responsesProxy.ts +225 -0
- package/lib/codex-sdk/tests/run.test.ts +687 -0
- package/lib/codex-sdk/tests/runStreamed.test.ts +211 -0
- package/lib/codex-sdk/tsconfig.json +24 -0
- package/lib/codex-sdk/tsup.config.ts +12 -0
- package/package.json +3 -1
- package/rejections.log +2 -0
- package/src/components/claude-sdk-manager.ts +33 -13
- package/src/components/codex-sdk-manager.ts +1248 -0
- package/src/components/message-handler-sse.ts +79 -8
- package/src/runner-sse.ts +174 -15
- package/src/types/claude.ts +12 -1
- package/src/types/runner-interface.ts +3 -1
- package/src/utils/model.ts +29 -0
- package/src/utils/status-line.ts +6 -6
- package/src/utils/codex-sdk.js +0 -448
- package/src/utils/sdk-demo.js +0 -34
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
import type { Thread, ThreadEvent, ThreadItem } from "@northflare/codex-sdk";
|
|
2
|
+
import { IRunnerApp } from "../types/runner-interface";
|
|
3
|
+
import { EnhancedRepositoryManager } from "./enhanced-repository-manager";
|
|
4
|
+
import { ConversationContext, ConversationConfig, Message } from "../types";
|
|
5
|
+
import { statusLineManager } from "../utils/status-line";
|
|
6
|
+
import { console } from "../utils/console";
|
|
7
|
+
import { expandEnv } from "../utils/expand-env";
|
|
8
|
+
import { parseModelValue } from "../utils/model";
|
|
9
|
+
import * as jwt from "jsonwebtoken";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
let codexSdkPromise: Promise<typeof import("@northflare/codex-sdk")> | null =
|
|
14
|
+
null;
|
|
15
|
+
// Use runtime dynamic import so the CommonJS build can load the ESM-only SDK.
|
|
16
|
+
const dynamicImport = new Function(
|
|
17
|
+
"specifier",
|
|
18
|
+
"return import(specifier);"
|
|
19
|
+
) as (specifier: string) => Promise<unknown>;
|
|
20
|
+
|
|
21
|
+
async function loadCodexSdk() {
|
|
22
|
+
if (!codexSdkPromise) {
|
|
23
|
+
codexSdkPromise = dynamicImport("@northflare/codex-sdk") as Promise<
|
|
24
|
+
typeof import("@northflare/codex-sdk")
|
|
25
|
+
>;
|
|
26
|
+
}
|
|
27
|
+
return codexSdkPromise;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type CodexThreadState = {
|
|
31
|
+
thread: Thread;
|
|
32
|
+
abortController: AbortController | null;
|
|
33
|
+
runPromise: Promise<void> | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ConversationDetails = {
|
|
37
|
+
id: string;
|
|
38
|
+
objectType: string;
|
|
39
|
+
objectId: string;
|
|
40
|
+
model: string;
|
|
41
|
+
globalInstructions: string;
|
|
42
|
+
workspaceInstructions: string;
|
|
43
|
+
permissionsMode: string;
|
|
44
|
+
agentSessionId?: string;
|
|
45
|
+
workspaceId?: string;
|
|
46
|
+
threadId?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type NormalizedItemEvent = {
|
|
50
|
+
type: string;
|
|
51
|
+
content: any;
|
|
52
|
+
subtype?: string;
|
|
53
|
+
toolCalls?: any;
|
|
54
|
+
isError?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class CodexManager {
|
|
58
|
+
private runner: IRunnerApp;
|
|
59
|
+
private repositoryManager: EnhancedRepositoryManager;
|
|
60
|
+
private threadStates: Map<string, CodexThreadState> = new Map();
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
runner: IRunnerApp,
|
|
64
|
+
repositoryManager: EnhancedRepositoryManager
|
|
65
|
+
) {
|
|
66
|
+
this.runner = runner;
|
|
67
|
+
this.repositoryManager = repositoryManager;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async startConversation(
|
|
71
|
+
conversationObjectType: "Task" | "TaskPlan",
|
|
72
|
+
conversationObjectId: string,
|
|
73
|
+
config: ConversationConfig,
|
|
74
|
+
initialMessages: Message[],
|
|
75
|
+
conversationData?: ConversationDetails
|
|
76
|
+
): Promise<ConversationContext> {
|
|
77
|
+
if (!conversationData?.id) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"startConversation requires conversationData with a valid conversation.id"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const conversationId = conversationData.id;
|
|
84
|
+
const agentSessionId = this.resolveSessionId(config, conversationData);
|
|
85
|
+
|
|
86
|
+
const rawModel =
|
|
87
|
+
conversationData?.model ||
|
|
88
|
+
(config as any)?.model ||
|
|
89
|
+
(config as any)?.defaultModel ||
|
|
90
|
+
"openai";
|
|
91
|
+
const { baseModel, reasoningEffort } = parseModelValue(rawModel);
|
|
92
|
+
const normalizedModel = baseModel || rawModel;
|
|
93
|
+
|
|
94
|
+
const metadata: Record<string, any> = {
|
|
95
|
+
instructionsInjected: false,
|
|
96
|
+
originalModelValue: rawModel,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (reasoningEffort) {
|
|
100
|
+
metadata["modelReasoningEffort"] = reasoningEffort;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const context: ConversationContext = {
|
|
104
|
+
conversationId,
|
|
105
|
+
agentSessionId,
|
|
106
|
+
conversationObjectType,
|
|
107
|
+
conversationObjectId,
|
|
108
|
+
taskId:
|
|
109
|
+
conversationObjectType === "Task" ? conversationObjectId : undefined,
|
|
110
|
+
workspaceId: config.workspaceId,
|
|
111
|
+
status: "starting",
|
|
112
|
+
config,
|
|
113
|
+
startedAt: new Date(),
|
|
114
|
+
lastActivityAt: new Date(),
|
|
115
|
+
model: normalizedModel,
|
|
116
|
+
globalInstructions: conversationData.globalInstructions || "",
|
|
117
|
+
workspaceInstructions: conversationData.workspaceInstructions || "",
|
|
118
|
+
permissionsMode:
|
|
119
|
+
conversationData.permissionsMode ||
|
|
120
|
+
(config as any)?.permissionsMode ||
|
|
121
|
+
"all",
|
|
122
|
+
provider: "openai",
|
|
123
|
+
metadata,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.runner.activeConversations_.set(conversationId, context);
|
|
127
|
+
|
|
128
|
+
console.log(`[CodexManager] Stored conversation context`, {
|
|
129
|
+
conversationId,
|
|
130
|
+
agentSessionId,
|
|
131
|
+
model: context.model,
|
|
132
|
+
permissionsMode: context.permissionsMode,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const workspacePath = await this.resolveWorkspacePath(context, config);
|
|
136
|
+
|
|
137
|
+
(context.metadata as Record<string, any>)["workspacePath"] = workspacePath;
|
|
138
|
+
|
|
139
|
+
const githubToken = context.workspaceId
|
|
140
|
+
? await this.fetchGithubTokens(context.workspaceId)
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
143
|
+
const toolToken = this.generateToolToken(context);
|
|
144
|
+
|
|
145
|
+
let mcpServers: Record<string, any> | undefined;
|
|
146
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
147
|
+
const expandedServers = expandEnv(config.mcpServers, {
|
|
148
|
+
TOOL_TOKEN: toolToken,
|
|
149
|
+
});
|
|
150
|
+
mcpServers = this.normalizeMcpServersForCodex(expandedServers);
|
|
151
|
+
console.log(
|
|
152
|
+
"[CodexManager] MCP servers configuration:",
|
|
153
|
+
JSON.stringify(mcpServers, null, 2)
|
|
154
|
+
);
|
|
155
|
+
(context.metadata as Record<string, any>)["mcpServers"] = mcpServers;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const configOverrides = this.buildConfigOverridesFromMcp(mcpServers);
|
|
159
|
+
|
|
160
|
+
const envVars = await this.buildEnvVars(config, githubToken, toolToken);
|
|
161
|
+
|
|
162
|
+
const { Codex } = await loadCodexSdk();
|
|
163
|
+
const codex = new Codex({
|
|
164
|
+
baseUrl:
|
|
165
|
+
(config as any)?.openaiBaseUrl ||
|
|
166
|
+
(config as any)?.codexBaseUrl ||
|
|
167
|
+
process.env["OPENAI_BASE_URL"],
|
|
168
|
+
apiKey:
|
|
169
|
+
(config as any)?.openaiApiKey ||
|
|
170
|
+
(config as any)?.codexApiKey ||
|
|
171
|
+
process.env["CODEX_API_KEY"] ||
|
|
172
|
+
process.env["OPENAI_API_KEY"],
|
|
173
|
+
env: envVars,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const threadOptions = {
|
|
177
|
+
model: context.model,
|
|
178
|
+
workingDirectory: workspacePath,
|
|
179
|
+
sandboxMode: this.mapSandboxMode(context.permissionsMode),
|
|
180
|
+
networkAccessEnabled: this.shouldEnableNetwork(context.permissionsMode),
|
|
181
|
+
webSearchEnabled: true,
|
|
182
|
+
// additionalDirectories: this.getAdditionalDirectories(config),
|
|
183
|
+
configOverrides,
|
|
184
|
+
skipGitRepoCheck: true,
|
|
185
|
+
modelReasoningEffort: reasoningEffort,
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
console.log(
|
|
189
|
+
"[CodexManager] Thread options:",
|
|
190
|
+
JSON.stringify(threadOptions, null, 2)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const thread = agentSessionId
|
|
194
|
+
? codex.resumeThread(agentSessionId, threadOptions)
|
|
195
|
+
: codex.startThread(threadOptions);
|
|
196
|
+
|
|
197
|
+
this.threadStates.set(conversationId, {
|
|
198
|
+
thread,
|
|
199
|
+
abortController: null,
|
|
200
|
+
runPromise: null,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
context.conversation = thread;
|
|
204
|
+
|
|
205
|
+
// Send initial messages sequentially without waiting for completion
|
|
206
|
+
if (initialMessages?.length) {
|
|
207
|
+
for (const message of initialMessages) {
|
|
208
|
+
const text = this.normalizeToText(message.content);
|
|
209
|
+
const prompt = this.buildPromptForMessage(context, text, true);
|
|
210
|
+
this.launchTurn(context, prompt);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return context;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async stopConversation(
|
|
218
|
+
_agentSessionId: string,
|
|
219
|
+
context: ConversationContext,
|
|
220
|
+
isRunnerShutdown: boolean = false,
|
|
221
|
+
reason?: string
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
context.status = "stopping";
|
|
224
|
+
|
|
225
|
+
await this.abortActiveRun(context.conversationId);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await this._finalizeConversation(
|
|
229
|
+
context,
|
|
230
|
+
false,
|
|
231
|
+
undefined,
|
|
232
|
+
reason || (isRunnerShutdown ? "runner_shutdown" : undefined)
|
|
233
|
+
);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(
|
|
236
|
+
`[CodexManager] Error finalizing conversation ${context.conversationId}:`,
|
|
237
|
+
error
|
|
238
|
+
);
|
|
239
|
+
} finally {
|
|
240
|
+
this.threadStates.delete(context.conversationId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
context.status = "stopped";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async resumeConversation(
|
|
247
|
+
conversationObjectType: "Task" | "TaskPlan",
|
|
248
|
+
conversationObjectId: string,
|
|
249
|
+
agentSessionId: string,
|
|
250
|
+
config: ConversationConfig,
|
|
251
|
+
conversationData?: ConversationDetails,
|
|
252
|
+
resumeMessage?: string
|
|
253
|
+
): Promise<string> {
|
|
254
|
+
console.log(`[CodexManager] Resuming conversation ${agentSessionId}`);
|
|
255
|
+
|
|
256
|
+
const context = await this.startConversation(
|
|
257
|
+
conversationObjectType,
|
|
258
|
+
conversationObjectId,
|
|
259
|
+
{ ...config, sessionId: agentSessionId },
|
|
260
|
+
[],
|
|
261
|
+
conversationData
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (resumeMessage) {
|
|
265
|
+
const prompt = this.buildPromptForMessage(context, resumeMessage, false);
|
|
266
|
+
this.launchTurn(context, prompt);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return context.agentSessionId;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async sendUserMessage(
|
|
273
|
+
conversationId: string,
|
|
274
|
+
content: any,
|
|
275
|
+
config?: ConversationConfig,
|
|
276
|
+
conversationObjectType?: "Task" | "TaskPlan",
|
|
277
|
+
conversationObjectId?: string,
|
|
278
|
+
conversation?: any,
|
|
279
|
+
agentSessionIdOverride?: string
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
console.log(`[CodexManager] sendUserMessage called`, {
|
|
282
|
+
conversationId,
|
|
283
|
+
hasConfig: !!config,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
let context = this.runner.getConversationContext(conversationId);
|
|
287
|
+
|
|
288
|
+
if (!context && conversation) {
|
|
289
|
+
const conversationDetails = conversation as ConversationDetails;
|
|
290
|
+
const resumeSessionId = this.resolveSessionId(
|
|
291
|
+
config,
|
|
292
|
+
conversationDetails,
|
|
293
|
+
agentSessionIdOverride
|
|
294
|
+
);
|
|
295
|
+
const startConfig: ConversationConfig = {
|
|
296
|
+
...(config || {}),
|
|
297
|
+
workspaceId:
|
|
298
|
+
conversationDetails.workspaceId || config?.workspaceId || undefined,
|
|
299
|
+
...(resumeSessionId ? { sessionId: resumeSessionId } : {}),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
context = await this.startConversation(
|
|
303
|
+
(conversationObjectType as "Task" | "TaskPlan") ||
|
|
304
|
+
(conversationDetails.objectType as "Task" | "TaskPlan"),
|
|
305
|
+
conversationObjectId || conversationDetails.objectId,
|
|
306
|
+
startConfig,
|
|
307
|
+
[],
|
|
308
|
+
conversationDetails
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!context) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`No active or fetchable conversation found for ${conversationId}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await this.abortActiveRun(conversationId);
|
|
319
|
+
|
|
320
|
+
const messageText = this.normalizeToText(content);
|
|
321
|
+
const prompt = this.buildPromptForMessage(context, messageText, false);
|
|
322
|
+
this.launchTurn(context, prompt);
|
|
323
|
+
|
|
324
|
+
context.lastActivityAt = new Date();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async buildEnvVars(
|
|
328
|
+
config: ConversationConfig,
|
|
329
|
+
githubToken?: string,
|
|
330
|
+
toolToken?: string
|
|
331
|
+
): Promise<Record<string, string>> {
|
|
332
|
+
const envVars: Record<string, string> = {
|
|
333
|
+
...(Object.fromEntries(
|
|
334
|
+
Object.entries(process.env).filter(([, value]) => value !== undefined)
|
|
335
|
+
) as Record<string, string>),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (config.codexAuth?.accessToken) {
|
|
339
|
+
envVars["OPENAI_ACCESS_TOKEN"] = config.codexAuth.accessToken;
|
|
340
|
+
} else if (config.accessToken) {
|
|
341
|
+
envVars["OPENAI_ACCESS_TOKEN"] = config.accessToken;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (githubToken) {
|
|
345
|
+
envVars["GITHUB_TOKEN"] = githubToken;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (toolToken) {
|
|
349
|
+
envVars["TOOL_TOKEN"] = toolToken;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (process.env["DEBUG"] === "true") {
|
|
353
|
+
envVars["DEBUG"] = "1";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const codexHome = await this.ensureCodexAuthHome(config);
|
|
357
|
+
if (codexHome) {
|
|
358
|
+
envVars["CODEX_HOME"] = codexHome;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return envVars;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async ensureCodexAuthHome(
|
|
365
|
+
config: ConversationConfig
|
|
366
|
+
): Promise<string | null> {
|
|
367
|
+
if (!config.codexAuth) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const runnerId = this.runner.getRunnerId();
|
|
372
|
+
const dataDir = this.runner.config_.dataDir;
|
|
373
|
+
|
|
374
|
+
if (!runnerId || !dataDir) {
|
|
375
|
+
console.warn(
|
|
376
|
+
"[CodexManager] Missing runnerId or dataDir; cannot prepare Codex auth directory"
|
|
377
|
+
);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const codexDir = path.join(dataDir, "codex", runnerId);
|
|
382
|
+
const authPayload = {
|
|
383
|
+
OPENAI_API_KEY: null,
|
|
384
|
+
tokens: {
|
|
385
|
+
id_token: config.codexAuth.idToken,
|
|
386
|
+
access_token: config.codexAuth.accessToken,
|
|
387
|
+
refresh_token: "",
|
|
388
|
+
account_id: config.codexAuth.accountId,
|
|
389
|
+
},
|
|
390
|
+
last_refresh: config.codexAuth.lastRefresh || new Date().toISOString(),
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
await fs.mkdir(codexDir, { recursive: true });
|
|
395
|
+
await fs.writeFile(
|
|
396
|
+
path.join(codexDir, "auth.json"),
|
|
397
|
+
JSON.stringify(authPayload, null, 2),
|
|
398
|
+
"utf-8"
|
|
399
|
+
);
|
|
400
|
+
return codexDir;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error(
|
|
403
|
+
"[CodexManager] Failed to persist Codex auth configuration:",
|
|
404
|
+
error
|
|
405
|
+
);
|
|
406
|
+
throw new Error(
|
|
407
|
+
"Runner failed to persist Codex credentials. Check runner logs for details."
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private buildPromptForMessage(
|
|
413
|
+
context: ConversationContext,
|
|
414
|
+
text: string,
|
|
415
|
+
forceInstructions: boolean
|
|
416
|
+
): string {
|
|
417
|
+
const trimmed = text ?? "";
|
|
418
|
+
const instructions = this.getInstructionPrefix(context);
|
|
419
|
+
|
|
420
|
+
const metadata =
|
|
421
|
+
(context.metadata as Record<string, any>) || ({} as Record<string, any>);
|
|
422
|
+
const shouldInjectInstructions =
|
|
423
|
+
forceInstructions || !metadata["instructionsInjected"];
|
|
424
|
+
|
|
425
|
+
if (instructions && shouldInjectInstructions) {
|
|
426
|
+
metadata["instructionsInjected"] = true;
|
|
427
|
+
context.metadata = metadata;
|
|
428
|
+
return `${instructions}\n\n${trimmed}`.trim();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return trimmed;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private getInstructionPrefix(context: ConversationContext): string {
|
|
435
|
+
const parts: string[] = [];
|
|
436
|
+
|
|
437
|
+
if (context.globalInstructions) {
|
|
438
|
+
parts.push(
|
|
439
|
+
`<global-instructions>\n${context.globalInstructions}\n</global-instructions>`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (context.workspaceInstructions) {
|
|
444
|
+
parts.push(
|
|
445
|
+
`<workspace-instructions>\n${context.workspaceInstructions}\n</workspace-instructions>`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return parts.join("\n\n");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private launchTurn(context: ConversationContext, prompt: string): void {
|
|
453
|
+
const state = this.threadStates.get(context.conversationId);
|
|
454
|
+
|
|
455
|
+
if (!state || !state.thread) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`Thread state missing for conversation ${context.conversationId}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const abortController = new AbortController();
|
|
462
|
+
state.abortController = abortController;
|
|
463
|
+
|
|
464
|
+
context.lastActivityAt = new Date();
|
|
465
|
+
|
|
466
|
+
const runPromise = this.streamThreadEvents(
|
|
467
|
+
context,
|
|
468
|
+
state.thread,
|
|
469
|
+
prompt,
|
|
470
|
+
abortController
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
state.runPromise = runPromise;
|
|
474
|
+
|
|
475
|
+
runPromise
|
|
476
|
+
.catch((error) => {
|
|
477
|
+
if (!this.isAbortError(error)) {
|
|
478
|
+
console.error(
|
|
479
|
+
`[CodexManager] Run failed for ${context.conversationId}:`,
|
|
480
|
+
error
|
|
481
|
+
);
|
|
482
|
+
this._handleConversationError(context, error as Error);
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
.finally(() => {
|
|
486
|
+
if (state.runPromise === runPromise) {
|
|
487
|
+
state.runPromise = null;
|
|
488
|
+
}
|
|
489
|
+
if (state.abortController === abortController) {
|
|
490
|
+
state.abortController = null;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async streamThreadEvents(
|
|
496
|
+
context: ConversationContext,
|
|
497
|
+
thread: Thread,
|
|
498
|
+
prompt: string,
|
|
499
|
+
abortController: AbortController
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
try {
|
|
502
|
+
const { events } = await thread.runStreamed(prompt, {
|
|
503
|
+
signal: abortController.signal,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
for await (const event of events) {
|
|
507
|
+
await this.handleThreadEvent(context, event);
|
|
508
|
+
}
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (this.isAbortError(error)) {
|
|
511
|
+
console.log(
|
|
512
|
+
`[CodexManager] Turn aborted for ${context.conversationId}`
|
|
513
|
+
);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async abortActiveRun(conversationId: string): Promise<void> {
|
|
522
|
+
const state = this.threadStates.get(conversationId);
|
|
523
|
+
if (!state || !state.runPromise) return;
|
|
524
|
+
|
|
525
|
+
if (state.abortController) {
|
|
526
|
+
state.abortController.abort();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
await state.runPromise;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (!this.isAbortError(error)) {
|
|
533
|
+
console.warn(
|
|
534
|
+
`[CodexManager] Run aborted with error for ${conversationId}:`,
|
|
535
|
+
error
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private async handleThreadEvent(
|
|
542
|
+
context: ConversationContext,
|
|
543
|
+
event: ThreadEvent
|
|
544
|
+
): Promise<void> {
|
|
545
|
+
try {
|
|
546
|
+
switch (event.type) {
|
|
547
|
+
case "thread.started": {
|
|
548
|
+
await this.handleThreadStarted(context, event.thread_id);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case "turn.started": {
|
|
552
|
+
context.status = "active";
|
|
553
|
+
await this.sendAgentMessage(context, "system", {
|
|
554
|
+
subtype: "turn.started",
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: "text",
|
|
558
|
+
text: `Turn started at ${new Date().toISOString()}`,
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
});
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "turn.completed": {
|
|
565
|
+
await this.sendAgentMessage(context, "result", {
|
|
566
|
+
subtype: "turn.completed",
|
|
567
|
+
content: [
|
|
568
|
+
{
|
|
569
|
+
type: "usage",
|
|
570
|
+
usage: event.usage,
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
});
|
|
574
|
+
context.status = "stopped";
|
|
575
|
+
await this._finalizeConversation(context, false);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case "turn.failed": {
|
|
579
|
+
const error = new Error(event.error?.message || "Turn failed");
|
|
580
|
+
await this.sendAgentMessage(context, "error", {
|
|
581
|
+
subtype: "turn.failed",
|
|
582
|
+
content: [
|
|
583
|
+
{
|
|
584
|
+
type: "text",
|
|
585
|
+
text: error.message,
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
isError: true,
|
|
589
|
+
});
|
|
590
|
+
await this._handleConversationError(context, error);
|
|
591
|
+
context.status = "stopped";
|
|
592
|
+
await this._finalizeConversation(context, true, error, "turn_failed");
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case "item.started":
|
|
596
|
+
case "item.updated":
|
|
597
|
+
case "item.completed": {
|
|
598
|
+
await this.forwardItemEvent(
|
|
599
|
+
context,
|
|
600
|
+
event.item,
|
|
601
|
+
event.type.split(".")[1] as "started" | "updated" | "completed"
|
|
602
|
+
);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "error": {
|
|
606
|
+
const fatalError = new Error(event.message || "Unknown error");
|
|
607
|
+
await this.sendAgentMessage(context, "error", {
|
|
608
|
+
subtype: "thread.error",
|
|
609
|
+
content: [{ type: "text", text: fatalError.message }],
|
|
610
|
+
isError: true,
|
|
611
|
+
});
|
|
612
|
+
await this._handleConversationError(context, fatalError);
|
|
613
|
+
context.status = "stopped";
|
|
614
|
+
await this._finalizeConversation(
|
|
615
|
+
context,
|
|
616
|
+
true,
|
|
617
|
+
fatalError,
|
|
618
|
+
"thread_error"
|
|
619
|
+
);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error("[CodexManager] Failed to handle thread event", {
|
|
625
|
+
event,
|
|
626
|
+
error,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private async forwardItemEvent(
|
|
632
|
+
context: ConversationContext,
|
|
633
|
+
item: ThreadItem,
|
|
634
|
+
phase: "started" | "updated" | "completed"
|
|
635
|
+
): Promise<void> {
|
|
636
|
+
const normalized = this.normalizeItemEvent(context, item, phase);
|
|
637
|
+
if (!normalized) return;
|
|
638
|
+
|
|
639
|
+
const { subtype, content, isError, toolCalls, type } = normalized;
|
|
640
|
+
const payload: {
|
|
641
|
+
subtype?: string;
|
|
642
|
+
content: any;
|
|
643
|
+
toolCalls?: any;
|
|
644
|
+
isError?: boolean;
|
|
645
|
+
} = {
|
|
646
|
+
subtype,
|
|
647
|
+
content,
|
|
648
|
+
isError,
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
if (toolCalls) {
|
|
652
|
+
payload.toolCalls = toolCalls;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await this.sendAgentMessage(context, type, payload);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private normalizeItemEvent(
|
|
659
|
+
context: ConversationContext,
|
|
660
|
+
item: ThreadItem,
|
|
661
|
+
phase: "started" | "updated" | "completed"
|
|
662
|
+
): NormalizedItemEvent | null {
|
|
663
|
+
switch (item.type) {
|
|
664
|
+
case "agent_message": {
|
|
665
|
+
if (phase !== "completed") return null;
|
|
666
|
+
return {
|
|
667
|
+
type: "assistant",
|
|
668
|
+
content: [
|
|
669
|
+
{
|
|
670
|
+
type: "text",
|
|
671
|
+
text: item.text || "",
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
case "reasoning": {
|
|
677
|
+
return {
|
|
678
|
+
type: "thinking",
|
|
679
|
+
content: [
|
|
680
|
+
{
|
|
681
|
+
type: "thinking",
|
|
682
|
+
thinking: item.text,
|
|
683
|
+
text: item.text,
|
|
684
|
+
phase,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
case "command_execution": {
|
|
690
|
+
return {
|
|
691
|
+
type: "system",
|
|
692
|
+
subtype: `command_execution.${phase}`,
|
|
693
|
+
content: [
|
|
694
|
+
{
|
|
695
|
+
type: "command_execution",
|
|
696
|
+
command: item.command,
|
|
697
|
+
aggregated_output: item.aggregated_output,
|
|
698
|
+
exit_code: item.exit_code,
|
|
699
|
+
status: item.status,
|
|
700
|
+
phase,
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
case "file_change": {
|
|
706
|
+
if (phase === "updated") return null;
|
|
707
|
+
return {
|
|
708
|
+
type: "system",
|
|
709
|
+
subtype: "file_change",
|
|
710
|
+
content: [
|
|
711
|
+
{
|
|
712
|
+
type: "file_change",
|
|
713
|
+
changes: item.changes,
|
|
714
|
+
status: item.status,
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
case "mcp_tool_call": {
|
|
720
|
+
const toolName = this.buildMcpToolName(item.server, item.tool);
|
|
721
|
+
const toolUseId = this.buildToolUseId(context, item.id);
|
|
722
|
+
|
|
723
|
+
if (item.status === "in_progress" || phase !== "completed") {
|
|
724
|
+
return {
|
|
725
|
+
type: "assistant",
|
|
726
|
+
subtype: "tool_use",
|
|
727
|
+
content: [
|
|
728
|
+
{
|
|
729
|
+
toolCalls: [
|
|
730
|
+
{
|
|
731
|
+
id: toolUseId,
|
|
732
|
+
name: toolName,
|
|
733
|
+
arguments: item.arguments,
|
|
734
|
+
server: item.server,
|
|
735
|
+
tool: item.tool,
|
|
736
|
+
status: item.status,
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
timestamp: new Date().toISOString(),
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (item.status === "failed") {
|
|
746
|
+
return {
|
|
747
|
+
type: "error",
|
|
748
|
+
subtype: "tool_result",
|
|
749
|
+
content: [
|
|
750
|
+
{
|
|
751
|
+
type: "text",
|
|
752
|
+
text: item.error?.message || "Tool call failed",
|
|
753
|
+
tool_use_id: toolUseId,
|
|
754
|
+
tool_name: toolName,
|
|
755
|
+
timestamp: new Date().toISOString(),
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
isError: true,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
type: "tool_result",
|
|
764
|
+
subtype: "tool_result",
|
|
765
|
+
content: [
|
|
766
|
+
{
|
|
767
|
+
type: "tool_result",
|
|
768
|
+
subtype: "tool_result",
|
|
769
|
+
tool_use_id: toolUseId,
|
|
770
|
+
content: item.result?.content || [],
|
|
771
|
+
structured_content: item.result?.structured_content,
|
|
772
|
+
metadata: {
|
|
773
|
+
server: item.server,
|
|
774
|
+
tool: item.tool,
|
|
775
|
+
name: toolName,
|
|
776
|
+
original_tool_use_id: item.id,
|
|
777
|
+
},
|
|
778
|
+
timestamp: new Date().toISOString(),
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
case "web_search": {
|
|
784
|
+
return {
|
|
785
|
+
type: "system",
|
|
786
|
+
subtype: `web_search.${phase}`,
|
|
787
|
+
content: [
|
|
788
|
+
{
|
|
789
|
+
type: "web_search",
|
|
790
|
+
query: item.query,
|
|
791
|
+
status: phase,
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
case "todo_list": {
|
|
797
|
+
return {
|
|
798
|
+
type: "system",
|
|
799
|
+
subtype: "todo_list",
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
802
|
+
type: "todo_list",
|
|
803
|
+
items: item.items,
|
|
804
|
+
phase,
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
case "error": {
|
|
810
|
+
return {
|
|
811
|
+
type: "error",
|
|
812
|
+
subtype: `item.${phase}`,
|
|
813
|
+
content: [
|
|
814
|
+
{
|
|
815
|
+
type: "text",
|
|
816
|
+
text: item.message,
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
isError: true,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
default:
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private async sendAgentMessage(
|
|
828
|
+
context: ConversationContext,
|
|
829
|
+
type: string,
|
|
830
|
+
{
|
|
831
|
+
subtype,
|
|
832
|
+
content,
|
|
833
|
+
toolCalls,
|
|
834
|
+
isError,
|
|
835
|
+
}: {
|
|
836
|
+
subtype?: string;
|
|
837
|
+
content: any;
|
|
838
|
+
toolCalls?: any;
|
|
839
|
+
isError?: boolean;
|
|
840
|
+
}
|
|
841
|
+
): Promise<void> {
|
|
842
|
+
const normalizedContent = Array.isArray(content)
|
|
843
|
+
? content
|
|
844
|
+
: content
|
|
845
|
+
? [content]
|
|
846
|
+
: [];
|
|
847
|
+
|
|
848
|
+
const payload = {
|
|
849
|
+
taskId: context.taskId,
|
|
850
|
+
conversationId: context.conversationId,
|
|
851
|
+
conversationObjectType: context.conversationObjectType,
|
|
852
|
+
conversationObjectId: context.conversationObjectId,
|
|
853
|
+
agentSessionId: context.agentSessionId,
|
|
854
|
+
type,
|
|
855
|
+
subtype,
|
|
856
|
+
content: normalizedContent,
|
|
857
|
+
toolCalls,
|
|
858
|
+
isError: Boolean(isError),
|
|
859
|
+
messageId: this.generateMessageId(context),
|
|
860
|
+
timestamp: new Date().toISOString(),
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
if (process.env["DEBUG"] === "true") {
|
|
864
|
+
console.log("[CodexManager] Sending message.agent", payload);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
await this.runner.notify("message.agent", payload);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private async handleThreadStarted(
|
|
871
|
+
context: ConversationContext,
|
|
872
|
+
threadId: string
|
|
873
|
+
): Promise<void> {
|
|
874
|
+
if (!threadId || threadId === context.agentSessionId) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const oldSessionId = context.agentSessionId;
|
|
879
|
+
context.agentSessionId = threadId;
|
|
880
|
+
|
|
881
|
+
await this.runner.notify("agentSessionId.changed", {
|
|
882
|
+
conversationId: context.conversationId,
|
|
883
|
+
conversationObjectType: context.conversationObjectType,
|
|
884
|
+
conversationObjectId: context.conversationObjectId,
|
|
885
|
+
oldAgentSessionId: oldSessionId,
|
|
886
|
+
newAgentSessionId: threadId,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private mapSandboxMode(
|
|
891
|
+
permissionsMode?: string
|
|
892
|
+
): "read-only" | "workspace-write" | "danger-full-access" {
|
|
893
|
+
const mode = (permissionsMode || "").toLowerCase();
|
|
894
|
+
if (mode === "read" || mode === "read_only") {
|
|
895
|
+
return "read-only";
|
|
896
|
+
}
|
|
897
|
+
if (mode === "workspace" || mode === "workspace-write") {
|
|
898
|
+
return "workspace-write";
|
|
899
|
+
}
|
|
900
|
+
return "danger-full-access";
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private shouldEnableNetwork(permissionsMode?: string): boolean {
|
|
904
|
+
const mode = (permissionsMode || "").toLowerCase();
|
|
905
|
+
return mode !== "read" && mode !== "read_only";
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private getAdditionalDirectories(
|
|
909
|
+
config: ConversationConfig
|
|
910
|
+
): string[] | undefined {
|
|
911
|
+
const additionalDirs: string[] = [];
|
|
912
|
+
if (config.runnerRepoPath) {
|
|
913
|
+
additionalDirs.push(config.runnerRepoPath);
|
|
914
|
+
}
|
|
915
|
+
return additionalDirs.length ? additionalDirs : undefined;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
private buildConfigOverridesFromMcp(
|
|
919
|
+
mcpServers?: Record<string, any>
|
|
920
|
+
): Record<string, unknown> | undefined {
|
|
921
|
+
if (!mcpServers) return undefined;
|
|
922
|
+
const overrides: Record<string, unknown> = {};
|
|
923
|
+
|
|
924
|
+
for (const [serverName, config] of Object.entries(mcpServers)) {
|
|
925
|
+
this.flattenOverrideObject(
|
|
926
|
+
`mcp_servers.${serverName}`,
|
|
927
|
+
config,
|
|
928
|
+
overrides
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return Object.keys(overrides).length ? overrides : undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private normalizeMcpServersForCodex(
|
|
936
|
+
mcpServers: Record<string, any>
|
|
937
|
+
): Record<string, any> {
|
|
938
|
+
const normalized: Record<string, any> = {};
|
|
939
|
+
for (const [serverName, config] of Object.entries(mcpServers)) {
|
|
940
|
+
normalized[serverName] = this.stripAuthorizationHeader(config);
|
|
941
|
+
}
|
|
942
|
+
return normalized;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
private buildMcpToolName(server?: string, tool?: string): string {
|
|
946
|
+
const safeServer = (server || "unknown").trim() || "unknown";
|
|
947
|
+
const safeTool = (tool || "unknown").trim() || "unknown";
|
|
948
|
+
return `mcp__${safeServer}__${safeTool}`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private buildToolUseId(context: ConversationContext, rawId: string): string {
|
|
952
|
+
const scope =
|
|
953
|
+
context.agentSessionId ||
|
|
954
|
+
context.conversationId ||
|
|
955
|
+
context.conversationObjectId ||
|
|
956
|
+
"codex";
|
|
957
|
+
return `${scope}:${rawId}`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private stripAuthorizationHeader(config: any): any {
|
|
961
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
962
|
+
return config;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const normalized = { ...config };
|
|
966
|
+
if (
|
|
967
|
+
normalized.bearer_token_env_var &&
|
|
968
|
+
normalized.headers &&
|
|
969
|
+
typeof normalized.headers === "object" &&
|
|
970
|
+
!Array.isArray(normalized.headers)
|
|
971
|
+
) {
|
|
972
|
+
const headers = { ...normalized.headers };
|
|
973
|
+
delete headers["Authorization"];
|
|
974
|
+
if (Object.keys(headers).length === 0) {
|
|
975
|
+
delete normalized.headers;
|
|
976
|
+
} else {
|
|
977
|
+
normalized.headers = headers;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return normalized;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private flattenOverrideObject(
|
|
985
|
+
prefix: string,
|
|
986
|
+
value: any,
|
|
987
|
+
target: Record<string, unknown>
|
|
988
|
+
): void {
|
|
989
|
+
if (value === undefined) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
993
|
+
target[prefix] = value;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const entries = Object.entries(value);
|
|
998
|
+
if (!entries.length) {
|
|
999
|
+
target[prefix] = {};
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
for (const [key, nested] of entries) {
|
|
1004
|
+
this.flattenOverrideObject(`${prefix}.${key}`, nested, target);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
private async resolveWorkspacePath(
|
|
1009
|
+
context: ConversationContext,
|
|
1010
|
+
config: ConversationConfig
|
|
1011
|
+
): Promise<string> {
|
|
1012
|
+
let workspacePath: string;
|
|
1013
|
+
const workspaceId = config.workspaceId;
|
|
1014
|
+
|
|
1015
|
+
if (config.runnerRepoPath) {
|
|
1016
|
+
workspacePath = config.runnerRepoPath;
|
|
1017
|
+
console.log(`[CodexManager] Using local workspace path ${workspacePath}`);
|
|
1018
|
+
|
|
1019
|
+
if (context.conversationObjectType === "Task") {
|
|
1020
|
+
const taskHandle = await this.repositoryManager.createLocalTaskHandle(
|
|
1021
|
+
context.conversationObjectId,
|
|
1022
|
+
workspacePath
|
|
1023
|
+
);
|
|
1024
|
+
(context as any).taskHandle = taskHandle;
|
|
1025
|
+
}
|
|
1026
|
+
return workspacePath;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (
|
|
1030
|
+
context.conversationObjectType === "Task" &&
|
|
1031
|
+
config.repository &&
|
|
1032
|
+
workspaceId
|
|
1033
|
+
) {
|
|
1034
|
+
if (!config.repository.url) {
|
|
1035
|
+
throw new Error("Repository URL is required for task conversations");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const taskHandle = await this.repositoryManager.createTaskWorktree(
|
|
1039
|
+
context.conversationObjectId,
|
|
1040
|
+
workspaceId,
|
|
1041
|
+
config.repository.url,
|
|
1042
|
+
config.repository.branch,
|
|
1043
|
+
config.githubToken
|
|
1044
|
+
);
|
|
1045
|
+
(context as any).taskHandle = taskHandle;
|
|
1046
|
+
workspacePath = taskHandle.worktreePath;
|
|
1047
|
+
return workspacePath;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (config.repository && workspaceId) {
|
|
1051
|
+
if (config.repository.type === "local" && config.repository.localPath) {
|
|
1052
|
+
workspacePath = config.repository.localPath;
|
|
1053
|
+
return workspacePath;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
workspacePath = await this.repositoryManager.checkoutRepository(
|
|
1057
|
+
workspaceId,
|
|
1058
|
+
config.repository.url,
|
|
1059
|
+
config.repository.branch,
|
|
1060
|
+
config.githubToken
|
|
1061
|
+
);
|
|
1062
|
+
return workspacePath;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (workspaceId) {
|
|
1066
|
+
workspacePath =
|
|
1067
|
+
await this.repositoryManager.getWorkspacePath(workspaceId);
|
|
1068
|
+
return workspacePath;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return process.cwd();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private async fetchGithubTokens(
|
|
1075
|
+
workspaceId: string
|
|
1076
|
+
): Promise<string | undefined> {
|
|
1077
|
+
try {
|
|
1078
|
+
const response = await fetch(
|
|
1079
|
+
`${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`,
|
|
1080
|
+
{
|
|
1081
|
+
method: "GET",
|
|
1082
|
+
headers: {
|
|
1083
|
+
Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
|
|
1084
|
+
},
|
|
1085
|
+
}
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
if (!response.ok) {
|
|
1089
|
+
console.error(
|
|
1090
|
+
`[CodexManager] Failed to fetch GitHub tokens: ${response.status}`
|
|
1091
|
+
);
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const data = (await response.json()) as { githubToken?: string };
|
|
1096
|
+
return data.githubToken;
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
console.error("[CodexManager] Error fetching GitHub tokens", error);
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private generateToolToken(context: ConversationContext): string | undefined {
|
|
1104
|
+
if (!context.config.mcpServers) return undefined;
|
|
1105
|
+
|
|
1106
|
+
const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
|
|
1107
|
+
const runnerUid = this.runner.getRunnerUid();
|
|
1108
|
+
|
|
1109
|
+
if (!runnerToken || !runnerUid) return undefined;
|
|
1110
|
+
|
|
1111
|
+
return jwt.sign(
|
|
1112
|
+
{
|
|
1113
|
+
conversationId: context.conversationId,
|
|
1114
|
+
runnerUid,
|
|
1115
|
+
},
|
|
1116
|
+
runnerToken,
|
|
1117
|
+
{
|
|
1118
|
+
expiresIn: "60m",
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private normalizeToText(value: any): string {
|
|
1124
|
+
if (typeof value === "string") return value;
|
|
1125
|
+
if (value == null) return "";
|
|
1126
|
+
|
|
1127
|
+
if (typeof value === "object") {
|
|
1128
|
+
if (typeof (value as any).text === "string") {
|
|
1129
|
+
return (value as any).text;
|
|
1130
|
+
}
|
|
1131
|
+
if (Array.isArray(value)) {
|
|
1132
|
+
const texts = value
|
|
1133
|
+
.map((entry) => {
|
|
1134
|
+
if (
|
|
1135
|
+
entry &&
|
|
1136
|
+
typeof entry === "object" &&
|
|
1137
|
+
typeof entry.text === "string"
|
|
1138
|
+
) {
|
|
1139
|
+
return entry.text;
|
|
1140
|
+
}
|
|
1141
|
+
return null;
|
|
1142
|
+
})
|
|
1143
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
1144
|
+
if (texts.length) {
|
|
1145
|
+
return texts.join(" ");
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof (value as any).content === "string") {
|
|
1149
|
+
return (value as any).content;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
return JSON.stringify(value);
|
|
1155
|
+
} catch {
|
|
1156
|
+
return String(value);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private normalizeSessionId(value?: string | null): string | undefined {
|
|
1161
|
+
if (!value) return undefined;
|
|
1162
|
+
const trimmed = value.trim();
|
|
1163
|
+
return trimmed.length ? trimmed : undefined;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private resolveSessionId(
|
|
1167
|
+
config?: ConversationConfig,
|
|
1168
|
+
conversationData?: ConversationDetails,
|
|
1169
|
+
override?: string | null
|
|
1170
|
+
): string {
|
|
1171
|
+
return (
|
|
1172
|
+
this.normalizeSessionId(override) ||
|
|
1173
|
+
this.normalizeSessionId(config?.sessionId) ||
|
|
1174
|
+
this.normalizeSessionId(conversationData?.agentSessionId) ||
|
|
1175
|
+
this.normalizeSessionId((conversationData as any)?.threadId) ||
|
|
1176
|
+
this.normalizeSessionId((conversationData as any)?.sessionId) ||
|
|
1177
|
+
""
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private isAbortError(error: unknown): boolean {
|
|
1182
|
+
if (!error) return false;
|
|
1183
|
+
return (
|
|
1184
|
+
(error as any).name === "AbortError" ||
|
|
1185
|
+
/aborted|abort/i.test((error as any).message || "")
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
private async _finalizeConversation(
|
|
1190
|
+
context: ConversationContext,
|
|
1191
|
+
hadError: boolean,
|
|
1192
|
+
error?: any,
|
|
1193
|
+
reason?: string
|
|
1194
|
+
): Promise<void> {
|
|
1195
|
+
if ((context as any)._finalized) return;
|
|
1196
|
+
(context as any)._finalized = true;
|
|
1197
|
+
|
|
1198
|
+
try {
|
|
1199
|
+
await this.runner.notify("conversation.end", {
|
|
1200
|
+
conversationId: context.conversationId,
|
|
1201
|
+
conversationObjectType: context.conversationObjectType,
|
|
1202
|
+
conversationObjectId: context.conversationObjectId,
|
|
1203
|
+
agentSessionId: context.agentSessionId,
|
|
1204
|
+
isError: hadError,
|
|
1205
|
+
errorMessage: error?.message,
|
|
1206
|
+
reason,
|
|
1207
|
+
});
|
|
1208
|
+
} catch (notifyError) {
|
|
1209
|
+
console.error(
|
|
1210
|
+
"[CodexManager] Failed to send conversation.end notification",
|
|
1211
|
+
notifyError
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
this.threadStates.delete(context.conversationId);
|
|
1216
|
+
this.runner.activeConversations_.delete(context.conversationId);
|
|
1217
|
+
statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private async _handleConversationError(
|
|
1221
|
+
context: ConversationContext,
|
|
1222
|
+
error: Error
|
|
1223
|
+
): Promise<void> {
|
|
1224
|
+
console.error(
|
|
1225
|
+
`[CodexManager] Conversation error for ${context.conversationId}:`,
|
|
1226
|
+
error
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
await this.runner.notify("error.report", {
|
|
1230
|
+
conversationId: context.conversationId,
|
|
1231
|
+
conversationObjectType: context.conversationObjectType,
|
|
1232
|
+
conversationObjectId: context.conversationObjectId,
|
|
1233
|
+
agentSessionId: context.agentSessionId,
|
|
1234
|
+
errorType: "codex_error",
|
|
1235
|
+
message: error.message,
|
|
1236
|
+
details: {
|
|
1237
|
+
stack: error.stack,
|
|
1238
|
+
timestamp: new Date(),
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
private generateMessageId(context: ConversationContext): string {
|
|
1244
|
+
return `${context.agentSessionId}-${Date.now()}-${Math.random()
|
|
1245
|
+
.toString(36)
|
|
1246
|
+
.slice(2)}`;
|
|
1247
|
+
}
|
|
1248
|
+
}
|