@northflare/runner 0.0.30 → 0.0.32
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/bin/northflare-runner +1 -1
- package/dist/chunk-3QTLJ4CG.js +33622 -0
- package/dist/chunk-3QTLJ4CG.js.map +1 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/dist-W7DZRE4U.js +365 -0
- package/dist/dist-W7DZRE4U.js.map +1 -0
- package/dist/index.d.ts +764 -5
- package/dist/index.js +9872 -202
- package/dist/index.js.map +1 -1
- package/dist/sdk-query-TRMSGGID-EIENWDKW.js +14 -0
- package/dist/sdk-query-TRMSGGID-EIENWDKW.js.map +1 -0
- package/package.json +17 -17
- package/tsup.config.ts +5 -2
- package/dist/components/claude-sdk-manager.d.ts +0 -60
- package/dist/components/claude-sdk-manager.d.ts.map +0 -1
- package/dist/components/claude-sdk-manager.js +0 -1378
- package/dist/components/claude-sdk-manager.js.map +0 -1
- package/dist/components/codex-sdk-manager.d.ts +0 -94
- package/dist/components/codex-sdk-manager.d.ts.map +0 -1
- package/dist/components/codex-sdk-manager.js +0 -1450
- package/dist/components/codex-sdk-manager.js.map +0 -1
- package/dist/components/enhanced-repository-manager.d.ts +0 -173
- package/dist/components/enhanced-repository-manager.d.ts.map +0 -1
- package/dist/components/enhanced-repository-manager.js +0 -1097
- package/dist/components/enhanced-repository-manager.js.map +0 -1
- package/dist/components/message-handler-sse.d.ts +0 -77
- package/dist/components/message-handler-sse.d.ts.map +0 -1
- package/dist/components/message-handler-sse.js +0 -1224
- package/dist/components/message-handler-sse.js.map +0 -1
- package/dist/components/northflare-agent-sdk-manager.d.ts +0 -58
- package/dist/components/northflare-agent-sdk-manager.d.ts.map +0 -1
- package/dist/components/northflare-agent-sdk-manager.js +0 -2032
- package/dist/components/northflare-agent-sdk-manager.js.map +0 -1
- package/dist/components/repository-manager.d.ts +0 -51
- package/dist/components/repository-manager.d.ts.map +0 -1
- package/dist/components/repository-manager.js +0 -256
- package/dist/components/repository-manager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/runner-sse.d.ts +0 -102
- package/dist/runner-sse.d.ts.map +0 -1
- package/dist/runner-sse.js +0 -877
- package/dist/runner-sse.js.map +0 -1
- package/dist/services/RunnerAPIClient.d.ts +0 -61
- package/dist/services/RunnerAPIClient.d.ts.map +0 -1
- package/dist/services/RunnerAPIClient.js +0 -187
- package/dist/services/RunnerAPIClient.js.map +0 -1
- package/dist/services/SSEClient.d.ts +0 -62
- package/dist/services/SSEClient.d.ts.map +0 -1
- package/dist/services/SSEClient.js +0 -225
- package/dist/services/SSEClient.js.map +0 -1
- package/dist/types/claude.d.ts +0 -80
- package/dist/types/claude.d.ts.map +0 -1
- package/dist/types/claude.js +0 -5
- package/dist/types/claude.js.map +0 -1
- package/dist/types/index.d.ts +0 -52
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -7
- package/dist/types/index.js.map +0 -1
- package/dist/types/messages.d.ts +0 -33
- package/dist/types/messages.d.ts.map +0 -1
- package/dist/types/messages.js +0 -5
- package/dist/types/messages.js.map +0 -1
- package/dist/types/runner-interface.d.ts +0 -38
- package/dist/types/runner-interface.d.ts.map +0 -1
- package/dist/types/runner-interface.js +0 -5
- package/dist/types/runner-interface.js.map +0 -1
- package/dist/utils/StateManager.d.ts +0 -61
- package/dist/utils/StateManager.d.ts.map +0 -1
- package/dist/utils/StateManager.js +0 -170
- package/dist/utils/StateManager.js.map +0 -1
- package/dist/utils/config.d.ts +0 -48
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -378
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/console.d.ts +0 -8
- package/dist/utils/console.d.ts.map +0 -1
- package/dist/utils/console.js +0 -31
- package/dist/utils/console.js.map +0 -1
- package/dist/utils/debug.d.ts +0 -12
- package/dist/utils/debug.d.ts.map +0 -1
- package/dist/utils/debug.js +0 -94
- package/dist/utils/debug.js.map +0 -1
- package/dist/utils/expand-env.d.ts +0 -2
- package/dist/utils/expand-env.d.ts.map +0 -1
- package/dist/utils/expand-env.js +0 -17
- package/dist/utils/expand-env.js.map +0 -1
- package/dist/utils/inactivity-timeout.d.ts +0 -19
- package/dist/utils/inactivity-timeout.d.ts.map +0 -1
- package/dist/utils/inactivity-timeout.js +0 -72
- package/dist/utils/inactivity-timeout.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -10
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -129
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/message-log.d.ts +0 -23
- package/dist/utils/message-log.d.ts.map +0 -1
- package/dist/utils/message-log.js +0 -69
- package/dist/utils/message-log.js.map +0 -1
- package/dist/utils/model.d.ts +0 -8
- package/dist/utils/model.d.ts.map +0 -1
- package/dist/utils/model.js +0 -37
- package/dist/utils/model.js.map +0 -1
- package/dist/utils/status-line.d.ts +0 -34
- package/dist/utils/status-line.d.ts.map +0 -1
- package/dist/utils/status-line.js +0 -131
- package/dist/utils/status-line.js.map +0 -1
- package/dist/utils/tool-response-sanitizer.d.ts +0 -9
- package/dist/utils/tool-response-sanitizer.d.ts.map +0 -1
- package/dist/utils/tool-response-sanitizer.js +0 -118
- package/dist/utils/tool-response-sanitizer.js.map +0 -1
- package/dist/utils/update-coordinator.d.ts +0 -53
- package/dist/utils/update-coordinator.d.ts.map +0 -1
- package/dist/utils/update-coordinator.js +0 -159
- package/dist/utils/update-coordinator.js.map +0 -1
- package/dist/utils/version.d.ts +0 -10
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -33
- package/dist/utils/version.js.map +0 -1
|
@@ -1,1450 +0,0 @@
|
|
|
1
|
-
import { statusLineManager } from '../utils/status-line.js';
|
|
2
|
-
import { createScopedConsole } from '../utils/console.js';
|
|
3
|
-
import { expandEnv } from '../utils/expand-env.js';
|
|
4
|
-
import { mapReasoningEffortForCodex, parseModelValue } from '../utils/model.js';
|
|
5
|
-
import jwt from "jsonwebtoken";
|
|
6
|
-
import fs from "fs/promises";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import { isDebugEnabledFor } from '../utils/debug.js';
|
|
9
|
-
import { tmpdir } from "os";
|
|
10
|
-
import crypto from "crypto";
|
|
11
|
-
import { simpleGit } from "simple-git";
|
|
12
|
-
const console = createScopedConsole(isDebugEnabledFor("manager") ? "manager" : "sdk");
|
|
13
|
-
function buildSystemPromptWithNorthflare(basePrompt, northflarePrompt) {
|
|
14
|
-
const parts = [northflarePrompt, basePrompt]
|
|
15
|
-
.filter((p) => typeof p === "string" && p.trim().length > 0)
|
|
16
|
-
.map((p) => p.trim());
|
|
17
|
-
if (parts.length === 0)
|
|
18
|
-
return undefined;
|
|
19
|
-
return parts.join("\n\n");
|
|
20
|
-
}
|
|
21
|
-
let codexSdkPromise = null;
|
|
22
|
-
async function loadCodexSdk() {
|
|
23
|
-
if (!codexSdkPromise) {
|
|
24
|
-
codexSdkPromise = import("@northflare/codex-sdk");
|
|
25
|
-
}
|
|
26
|
-
return codexSdkPromise;
|
|
27
|
-
}
|
|
28
|
-
export class CodexManager {
|
|
29
|
-
runner;
|
|
30
|
-
repositoryManager;
|
|
31
|
-
threadStates = new Map();
|
|
32
|
-
constructor(runner, repositoryManager) {
|
|
33
|
-
this.runner = runner;
|
|
34
|
-
this.repositoryManager = repositoryManager;
|
|
35
|
-
}
|
|
36
|
-
async startConversation(conversationObjectType, conversationObjectId, config, initialMessages, conversationData, provider) {
|
|
37
|
-
if (!conversationData?.id) {
|
|
38
|
-
throw new Error("startConversation requires conversationData with a valid conversation.id");
|
|
39
|
-
}
|
|
40
|
-
const conversationId = conversationData.id;
|
|
41
|
-
const agentSessionId = this.resolveSessionId(config, conversationData);
|
|
42
|
-
const pendingSummary = this.runner.consumePendingConversationSummary(conversationId);
|
|
43
|
-
const rawSummary = typeof conversationData.summary === "string"
|
|
44
|
-
? conversationData.summary
|
|
45
|
-
: pendingSummary;
|
|
46
|
-
const normalizedSummary = typeof rawSummary === "string"
|
|
47
|
-
? rawSummary.replace(/\s+/g, " ").trim()
|
|
48
|
-
: null;
|
|
49
|
-
const rawModel = conversationData?.model ||
|
|
50
|
-
config?.model ||
|
|
51
|
-
config?.defaultModel ||
|
|
52
|
-
"openai";
|
|
53
|
-
const { baseModel, reasoningEffort } = parseModelValue(rawModel);
|
|
54
|
-
const codexReasoningEffort = mapReasoningEffortForCodex(reasoningEffort);
|
|
55
|
-
const normalizedModel = baseModel || rawModel;
|
|
56
|
-
// Debug logging for model parsing
|
|
57
|
-
console.log("[CodexManager] Model parsing debug:", {
|
|
58
|
-
rawModel,
|
|
59
|
-
baseModel,
|
|
60
|
-
reasoningEffort,
|
|
61
|
-
codexReasoningEffort,
|
|
62
|
-
normalizedModel,
|
|
63
|
-
});
|
|
64
|
-
// Determine the actual provider - can be passed explicitly or detected from config
|
|
65
|
-
const resolvedProvider = provider ||
|
|
66
|
-
config?.providerType ||
|
|
67
|
-
conversationData?.providerType ||
|
|
68
|
-
"openai";
|
|
69
|
-
// Optional system prompt handling (matches Northflare/Claude managers)
|
|
70
|
-
const providedSystemPrompt = buildSystemPromptWithNorthflare(config.systemPrompt || conversationData?.systemPrompt, config?.northflareSystemPrompt ||
|
|
71
|
-
conversationData?.northflareSystemPrompt);
|
|
72
|
-
const systemPromptMode = config.systemPromptMode ||
|
|
73
|
-
conversationData?.systemPromptMode ||
|
|
74
|
-
"append";
|
|
75
|
-
const metadata = {
|
|
76
|
-
instructionsInjected: false,
|
|
77
|
-
originalModelValue: rawModel,
|
|
78
|
-
};
|
|
79
|
-
metadata["hasSystemPrompt"] = Boolean(providedSystemPrompt);
|
|
80
|
-
metadata["systemPromptMode"] = systemPromptMode;
|
|
81
|
-
if (reasoningEffort) {
|
|
82
|
-
metadata["modelReasoningEffort"] = reasoningEffort;
|
|
83
|
-
}
|
|
84
|
-
const context = {
|
|
85
|
-
conversationId,
|
|
86
|
-
agentSessionId,
|
|
87
|
-
conversationObjectType,
|
|
88
|
-
conversationObjectId,
|
|
89
|
-
taskId: conversationObjectType === "Task" ? conversationObjectId : undefined,
|
|
90
|
-
workspaceId: config.workspaceId,
|
|
91
|
-
status: "starting",
|
|
92
|
-
config,
|
|
93
|
-
startedAt: new Date(),
|
|
94
|
-
lastActivityAt: new Date(),
|
|
95
|
-
model: normalizedModel,
|
|
96
|
-
summary: normalizedSummary,
|
|
97
|
-
globalInstructions: conversationData.globalInstructions || "",
|
|
98
|
-
workspaceInstructions: conversationData.workspaceInstructions || "",
|
|
99
|
-
permissionsMode: conversationData.permissionsMode ||
|
|
100
|
-
config?.permissionsMode ||
|
|
101
|
-
"all",
|
|
102
|
-
provider: resolvedProvider,
|
|
103
|
-
metadata,
|
|
104
|
-
};
|
|
105
|
-
this.runner.activeConversations_.set(conversationId, context);
|
|
106
|
-
console.log(`[CodexManager] Stored conversation context`, {
|
|
107
|
-
conversationId,
|
|
108
|
-
agentSessionId,
|
|
109
|
-
model: context.model,
|
|
110
|
-
permissionsMode: context.permissionsMode,
|
|
111
|
-
});
|
|
112
|
-
const workspacePath = await this.resolveWorkspacePath(context, config);
|
|
113
|
-
context.metadata["workspacePath"] = workspacePath;
|
|
114
|
-
const githubToken = context.workspaceId
|
|
115
|
-
? await this.fetchGithubTokens(context.workspaceId)
|
|
116
|
-
: undefined;
|
|
117
|
-
const toolToken = this.generateToolToken(context);
|
|
118
|
-
let mcpServers;
|
|
119
|
-
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
120
|
-
const expandedServers = expandEnv(config.mcpServers, {
|
|
121
|
-
TOOL_TOKEN: toolToken,
|
|
122
|
-
});
|
|
123
|
-
mcpServers = this.normalizeMcpServersForCodex(expandedServers);
|
|
124
|
-
console.log("[CodexManager] MCP servers configuration:", JSON.stringify(mcpServers, null, 2));
|
|
125
|
-
context.metadata["mcpServers"] = mcpServers;
|
|
126
|
-
}
|
|
127
|
-
let configOverrides = this.buildConfigOverridesFromMcp(mcpServers);
|
|
128
|
-
// Add OpenRouter config overrides if using OpenRouter provider
|
|
129
|
-
if (resolvedProvider === "openrouter") {
|
|
130
|
-
const openRouterOverrides = this.buildOpenRouterConfigOverrides();
|
|
131
|
-
configOverrides = {
|
|
132
|
-
...configOverrides,
|
|
133
|
-
...openRouterOverrides,
|
|
134
|
-
};
|
|
135
|
-
console.log("[CodexManager] OpenRouter config overrides:", JSON.stringify(openRouterOverrides, null, 2));
|
|
136
|
-
}
|
|
137
|
-
// Inject system prompt via developer_instructions (developer role message)
|
|
138
|
-
if (providedSystemPrompt) {
|
|
139
|
-
const trimmedPrompt = providedSystemPrompt.trim();
|
|
140
|
-
if (trimmedPrompt.length) {
|
|
141
|
-
const systemPromptOverrides = {
|
|
142
|
-
developer_instructions: trimmedPrompt,
|
|
143
|
-
};
|
|
144
|
-
configOverrides = {
|
|
145
|
-
...configOverrides,
|
|
146
|
-
...systemPromptOverrides,
|
|
147
|
-
};
|
|
148
|
-
console.log("[CodexManager] Applied system prompt overrides", {
|
|
149
|
-
mode: systemPromptMode,
|
|
150
|
-
hasSystemPrompt: true,
|
|
151
|
-
replacesBase: systemPromptMode === "replace",
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const envVars = await this.buildEnvVars(config, githubToken, toolToken, resolvedProvider);
|
|
156
|
-
const { Codex } = await loadCodexSdk();
|
|
157
|
-
const codex = new Codex({
|
|
158
|
-
baseUrl: config?.openaiBaseUrl ||
|
|
159
|
-
config?.codexBaseUrl ||
|
|
160
|
-
process.env["OPENAI_BASE_URL"],
|
|
161
|
-
apiKey: config?.openaiApiKey ||
|
|
162
|
-
config?.codexApiKey ||
|
|
163
|
-
process.env["CODEX_API_KEY"] ||
|
|
164
|
-
process.env["OPENAI_API_KEY"],
|
|
165
|
-
env: envVars,
|
|
166
|
-
});
|
|
167
|
-
// Strip provider prefix (e.g., "openrouter:") from model name if present
|
|
168
|
-
const modelName = context.model.includes(":")
|
|
169
|
-
? context.model.split(":").slice(1).join(":")
|
|
170
|
-
: context.model;
|
|
171
|
-
// Debug: log what model name we're using
|
|
172
|
-
console.log("[CodexManager] Model name processing:", {
|
|
173
|
-
contextModel: context.model,
|
|
174
|
-
hasColon: context.model.includes(":"),
|
|
175
|
-
modelNameAfterStrip: modelName,
|
|
176
|
-
});
|
|
177
|
-
const threadOptions = {
|
|
178
|
-
model: modelName,
|
|
179
|
-
workingDirectory: workspacePath,
|
|
180
|
-
sandboxMode: this.mapSandboxMode(context.permissionsMode),
|
|
181
|
-
networkAccessEnabled: this.shouldEnableNetwork(context.permissionsMode),
|
|
182
|
-
webSearchEnabled: true,
|
|
183
|
-
// additionalDirectories: this.getAdditionalDirectories(config),
|
|
184
|
-
configOverrides,
|
|
185
|
-
skipGitRepoCheck: true,
|
|
186
|
-
modelReasoningEffort: codexReasoningEffort,
|
|
187
|
-
};
|
|
188
|
-
console.log("[CodexManager] Thread options:", JSON.stringify(threadOptions, null, 2));
|
|
189
|
-
const thread = agentSessionId
|
|
190
|
-
? codex.resumeThread(agentSessionId, threadOptions)
|
|
191
|
-
: codex.startThread(threadOptions);
|
|
192
|
-
this.threadStates.set(conversationId, {
|
|
193
|
-
thread,
|
|
194
|
-
abortController: null,
|
|
195
|
-
runPromise: null,
|
|
196
|
-
});
|
|
197
|
-
context.conversation = thread;
|
|
198
|
-
// Launch the first turn using the provided initial messages.
|
|
199
|
-
// Supports multimodal (images/documents) by converting to Codex Input.
|
|
200
|
-
if (initialMessages?.length) {
|
|
201
|
-
const initialInput = await this.buildInitialInput(initialMessages, context);
|
|
202
|
-
this.launchTurn(context, initialInput);
|
|
203
|
-
}
|
|
204
|
-
return context;
|
|
205
|
-
}
|
|
206
|
-
async stopConversation(_agentSessionId, context, isRunnerShutdown = false, reason) {
|
|
207
|
-
context._stopRequested = true;
|
|
208
|
-
context._stopReason =
|
|
209
|
-
reason || (isRunnerShutdown ? "runner_shutdown" : undefined);
|
|
210
|
-
context.status = "stopping";
|
|
211
|
-
await this.abortActiveRun(context.conversationId);
|
|
212
|
-
try {
|
|
213
|
-
await this._finalizeConversation(context, false, undefined, context._stopReason);
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
console.error(`[CodexManager] Error finalizing conversation ${context.conversationId}:`, error);
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
this.threadStates.delete(context.conversationId);
|
|
220
|
-
}
|
|
221
|
-
context.status = "stopped";
|
|
222
|
-
}
|
|
223
|
-
async resumeConversation(conversationObjectType, conversationObjectId, agentSessionId, config, conversationData, resumeMessage, provider) {
|
|
224
|
-
console.log(`[CodexManager] Resuming conversation ${agentSessionId}`);
|
|
225
|
-
const context = await this.startConversation(conversationObjectType, conversationObjectId, { ...config, sessionId: agentSessionId }, [], conversationData, provider);
|
|
226
|
-
if (resumeMessage) {
|
|
227
|
-
const prompt = this.buildPromptForMessage(context, resumeMessage, false);
|
|
228
|
-
this.launchTurn(context, prompt);
|
|
229
|
-
}
|
|
230
|
-
return context.agentSessionId;
|
|
231
|
-
}
|
|
232
|
-
async sendUserMessage(conversationId, content, config, conversationObjectType, conversationObjectId, conversation, agentSessionIdOverride, provider) {
|
|
233
|
-
console.log(`[CodexManager] sendUserMessage called`, {
|
|
234
|
-
conversationId,
|
|
235
|
-
hasConfig: !!config,
|
|
236
|
-
});
|
|
237
|
-
let context = this.runner.getConversationContext(conversationId);
|
|
238
|
-
if (!context && conversation) {
|
|
239
|
-
const conversationDetails = conversation;
|
|
240
|
-
const resumeSessionId = this.resolveSessionId(config, conversationDetails, agentSessionIdOverride);
|
|
241
|
-
const startConfig = {
|
|
242
|
-
...(config || {}),
|
|
243
|
-
workspaceId: conversationDetails.workspaceId || config?.workspaceId || undefined,
|
|
244
|
-
...(resumeSessionId ? { sessionId: resumeSessionId } : {}),
|
|
245
|
-
};
|
|
246
|
-
context = await this.startConversation(conversationObjectType ||
|
|
247
|
-
conversationDetails.objectType, conversationObjectId || conversationDetails.objectId, startConfig, [], conversationDetails, provider);
|
|
248
|
-
}
|
|
249
|
-
if (!context) {
|
|
250
|
-
throw new Error(`No active or fetchable conversation found for ${conversationId}`);
|
|
251
|
-
}
|
|
252
|
-
await this.abortActiveRun(conversationId);
|
|
253
|
-
const input = await this.normalizeToInput(content, context, false);
|
|
254
|
-
this.launchTurn(context, input);
|
|
255
|
-
context.lastActivityAt = new Date();
|
|
256
|
-
}
|
|
257
|
-
async _handleRunCompletion(context, hadError, error) {
|
|
258
|
-
if (context._finalized)
|
|
259
|
-
return;
|
|
260
|
-
const inFlight = context._runCompletionInFlight;
|
|
261
|
-
if (inFlight)
|
|
262
|
-
return inFlight;
|
|
263
|
-
const promise = (async () => {
|
|
264
|
-
const isTaskConversation = context.conversationObjectType === "Task";
|
|
265
|
-
const stopRequested = context._stopRequested === true;
|
|
266
|
-
const taskHandle = context.taskHandle;
|
|
267
|
-
const shouldRunGitFlow = context.config.useWorktrees !== false &&
|
|
268
|
-
isTaskConversation &&
|
|
269
|
-
!!taskHandle &&
|
|
270
|
-
taskHandle.branch !== "local";
|
|
271
|
-
if (!shouldRunGitFlow || hadError || stopRequested) {
|
|
272
|
-
await this._finalizeConversation(context, hadError, error);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
const taskId = context.conversationObjectId;
|
|
276
|
-
const baseBranch = context.config.repository?.branch || "main";
|
|
277
|
-
try {
|
|
278
|
-
const taskState = await this.repositoryManager.getTaskState(taskId);
|
|
279
|
-
const taskRepoInfo = await this.repositoryManager.getTaskRepoInfo(taskId);
|
|
280
|
-
if (!taskState ||
|
|
281
|
-
!taskRepoInfo ||
|
|
282
|
-
!taskRepoInfo.worktreePath ||
|
|
283
|
-
taskState.branch === "local") {
|
|
284
|
-
await this._finalizeConversation(context, false);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const isLocalRepo = taskRepoInfo.repoKey.startsWith("local__");
|
|
288
|
-
const mergeWorkdir = isLocalRepo
|
|
289
|
-
? taskRepoInfo.controlPath
|
|
290
|
-
: taskRepoInfo.worktreePath;
|
|
291
|
-
// If we already have unresolved conflicts, immediately ask the agent to resolve them.
|
|
292
|
-
const taskGit = simpleGit(taskRepoInfo.worktreePath);
|
|
293
|
-
const initialTaskStatus = await taskGit.status();
|
|
294
|
-
if (initialTaskStatus.conflicted.length > 0) {
|
|
295
|
-
await this._resumeForGitConflictResolution(context, {
|
|
296
|
-
kind: "conflicts",
|
|
297
|
-
baseBranch,
|
|
298
|
-
conflicts: initialTaskStatus.conflicted,
|
|
299
|
-
workdir: taskRepoInfo.worktreePath,
|
|
300
|
-
});
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (isLocalRepo && mergeWorkdir !== taskRepoInfo.worktreePath) {
|
|
304
|
-
const mergeGit = simpleGit(mergeWorkdir);
|
|
305
|
-
const mergeStatus = await mergeGit.status();
|
|
306
|
-
if (mergeStatus.conflicted.length > 0) {
|
|
307
|
-
await this._resumeForGitConflictResolution(context, {
|
|
308
|
-
kind: "conflicts",
|
|
309
|
-
baseBranch,
|
|
310
|
-
conflicts: mergeStatus.conflicted,
|
|
311
|
-
workdir: mergeWorkdir,
|
|
312
|
-
});
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Ensure we're on the task branch for commit/rebase operations.
|
|
317
|
-
const currentBranch = (await taskGit.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
318
|
-
if (currentBranch !== taskState.branch) {
|
|
319
|
-
await taskGit.checkout(taskState.branch);
|
|
320
|
-
}
|
|
321
|
-
const status = await taskGit.status();
|
|
322
|
-
if (!status.isClean()) {
|
|
323
|
-
await this.repositoryManager.stageAll(taskId);
|
|
324
|
-
try {
|
|
325
|
-
const previousCommitCount = taskState?.commitCount ?? 0;
|
|
326
|
-
const baseSubject = (context.summary ?? "").replace(/\s+/g, " ").trim() ||
|
|
327
|
-
`Task ${taskId}`;
|
|
328
|
-
const message = previousCommitCount > 0
|
|
329
|
-
? `${baseSubject} (followup #${previousCommitCount})`
|
|
330
|
-
: baseSubject;
|
|
331
|
-
await this.repositoryManager.commit(taskId, message);
|
|
332
|
-
}
|
|
333
|
-
catch (commitError) {
|
|
334
|
-
const msg = commitError instanceof Error
|
|
335
|
-
? commitError.message
|
|
336
|
-
: String(commitError);
|
|
337
|
-
if (!msg.toLowerCase().includes("nothing to commit")) {
|
|
338
|
-
throw commitError;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
const integrateResult = await this.repositoryManager.integrateTask(taskId, baseBranch, "no-ff");
|
|
343
|
-
if (!integrateResult.success) {
|
|
344
|
-
await this._resumeForGitConflictResolution(context, {
|
|
345
|
-
kind: integrateResult.phase,
|
|
346
|
-
baseBranch,
|
|
347
|
-
conflicts: integrateResult.conflicts ?? [],
|
|
348
|
-
workdir: integrateResult.conflictWorkdir,
|
|
349
|
-
error: integrateResult.error,
|
|
350
|
-
});
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
// Worktree is no longer needed after a successful merge; preserve the branch
|
|
354
|
-
await this.repositoryManager.removeTaskWorktree(taskId, {
|
|
355
|
-
preserveBranch: true,
|
|
356
|
-
});
|
|
357
|
-
const wsId = context.config.workspaceId || context.workspaceId;
|
|
358
|
-
const repoUrl = context.config.repository?.url;
|
|
359
|
-
if (wsId && repoUrl) {
|
|
360
|
-
await this.repositoryManager.syncWorkspaceWorktree(wsId, repoUrl, baseBranch);
|
|
361
|
-
}
|
|
362
|
-
await this._finalizeConversation(context, false);
|
|
363
|
-
}
|
|
364
|
-
catch (mergeError) {
|
|
365
|
-
console.error("[CodexManager] Post-task git flow failed", mergeError);
|
|
366
|
-
await this._finalizeConversation(context, true, mergeError);
|
|
367
|
-
}
|
|
368
|
-
})();
|
|
369
|
-
context._runCompletionInFlight = promise.finally(() => {
|
|
370
|
-
delete context._runCompletionInFlight;
|
|
371
|
-
});
|
|
372
|
-
return context._runCompletionInFlight;
|
|
373
|
-
}
|
|
374
|
-
async _resumeForGitConflictResolution(context, payload) {
|
|
375
|
-
const { kind, baseBranch, conflicts, workdir, error } = payload;
|
|
376
|
-
const conflictsList = conflicts?.length
|
|
377
|
-
? `\n\nConflicted files:\n${conflicts.map((f) => `- ${f}`).join("\n")}`
|
|
378
|
-
: "";
|
|
379
|
-
const workdirHint = workdir ? `\n\nWork directory:\n- ${workdir}` : "";
|
|
380
|
-
const errorHint = error ? `\n\nError:\n${error}` : "";
|
|
381
|
-
const hasConflicts = !!conflicts?.length;
|
|
382
|
-
let instruction;
|
|
383
|
-
if (kind === "rebase") {
|
|
384
|
-
instruction = hasConflicts
|
|
385
|
-
? `<system-instructions>\nA git rebase onto \`${baseBranch}\` produced merge conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory, then run:\n- git add -A\n- git rebase --continue\n\nWhen the rebase finishes cleanly, reply briefly so I can retry the integration.\n</system-instructions>`
|
|
386
|
-
: `<system-instructions>\nA git rebase onto \`${baseBranch}\` failed.${workdirHint}${errorHint}\n\nNo conflicted files were reported. Please run:\n- git status\n\nFix the error (or complete/abort any in-progress git operation) and reply briefly so I can retry the integration.\n</system-instructions>`;
|
|
387
|
-
}
|
|
388
|
-
else if (kind === "merge") {
|
|
389
|
-
instruction = hasConflicts
|
|
390
|
-
? `<system-instructions>\nA git merge into \`${baseBranch}\` produced merge conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory, then run:\n- git add -A\n- git commit -m \"Resolve merge conflicts for ${context.conversationObjectId}\"\n\nWhen the working tree is clean, reply briefly so I can retry the integration.\n</system-instructions>`
|
|
391
|
-
: `<system-instructions>\nA git merge into \`${baseBranch}\` failed.${workdirHint}${errorHint}\n\nNo conflicted files were reported. Please run:\n- git status\n\nFix the error (or complete/abort any in-progress git operation) and reply briefly so I can retry the integration.\n</system-instructions>`;
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
instruction = `<system-instructions>\nGit has unresolved conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory and complete the in-progress git operation (rebase or merge). When the working tree is clean, reply briefly so I can retry the integration.\n</system-instructions>`;
|
|
395
|
-
}
|
|
396
|
-
const conversationData = {
|
|
397
|
-
id: context.conversationId,
|
|
398
|
-
objectType: context.conversationObjectType,
|
|
399
|
-
objectId: context.conversationObjectId,
|
|
400
|
-
model: context.model,
|
|
401
|
-
summary: context.summary ?? null,
|
|
402
|
-
globalInstructions: context.globalInstructions,
|
|
403
|
-
workspaceInstructions: context.workspaceInstructions,
|
|
404
|
-
permissionsMode: context.permissionsMode,
|
|
405
|
-
agentSessionId: context.agentSessionId,
|
|
406
|
-
workspaceId: context.workspaceId,
|
|
407
|
-
};
|
|
408
|
-
await this.resumeConversation(context.conversationObjectType, context.conversationObjectId, context.agentSessionId, context.config, conversationData, instruction, context.provider);
|
|
409
|
-
}
|
|
410
|
-
async buildEnvVars(config, githubToken, toolToken, provider) {
|
|
411
|
-
const envVars = {
|
|
412
|
-
...Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)),
|
|
413
|
-
};
|
|
414
|
-
if (config.codexAuth?.accessToken) {
|
|
415
|
-
envVars["OPENAI_ACCESS_TOKEN"] = config.codexAuth.accessToken;
|
|
416
|
-
}
|
|
417
|
-
else if (config.accessToken) {
|
|
418
|
-
envVars["OPENAI_ACCESS_TOKEN"] = config.accessToken;
|
|
419
|
-
}
|
|
420
|
-
if (githubToken) {
|
|
421
|
-
envVars["GITHUB_TOKEN"] = githubToken;
|
|
422
|
-
}
|
|
423
|
-
if (toolToken) {
|
|
424
|
-
envVars["TOOL_TOKEN"] = toolToken;
|
|
425
|
-
}
|
|
426
|
-
if (isDebugEnabledFor("sdk")) {
|
|
427
|
-
envVars["DEBUG"] = "1";
|
|
428
|
-
}
|
|
429
|
-
// Add OpenRouter/Groq API keys when using Northflare-routed providers
|
|
430
|
-
if (provider === "openrouter" || provider === "groq") {
|
|
431
|
-
const openRouterApiKey = config.openRouterApiKey ||
|
|
432
|
-
process.env["OPENROUTER_API_KEY"];
|
|
433
|
-
if (openRouterApiKey) {
|
|
434
|
-
envVars["OPENROUTER_API_KEY"] = openRouterApiKey;
|
|
435
|
-
}
|
|
436
|
-
const groqApiKey = config.groqApiKey ||
|
|
437
|
-
process.env["GROQ_API_KEY"];
|
|
438
|
-
if (groqApiKey) {
|
|
439
|
-
envVars["GROQ_API_KEY"] = groqApiKey;
|
|
440
|
-
}
|
|
441
|
-
if (!openRouterApiKey && !groqApiKey) {
|
|
442
|
-
console.warn("[CodexManager] OpenRouter/Groq provider selected but no API key found");
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const codexHome = await this.ensureCodexAuthHome(config);
|
|
446
|
-
if (codexHome) {
|
|
447
|
-
envVars["CODEX_HOME"] = codexHome;
|
|
448
|
-
}
|
|
449
|
-
return envVars;
|
|
450
|
-
}
|
|
451
|
-
async ensureCodexAuthHome(config) {
|
|
452
|
-
if (!config.codexAuth) {
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
const runnerId = this.runner.getRunnerId();
|
|
456
|
-
const dataDir = this.runner.config_.dataDir;
|
|
457
|
-
if (!runnerId || !dataDir) {
|
|
458
|
-
console.warn("[CodexManager] Missing runnerId or dataDir; cannot prepare Codex auth directory");
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
const codexDir = path.join(dataDir, "codex", runnerId);
|
|
462
|
-
const authPayload = {
|
|
463
|
-
OPENAI_API_KEY: null,
|
|
464
|
-
tokens: {
|
|
465
|
-
id_token: config.codexAuth.idToken,
|
|
466
|
-
access_token: config.codexAuth.accessToken,
|
|
467
|
-
refresh_token: "",
|
|
468
|
-
account_id: config.codexAuth.accountId,
|
|
469
|
-
},
|
|
470
|
-
last_refresh: config.codexAuth.lastRefresh || new Date().toISOString(),
|
|
471
|
-
};
|
|
472
|
-
try {
|
|
473
|
-
await fs.mkdir(codexDir, { recursive: true });
|
|
474
|
-
await fs.writeFile(path.join(codexDir, "auth.json"), JSON.stringify(authPayload, null, 2), "utf-8");
|
|
475
|
-
return codexDir;
|
|
476
|
-
}
|
|
477
|
-
catch (error) {
|
|
478
|
-
console.error("[CodexManager] Failed to persist Codex auth configuration:", error);
|
|
479
|
-
throw new Error("Runner failed to persist Codex credentials. Check runner logs for details.");
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
buildPromptForMessage(context, text, forceInstructions) {
|
|
483
|
-
const trimmed = text ?? "";
|
|
484
|
-
const instructions = this.getInstructionPrefix(context);
|
|
485
|
-
const metadata = context.metadata || {};
|
|
486
|
-
const shouldInjectInstructions = forceInstructions || !metadata["instructionsInjected"];
|
|
487
|
-
if (instructions && shouldInjectInstructions) {
|
|
488
|
-
metadata["instructionsInjected"] = true;
|
|
489
|
-
context.metadata = metadata;
|
|
490
|
-
return `${instructions}\n\n${trimmed}`.trim();
|
|
491
|
-
}
|
|
492
|
-
return trimmed;
|
|
493
|
-
}
|
|
494
|
-
getInstructionPrefix(context) {
|
|
495
|
-
const parts = [];
|
|
496
|
-
if (context.globalInstructions) {
|
|
497
|
-
parts.push(`<global-instructions>\n${context.globalInstructions}\n</global-instructions>`);
|
|
498
|
-
}
|
|
499
|
-
if (context.workspaceInstructions) {
|
|
500
|
-
parts.push(`<workspace-instructions>\n${context.workspaceInstructions}\n</workspace-instructions>`);
|
|
501
|
-
}
|
|
502
|
-
return parts.join("\n\n");
|
|
503
|
-
}
|
|
504
|
-
formatInitialMessages(initialMessages) {
|
|
505
|
-
return initialMessages
|
|
506
|
-
.map((msg) => {
|
|
507
|
-
const role = msg.role || "user";
|
|
508
|
-
const content = this.normalizeToText(msg.content);
|
|
509
|
-
return `<${role}-message>\n${content}\n</${role}-message>`;
|
|
510
|
-
})
|
|
511
|
-
.join("\n\n");
|
|
512
|
-
}
|
|
513
|
-
async buildInitialInput(initialMessages, context) {
|
|
514
|
-
const hasMultimodal = initialMessages.some((msg) => this.hasMultimodalContent(msg.content));
|
|
515
|
-
if (!hasMultimodal) {
|
|
516
|
-
const promptText = this.formatInitialMessages(initialMessages);
|
|
517
|
-
return this.buildPromptForMessage(context, promptText, true);
|
|
518
|
-
}
|
|
519
|
-
// Preserve role markers while keeping image blocks attached
|
|
520
|
-
const combinedBlocks = [];
|
|
521
|
-
for (const msg of initialMessages) {
|
|
522
|
-
const role = msg.role || "user";
|
|
523
|
-
const content = msg.content;
|
|
524
|
-
// Start role section
|
|
525
|
-
combinedBlocks.push({
|
|
526
|
-
type: "text",
|
|
527
|
-
text: `<${role}-message>`,
|
|
528
|
-
});
|
|
529
|
-
if (Array.isArray(content)) {
|
|
530
|
-
for (const item of content) {
|
|
531
|
-
if (!item || typeof item !== "object")
|
|
532
|
-
continue;
|
|
533
|
-
if (item.type === "text" && typeof item.text === "string") {
|
|
534
|
-
combinedBlocks.push({ type: "text", text: item.text });
|
|
535
|
-
}
|
|
536
|
-
else if (item.type === "image" || item.type === "document") {
|
|
537
|
-
combinedBlocks.push(item);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
combinedBlocks.push({
|
|
543
|
-
type: "text",
|
|
544
|
-
text: this.normalizeToText(content),
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
// Close role section
|
|
548
|
-
combinedBlocks.push({
|
|
549
|
-
type: "text",
|
|
550
|
-
text: `</${role}-message>`,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
const input = await this.normalizeToInput(combinedBlocks, context, true);
|
|
554
|
-
if (isDebugEnabledFor("manager") || isDebugEnabledFor("sdk")) {
|
|
555
|
-
console.log("[CodexManager] buildInitialInput multimodal payload", {
|
|
556
|
-
conversationId: context.conversationId,
|
|
557
|
-
blocks: combinedBlocks.map((b) => b.type),
|
|
558
|
-
inputType: Array.isArray(input) ? "array" : "string",
|
|
559
|
-
textItems: Array.isArray(input)
|
|
560
|
-
? input.filter((i) => i.type === "text").length
|
|
561
|
-
: 1,
|
|
562
|
-
imageItems: Array.isArray(input)
|
|
563
|
-
? input.filter((i) => i.type === "local_image").length
|
|
564
|
-
: 0,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
return input;
|
|
568
|
-
}
|
|
569
|
-
launchTurn(context, prompt) {
|
|
570
|
-
const state = this.threadStates.get(context.conversationId);
|
|
571
|
-
if (!state || !state.thread) {
|
|
572
|
-
throw new Error(`Thread state missing for conversation ${context.conversationId}`);
|
|
573
|
-
}
|
|
574
|
-
const abortController = new AbortController();
|
|
575
|
-
state.abortController = abortController;
|
|
576
|
-
context.lastActivityAt = new Date();
|
|
577
|
-
const runPromise = this.streamThreadEvents(context, state.thread, prompt, abortController);
|
|
578
|
-
state.runPromise = runPromise;
|
|
579
|
-
runPromise
|
|
580
|
-
.catch((error) => {
|
|
581
|
-
if (!this.isAbortError(error)) {
|
|
582
|
-
console.error(`[CodexManager] Run failed for ${context.conversationId}:`, error);
|
|
583
|
-
this._handleConversationError(context, error);
|
|
584
|
-
}
|
|
585
|
-
})
|
|
586
|
-
.finally(() => {
|
|
587
|
-
if (state.runPromise === runPromise) {
|
|
588
|
-
state.runPromise = null;
|
|
589
|
-
}
|
|
590
|
-
if (state.abortController === abortController) {
|
|
591
|
-
state.abortController = null;
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
async streamThreadEvents(context, thread, prompt, abortController) {
|
|
596
|
-
try {
|
|
597
|
-
const { events } = await thread.runStreamed(prompt, {
|
|
598
|
-
signal: abortController.signal,
|
|
599
|
-
});
|
|
600
|
-
for await (const event of events) {
|
|
601
|
-
await this.handleThreadEvent(context, event);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
catch (error) {
|
|
605
|
-
if (this.isAbortError(error)) {
|
|
606
|
-
console.log(`[CodexManager] Turn aborted for ${context.conversationId}`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
throw error;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
async abortActiveRun(conversationId) {
|
|
613
|
-
const state = this.threadStates.get(conversationId);
|
|
614
|
-
if (!state || !state.runPromise)
|
|
615
|
-
return;
|
|
616
|
-
if (state.abortController) {
|
|
617
|
-
state.abortController.abort();
|
|
618
|
-
}
|
|
619
|
-
try {
|
|
620
|
-
await state.runPromise;
|
|
621
|
-
}
|
|
622
|
-
catch (error) {
|
|
623
|
-
if (!this.isAbortError(error)) {
|
|
624
|
-
console.warn(`[CodexManager] Run aborted with error for ${conversationId}:`, error);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
async handleThreadEvent(context, event) {
|
|
629
|
-
try {
|
|
630
|
-
this.logRawThreadEvent(event);
|
|
631
|
-
switch (event.type) {
|
|
632
|
-
case "thread.started": {
|
|
633
|
-
await this.handleThreadStarted(context, event.thread_id);
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
case "turn.started": {
|
|
637
|
-
context.status = "active";
|
|
638
|
-
await this.sendAgentMessage(context, "system", {
|
|
639
|
-
subtype: "turn.started",
|
|
640
|
-
content: [
|
|
641
|
-
{
|
|
642
|
-
type: "text",
|
|
643
|
-
text: `Turn started at ${new Date().toISOString()}`,
|
|
644
|
-
},
|
|
645
|
-
],
|
|
646
|
-
});
|
|
647
|
-
break;
|
|
648
|
-
}
|
|
649
|
-
case "turn.completed": {
|
|
650
|
-
await this.sendAgentMessage(context, "result", {
|
|
651
|
-
subtype: "turn.completed",
|
|
652
|
-
content: [
|
|
653
|
-
{
|
|
654
|
-
type: "usage",
|
|
655
|
-
usage: event.usage,
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
});
|
|
659
|
-
context.status = "stopped";
|
|
660
|
-
await this._handleRunCompletion(context, false);
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
case "turn.failed": {
|
|
664
|
-
const error = new Error(event.error?.message || "Turn failed");
|
|
665
|
-
await this.sendAgentMessage(context, "error", {
|
|
666
|
-
subtype: "turn.failed",
|
|
667
|
-
content: [
|
|
668
|
-
{
|
|
669
|
-
type: "text",
|
|
670
|
-
text: error.message,
|
|
671
|
-
},
|
|
672
|
-
],
|
|
673
|
-
isError: true,
|
|
674
|
-
});
|
|
675
|
-
await this._handleConversationError(context, error);
|
|
676
|
-
context.status = "stopped";
|
|
677
|
-
await this._finalizeConversation(context, true, error, "turn_failed");
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
case "item.started":
|
|
681
|
-
case "item.updated":
|
|
682
|
-
case "item.completed": {
|
|
683
|
-
await this.forwardItemEvent(context, event.item, event.type.split(".")[1]);
|
|
684
|
-
break;
|
|
685
|
-
}
|
|
686
|
-
case "error": {
|
|
687
|
-
const fatalError = new Error(event.message || "Unknown error");
|
|
688
|
-
await this.sendAgentMessage(context, "error", {
|
|
689
|
-
subtype: "thread.error",
|
|
690
|
-
content: [{ type: "text", text: fatalError.message }],
|
|
691
|
-
isError: true,
|
|
692
|
-
});
|
|
693
|
-
await this._handleConversationError(context, fatalError);
|
|
694
|
-
context.status = "stopped";
|
|
695
|
-
await this._finalizeConversation(context, true, fatalError, "thread_error");
|
|
696
|
-
break;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
console.error("[CodexManager] Failed to handle thread event", {
|
|
702
|
-
event,
|
|
703
|
-
error,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
async forwardItemEvent(context, item, phase) {
|
|
708
|
-
const normalized = this.normalizeItemEvent(context, item, phase);
|
|
709
|
-
if (!normalized)
|
|
710
|
-
return;
|
|
711
|
-
const { subtype, content, isError, toolCalls, type } = normalized;
|
|
712
|
-
const payload = {
|
|
713
|
-
subtype,
|
|
714
|
-
content,
|
|
715
|
-
isError,
|
|
716
|
-
};
|
|
717
|
-
if (toolCalls) {
|
|
718
|
-
payload.toolCalls = toolCalls;
|
|
719
|
-
}
|
|
720
|
-
await this.sendAgentMessage(context, type, payload);
|
|
721
|
-
}
|
|
722
|
-
normalizeItemEvent(context, item, phase) {
|
|
723
|
-
switch (item.type) {
|
|
724
|
-
case "agent_message": {
|
|
725
|
-
if (phase !== "completed")
|
|
726
|
-
return null;
|
|
727
|
-
return {
|
|
728
|
-
type: "assistant",
|
|
729
|
-
content: [
|
|
730
|
-
{
|
|
731
|
-
type: "text",
|
|
732
|
-
text: item.text || "",
|
|
733
|
-
},
|
|
734
|
-
],
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
case "reasoning": {
|
|
738
|
-
return {
|
|
739
|
-
type: "thinking",
|
|
740
|
-
content: [
|
|
741
|
-
{
|
|
742
|
-
type: "thinking",
|
|
743
|
-
thinking: item.text,
|
|
744
|
-
text: item.text,
|
|
745
|
-
phase,
|
|
746
|
-
},
|
|
747
|
-
],
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
case "command_execution": {
|
|
751
|
-
// Namespace command_execution tool use IDs so they don't collide
|
|
752
|
-
// with MCP tool call IDs that may reuse the same raw item.id.
|
|
753
|
-
const internalNamespace = "codex_command";
|
|
754
|
-
const toolUseId = this.buildToolUseId(context, `${internalNamespace}:${item.id}`);
|
|
755
|
-
const timestamp = new Date().toISOString();
|
|
756
|
-
const isTerminal = phase === "completed" ||
|
|
757
|
-
item.status === "completed" ||
|
|
758
|
-
item.status === "failed";
|
|
759
|
-
if (!isTerminal) {
|
|
760
|
-
if (phase !== "started") {
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
type: "assistant",
|
|
765
|
-
subtype: "tool_use",
|
|
766
|
-
content: [
|
|
767
|
-
{
|
|
768
|
-
toolCalls: [
|
|
769
|
-
{
|
|
770
|
-
id: toolUseId,
|
|
771
|
-
name: "codex_command",
|
|
772
|
-
arguments: {
|
|
773
|
-
command: item.command,
|
|
774
|
-
status: item.status,
|
|
775
|
-
},
|
|
776
|
-
status: item.status,
|
|
777
|
-
},
|
|
778
|
-
],
|
|
779
|
-
timestamp,
|
|
780
|
-
},
|
|
781
|
-
],
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
|
|
785
|
-
const isError = item.status === "failed" || (exitCode ?? 0) !== 0;
|
|
786
|
-
return {
|
|
787
|
-
type: "tool_result",
|
|
788
|
-
subtype: "tool_result",
|
|
789
|
-
content: [
|
|
790
|
-
{
|
|
791
|
-
type: "tool_result",
|
|
792
|
-
tool_use_id: toolUseId,
|
|
793
|
-
content: {
|
|
794
|
-
kind: "codex_command_result",
|
|
795
|
-
command: item.command,
|
|
796
|
-
output: item.aggregated_output || "",
|
|
797
|
-
exitCode,
|
|
798
|
-
status: item.status,
|
|
799
|
-
},
|
|
800
|
-
timestamp,
|
|
801
|
-
},
|
|
802
|
-
],
|
|
803
|
-
isError,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
case "file_change": {
|
|
807
|
-
if (phase === "updated")
|
|
808
|
-
return null;
|
|
809
|
-
return {
|
|
810
|
-
type: "file_change",
|
|
811
|
-
subtype: "file_change",
|
|
812
|
-
content: [
|
|
813
|
-
{
|
|
814
|
-
type: "file_change",
|
|
815
|
-
changes: item.changes,
|
|
816
|
-
status: item.status,
|
|
817
|
-
},
|
|
818
|
-
],
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
case "mcp_tool_call": {
|
|
822
|
-
const toolName = this.buildMcpToolName(item.server, item.tool);
|
|
823
|
-
// Namespace MCP tool call IDs with the MCP tool name so they don't
|
|
824
|
-
// collide with internal tool IDs or MCP calls from other servers.
|
|
825
|
-
const namespacedRawId = `mcp:${toolName}:${item.id}`;
|
|
826
|
-
const toolUseId = this.buildToolUseId(context, namespacedRawId);
|
|
827
|
-
if (item.status === "in_progress" || phase !== "completed") {
|
|
828
|
-
return {
|
|
829
|
-
type: "assistant",
|
|
830
|
-
subtype: "tool_use",
|
|
831
|
-
content: [
|
|
832
|
-
{
|
|
833
|
-
toolCalls: [
|
|
834
|
-
{
|
|
835
|
-
id: toolUseId,
|
|
836
|
-
name: toolName,
|
|
837
|
-
arguments: item.arguments,
|
|
838
|
-
server: item.server,
|
|
839
|
-
tool: item.tool,
|
|
840
|
-
status: item.status,
|
|
841
|
-
},
|
|
842
|
-
],
|
|
843
|
-
timestamp: new Date().toISOString(),
|
|
844
|
-
},
|
|
845
|
-
],
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
if (item.status === "failed") {
|
|
849
|
-
return {
|
|
850
|
-
type: "error",
|
|
851
|
-
subtype: "tool_result",
|
|
852
|
-
content: [
|
|
853
|
-
{
|
|
854
|
-
type: "text",
|
|
855
|
-
text: item.error?.message || "Tool call failed",
|
|
856
|
-
tool_use_id: toolUseId,
|
|
857
|
-
tool_name: toolName,
|
|
858
|
-
timestamp: new Date().toISOString(),
|
|
859
|
-
},
|
|
860
|
-
],
|
|
861
|
-
isError: true,
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
return {
|
|
865
|
-
type: "tool_result",
|
|
866
|
-
subtype: "tool_result",
|
|
867
|
-
content: [
|
|
868
|
-
{
|
|
869
|
-
type: "tool_result",
|
|
870
|
-
subtype: "tool_result",
|
|
871
|
-
tool_use_id: toolUseId,
|
|
872
|
-
content: item.result?.content || [],
|
|
873
|
-
structured_content: item.result?.structured_content,
|
|
874
|
-
metadata: {
|
|
875
|
-
server: item.server,
|
|
876
|
-
tool: item.tool,
|
|
877
|
-
name: toolName,
|
|
878
|
-
original_tool_use_id: item.id,
|
|
879
|
-
},
|
|
880
|
-
timestamp: new Date().toISOString(),
|
|
881
|
-
},
|
|
882
|
-
],
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
case "web_search": {
|
|
886
|
-
return {
|
|
887
|
-
type: "system",
|
|
888
|
-
subtype: `web_search.${phase}`,
|
|
889
|
-
content: [
|
|
890
|
-
{
|
|
891
|
-
type: "web_search",
|
|
892
|
-
query: item.query,
|
|
893
|
-
status: phase,
|
|
894
|
-
},
|
|
895
|
-
],
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
case "todo_list": {
|
|
899
|
-
return {
|
|
900
|
-
type: "system",
|
|
901
|
-
subtype: "todo_list",
|
|
902
|
-
content: [
|
|
903
|
-
{
|
|
904
|
-
type: "todo_list",
|
|
905
|
-
items: item.items,
|
|
906
|
-
phase,
|
|
907
|
-
},
|
|
908
|
-
],
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
case "error": {
|
|
912
|
-
return {
|
|
913
|
-
type: "error",
|
|
914
|
-
subtype: `item.${phase}`,
|
|
915
|
-
content: [
|
|
916
|
-
{
|
|
917
|
-
type: "text",
|
|
918
|
-
text: item.message,
|
|
919
|
-
},
|
|
920
|
-
],
|
|
921
|
-
isError: true,
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
default:
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
async sendAgentMessage(context, type, { subtype, content, toolCalls, isError, }) {
|
|
929
|
-
const normalizedContent = Array.isArray(content)
|
|
930
|
-
? content
|
|
931
|
-
: content
|
|
932
|
-
? [content]
|
|
933
|
-
: [];
|
|
934
|
-
const payload = {
|
|
935
|
-
taskId: context.taskId,
|
|
936
|
-
conversationId: context.conversationId,
|
|
937
|
-
conversationObjectType: context.conversationObjectType,
|
|
938
|
-
conversationObjectId: context.conversationObjectId,
|
|
939
|
-
agentSessionId: context.agentSessionId,
|
|
940
|
-
type,
|
|
941
|
-
subtype,
|
|
942
|
-
content: normalizedContent,
|
|
943
|
-
toolCalls,
|
|
944
|
-
isError: Boolean(isError),
|
|
945
|
-
messageId: this.generateMessageId(context),
|
|
946
|
-
timestamp: new Date().toISOString(),
|
|
947
|
-
};
|
|
948
|
-
if (isDebugEnabledFor("manager")) {
|
|
949
|
-
console.log("[CodexManager] Sending message.agent", payload);
|
|
950
|
-
}
|
|
951
|
-
await this.runner.notify("message.agent", payload);
|
|
952
|
-
}
|
|
953
|
-
async handleThreadStarted(context, threadId) {
|
|
954
|
-
if (!threadId || threadId === context.agentSessionId) {
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
const oldSessionId = context.agentSessionId;
|
|
958
|
-
context.agentSessionId = threadId;
|
|
959
|
-
await this.runner.notify("agentSessionId.changed", {
|
|
960
|
-
conversationId: context.conversationId,
|
|
961
|
-
conversationObjectType: context.conversationObjectType,
|
|
962
|
-
conversationObjectId: context.conversationObjectId,
|
|
963
|
-
oldAgentSessionId: oldSessionId,
|
|
964
|
-
newAgentSessionId: threadId,
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
mapSandboxMode(permissionsMode) {
|
|
968
|
-
const mode = (permissionsMode || "").toLowerCase();
|
|
969
|
-
if (mode === "read" || mode === "read_only") {
|
|
970
|
-
return "read-only";
|
|
971
|
-
}
|
|
972
|
-
// "project" mode allows bash but no file writes - Codex doesn't have fine-grained control,
|
|
973
|
-
// so we use workspace-write which allows both bash and file writes
|
|
974
|
-
if (mode === "project" || mode === "workspace" || mode === "workspace-write") {
|
|
975
|
-
return "workspace-write";
|
|
976
|
-
}
|
|
977
|
-
return "danger-full-access";
|
|
978
|
-
}
|
|
979
|
-
shouldEnableNetwork(permissionsMode) {
|
|
980
|
-
const mode = (permissionsMode || "").toLowerCase();
|
|
981
|
-
// project mode allows network (for fetching dependencies, running tests, etc.)
|
|
982
|
-
if (mode === "project")
|
|
983
|
-
return true;
|
|
984
|
-
return mode !== "read" && mode !== "read_only";
|
|
985
|
-
}
|
|
986
|
-
getAdditionalDirectories(config) {
|
|
987
|
-
const additionalDirs = [];
|
|
988
|
-
if (config.runnerRepoPath) {
|
|
989
|
-
additionalDirs.push(config.runnerRepoPath);
|
|
990
|
-
}
|
|
991
|
-
return additionalDirs.length ? additionalDirs : undefined;
|
|
992
|
-
}
|
|
993
|
-
buildConfigOverridesFromMcp(mcpServers) {
|
|
994
|
-
if (!mcpServers)
|
|
995
|
-
return undefined;
|
|
996
|
-
const overrides = {};
|
|
997
|
-
for (const [serverName, config] of Object.entries(mcpServers)) {
|
|
998
|
-
this.flattenOverrideObject(`mcp_servers.${serverName}`, config, overrides);
|
|
999
|
-
}
|
|
1000
|
-
return Object.keys(overrides).length ? overrides : undefined;
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Build Codex config overrides for OpenRouter provider.
|
|
1004
|
-
* These correspond to the Codex CLI flags:
|
|
1005
|
-
* -c model_provider=openrouter
|
|
1006
|
-
* -c model_providers.openrouter.name=openrouter
|
|
1007
|
-
* -c model_providers.openrouter.base_url=https://openrouter.ai/api/v1
|
|
1008
|
-
* -c model_providers.openrouter.env_key=OPENROUTER_API_KEY
|
|
1009
|
-
*/
|
|
1010
|
-
buildOpenRouterConfigOverrides() {
|
|
1011
|
-
return {
|
|
1012
|
-
model_provider: "openrouter",
|
|
1013
|
-
"model_providers.openrouter.name": "openrouter",
|
|
1014
|
-
"model_providers.openrouter.base_url": "https://openrouter.ai/api/v1",
|
|
1015
|
-
"model_providers.openrouter.env_key": "OPENROUTER_API_KEY",
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
normalizeMcpServersForCodex(mcpServers) {
|
|
1019
|
-
const normalized = {};
|
|
1020
|
-
for (const [serverName, config] of Object.entries(mcpServers)) {
|
|
1021
|
-
normalized[serverName] = this.stripAuthorizationHeader(config);
|
|
1022
|
-
}
|
|
1023
|
-
return normalized;
|
|
1024
|
-
}
|
|
1025
|
-
buildMcpToolName(server, tool) {
|
|
1026
|
-
const safeServer = (server || "unknown").trim() || "unknown";
|
|
1027
|
-
const safeTool = (tool || "unknown").trim() || "unknown";
|
|
1028
|
-
return `mcp__${safeServer}__${safeTool}`;
|
|
1029
|
-
}
|
|
1030
|
-
logRawThreadEvent(event) {
|
|
1031
|
-
if (!isDebugEnabledFor("sdk")) {
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
try {
|
|
1035
|
-
console.log("[CodexManager] RAW Codex event FULL:", this.safeStringify(event));
|
|
1036
|
-
const summary = {
|
|
1037
|
-
type: event?.type,
|
|
1038
|
-
keys: Object.keys(event || {}),
|
|
1039
|
-
hasItem: Boolean(event?.item),
|
|
1040
|
-
itemType: event?.item?.type,
|
|
1041
|
-
hasUsage: Boolean(event?.usage),
|
|
1042
|
-
threadId: event?.thread_id,
|
|
1043
|
-
turnId: event?.turn_id,
|
|
1044
|
-
};
|
|
1045
|
-
console.log("[CodexManager] RAW Codex event summary:", summary);
|
|
1046
|
-
if (event?.item) {
|
|
1047
|
-
console.log("[CodexManager] RAW Codex event item:", this.safeStringify(event.item));
|
|
1048
|
-
}
|
|
1049
|
-
if (event?.usage) {
|
|
1050
|
-
console.log("[CodexManager] RAW Codex usage:", event.usage);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
catch (error) {
|
|
1054
|
-
console.warn("[CodexManager] Failed to log raw Codex event:", error);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
safeStringify(value) {
|
|
1058
|
-
const seen = new WeakSet();
|
|
1059
|
-
return JSON.stringify(value, (key, nested) => {
|
|
1060
|
-
if (typeof nested === "function")
|
|
1061
|
-
return undefined;
|
|
1062
|
-
if (typeof nested === "bigint")
|
|
1063
|
-
return nested.toString();
|
|
1064
|
-
if (typeof nested === "object" && nested !== null) {
|
|
1065
|
-
if (seen.has(nested))
|
|
1066
|
-
return "[Circular]";
|
|
1067
|
-
seen.add(nested);
|
|
1068
|
-
}
|
|
1069
|
-
return nested;
|
|
1070
|
-
}, 2);
|
|
1071
|
-
}
|
|
1072
|
-
buildToolUseId(context, rawId) {
|
|
1073
|
-
const scope = context.agentSessionId ||
|
|
1074
|
-
context.conversationId ||
|
|
1075
|
-
context.conversationObjectId ||
|
|
1076
|
-
"codex";
|
|
1077
|
-
return `${scope}:${rawId}`;
|
|
1078
|
-
}
|
|
1079
|
-
stripAuthorizationHeader(config) {
|
|
1080
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
1081
|
-
return config;
|
|
1082
|
-
}
|
|
1083
|
-
const normalized = { ...config };
|
|
1084
|
-
if (normalized.bearer_token_env_var &&
|
|
1085
|
-
normalized.headers &&
|
|
1086
|
-
typeof normalized.headers === "object" &&
|
|
1087
|
-
!Array.isArray(normalized.headers)) {
|
|
1088
|
-
const headers = { ...normalized.headers };
|
|
1089
|
-
delete headers["Authorization"];
|
|
1090
|
-
if (Object.keys(headers).length === 0) {
|
|
1091
|
-
delete normalized.headers;
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
normalized.headers = headers;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return normalized;
|
|
1098
|
-
}
|
|
1099
|
-
flattenOverrideObject(prefix, value, target) {
|
|
1100
|
-
if (value === undefined) {
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1104
|
-
target[prefix] = value;
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
const entries = Object.entries(value);
|
|
1108
|
-
if (!entries.length) {
|
|
1109
|
-
target[prefix] = {};
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
for (const [key, nested] of entries) {
|
|
1113
|
-
this.flattenOverrideObject(`${prefix}.${key}`, nested, target);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
async resolveWorkspacePath(context, config) {
|
|
1117
|
-
let workspacePath;
|
|
1118
|
-
const workspaceId = config.workspaceId;
|
|
1119
|
-
const useWorktrees = config.useWorktrees !== false;
|
|
1120
|
-
if (config.runnerRepoPath) {
|
|
1121
|
-
workspacePath = config.runnerRepoPath;
|
|
1122
|
-
console.log(`[CodexManager] Using local workspace path ${workspacePath}`);
|
|
1123
|
-
if (context.conversationObjectType === "Task" && workspaceId) {
|
|
1124
|
-
const hasGitDir = await fs
|
|
1125
|
-
.access(path.join(workspacePath, ".git"))
|
|
1126
|
-
.then(() => true)
|
|
1127
|
-
.catch(() => false);
|
|
1128
|
-
if (hasGitDir && useWorktrees) {
|
|
1129
|
-
const repoUrl = config.repository?.url || `file://${workspacePath}`;
|
|
1130
|
-
const baseBranch = config.repository?.branch || "main";
|
|
1131
|
-
const taskHandle = await this.repositoryManager.createTaskWorktree(context.conversationObjectId, workspaceId, repoUrl, baseBranch, config.githubToken);
|
|
1132
|
-
context.taskHandle = taskHandle;
|
|
1133
|
-
workspacePath = taskHandle.worktreePath;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
return workspacePath;
|
|
1137
|
-
}
|
|
1138
|
-
if (context.conversationObjectType === "Task" &&
|
|
1139
|
-
config.repository &&
|
|
1140
|
-
workspaceId &&
|
|
1141
|
-
useWorktrees) {
|
|
1142
|
-
if (!config.repository.url) {
|
|
1143
|
-
throw new Error("Repository URL is required for task conversations");
|
|
1144
|
-
}
|
|
1145
|
-
const taskHandle = await this.repositoryManager.createTaskWorktree(context.conversationObjectId, workspaceId, config.repository.url, config.repository.branch, config.githubToken);
|
|
1146
|
-
context.taskHandle = taskHandle;
|
|
1147
|
-
workspacePath = taskHandle.worktreePath;
|
|
1148
|
-
return workspacePath;
|
|
1149
|
-
}
|
|
1150
|
-
if (config.repository && workspaceId) {
|
|
1151
|
-
if (config.repository.type === "local" && config.repository.localPath) {
|
|
1152
|
-
workspacePath = config.repository.localPath;
|
|
1153
|
-
return workspacePath;
|
|
1154
|
-
}
|
|
1155
|
-
workspacePath = await this.repositoryManager.checkoutRepository(workspaceId, config.repository.url, config.repository.branch, config.githubToken);
|
|
1156
|
-
return workspacePath;
|
|
1157
|
-
}
|
|
1158
|
-
if (workspaceId) {
|
|
1159
|
-
workspacePath =
|
|
1160
|
-
await this.repositoryManager.getWorkspacePath(workspaceId);
|
|
1161
|
-
return workspacePath;
|
|
1162
|
-
}
|
|
1163
|
-
return process.cwd();
|
|
1164
|
-
}
|
|
1165
|
-
async fetchGithubTokens(workspaceId) {
|
|
1166
|
-
try {
|
|
1167
|
-
const response = await fetch(`${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`, {
|
|
1168
|
-
method: "GET",
|
|
1169
|
-
headers: {
|
|
1170
|
-
Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
|
|
1171
|
-
},
|
|
1172
|
-
});
|
|
1173
|
-
if (!response.ok) {
|
|
1174
|
-
console.error(`[CodexManager] Failed to fetch GitHub tokens: ${response.status}`);
|
|
1175
|
-
return undefined;
|
|
1176
|
-
}
|
|
1177
|
-
const data = (await response.json());
|
|
1178
|
-
return data.githubToken;
|
|
1179
|
-
}
|
|
1180
|
-
catch (error) {
|
|
1181
|
-
console.error("[CodexManager] Error fetching GitHub tokens", error);
|
|
1182
|
-
return undefined;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
generateToolToken(context) {
|
|
1186
|
-
if (!context.config.mcpServers)
|
|
1187
|
-
return undefined;
|
|
1188
|
-
const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
|
|
1189
|
-
const runnerUid = this.runner.getRunnerUid();
|
|
1190
|
-
if (!runnerToken || !runnerUid)
|
|
1191
|
-
return undefined;
|
|
1192
|
-
return jwt.sign({
|
|
1193
|
-
conversationId: context.conversationId,
|
|
1194
|
-
runnerUid,
|
|
1195
|
-
}, runnerToken, {
|
|
1196
|
-
expiresIn: "60m",
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Detect whether incoming content contains multimodal blocks (image/document).
|
|
1201
|
-
*/
|
|
1202
|
-
hasMultimodalContent(value) {
|
|
1203
|
-
if (!Array.isArray(value))
|
|
1204
|
-
return false;
|
|
1205
|
-
return value.some((item) => item &&
|
|
1206
|
-
typeof item === "object" &&
|
|
1207
|
-
(item.type === "image" || item.type === "document"));
|
|
1208
|
-
}
|
|
1209
|
-
/**
|
|
1210
|
-
* Normalize incoming content into the Codex SDK Input shape.
|
|
1211
|
-
* - Text-only -> single string (lets SDK do fast path)
|
|
1212
|
-
* - Multimodal -> [{ text }, ...{ local_image }]
|
|
1213
|
-
*/
|
|
1214
|
-
async normalizeToInput(value, context, forceInstructions) {
|
|
1215
|
-
if (Array.isArray(value) && this.hasMultimodalContent(value)) {
|
|
1216
|
-
const inputs = [];
|
|
1217
|
-
// Collect any text blocks (or document placeholders) to keep context
|
|
1218
|
-
const textChunks = [];
|
|
1219
|
-
for (const item of value) {
|
|
1220
|
-
if (!item || typeof item !== "object")
|
|
1221
|
-
continue;
|
|
1222
|
-
if (item.type === "text" && typeof item.text === "string") {
|
|
1223
|
-
textChunks.push(item.text);
|
|
1224
|
-
}
|
|
1225
|
-
else if (item.type === "document" && item.source) {
|
|
1226
|
-
const docUrl = item.source.type === "url" ? item.source.url : item.source.data;
|
|
1227
|
-
if (docUrl) {
|
|
1228
|
-
textChunks.push(`[Document: ${docUrl}]`);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
const textPart = this.buildPromptForMessage(context, textChunks.join(" ").trim(), forceInstructions);
|
|
1233
|
-
if (textPart) {
|
|
1234
|
-
inputs.push({ type: "text", text: textPart });
|
|
1235
|
-
}
|
|
1236
|
-
// Download image blocks to temp files and attach as local_image
|
|
1237
|
-
const imagePaths = [];
|
|
1238
|
-
for (const item of value) {
|
|
1239
|
-
if (!item || typeof item !== "object" || item.type !== "image" || !item.source) {
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
const imageUrl = item.source.type === "url" ? item.source.url : item.source.data;
|
|
1243
|
-
if (!imageUrl)
|
|
1244
|
-
continue;
|
|
1245
|
-
const localPath = await this.downloadImageToTemp(imageUrl);
|
|
1246
|
-
inputs.push({ type: "local_image", path: localPath });
|
|
1247
|
-
imagePaths.push(localPath);
|
|
1248
|
-
}
|
|
1249
|
-
// If no text and instructions were never injected, inject an empty prompt to carry instructions
|
|
1250
|
-
if (!inputs.some((i) => i.type === "text")) {
|
|
1251
|
-
const instructionOnly = this.buildPromptForMessage(context, "", forceInstructions);
|
|
1252
|
-
if (instructionOnly) {
|
|
1253
|
-
inputs.unshift({ type: "text", text: instructionOnly });
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
if (isDebugEnabledFor("manager") || isDebugEnabledFor("sdk")) {
|
|
1257
|
-
console.log("[CodexManager] normalizeToInput multimodal", {
|
|
1258
|
-
conversationId: context.conversationId,
|
|
1259
|
-
imageCount: imagePaths.length,
|
|
1260
|
-
imagePaths,
|
|
1261
|
-
textIncluded: inputs.some((i) => i.type === "text"),
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
return inputs;
|
|
1265
|
-
}
|
|
1266
|
-
// Fallback to text-only path
|
|
1267
|
-
const messageText = this.normalizeToText(value);
|
|
1268
|
-
return this.buildPromptForMessage(context, messageText, forceInstructions);
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* Download an image (http(s) or data URL) to a temp file and return the path.
|
|
1272
|
-
*/
|
|
1273
|
-
async downloadImageToTemp(url) {
|
|
1274
|
-
// Allow local filesystem paths to be passed through unchanged
|
|
1275
|
-
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
|
|
1276
|
-
return path.isAbsolute(url) ? url : path.resolve(url);
|
|
1277
|
-
}
|
|
1278
|
-
// Handle data URLs (e.g., data:image/png;base64,...)
|
|
1279
|
-
if (url.startsWith("data:")) {
|
|
1280
|
-
const commaIdx = url.indexOf(",");
|
|
1281
|
-
if (commaIdx === -1) {
|
|
1282
|
-
throw new Error("Invalid data URL for image");
|
|
1283
|
-
}
|
|
1284
|
-
const header = url.slice(5, commaIdx); // remove "data:"
|
|
1285
|
-
const dataPart = url.slice(commaIdx + 1);
|
|
1286
|
-
const isBase64 = header.endsWith(";base64");
|
|
1287
|
-
const mime = header.replace(";base64", "");
|
|
1288
|
-
const ext = this.mimeToExtension(mime) || "png";
|
|
1289
|
-
const buffer = isBase64
|
|
1290
|
-
? Buffer.from(dataPart, "base64")
|
|
1291
|
-
: Buffer.from(decodeURIComponent(dataPart), "utf-8");
|
|
1292
|
-
return await this.writeBufferToTemp(buffer, ext);
|
|
1293
|
-
}
|
|
1294
|
-
// Standard remote URL
|
|
1295
|
-
const response = await fetch(url);
|
|
1296
|
-
if (!response.ok) {
|
|
1297
|
-
throw new Error(`Failed to download image (${response.status}) from ${url}`);
|
|
1298
|
-
}
|
|
1299
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1300
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
1301
|
-
const ext = this.inferExtensionFromUrl(url) || "png";
|
|
1302
|
-
return await this.writeBufferToTemp(buffer, ext);
|
|
1303
|
-
}
|
|
1304
|
-
async writeBufferToTemp(buffer, ext) {
|
|
1305
|
-
const dir = path.join(tmpdir(), "codex-images");
|
|
1306
|
-
await fs.mkdir(dir, { recursive: true });
|
|
1307
|
-
const filename = `${crypto.randomUUID()}.${ext}`;
|
|
1308
|
-
const fullPath = path.join(dir, filename);
|
|
1309
|
-
await fs.writeFile(fullPath, buffer);
|
|
1310
|
-
return fullPath;
|
|
1311
|
-
}
|
|
1312
|
-
inferExtensionFromUrl(url) {
|
|
1313
|
-
try {
|
|
1314
|
-
const parsed = new URL(url);
|
|
1315
|
-
const pathname = parsed.pathname;
|
|
1316
|
-
const ext = path.extname(pathname);
|
|
1317
|
-
if (ext) {
|
|
1318
|
-
return ext.replace(".", "").split(/[?#]/)[0] || null;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
catch {
|
|
1322
|
-
// Not a valid URL; fall through
|
|
1323
|
-
}
|
|
1324
|
-
return null;
|
|
1325
|
-
}
|
|
1326
|
-
mimeToExtension(mime) {
|
|
1327
|
-
if (!mime)
|
|
1328
|
-
return null;
|
|
1329
|
-
const map = {
|
|
1330
|
-
"image/png": "png",
|
|
1331
|
-
"image/jpeg": "jpg",
|
|
1332
|
-
"image/jpg": "jpg",
|
|
1333
|
-
"image/webp": "webp",
|
|
1334
|
-
"image/gif": "gif",
|
|
1335
|
-
};
|
|
1336
|
-
return map[mime] || null;
|
|
1337
|
-
}
|
|
1338
|
-
normalizeToText(value) {
|
|
1339
|
-
if (typeof value === "string")
|
|
1340
|
-
return value;
|
|
1341
|
-
if (value == null)
|
|
1342
|
-
return "";
|
|
1343
|
-
if (typeof value === "object") {
|
|
1344
|
-
if (typeof value.text === "string") {
|
|
1345
|
-
return value.text;
|
|
1346
|
-
}
|
|
1347
|
-
if (Array.isArray(value)) {
|
|
1348
|
-
const texts = value
|
|
1349
|
-
.map((entry) => {
|
|
1350
|
-
if (entry &&
|
|
1351
|
-
typeof entry === "object" &&
|
|
1352
|
-
typeof entry.text === "string") {
|
|
1353
|
-
return entry.text;
|
|
1354
|
-
}
|
|
1355
|
-
return null;
|
|
1356
|
-
})
|
|
1357
|
-
.filter((entry) => Boolean(entry));
|
|
1358
|
-
if (texts.length) {
|
|
1359
|
-
return texts.join(" ");
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof value.content === "string") {
|
|
1363
|
-
return value.content;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
try {
|
|
1367
|
-
return JSON.stringify(value);
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
return String(value);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
normalizeSessionId(value) {
|
|
1374
|
-
if (!value)
|
|
1375
|
-
return undefined;
|
|
1376
|
-
const trimmed = value.trim();
|
|
1377
|
-
return trimmed.length ? trimmed : undefined;
|
|
1378
|
-
}
|
|
1379
|
-
resolveSessionId(config, conversationData, override) {
|
|
1380
|
-
return (this.normalizeSessionId(override) ||
|
|
1381
|
-
this.normalizeSessionId(config?.sessionId) ||
|
|
1382
|
-
this.normalizeSessionId(conversationData?.agentSessionId) ||
|
|
1383
|
-
this.normalizeSessionId(conversationData?.threadId) ||
|
|
1384
|
-
this.normalizeSessionId(conversationData?.sessionId) ||
|
|
1385
|
-
"");
|
|
1386
|
-
}
|
|
1387
|
-
isAbortError(error) {
|
|
1388
|
-
if (!error)
|
|
1389
|
-
return false;
|
|
1390
|
-
return (error.name === "AbortError" ||
|
|
1391
|
-
/aborted|abort/i.test(error.message || ""));
|
|
1392
|
-
}
|
|
1393
|
-
async _finalizeConversation(context, hadError, error, reason) {
|
|
1394
|
-
// Synchronous idempotency check - must happen before any async operations
|
|
1395
|
-
if (context._finalized)
|
|
1396
|
-
return;
|
|
1397
|
-
context._finalized = true;
|
|
1398
|
-
// Mark as completed immediately to prevent restart on catch-up
|
|
1399
|
-
// This is synchronous and happens before any async operations
|
|
1400
|
-
this.runner.markConversationCompleted(context.conversationId);
|
|
1401
|
-
// Clean up local state synchronously
|
|
1402
|
-
this.threadStates.delete(context.conversationId);
|
|
1403
|
-
this.runner.activeConversations_.delete(context.conversationId);
|
|
1404
|
-
statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
|
|
1405
|
-
// Now do async notification (after all sync cleanup)
|
|
1406
|
-
try {
|
|
1407
|
-
await this.runner.notify("conversation.end", {
|
|
1408
|
-
conversationId: context.conversationId,
|
|
1409
|
-
conversationObjectType: context.conversationObjectType,
|
|
1410
|
-
conversationObjectId: context.conversationObjectId,
|
|
1411
|
-
agentSessionId: context.agentSessionId,
|
|
1412
|
-
isError: hadError,
|
|
1413
|
-
errorMessage: error?.message,
|
|
1414
|
-
reason,
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
catch (notifyError) {
|
|
1418
|
-
console.error("[CodexManager] Failed to send conversation.end notification", notifyError);
|
|
1419
|
-
}
|
|
1420
|
-
// Notify update coordinator that a conversation has ended
|
|
1421
|
-
// This may trigger an auto-update if one is pending and runner is now idle
|
|
1422
|
-
try {
|
|
1423
|
-
await this.runner.onConversationEnd();
|
|
1424
|
-
}
|
|
1425
|
-
catch (e) {
|
|
1426
|
-
console.error("[CodexManager] Failed to notify onConversationEnd:", e);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
async _handleConversationError(context, error) {
|
|
1430
|
-
console.error(`[CodexManager] Conversation error for ${context.conversationId}:`, error);
|
|
1431
|
-
await this.runner.notify("error.report", {
|
|
1432
|
-
conversationId: context.conversationId,
|
|
1433
|
-
conversationObjectType: context.conversationObjectType,
|
|
1434
|
-
conversationObjectId: context.conversationObjectId,
|
|
1435
|
-
agentSessionId: context.agentSessionId,
|
|
1436
|
-
errorType: "codex_error",
|
|
1437
|
-
message: error.message,
|
|
1438
|
-
details: {
|
|
1439
|
-
stack: error.stack,
|
|
1440
|
-
timestamp: new Date(),
|
|
1441
|
-
},
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
generateMessageId(context) {
|
|
1445
|
-
return `${context.agentSessionId}-${Date.now()}-${Math.random()
|
|
1446
|
-
.toString(36)
|
|
1447
|
-
.slice(2)}`;
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
//# sourceMappingURL=codex-sdk-manager.js.map
|