@codex-native/sdk 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +482 -5
- package/codex_native.darwin-arm64.node +0 -0
- package/dist/chunk-ZTUGAPWF.mjs +1996 -0
- package/dist/chunk-ZTUGAPWF.mjs.map +1 -0
- package/dist/cli.cjs +3510 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1661 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +4722 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +1944 -272
- package/dist/index.d.ts +2234 -0
- package/dist/index.mjs +2501 -454
- package/dist/index.mjs.map +1 -1
- package/index.d.ts +487 -0
- package/index.js +626 -0
- package/npm/darwin-arm64/README.md +2 -2
- package/npm/darwin-arm64/package.json +3 -3
- package/npm/darwin-x64/README.md +2 -2
- package/npm/darwin-x64/package.json +3 -3
- package/npm/linux-arm64-gnu/README.md +2 -2
- package/npm/linux-arm64-gnu/package.json +3 -3
- package/npm/linux-arm64-musl/README.md +2 -2
- package/npm/linux-arm64-musl/package.json +3 -3
- package/npm/linux-x64-gnu/README.md +2 -2
- package/npm/linux-x64-gnu/package.json +3 -3
- package/npm/linux-x64-musl/README.md +2 -2
- package/npm/linux-x64-musl/package.json +3 -3
- package/npm/win32-arm64-msvc/README.md +2 -2
- package/npm/win32-arm64-msvc/package.json +3 -3
- package/npm/win32-x64-msvc/README.md +2 -2
- package/npm/win32-x64-msvc/package.json +3 -3
- package/package.json +61 -22
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4722 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
CloudTasks: () => CloudTasks,
|
|
34
|
+
Codex: () => Codex,
|
|
35
|
+
CodexProvider: () => CodexProvider,
|
|
36
|
+
DEFAULT_RERANKER_BATCH_SIZE: () => DEFAULT_RERANKER_BATCH_SIZE,
|
|
37
|
+
DEFAULT_RERANKER_TOP_K: () => DEFAULT_RERANKER_TOP_K,
|
|
38
|
+
DEFAULT_REVERIE_LIMIT: () => DEFAULT_REVERIE_LIMIT,
|
|
39
|
+
DEFAULT_REVERIE_MAX_CANDIDATES: () => DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
40
|
+
DEFAULT_SERVERS: () => DEFAULT_SERVERS,
|
|
41
|
+
LogLevel: () => LogLevel,
|
|
42
|
+
Logger: () => Logger,
|
|
43
|
+
LspDiagnosticsBridge: () => LspDiagnosticsBridge,
|
|
44
|
+
LspManager: () => LspManager,
|
|
45
|
+
OpenCodeAgent: () => OpenCodeAgent,
|
|
46
|
+
REVERIE_CANDIDATE_MULTIPLIER: () => REVERIE_CANDIDATE_MULTIPLIER,
|
|
47
|
+
REVERIE_EMBED_MODEL: () => REVERIE_EMBED_MODEL,
|
|
48
|
+
REVERIE_LLM_GRADE_THRESHOLD: () => REVERIE_LLM_GRADE_THRESHOLD,
|
|
49
|
+
REVERIE_RERANKER_MODEL: () => REVERIE_RERANKER_MODEL,
|
|
50
|
+
ScopedLogger: () => ScopedLogger,
|
|
51
|
+
Thread: () => Thread,
|
|
52
|
+
applyFileReveriePipeline: () => applyFileReveriePipeline,
|
|
53
|
+
applyQualityPipeline: () => applyQualityPipeline,
|
|
54
|
+
applyReveriePipeline: () => applyReveriePipeline,
|
|
55
|
+
attachLspDiagnostics: () => attachLspDiagnostics,
|
|
56
|
+
buildBranchContext: () => buildBranchContext,
|
|
57
|
+
buildFileContext: () => buildFileContext,
|
|
58
|
+
buildProjectContext: () => buildProjectContext,
|
|
59
|
+
codexTool: () => codexTool,
|
|
60
|
+
collectRepoDiffSummary: () => collectRepoDiffSummary,
|
|
61
|
+
contextToQuery: () => contextToQuery,
|
|
62
|
+
createThreadLogger: () => createThreadLogger,
|
|
63
|
+
deduplicateReverieInsights: () => deduplicateReverieInsights,
|
|
64
|
+
encodeToToon: () => encodeToToon,
|
|
65
|
+
evAssistantMessage: () => evAssistantMessage,
|
|
66
|
+
evCompleted: () => evCompleted,
|
|
67
|
+
evFunctionCall: () => evFunctionCall,
|
|
68
|
+
evResponseCreated: () => evResponseCreated,
|
|
69
|
+
extractKeySymbols: () => extractKeySymbols,
|
|
70
|
+
fastEmbedEmbed: () => fastEmbedEmbed,
|
|
71
|
+
fastEmbedInit: () => fastEmbedInit,
|
|
72
|
+
filterBySeverity: () => filterBySeverity,
|
|
73
|
+
findServerForFile: () => findServerForFile,
|
|
74
|
+
formatDiagnosticsForBackgroundEvent: () => formatDiagnosticsForBackgroundEvent,
|
|
75
|
+
formatDiagnosticsForTool: () => formatDiagnosticsForTool,
|
|
76
|
+
formatDiagnosticsWithSummary: () => formatDiagnosticsWithSummary,
|
|
77
|
+
formatFileList: () => formatFileList,
|
|
78
|
+
formatStream: () => formatStream,
|
|
79
|
+
gradeReverieRelevance: () => gradeReverieRelevance,
|
|
80
|
+
gradeReveriesInParallel: () => gradeReveriesInParallel,
|
|
81
|
+
isValidReverieExcerpt: () => isValidReverieExcerpt,
|
|
82
|
+
logApprovedReveries: () => logApprovedReveries,
|
|
83
|
+
logLLMGrading: () => logLLMGrading,
|
|
84
|
+
logLevelResults: () => logLevelResults,
|
|
85
|
+
logMultiLevelSearch: () => logMultiLevelSearch,
|
|
86
|
+
logMultiLevelSummary: () => logMultiLevelSummary,
|
|
87
|
+
logReverieFiltering: () => logReverieFiltering,
|
|
88
|
+
logReverieHintQuality: () => logReverieHintQuality,
|
|
89
|
+
logReverieInsights: () => logReverieInsights,
|
|
90
|
+
logReverieSearch: () => logReverieSearch,
|
|
91
|
+
logger: () => logger,
|
|
92
|
+
resolveWorkspaceRoot: () => resolveWorkspaceRoot,
|
|
93
|
+
reverieGetConversationInsights: () => reverieGetConversationInsights,
|
|
94
|
+
reverieIndexSemantic: () => reverieIndexSemantic,
|
|
95
|
+
reverieListConversations: () => reverieListConversations,
|
|
96
|
+
reverieSearchConversations: () => reverieSearchConversations,
|
|
97
|
+
reverieSearchSemantic: () => reverieSearchSemantic,
|
|
98
|
+
runThreadTurnWithLogs: () => runThreadTurnWithLogs,
|
|
99
|
+
runTui: () => runTui,
|
|
100
|
+
searchBranchLevel: () => searchBranchLevel,
|
|
101
|
+
searchFileLevel: () => searchFileLevel,
|
|
102
|
+
searchMultiLevel: () => searchMultiLevel,
|
|
103
|
+
searchProjectLevel: () => searchProjectLevel,
|
|
104
|
+
searchReveries: () => searchReveries,
|
|
105
|
+
sse: () => sse,
|
|
106
|
+
startTui: () => startTui,
|
|
107
|
+
summarizeDiagnostics: () => summarizeDiagnostics,
|
|
108
|
+
tokenizerCount: () => tokenizerCount,
|
|
109
|
+
tokenizerDecode: () => tokenizerDecode,
|
|
110
|
+
tokenizerEncode: () => tokenizerEncode,
|
|
111
|
+
truncateText: () => truncate
|
|
112
|
+
});
|
|
113
|
+
module.exports = __toCommonJS(src_exports);
|
|
114
|
+
|
|
115
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
116
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
|
|
117
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
118
|
+
|
|
119
|
+
// src/thread.ts
|
|
120
|
+
var fs5 = __toESM(require("fs"));
|
|
121
|
+
var path8 = __toESM(require("path"));
|
|
122
|
+
|
|
123
|
+
// src/events/convert.ts
|
|
124
|
+
function convertRustEventToThreadEvent(rustEvent) {
|
|
125
|
+
if (rustEvent?.ThreadStarted) {
|
|
126
|
+
return {
|
|
127
|
+
type: "thread.started",
|
|
128
|
+
thread_id: rustEvent.ThreadStarted.thread_id
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (rustEvent?.TurnStarted) {
|
|
132
|
+
return { type: "turn.started" };
|
|
133
|
+
}
|
|
134
|
+
if (rustEvent?.TurnCompleted) {
|
|
135
|
+
return {
|
|
136
|
+
type: "turn.completed",
|
|
137
|
+
usage: rustEvent.TurnCompleted.usage
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (rustEvent?.TurnFailed) {
|
|
141
|
+
return {
|
|
142
|
+
type: "turn.failed",
|
|
143
|
+
error: rustEvent.TurnFailed.error
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (rustEvent?.ItemStarted) {
|
|
147
|
+
return {
|
|
148
|
+
type: "item.started",
|
|
149
|
+
item: rustEvent.ItemStarted.item
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (rustEvent?.ItemUpdated) {
|
|
153
|
+
return {
|
|
154
|
+
type: "item.updated",
|
|
155
|
+
item: rustEvent.ItemUpdated.item
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (rustEvent?.ItemCompleted) {
|
|
159
|
+
return {
|
|
160
|
+
type: "item.completed",
|
|
161
|
+
item: rustEvent.ItemCompleted.item
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (rustEvent?.Error) {
|
|
165
|
+
return {
|
|
166
|
+
type: "error",
|
|
167
|
+
message: rustEvent.Error.message
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (rustEvent?.BackgroundEvent) {
|
|
171
|
+
return {
|
|
172
|
+
type: "background_event",
|
|
173
|
+
message: rustEvent.BackgroundEvent.message
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (rustEvent?.type === "background_event" && typeof rustEvent.message === "string") {
|
|
177
|
+
return {
|
|
178
|
+
type: "background_event",
|
|
179
|
+
message: rustEvent.message
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (rustEvent?.type === "plan_update_scheduled" && rustEvent.plan) {
|
|
183
|
+
const planData = rustEvent.plan;
|
|
184
|
+
const planItems = planData.plan || [];
|
|
185
|
+
return {
|
|
186
|
+
type: "item.completed",
|
|
187
|
+
item: {
|
|
188
|
+
id: `plan-${Date.now()}`,
|
|
189
|
+
type: "todo_list",
|
|
190
|
+
items: planItems.map((item) => ({
|
|
191
|
+
text: item.step,
|
|
192
|
+
completed: item.status === "completed"
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (rustEvent?.type) {
|
|
198
|
+
return rustEvent;
|
|
199
|
+
}
|
|
200
|
+
return rustEvent;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/outputSchemaFile.ts
|
|
204
|
+
var import_node_fs = require("fs");
|
|
205
|
+
var import_node_os = __toESM(require("os"));
|
|
206
|
+
var import_node_path = __toESM(require("path"));
|
|
207
|
+
function normalizeOutputSchema(schema) {
|
|
208
|
+
if (schema === void 0) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
if (isJsonObject(schema) && (schema.type === "json_schema" || schema.type === "json-schema") && isJsonObject(schema.json_schema) && isJsonObject(schema.json_schema.schema)) {
|
|
212
|
+
const strict = typeof schema.json_schema.strict === "boolean" ? schema.json_schema.strict : true;
|
|
213
|
+
return normalizeJsonSchemaObject(schema.json_schema.schema, strict);
|
|
214
|
+
}
|
|
215
|
+
if (isJsonObject(schema) && isJsonObject(schema.schema)) {
|
|
216
|
+
const strict = typeof schema.strict === "boolean" ? schema.strict : true;
|
|
217
|
+
return normalizeJsonSchemaObject(schema.schema, strict);
|
|
218
|
+
}
|
|
219
|
+
if (!isJsonObject(schema)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
"outputSchema must be a plain JSON object or an OpenAI-style json_schema wrapper"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return normalizeJsonSchemaObject(schema, true);
|
|
225
|
+
}
|
|
226
|
+
async function createOutputSchemaFile(schema) {
|
|
227
|
+
const normalizedSchema = normalizeOutputSchema(schema);
|
|
228
|
+
if (!normalizedSchema) {
|
|
229
|
+
return { cleanup: async () => {
|
|
230
|
+
} };
|
|
231
|
+
}
|
|
232
|
+
const schemaDir = await import_node_fs.promises.mkdtemp(import_node_path.default.join(import_node_os.default.tmpdir(), "codex-output-schema-"));
|
|
233
|
+
const schemaPath = import_node_path.default.join(schemaDir, "schema.json");
|
|
234
|
+
const cleanup = async () => {
|
|
235
|
+
try {
|
|
236
|
+
await import_node_fs.promises.rm(schemaDir, { recursive: true, force: true });
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
await import_node_fs.promises.writeFile(schemaPath, JSON.stringify(normalizedSchema), "utf8");
|
|
242
|
+
return { schemaPath, cleanup };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
await cleanup();
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function isJsonObject(value) {
|
|
249
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
250
|
+
}
|
|
251
|
+
function normalizeJsonSchemaObject(schema, strict) {
|
|
252
|
+
const record = { ...schema };
|
|
253
|
+
const hasExplicitAdditional = typeof record.additionalProperties === "boolean" || typeof record.additionalProperties === "object";
|
|
254
|
+
const additionalProperties = hasExplicitAdditional ? record.additionalProperties : strict ? false : record.additionalProperties;
|
|
255
|
+
return {
|
|
256
|
+
...record,
|
|
257
|
+
...hasExplicitAdditional || strict ? { additionalProperties } : {}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/nativeBinding.ts
|
|
262
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
263
|
+
var import_node_module = require("module");
|
|
264
|
+
var import_node_path2 = __toESM(require("path"));
|
|
265
|
+
var import_node_url = require("url");
|
|
266
|
+
var CLI_ENTRYPOINT_ENV = "CODEX_NODE_CLI_ENTRYPOINT";
|
|
267
|
+
function ensureCliEntrypointEnv() {
|
|
268
|
+
if (process.env[CLI_ENTRYPOINT_ENV]) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const filename = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
272
|
+
const dirname3 = import_node_path2.default.dirname(filename);
|
|
273
|
+
const candidates = [
|
|
274
|
+
import_node_path2.default.resolve(dirname3, "cli.cjs"),
|
|
275
|
+
import_node_path2.default.resolve(dirname3, "../cli.cjs"),
|
|
276
|
+
import_node_path2.default.resolve(dirname3, "../dist/cli.cjs")
|
|
277
|
+
];
|
|
278
|
+
for (const candidate of candidates) {
|
|
279
|
+
if (import_node_fs2.default.existsSync(candidate)) {
|
|
280
|
+
process.env[CLI_ENTRYPOINT_ENV] = candidate;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
ensureCliEntrypointEnv();
|
|
286
|
+
var cachedBinding;
|
|
287
|
+
function getImportMetaUrl2() {
|
|
288
|
+
try {
|
|
289
|
+
return Function(
|
|
290
|
+
"return typeof import.meta !== 'undefined' && import.meta.url ? import.meta.url : undefined;"
|
|
291
|
+
)();
|
|
292
|
+
} catch {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function resolveBindingEntryPath() {
|
|
297
|
+
if (typeof __dirname === "string") {
|
|
298
|
+
return import_node_path2.default.resolve(__dirname, "..", "index.js");
|
|
299
|
+
}
|
|
300
|
+
const importMetaUrl2 = getImportMetaUrl2();
|
|
301
|
+
if (importMetaUrl2) {
|
|
302
|
+
try {
|
|
303
|
+
const filePath = (0, import_node_url.fileURLToPath)(importMetaUrl2);
|
|
304
|
+
return import_node_path2.default.resolve(import_node_path2.default.dirname(filePath), "..", "index.js");
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return import_node_path2.default.resolve(process.cwd(), "index.js");
|
|
309
|
+
}
|
|
310
|
+
function resolveRequire() {
|
|
311
|
+
const globalRequire = globalThis.require;
|
|
312
|
+
if (typeof globalRequire === "function") {
|
|
313
|
+
return globalRequire;
|
|
314
|
+
}
|
|
315
|
+
if (typeof __filename === "string") {
|
|
316
|
+
try {
|
|
317
|
+
return (0, import_node_module.createRequire)(__filename);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const importMetaUrl2 = getImportMetaUrl2();
|
|
322
|
+
if (importMetaUrl2) {
|
|
323
|
+
try {
|
|
324
|
+
return (0, import_node_module.createRequire)(importMetaUrl2);
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const fallbackBase = typeof __dirname === "string" ? __dirname : process.cwd();
|
|
329
|
+
const fallbackPath = import_node_path2.default.join(fallbackBase, "noop.js");
|
|
330
|
+
return (0, import_node_module.createRequire)(fallbackPath);
|
|
331
|
+
}
|
|
332
|
+
function getNativeBinding() {
|
|
333
|
+
if (cachedBinding !== void 0) {
|
|
334
|
+
return cachedBinding;
|
|
335
|
+
}
|
|
336
|
+
const requireFn = resolveRequire();
|
|
337
|
+
const envPath = process.env.CODEX_NATIVE_BINDING;
|
|
338
|
+
if (envPath && envPath.length > 0) {
|
|
339
|
+
process.env.NAPI_RS_NATIVE_LIBRARY_PATH = envPath;
|
|
340
|
+
}
|
|
341
|
+
const bindingEntryPath = resolveBindingEntryPath();
|
|
342
|
+
try {
|
|
343
|
+
const binding = requireFn(bindingEntryPath);
|
|
344
|
+
binding.ensureTokioRuntime?.();
|
|
345
|
+
cachedBinding = binding;
|
|
346
|
+
return cachedBinding;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn("Failed to load native NAPI binding:", error);
|
|
349
|
+
cachedBinding = null;
|
|
350
|
+
return cachedBinding;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function reverieListConversations(codexHomePath, limit, offset) {
|
|
354
|
+
const binding = getNativeBinding();
|
|
355
|
+
if (!binding?.reverieListConversations) throw new Error("Native binding not available or reverie functions not supported");
|
|
356
|
+
return binding.reverieListConversations(codexHomePath, limit, offset);
|
|
357
|
+
}
|
|
358
|
+
async function reverieSearchConversations(codexHomePath, query, limit) {
|
|
359
|
+
const binding = getNativeBinding();
|
|
360
|
+
if (!binding?.reverieSearchConversations) throw new Error("Native binding not available or reverie functions not supported");
|
|
361
|
+
return binding.reverieSearchConversations(codexHomePath, query, limit);
|
|
362
|
+
}
|
|
363
|
+
async function reverieSearchSemantic(codexHomePath, context, options) {
|
|
364
|
+
const binding = getNativeBinding();
|
|
365
|
+
if (!binding?.reverieSearchSemantic) throw new Error("Native binding not available or reverie functions not supported");
|
|
366
|
+
return binding.reverieSearchSemantic(codexHomePath, context, options);
|
|
367
|
+
}
|
|
368
|
+
async function reverieIndexSemantic(codexHomePath, options) {
|
|
369
|
+
const binding = getNativeBinding();
|
|
370
|
+
if (!binding?.reverieIndexSemantic) throw new Error("Native binding not available or reverie functions not supported");
|
|
371
|
+
return binding.reverieIndexSemantic(codexHomePath, options);
|
|
372
|
+
}
|
|
373
|
+
async function reverieGetConversationInsights(conversationPath, query) {
|
|
374
|
+
const binding = getNativeBinding();
|
|
375
|
+
if (!binding?.reverieGetConversationInsights) throw new Error("Native binding not available or reverie functions not supported");
|
|
376
|
+
return binding.reverieGetConversationInsights(conversationPath, query);
|
|
377
|
+
}
|
|
378
|
+
function encodeToToon(value) {
|
|
379
|
+
const binding = getNativeBinding();
|
|
380
|
+
if (!binding?.toonEncode) throw new Error("Native binding not available or toon encoder not supported");
|
|
381
|
+
return binding.toonEncode(value);
|
|
382
|
+
}
|
|
383
|
+
async function fastEmbedInit(options) {
|
|
384
|
+
const binding = getNativeBinding();
|
|
385
|
+
if (!binding?.fastEmbedInit) throw new Error("Native binding not available or FastEmbed functions not supported");
|
|
386
|
+
await binding.fastEmbedInit(options);
|
|
387
|
+
}
|
|
388
|
+
async function fastEmbedEmbed(request) {
|
|
389
|
+
const binding = getNativeBinding();
|
|
390
|
+
if (!binding?.fastEmbedEmbed) throw new Error("Native binding not available or FastEmbed functions not supported");
|
|
391
|
+
return binding.fastEmbedEmbed(request);
|
|
392
|
+
}
|
|
393
|
+
function tokenizerCount(text, options) {
|
|
394
|
+
const binding = getNativeBinding();
|
|
395
|
+
if (!binding?.tokenizerCount) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
396
|
+
return binding.tokenizerCount(text, options);
|
|
397
|
+
}
|
|
398
|
+
function tokenizerEncode(text, options) {
|
|
399
|
+
const binding = getNativeBinding();
|
|
400
|
+
if (!binding?.tokenizerEncode) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
401
|
+
return binding.tokenizerEncode(text, options);
|
|
402
|
+
}
|
|
403
|
+
function tokenizerDecode(tokens, options) {
|
|
404
|
+
const binding = getNativeBinding();
|
|
405
|
+
if (!binding?.tokenizerDecode) throw new Error("Native binding not available or tokenizer functions not supported");
|
|
406
|
+
return binding.tokenizerDecode(tokens, options);
|
|
407
|
+
}
|
|
408
|
+
async function collectRepoDiffSummary(options) {
|
|
409
|
+
const binding = getNativeBinding();
|
|
410
|
+
if (!binding?.collectRepoDiffSummary) {
|
|
411
|
+
throw new Error("Native binding not available or repo diff helpers not supported");
|
|
412
|
+
}
|
|
413
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
414
|
+
const nativeOptions = options && (options.maxFiles !== void 0 || options.diffContextLines !== void 0 || options.diffCharLimit !== void 0) ? {
|
|
415
|
+
maxFiles: options.maxFiles,
|
|
416
|
+
diffContextLines: options.diffContextLines,
|
|
417
|
+
diffCharLimit: options.diffCharLimit
|
|
418
|
+
} : void 0;
|
|
419
|
+
return binding.collectRepoDiffSummary(cwd, options?.baseBranchOverride, nativeOptions);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/tui.ts
|
|
423
|
+
function startTui(request) {
|
|
424
|
+
const binding = getNativeBinding();
|
|
425
|
+
if (!binding) {
|
|
426
|
+
throw new Error("Native binding is not available");
|
|
427
|
+
}
|
|
428
|
+
if (typeof binding.startTui === "function") {
|
|
429
|
+
const nativeSession = binding.startTui(request);
|
|
430
|
+
return wrapNativeSession(nativeSession);
|
|
431
|
+
}
|
|
432
|
+
if (typeof binding.runTui === "function") {
|
|
433
|
+
return createLegacySession(binding, request);
|
|
434
|
+
}
|
|
435
|
+
throw new Error("Native binding does not expose startTui or runTui");
|
|
436
|
+
}
|
|
437
|
+
async function runTui(request, options = {}) {
|
|
438
|
+
const session = startTui(request);
|
|
439
|
+
const { signal } = options;
|
|
440
|
+
let abortListener;
|
|
441
|
+
try {
|
|
442
|
+
if (signal) {
|
|
443
|
+
if (signal.aborted) {
|
|
444
|
+
session.shutdown();
|
|
445
|
+
} else {
|
|
446
|
+
abortListener = () => session.shutdown();
|
|
447
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return await session.wait();
|
|
451
|
+
} finally {
|
|
452
|
+
if (abortListener && signal) {
|
|
453
|
+
signal.removeEventListener("abort", abortListener);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function wrapNativeSession(nativeSession) {
|
|
458
|
+
return {
|
|
459
|
+
wait: () => nativeSession.wait(),
|
|
460
|
+
shutdown: () => nativeSession.shutdown(),
|
|
461
|
+
get closed() {
|
|
462
|
+
return nativeSession.closed;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function createLegacySession(binding, request) {
|
|
467
|
+
if (typeof binding.runTui !== "function") {
|
|
468
|
+
throw new Error("Native binding does not expose runTui");
|
|
469
|
+
}
|
|
470
|
+
let closed = false;
|
|
471
|
+
const promise = binding.runTui(request).then(
|
|
472
|
+
(result) => {
|
|
473
|
+
closed = true;
|
|
474
|
+
return result;
|
|
475
|
+
},
|
|
476
|
+
(error) => {
|
|
477
|
+
closed = true;
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
wait: () => promise,
|
|
483
|
+
shutdown() {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"Programmatic shutdown is not supported by this native binding build. Rebuild the SDK to enable startTui()."
|
|
486
|
+
);
|
|
487
|
+
},
|
|
488
|
+
get closed() {
|
|
489
|
+
return closed;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/lsp/bridge.ts
|
|
495
|
+
var path7 = __toESM(require("path"));
|
|
496
|
+
|
|
497
|
+
// src/lsp/manager.ts
|
|
498
|
+
var path5 = __toESM(require("path"));
|
|
499
|
+
|
|
500
|
+
// src/lsp/servers.ts
|
|
501
|
+
var fs3 = __toESM(require("fs"));
|
|
502
|
+
var path3 = __toESM(require("path"));
|
|
503
|
+
var MARKERS_NODE = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock"];
|
|
504
|
+
var MARKERS_PY = ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py", "setup.cfg", "poetry.lock"];
|
|
505
|
+
var MARKERS_RUST = ["Cargo.toml"];
|
|
506
|
+
var DEFAULT_SERVERS = [
|
|
507
|
+
{
|
|
508
|
+
id: "typescript",
|
|
509
|
+
displayName: "TypeScript Language Server",
|
|
510
|
+
command: ["typescript-language-server", "--stdio"],
|
|
511
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
512
|
+
workspace: { type: "markers", include: MARKERS_NODE }
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
id: "pyright",
|
|
516
|
+
displayName: "Pyright",
|
|
517
|
+
command: ["pyright-langserver", "--stdio"],
|
|
518
|
+
extensions: [".py", ".pyi"],
|
|
519
|
+
workspace: { type: "markers", include: MARKERS_PY }
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: "rust-analyzer",
|
|
523
|
+
displayName: "rust-analyzer",
|
|
524
|
+
command: ["rust-analyzer"],
|
|
525
|
+
extensions: [".rs"],
|
|
526
|
+
workspace: { type: "markers", include: MARKERS_RUST }
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
function findServerForFile(filePath) {
|
|
530
|
+
const lower = filePath.toLowerCase();
|
|
531
|
+
return DEFAULT_SERVERS.find((server) => server.extensions.some((ext) => lower.endsWith(ext)));
|
|
532
|
+
}
|
|
533
|
+
function resolveWorkspaceRoot(filePath, locator, fallbackDir) {
|
|
534
|
+
if (!locator) {
|
|
535
|
+
return fallbackDir;
|
|
536
|
+
}
|
|
537
|
+
if (locator.type === "fixed") {
|
|
538
|
+
return locator.path;
|
|
539
|
+
}
|
|
540
|
+
const include = locator.include ?? [];
|
|
541
|
+
const exclude = locator.exclude ?? [];
|
|
542
|
+
let current = fs3.statSync(filePath, { throwIfNoEntry: false })?.isDirectory() ? filePath : path3.dirname(filePath);
|
|
543
|
+
const root = path3.parse(current).root;
|
|
544
|
+
while (true) {
|
|
545
|
+
if (exclude.some((pattern) => fs3.existsSync(path3.join(current, pattern)))) {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
if (include.some((pattern) => fs3.existsSync(path3.join(current, pattern)))) {
|
|
549
|
+
return current;
|
|
550
|
+
}
|
|
551
|
+
if (current === root) {
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
const parent = path3.dirname(current);
|
|
555
|
+
if (parent === current) {
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
current = parent;
|
|
559
|
+
}
|
|
560
|
+
return fallbackDir;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/lsp/client.ts
|
|
564
|
+
var import_node_child_process = require("child_process");
|
|
565
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
566
|
+
var path4 = __toESM(require("path"));
|
|
567
|
+
var import_node_url2 = require("url");
|
|
568
|
+
var import_node_events = require("events");
|
|
569
|
+
var import_vscode_jsonrpc = require("vscode-jsonrpc");
|
|
570
|
+
var import_main = require("vscode-jsonrpc/lib/node/main.js");
|
|
571
|
+
var import_vscode_languageserver_types = require("vscode-languageserver-types");
|
|
572
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
573
|
+
var LspClient = class _LspClient {
|
|
574
|
+
constructor(config, root) {
|
|
575
|
+
this.config = config;
|
|
576
|
+
this.root = root;
|
|
577
|
+
}
|
|
578
|
+
connection = null;
|
|
579
|
+
process = null;
|
|
580
|
+
diagnostics = /* @__PURE__ */ new Map();
|
|
581
|
+
versions = /* @__PURE__ */ new Map();
|
|
582
|
+
emitter = new import_node_events.EventEmitter();
|
|
583
|
+
static async start(server, root) {
|
|
584
|
+
const client = new _LspClient(server, root);
|
|
585
|
+
await client.initialize();
|
|
586
|
+
return client;
|
|
587
|
+
}
|
|
588
|
+
async initialize() {
|
|
589
|
+
const [command, ...args] = this.config.command;
|
|
590
|
+
if (!command) {
|
|
591
|
+
throw new Error(`LSP server ${this.config.id} is missing a command executable`);
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
this.process = (0, import_node_child_process.spawn)(command, args, {
|
|
595
|
+
cwd: this.root,
|
|
596
|
+
env: { ...process.env, ...this.config.env },
|
|
597
|
+
stdio: "pipe"
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
throw new Error(`Failed to spawn ${this.config.displayName} (${command}): ${String(error)}`);
|
|
601
|
+
}
|
|
602
|
+
const child = this.process;
|
|
603
|
+
child.stderr.on("data", (_chunk) => {
|
|
604
|
+
});
|
|
605
|
+
const reader = new import_main.StreamMessageReader(child.stdout);
|
|
606
|
+
const writer = new import_main.StreamMessageWriter(child.stdin);
|
|
607
|
+
this.connection = (0, import_vscode_jsonrpc.createMessageConnection)(reader, writer);
|
|
608
|
+
this.connection.onNotification("textDocument/publishDiagnostics", (payload) => {
|
|
609
|
+
const fsPath = (0, import_node_url2.fileURLToPath)(payload.uri);
|
|
610
|
+
this.diagnostics.set(fsPath, payload.diagnostics);
|
|
611
|
+
this.emitter.emit(`diagnostics:${fsPath}`);
|
|
612
|
+
});
|
|
613
|
+
this.connection.onError((err) => {
|
|
614
|
+
console.warn(`[lsp:${this.config.id}] connection error`, err);
|
|
615
|
+
});
|
|
616
|
+
this.connection.listen();
|
|
617
|
+
await this.connection.sendRequest("initialize", {
|
|
618
|
+
rootUri: (0, import_node_url2.pathToFileURL)(this.root).href,
|
|
619
|
+
processId: process.pid,
|
|
620
|
+
initializationOptions: this.config.initializationOptions ?? {},
|
|
621
|
+
capabilities: {
|
|
622
|
+
textDocument: {
|
|
623
|
+
synchronization: {
|
|
624
|
+
didOpen: true,
|
|
625
|
+
didChange: true
|
|
626
|
+
},
|
|
627
|
+
publishDiagnostics: {
|
|
628
|
+
versionSupport: true
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
workspace: {
|
|
632
|
+
workspaceFolders: true
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
workspaceFolders: [
|
|
636
|
+
{
|
|
637
|
+
name: path4.basename(this.root),
|
|
638
|
+
uri: (0, import_node_url2.pathToFileURL)(this.root).href
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
});
|
|
642
|
+
await this.connection.sendNotification("initialized", {});
|
|
643
|
+
}
|
|
644
|
+
async openFile(filePath, waitForDiagnostics) {
|
|
645
|
+
if (!this.connection) return;
|
|
646
|
+
const absolute = path4.resolve(filePath);
|
|
647
|
+
const text = await fs4.readFile(absolute, "utf8");
|
|
648
|
+
const uri = (0, import_node_url2.pathToFileURL)(absolute).href;
|
|
649
|
+
const languageId = detectLanguageId(absolute);
|
|
650
|
+
const existingVersion = this.versions.get(absolute);
|
|
651
|
+
if (existingVersion === void 0) {
|
|
652
|
+
this.versions.set(absolute, 0);
|
|
653
|
+
await this.connection.sendNotification("textDocument/didOpen", {
|
|
654
|
+
textDocument: {
|
|
655
|
+
uri,
|
|
656
|
+
languageId,
|
|
657
|
+
version: 0,
|
|
658
|
+
text
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
const next = existingVersion + 1;
|
|
663
|
+
this.versions.set(absolute, next);
|
|
664
|
+
await this.connection.sendNotification("textDocument/didChange", {
|
|
665
|
+
textDocument: {
|
|
666
|
+
uri,
|
|
667
|
+
version: next
|
|
668
|
+
},
|
|
669
|
+
contentChanges: [{ text }]
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (waitForDiagnostics) {
|
|
673
|
+
await this.waitForDiagnostics(absolute);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
getDiagnostics(filePath) {
|
|
677
|
+
const absolute = path4.resolve(filePath);
|
|
678
|
+
return this.diagnostics.get(absolute) ?? [];
|
|
679
|
+
}
|
|
680
|
+
waitForDiagnostics(filePath, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
681
|
+
const absolute = path4.resolve(filePath);
|
|
682
|
+
return new Promise((resolve5) => {
|
|
683
|
+
const timer = setTimeout(resolve5, timeoutMs).unref();
|
|
684
|
+
this.emitter.once(`diagnostics:${absolute}`, () => {
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
resolve5();
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async shutdown() {
|
|
691
|
+
try {
|
|
692
|
+
await this.connection?.dispose();
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
if (this.process && !this.process.killed) {
|
|
696
|
+
this.process.kill();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
function detectLanguageId(filePath) {
|
|
701
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
702
|
+
switch (ext) {
|
|
703
|
+
case ".ts":
|
|
704
|
+
case ".mts":
|
|
705
|
+
case ".cts":
|
|
706
|
+
return "typescript";
|
|
707
|
+
case ".tsx":
|
|
708
|
+
return "typescriptreact";
|
|
709
|
+
case ".js":
|
|
710
|
+
case ".mjs":
|
|
711
|
+
case ".cjs":
|
|
712
|
+
return "javascript";
|
|
713
|
+
case ".jsx":
|
|
714
|
+
return "javascriptreact";
|
|
715
|
+
case ".py":
|
|
716
|
+
case ".pyi":
|
|
717
|
+
return "python";
|
|
718
|
+
case ".rs":
|
|
719
|
+
return "rust";
|
|
720
|
+
default:
|
|
721
|
+
return "plaintext";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function normalizeSeverity(severity) {
|
|
725
|
+
switch (severity) {
|
|
726
|
+
case import_vscode_languageserver_types.DiagnosticSeverity.Error:
|
|
727
|
+
return "error";
|
|
728
|
+
case import_vscode_languageserver_types.DiagnosticSeverity.Warning:
|
|
729
|
+
return "warning";
|
|
730
|
+
case import_vscode_languageserver_types.DiagnosticSeverity.Information:
|
|
731
|
+
return "info";
|
|
732
|
+
case import_vscode_languageserver_types.DiagnosticSeverity.Hint:
|
|
733
|
+
return "hint";
|
|
734
|
+
default:
|
|
735
|
+
return "error";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/lsp/manager.ts
|
|
740
|
+
var LspManager = class {
|
|
741
|
+
constructor(options) {
|
|
742
|
+
this.options = options;
|
|
743
|
+
}
|
|
744
|
+
clients = /* @__PURE__ */ new Map();
|
|
745
|
+
async collectDiagnostics(files) {
|
|
746
|
+
const unique = Array.from(new Set(files.map((file) => path5.resolve(file))));
|
|
747
|
+
const results = [];
|
|
748
|
+
for (const filePath of unique) {
|
|
749
|
+
const server = findServerForFile(filePath);
|
|
750
|
+
if (!server) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const root = resolveWorkspaceRoot(filePath, server.workspace, this.options.workingDirectory);
|
|
754
|
+
const client = await this.getClient(server, root);
|
|
755
|
+
if (!client) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
await client.openFile(filePath, this.options.waitForDiagnostics !== false);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
console.warn(`[lsp] failed to open ${filePath}:`, error);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const normalized = client.getDiagnostics(filePath).map((diag) => normalizeDiagnostic(diag)).filter((diag) => diag.message.trim().length > 0);
|
|
765
|
+
if (normalized.length > 0) {
|
|
766
|
+
results.push({ path: filePath, diagnostics: normalized });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return results;
|
|
770
|
+
}
|
|
771
|
+
async dispose() {
|
|
772
|
+
await Promise.all(
|
|
773
|
+
Array.from(this.clients.values()).map(async (promise) => {
|
|
774
|
+
const client = await promise;
|
|
775
|
+
await client?.shutdown();
|
|
776
|
+
})
|
|
777
|
+
);
|
|
778
|
+
this.clients.clear();
|
|
779
|
+
}
|
|
780
|
+
async getClient(server, root) {
|
|
781
|
+
const key = `${server.id}:${root}`;
|
|
782
|
+
let existing = this.clients.get(key);
|
|
783
|
+
if (!existing) {
|
|
784
|
+
existing = this.createClient(server, root);
|
|
785
|
+
this.clients.set(key, existing);
|
|
786
|
+
}
|
|
787
|
+
const client = await existing;
|
|
788
|
+
if (!client) {
|
|
789
|
+
this.clients.delete(key);
|
|
790
|
+
}
|
|
791
|
+
return client;
|
|
792
|
+
}
|
|
793
|
+
async createClient(server, root) {
|
|
794
|
+
try {
|
|
795
|
+
return await LspClient.start(server, root);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.warn(`[lsp] unable to start ${server.displayName}:`, error);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
function normalizeDiagnostic(diag) {
|
|
803
|
+
return {
|
|
804
|
+
message: diag.message ?? "",
|
|
805
|
+
severity: normalizeSeverity(diag.severity),
|
|
806
|
+
source: diag.source,
|
|
807
|
+
code: diag.code,
|
|
808
|
+
range: diag.range
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/lsp/format.ts
|
|
813
|
+
var path6 = __toESM(require("path"));
|
|
814
|
+
var MAX_DIAGNOSTICS_PER_FILE = 5;
|
|
815
|
+
function formatDiagnosticsForTool(diagnostics) {
|
|
816
|
+
return diagnostics.map(({ path: filePath, diagnostics: entries }) => {
|
|
817
|
+
const rel = filePath;
|
|
818
|
+
const lines = entries.slice(0, MAX_DIAGNOSTICS_PER_FILE).map((diag) => {
|
|
819
|
+
const { line, character } = diag.range.start;
|
|
820
|
+
const location = `${line + 1}:${character + 1}`;
|
|
821
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
822
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
823
|
+
});
|
|
824
|
+
const trimmed = entries.length > MAX_DIAGNOSTICS_PER_FILE ? " - \u2026" : "";
|
|
825
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
826
|
+
}).join("\n");
|
|
827
|
+
}
|
|
828
|
+
function formatDiagnosticsForBackgroundEvent(diagnostics, cwd) {
|
|
829
|
+
return diagnostics.map(({ path: filePath, diagnostics: entries }) => {
|
|
830
|
+
const rel = path6.relative(cwd, filePath) || filePath;
|
|
831
|
+
const lines = entries.slice(0, MAX_DIAGNOSTICS_PER_FILE).map((diag) => {
|
|
832
|
+
const { line, character } = diag.range.start;
|
|
833
|
+
const location = `${line + 1}:${character + 1}`;
|
|
834
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
835
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
836
|
+
});
|
|
837
|
+
const trimmed = entries.length > MAX_DIAGNOSTICS_PER_FILE ? " - \u2026" : "";
|
|
838
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
839
|
+
}).join("\n");
|
|
840
|
+
}
|
|
841
|
+
function filterBySeverity(diagnostics, minSeverity = "error") {
|
|
842
|
+
const severityOrder = {
|
|
843
|
+
error: 0,
|
|
844
|
+
warning: 1,
|
|
845
|
+
info: 2,
|
|
846
|
+
hint: 3
|
|
847
|
+
};
|
|
848
|
+
const threshold = severityOrder[minSeverity];
|
|
849
|
+
return diagnostics.map((file) => ({
|
|
850
|
+
...file,
|
|
851
|
+
diagnostics: file.diagnostics.filter(
|
|
852
|
+
(diag) => severityOrder[diag.severity] <= threshold
|
|
853
|
+
)
|
|
854
|
+
})).filter((file) => file.diagnostics.length > 0);
|
|
855
|
+
}
|
|
856
|
+
function summarizeDiagnostics(diagnostics) {
|
|
857
|
+
let errorCount = 0;
|
|
858
|
+
let warningCount = 0;
|
|
859
|
+
let infoCount = 0;
|
|
860
|
+
let hintCount = 0;
|
|
861
|
+
for (const file of diagnostics) {
|
|
862
|
+
for (const diag of file.diagnostics) {
|
|
863
|
+
switch (diag.severity) {
|
|
864
|
+
case "error":
|
|
865
|
+
errorCount++;
|
|
866
|
+
break;
|
|
867
|
+
case "warning":
|
|
868
|
+
warningCount++;
|
|
869
|
+
break;
|
|
870
|
+
case "info":
|
|
871
|
+
infoCount++;
|
|
872
|
+
break;
|
|
873
|
+
case "hint":
|
|
874
|
+
hintCount++;
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
fileCount: diagnostics.length,
|
|
881
|
+
errorCount,
|
|
882
|
+
warningCount,
|
|
883
|
+
infoCount,
|
|
884
|
+
hintCount,
|
|
885
|
+
totalCount: errorCount + warningCount + infoCount + hintCount
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function formatDiagnosticsWithSummary(diagnostics, cwd, options = {}) {
|
|
889
|
+
const filtered = options.minSeverity ? filterBySeverity(diagnostics, options.minSeverity) : diagnostics;
|
|
890
|
+
if (filtered.length === 0) {
|
|
891
|
+
return "No diagnostics found.";
|
|
892
|
+
}
|
|
893
|
+
const summary = summarizeDiagnostics(filtered);
|
|
894
|
+
const maxPerFile = options.maxPerFile ?? MAX_DIAGNOSTICS_PER_FILE;
|
|
895
|
+
const header = `LSP Diagnostics Summary: ${summary.errorCount} error${summary.errorCount !== 1 ? "s" : ""}, ${summary.warningCount} warning${summary.warningCount !== 1 ? "s" : ""} across ${summary.fileCount} file${summary.fileCount !== 1 ? "s" : ""}`;
|
|
896
|
+
const details = filtered.map(({ path: filePath, diagnostics: entries }) => {
|
|
897
|
+
const rel = path6.relative(cwd, filePath) || filePath;
|
|
898
|
+
const lines = entries.slice(0, maxPerFile).map((diag) => {
|
|
899
|
+
const { line, character } = diag.range.start;
|
|
900
|
+
const location = `${line + 1}:${character + 1}`;
|
|
901
|
+
const source = diag.source ? ` \xB7 ${diag.source}` : "";
|
|
902
|
+
return ` - [${diag.severity.toUpperCase()}] ${diag.message} (${location}${source})`;
|
|
903
|
+
});
|
|
904
|
+
const trimmed = entries.length > maxPerFile ? ` - \u2026 (${entries.length - maxPerFile} more)` : "";
|
|
905
|
+
return [`\u2022 ${rel}`, ...lines, trimmed].filter(Boolean).join("\n");
|
|
906
|
+
}).join("\n");
|
|
907
|
+
return `${header}
|
|
908
|
+
|
|
909
|
+
${details}`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/lsp/bridge.ts
|
|
913
|
+
var LspDiagnosticsBridge = class {
|
|
914
|
+
constructor(options) {
|
|
915
|
+
this.options = options;
|
|
916
|
+
this.manager = new LspManager(options);
|
|
917
|
+
}
|
|
918
|
+
manager;
|
|
919
|
+
attached = /* @__PURE__ */ new WeakSet();
|
|
920
|
+
attach(thread) {
|
|
921
|
+
if (this.attached.has(thread)) {
|
|
922
|
+
return () => {
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
this.attached.add(thread);
|
|
926
|
+
const unsubscribe = thread.onEvent((event) => {
|
|
927
|
+
if (event.type !== "item.completed") {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (event.item.type === "file_change") {
|
|
931
|
+
const targets = event.item.changes.filter((change) => change.kind !== "delete").map((change) => path7.resolve(this.options.workingDirectory, change.path));
|
|
932
|
+
if (targets.length === 0) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
void this.processDiagnostics(thread, targets);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (event.item.type === "mcp_tool_call") {
|
|
939
|
+
const targets = extractReadFileTargets(event.item, this.options.workingDirectory);
|
|
940
|
+
if (targets.length === 0) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
void this.processDiagnostics(thread, targets);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
return () => {
|
|
947
|
+
this.attached.delete(thread);
|
|
948
|
+
unsubscribe();
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async dispose() {
|
|
952
|
+
await this.manager.dispose();
|
|
953
|
+
}
|
|
954
|
+
async processDiagnostics(thread, files) {
|
|
955
|
+
try {
|
|
956
|
+
const diagnostics = await this.manager.collectDiagnostics(files);
|
|
957
|
+
if (diagnostics.length === 0) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const summary = formatDiagnosticsForBackgroundEvent(
|
|
961
|
+
diagnostics,
|
|
962
|
+
this.options.workingDirectory
|
|
963
|
+
);
|
|
964
|
+
console.log(`
|
|
965
|
+
\u{1F4DF} LSP diagnostics detected:
|
|
966
|
+
${summary}
|
|
967
|
+
`);
|
|
968
|
+
try {
|
|
969
|
+
await thread.sendBackgroundEvent(`LSP diagnostics detected:
|
|
970
|
+
${summary}`);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
} catch (error) {
|
|
974
|
+
console.warn("[lsp] failed to collect diagnostics", error);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
function extractReadFileTargets(item, cwd) {
|
|
979
|
+
if (item.type !== "mcp_tool_call") {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
const toolName = item.tool?.toLowerCase?.();
|
|
983
|
+
if (toolName !== "read_file" && toolName !== "read_file_v2") {
|
|
984
|
+
return [];
|
|
985
|
+
}
|
|
986
|
+
let args = item.arguments;
|
|
987
|
+
if (typeof args === "string") {
|
|
988
|
+
try {
|
|
989
|
+
args = JSON.parse(args);
|
|
990
|
+
} catch {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!args || typeof args !== "object") {
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
997
|
+
const filePath = args.file_path ?? args.path;
|
|
998
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
|
|
1002
|
+
return [resolved];
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/lsp/hooks.ts
|
|
1006
|
+
function attachLspDiagnostics(thread, options) {
|
|
1007
|
+
const bridge = new LspDiagnosticsBridge(options);
|
|
1008
|
+
const detach = bridge.attach(thread);
|
|
1009
|
+
return () => {
|
|
1010
|
+
detach();
|
|
1011
|
+
void bridge.dispose().catch((error) => {
|
|
1012
|
+
console.warn("Failed to dispose LSP bridge", error);
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/thread.ts
|
|
1018
|
+
var UNTRUSTED_DIRECTORY_ERROR = "Not inside a trusted directory and --skip-git-repo-check was not specified.";
|
|
1019
|
+
function findGitRoot(startDir) {
|
|
1020
|
+
let current = path8.resolve(startDir);
|
|
1021
|
+
while (true) {
|
|
1022
|
+
const gitPath = path8.join(current, ".git");
|
|
1023
|
+
if (fs5.existsSync(gitPath)) {
|
|
1024
|
+
try {
|
|
1025
|
+
const stats = fs5.statSync(gitPath);
|
|
1026
|
+
if (stats.isDirectory() || stats.isFile()) {
|
|
1027
|
+
return current;
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const parent = path8.dirname(current);
|
|
1033
|
+
if (parent === current) {
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
current = parent;
|
|
1037
|
+
}
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
function assertTrustedDirectory(workingDirectory) {
|
|
1041
|
+
const directory = workingDirectory ? path8.resolve(workingDirectory) : process.cwd();
|
|
1042
|
+
if (!findGitRoot(directory)) {
|
|
1043
|
+
throw new Error(UNTRUSTED_DIRECTORY_ERROR);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
var Thread = class _Thread {
|
|
1047
|
+
_exec;
|
|
1048
|
+
_options;
|
|
1049
|
+
_id;
|
|
1050
|
+
_threadOptions;
|
|
1051
|
+
_eventListeners = [];
|
|
1052
|
+
_approvalHandler = null;
|
|
1053
|
+
/** Returns the ID of the thread. Populated after the first turn starts. */
|
|
1054
|
+
get id() {
|
|
1055
|
+
return this._id;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Register an event listener for thread events.
|
|
1059
|
+
* @param listener Callback function that receives ThreadEvent objects
|
|
1060
|
+
* @returns Unsubscribe function to remove the listener
|
|
1061
|
+
*/
|
|
1062
|
+
onEvent(listener) {
|
|
1063
|
+
this._eventListeners.push(listener);
|
|
1064
|
+
return () => {
|
|
1065
|
+
const index = this._eventListeners.indexOf(listener);
|
|
1066
|
+
if (index !== -1) {
|
|
1067
|
+
this._eventListeners.splice(index, 1);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Remove an event listener.
|
|
1073
|
+
* @param listener The listener function to remove
|
|
1074
|
+
*/
|
|
1075
|
+
offEvent(listener) {
|
|
1076
|
+
const index = this._eventListeners.indexOf(listener);
|
|
1077
|
+
if (index !== -1) {
|
|
1078
|
+
this._eventListeners.splice(index, 1);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Register a callback to handle approval requests from the agent.
|
|
1083
|
+
* The handler should return true to approve the action, false to deny it.
|
|
1084
|
+
*
|
|
1085
|
+
* @param handler Callback function that receives ApprovalRequest and returns approval decision
|
|
1086
|
+
* @example
|
|
1087
|
+
* ```typescript
|
|
1088
|
+
* thread.onApprovalRequest(async (request) => {
|
|
1089
|
+
* console.log(`Approval requested for ${request.type}`);
|
|
1090
|
+
* return true; // Auto-approve
|
|
1091
|
+
* });
|
|
1092
|
+
* ```
|
|
1093
|
+
*/
|
|
1094
|
+
onApprovalRequest(handler) {
|
|
1095
|
+
this._approvalHandler = handler;
|
|
1096
|
+
const binding = getNativeBinding();
|
|
1097
|
+
if (binding && typeof binding.registerApprovalCallback === "function") {
|
|
1098
|
+
binding.registerApprovalCallback(handler);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Emit a background notification while the agent is running the current turn.
|
|
1103
|
+
* The message is surfaced to event subscribers but does not modify the user input queue.
|
|
1104
|
+
*
|
|
1105
|
+
* @throws Error if the thread has not been started yet.
|
|
1106
|
+
*/
|
|
1107
|
+
async sendBackgroundEvent(message) {
|
|
1108
|
+
const trimmed = message?.toString();
|
|
1109
|
+
if (!trimmed || trimmed.trim().length === 0) {
|
|
1110
|
+
throw new Error("Background event message must be a non-empty string");
|
|
1111
|
+
}
|
|
1112
|
+
if (!this._id) {
|
|
1113
|
+
throw new Error("Cannot emit a background event before the thread has started");
|
|
1114
|
+
}
|
|
1115
|
+
const binding = getNativeBinding();
|
|
1116
|
+
if (!binding || typeof binding.emitBackgroundEvent !== "function") {
|
|
1117
|
+
throw new Error("emitBackgroundEvent is not available in this build");
|
|
1118
|
+
}
|
|
1119
|
+
await binding.emitBackgroundEvent({ threadId: this._id, message: trimmed });
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Programmatically update the agent's plan/todo list.
|
|
1123
|
+
* The plan will be applied at the start of the next turn.
|
|
1124
|
+
*
|
|
1125
|
+
* @param args The plan update arguments
|
|
1126
|
+
* @throws Error if no thread ID is available
|
|
1127
|
+
*/
|
|
1128
|
+
updatePlan(args) {
|
|
1129
|
+
if (!this._id) {
|
|
1130
|
+
throw new Error("Cannot update plan: no active thread");
|
|
1131
|
+
}
|
|
1132
|
+
const binding = getNativeBinding();
|
|
1133
|
+
if (!binding || typeof binding.emitPlanUpdate !== "function") {
|
|
1134
|
+
throw new Error("emitPlanUpdate is not available in this build");
|
|
1135
|
+
}
|
|
1136
|
+
binding.emitPlanUpdate({
|
|
1137
|
+
threadId: this._id,
|
|
1138
|
+
explanation: args.explanation,
|
|
1139
|
+
plan: args.plan
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Modify the agent's plan/todo list with granular operations.
|
|
1144
|
+
* Changes will be applied at the start of the next turn.
|
|
1145
|
+
*
|
|
1146
|
+
* @param operations Array of operations to perform on the plan
|
|
1147
|
+
* @throws Error if no thread ID is available
|
|
1148
|
+
*/
|
|
1149
|
+
modifyPlan(operations) {
|
|
1150
|
+
if (!this._id) {
|
|
1151
|
+
throw new Error("Cannot modify plan: no active thread");
|
|
1152
|
+
}
|
|
1153
|
+
const binding = getNativeBinding();
|
|
1154
|
+
if (!binding || typeof binding.modifyPlan !== "function") {
|
|
1155
|
+
throw new Error("modifyPlan is not available in this build");
|
|
1156
|
+
}
|
|
1157
|
+
binding.modifyPlan({
|
|
1158
|
+
threadId: this._id,
|
|
1159
|
+
operations
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Add a new todo item to the agent's plan.
|
|
1164
|
+
*
|
|
1165
|
+
* @param step The todo step description
|
|
1166
|
+
* @param status The initial status (defaults to "pending")
|
|
1167
|
+
*/
|
|
1168
|
+
addTodo(step, status = "pending") {
|
|
1169
|
+
this.modifyPlan([{ type: "add", item: { step, status } }]);
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Update an existing todo item.
|
|
1173
|
+
*
|
|
1174
|
+
* @param index The index of the todo item to update
|
|
1175
|
+
* @param updates The updates to apply
|
|
1176
|
+
*/
|
|
1177
|
+
updateTodo(index, updates) {
|
|
1178
|
+
this.modifyPlan([{ type: "update", index, updates }]);
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Remove a todo item from the plan.
|
|
1182
|
+
*
|
|
1183
|
+
* @param index The index of the todo item to remove
|
|
1184
|
+
*/
|
|
1185
|
+
removeTodo(index) {
|
|
1186
|
+
this.modifyPlan([{ type: "remove", index }]);
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Reorder the todo items in the plan.
|
|
1190
|
+
*
|
|
1191
|
+
* @param newOrder Array of indices representing the new order
|
|
1192
|
+
*/
|
|
1193
|
+
reorderTodos(newOrder) {
|
|
1194
|
+
this.modifyPlan([{ type: "reorder", newOrder }]);
|
|
1195
|
+
}
|
|
1196
|
+
/** Compacts the conversation history for this thread using Codex's builtin compaction. */
|
|
1197
|
+
async compact() {
|
|
1198
|
+
const skipGitRepoCheck = this._threadOptions?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1199
|
+
if (!skipGitRepoCheck) {
|
|
1200
|
+
assertTrustedDirectory(this._threadOptions?.workingDirectory);
|
|
1201
|
+
}
|
|
1202
|
+
const events = await this._exec.compact({
|
|
1203
|
+
input: "compact",
|
|
1204
|
+
threadId: this._id,
|
|
1205
|
+
baseUrl: this._options.baseUrl,
|
|
1206
|
+
apiKey: this._options.apiKey,
|
|
1207
|
+
model: this._threadOptions?.model ?? this._options.defaultModel,
|
|
1208
|
+
sandboxMode: this._threadOptions?.sandboxMode,
|
|
1209
|
+
approvalMode: this._threadOptions?.approvalMode,
|
|
1210
|
+
workspaceWriteOptions: this._threadOptions?.workspaceWriteOptions,
|
|
1211
|
+
workingDirectory: this._threadOptions?.workingDirectory,
|
|
1212
|
+
skipGitRepoCheck,
|
|
1213
|
+
modelProvider: this._options.modelProvider
|
|
1214
|
+
});
|
|
1215
|
+
if (!Array.isArray(events)) {
|
|
1216
|
+
throw new Error("Compact did not return event list");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Fork this thread at the specified user message, returning a new thread that starts
|
|
1221
|
+
* from the conversation history prior to that message.
|
|
1222
|
+
*
|
|
1223
|
+
* @param options Fork configuration including which user message to branch before and optional thread overrides.
|
|
1224
|
+
*/
|
|
1225
|
+
async fork(options) {
|
|
1226
|
+
if (!this._id) {
|
|
1227
|
+
throw new Error("Cannot fork: no active thread");
|
|
1228
|
+
}
|
|
1229
|
+
const nthUserMessage = options?.nthUserMessage;
|
|
1230
|
+
if (typeof nthUserMessage !== "number" || !Number.isInteger(nthUserMessage) || nthUserMessage < 0) {
|
|
1231
|
+
throw new Error("nthUserMessage must be a non-negative integer");
|
|
1232
|
+
}
|
|
1233
|
+
const overrides = options.threadOptions ?? {};
|
|
1234
|
+
const nextThreadOptions = {
|
|
1235
|
+
...this._threadOptions,
|
|
1236
|
+
...overrides
|
|
1237
|
+
};
|
|
1238
|
+
const skipGitRepoCheck = nextThreadOptions.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1239
|
+
nextThreadOptions.skipGitRepoCheck = skipGitRepoCheck;
|
|
1240
|
+
if (!skipGitRepoCheck) {
|
|
1241
|
+
assertTrustedDirectory(nextThreadOptions.workingDirectory);
|
|
1242
|
+
}
|
|
1243
|
+
const forkArgs = {
|
|
1244
|
+
threadId: this._id,
|
|
1245
|
+
nthUserMessage,
|
|
1246
|
+
baseUrl: this._options.baseUrl,
|
|
1247
|
+
apiKey: this._options.apiKey,
|
|
1248
|
+
model: nextThreadOptions.model ?? this._options.defaultModel,
|
|
1249
|
+
oss: nextThreadOptions.oss,
|
|
1250
|
+
sandboxMode: nextThreadOptions.sandboxMode,
|
|
1251
|
+
approvalMode: nextThreadOptions.approvalMode,
|
|
1252
|
+
workspaceWriteOptions: nextThreadOptions.workspaceWriteOptions,
|
|
1253
|
+
workingDirectory: nextThreadOptions.workingDirectory,
|
|
1254
|
+
skipGitRepoCheck,
|
|
1255
|
+
fullAuto: nextThreadOptions.fullAuto,
|
|
1256
|
+
modelProvider: this._options.modelProvider
|
|
1257
|
+
};
|
|
1258
|
+
const result = await this._exec.fork(forkArgs);
|
|
1259
|
+
return new _Thread(
|
|
1260
|
+
this._exec,
|
|
1261
|
+
this._options,
|
|
1262
|
+
nextThreadOptions,
|
|
1263
|
+
result.threadId
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
/* @internal */
|
|
1267
|
+
constructor(exec, options, threadOptions, id = null) {
|
|
1268
|
+
this._exec = exec;
|
|
1269
|
+
this._options = options;
|
|
1270
|
+
this._id = id;
|
|
1271
|
+
this._threadOptions = threadOptions;
|
|
1272
|
+
}
|
|
1273
|
+
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
|
1274
|
+
async runStreamed(input, turnOptions = {}) {
|
|
1275
|
+
return { events: this.runStreamedInternal(input, turnOptions, false) };
|
|
1276
|
+
}
|
|
1277
|
+
async *runStreamedInternal(input, turnOptions = {}, emitRawEvents = true) {
|
|
1278
|
+
const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema);
|
|
1279
|
+
const needsSchemaFile = this._exec.requiresOutputSchemaFile();
|
|
1280
|
+
const schemaFile = needsSchemaFile ? await createOutputSchemaFile(normalizedSchema) : { schemaPath: void 0, cleanup: async () => {
|
|
1281
|
+
} };
|
|
1282
|
+
const options = this._threadOptions;
|
|
1283
|
+
const { prompt, images } = normalizeInput(input);
|
|
1284
|
+
const skipGitRepoCheck = options?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1285
|
+
if (!skipGitRepoCheck) {
|
|
1286
|
+
assertTrustedDirectory(options?.workingDirectory);
|
|
1287
|
+
}
|
|
1288
|
+
const generator = this._exec.run({
|
|
1289
|
+
input: prompt,
|
|
1290
|
+
baseUrl: this._options.baseUrl,
|
|
1291
|
+
apiKey: this._options.apiKey,
|
|
1292
|
+
threadId: this._id,
|
|
1293
|
+
images,
|
|
1294
|
+
model: options?.model,
|
|
1295
|
+
oss: turnOptions?.oss ?? options?.oss,
|
|
1296
|
+
sandboxMode: options?.sandboxMode,
|
|
1297
|
+
approvalMode: options?.approvalMode,
|
|
1298
|
+
workspaceWriteOptions: options?.workspaceWriteOptions,
|
|
1299
|
+
workingDirectory: options?.workingDirectory,
|
|
1300
|
+
skipGitRepoCheck,
|
|
1301
|
+
outputSchemaFile: schemaFile.schemaPath,
|
|
1302
|
+
outputSchema: normalizedSchema,
|
|
1303
|
+
fullAuto: options?.fullAuto
|
|
1304
|
+
});
|
|
1305
|
+
try {
|
|
1306
|
+
for await (const item of generator) {
|
|
1307
|
+
let parsed;
|
|
1308
|
+
try {
|
|
1309
|
+
parsed = JSON.parse(item);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
throw new Error(`Failed to parse item: ${item}. Parse error: ${error}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (parsed === null) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (emitRawEvents) {
|
|
1317
|
+
yield { type: "raw_event", raw: parsed };
|
|
1318
|
+
}
|
|
1319
|
+
const threadEvent = convertRustEventToThreadEvent(parsed);
|
|
1320
|
+
if (threadEvent.type === "thread.started") {
|
|
1321
|
+
this._id = threadEvent.thread_id;
|
|
1322
|
+
}
|
|
1323
|
+
for (const listener of this._eventListeners) {
|
|
1324
|
+
try {
|
|
1325
|
+
listener(threadEvent);
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
console.warn("Thread event listener threw error:", error);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
yield threadEvent;
|
|
1331
|
+
}
|
|
1332
|
+
} finally {
|
|
1333
|
+
await schemaFile.cleanup();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/** Provides the input to the agent and returns the completed turn. */
|
|
1337
|
+
async run(input, turnOptions = {}) {
|
|
1338
|
+
const generator = this.runStreamedInternal(input, turnOptions, true);
|
|
1339
|
+
const items = [];
|
|
1340
|
+
let finalResponse = "";
|
|
1341
|
+
let usage = null;
|
|
1342
|
+
let turnFailure = null;
|
|
1343
|
+
for await (const event of generator) {
|
|
1344
|
+
if (event.type === "item.completed") {
|
|
1345
|
+
if (event.item.type === "agent_message") {
|
|
1346
|
+
finalResponse = event.item.text;
|
|
1347
|
+
}
|
|
1348
|
+
items.push(event.item);
|
|
1349
|
+
} else if (event.type === "turn.completed") {
|
|
1350
|
+
usage = event.usage;
|
|
1351
|
+
} else if (event.type === "turn.failed") {
|
|
1352
|
+
turnFailure = event.error;
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (turnFailure) {
|
|
1357
|
+
throw new Error(turnFailure.message);
|
|
1358
|
+
}
|
|
1359
|
+
return { items, finalResponse, usage };
|
|
1360
|
+
}
|
|
1361
|
+
buildTuiRequest(overrides = {}) {
|
|
1362
|
+
const skipGitRepoCheck = this._threadOptions?.skipGitRepoCheck ?? (typeof process !== "undefined" && process.env && process.env.CODEX_TEST_SKIP_GIT_REPO_CHECK === "1");
|
|
1363
|
+
if (!skipGitRepoCheck) {
|
|
1364
|
+
assertTrustedDirectory(this._threadOptions?.workingDirectory);
|
|
1365
|
+
}
|
|
1366
|
+
const request = { ...overrides };
|
|
1367
|
+
const assignIfUndefined = (key, value) => {
|
|
1368
|
+
if (request[key] === void 0 && value !== void 0) {
|
|
1369
|
+
request[key] = value;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
assignIfUndefined("model", this._threadOptions?.model ?? this._options.defaultModel);
|
|
1373
|
+
assignIfUndefined("oss", this._threadOptions?.oss);
|
|
1374
|
+
assignIfUndefined("sandboxMode", this._threadOptions?.sandboxMode);
|
|
1375
|
+
assignIfUndefined("approvalMode", this._threadOptions?.approvalMode);
|
|
1376
|
+
assignIfUndefined("fullAuto", this._threadOptions?.fullAuto);
|
|
1377
|
+
assignIfUndefined("workingDirectory", this._threadOptions?.workingDirectory);
|
|
1378
|
+
assignIfUndefined("baseUrl", this._options.baseUrl);
|
|
1379
|
+
assignIfUndefined("apiKey", this._options.apiKey);
|
|
1380
|
+
if (request.resumeSessionId === void 0 && request.resumePicker !== true && request.resumeLast !== true && this._id) {
|
|
1381
|
+
request.resumeSessionId = this._id;
|
|
1382
|
+
}
|
|
1383
|
+
return request;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Launches the interactive Codex TUI (Terminal User Interface) for this thread and returns a session handle.
|
|
1387
|
+
*
|
|
1388
|
+
* The handle allows advanced workflows where the TUI can be started and stopped programmatically,
|
|
1389
|
+
* while preserving the underlying conversation state.
|
|
1390
|
+
*/
|
|
1391
|
+
launchTui(overrides = {}) {
|
|
1392
|
+
const request = this.buildTuiRequest(overrides);
|
|
1393
|
+
const detachLsp = this.attachDefaultLspBridge(request);
|
|
1394
|
+
const session = startTui(request);
|
|
1395
|
+
return this.wrapTuiSession(session, detachLsp);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Launches the interactive Codex TUI (Terminal User Interface) for this thread.
|
|
1399
|
+
*
|
|
1400
|
+
* This method enables seamless transition from programmatic agent interaction to
|
|
1401
|
+
* interactive terminal chat within the same session. The TUI takes over the terminal
|
|
1402
|
+
* and allows you to continue the conversation interactively.
|
|
1403
|
+
*
|
|
1404
|
+
* @param overrides - Optional configuration to override thread defaults. Supports all TUI options
|
|
1405
|
+
* including prompt, sandbox mode, approval mode, and resume options.
|
|
1406
|
+
* @param options - Optional run options including an AbortSignal to request shutdown.
|
|
1407
|
+
* @returns A Promise that resolves to TUI exit information including:
|
|
1408
|
+
* - tokenUsage: Token consumption statistics
|
|
1409
|
+
* - conversationId: Session ID for resuming later
|
|
1410
|
+
* - updateAction: Optional suggested update command
|
|
1411
|
+
* @throws {Error} If not in a trusted git repository (unless skipGitRepoCheck is set)
|
|
1412
|
+
* @throws {Error} If the terminal is not interactive (TTY required)
|
|
1413
|
+
*/
|
|
1414
|
+
async tui(overrides = {}, options = {}) {
|
|
1415
|
+
const request = this.buildTuiRequest(overrides);
|
|
1416
|
+
const detachLsp = this.attachDefaultLspBridge(request);
|
|
1417
|
+
try {
|
|
1418
|
+
return await runTui(request, options);
|
|
1419
|
+
} finally {
|
|
1420
|
+
detachLsp();
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
wrapTuiSession(session, cleanup) {
|
|
1424
|
+
let released = false;
|
|
1425
|
+
const release = () => {
|
|
1426
|
+
if (released) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
released = true;
|
|
1430
|
+
cleanup();
|
|
1431
|
+
};
|
|
1432
|
+
return {
|
|
1433
|
+
wait: async () => {
|
|
1434
|
+
try {
|
|
1435
|
+
return await session.wait();
|
|
1436
|
+
} finally {
|
|
1437
|
+
release();
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
shutdown: () => {
|
|
1441
|
+
release();
|
|
1442
|
+
session.shutdown();
|
|
1443
|
+
},
|
|
1444
|
+
get closed() {
|
|
1445
|
+
return session.closed;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
attachDefaultLspBridge(request) {
|
|
1450
|
+
const workingDirectory = request.workingDirectory ?? this._threadOptions?.workingDirectory ?? (typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".");
|
|
1451
|
+
return attachLspDiagnostics(this, {
|
|
1452
|
+
workingDirectory,
|
|
1453
|
+
waitForDiagnostics: true
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
function normalizeInput(input) {
|
|
1458
|
+
if (typeof input === "string") {
|
|
1459
|
+
return { prompt: input, images: [] };
|
|
1460
|
+
}
|
|
1461
|
+
const promptParts = [];
|
|
1462
|
+
const images = [];
|
|
1463
|
+
for (const item of input) {
|
|
1464
|
+
if (item.type === "text") {
|
|
1465
|
+
promptParts.push(item.text);
|
|
1466
|
+
} else if (item.type === "local_image") {
|
|
1467
|
+
images.push(item.path);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return { prompt: promptParts.join("\n\n"), images };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/exec.ts
|
|
1474
|
+
var CodexExec = class {
|
|
1475
|
+
native;
|
|
1476
|
+
constructor() {
|
|
1477
|
+
const nativeBinding = getNativeBinding();
|
|
1478
|
+
if (!nativeBinding) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
"Native NAPI binding not available. Make sure @openai/codex-native is properly installed and built."
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
this.native = nativeBinding;
|
|
1484
|
+
}
|
|
1485
|
+
requiresOutputSchemaFile() {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
async *run(args) {
|
|
1489
|
+
const binding = this.native;
|
|
1490
|
+
const queue = new AsyncQueue();
|
|
1491
|
+
validateModel(args.model, args.oss === true);
|
|
1492
|
+
const request = {
|
|
1493
|
+
prompt: args.input,
|
|
1494
|
+
threadId: args.threadId ?? void 0,
|
|
1495
|
+
images: args.images && args.images.length > 0 ? args.images : void 0,
|
|
1496
|
+
model: args.model,
|
|
1497
|
+
oss: args.oss,
|
|
1498
|
+
approvalMode: args.approvalMode,
|
|
1499
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1500
|
+
sandboxMode: args.sandboxMode,
|
|
1501
|
+
workingDirectory: args.workingDirectory,
|
|
1502
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1503
|
+
outputSchema: args.outputSchema,
|
|
1504
|
+
baseUrl: args.baseUrl,
|
|
1505
|
+
apiKey: args.apiKey,
|
|
1506
|
+
modelProvider: args.modelProvider,
|
|
1507
|
+
fullAuto: args.fullAuto,
|
|
1508
|
+
reviewMode: args.review ? true : void 0,
|
|
1509
|
+
reviewHint: args.review?.userFacingHint
|
|
1510
|
+
};
|
|
1511
|
+
let runPromise = Promise.resolve();
|
|
1512
|
+
try {
|
|
1513
|
+
runPromise = binding.runThreadStream(request, (err, eventJson) => {
|
|
1514
|
+
if (err) {
|
|
1515
|
+
queue.fail(err);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
queue.push(eventJson ?? "null");
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
queue.fail(error);
|
|
1522
|
+
}
|
|
1523
|
+
}).then(
|
|
1524
|
+
() => {
|
|
1525
|
+
queue.end();
|
|
1526
|
+
},
|
|
1527
|
+
(error) => {
|
|
1528
|
+
queue.fail(error);
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
queue.fail(error);
|
|
1533
|
+
throw error;
|
|
1534
|
+
}
|
|
1535
|
+
let loopError;
|
|
1536
|
+
try {
|
|
1537
|
+
for await (const value of queue) {
|
|
1538
|
+
yield value;
|
|
1539
|
+
}
|
|
1540
|
+
await runPromise;
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
loopError = error;
|
|
1543
|
+
throw error;
|
|
1544
|
+
} finally {
|
|
1545
|
+
queue.end();
|
|
1546
|
+
if (loopError) {
|
|
1547
|
+
await runPromise.catch(() => {
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async compact(args) {
|
|
1553
|
+
validateModel(args.model, args.oss === true);
|
|
1554
|
+
const request = {
|
|
1555
|
+
prompt: args.input,
|
|
1556
|
+
threadId: args.threadId ?? void 0,
|
|
1557
|
+
images: args.images && args.images.length > 0 ? args.images : void 0,
|
|
1558
|
+
model: args.model,
|
|
1559
|
+
modelProvider: args.modelProvider,
|
|
1560
|
+
oss: args.oss,
|
|
1561
|
+
sandboxMode: args.sandboxMode,
|
|
1562
|
+
approvalMode: args.approvalMode,
|
|
1563
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1564
|
+
workingDirectory: args.workingDirectory,
|
|
1565
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1566
|
+
outputSchema: args.outputSchema,
|
|
1567
|
+
baseUrl: args.baseUrl,
|
|
1568
|
+
apiKey: args.apiKey,
|
|
1569
|
+
fullAuto: args.fullAuto,
|
|
1570
|
+
reviewMode: args.review ? true : void 0,
|
|
1571
|
+
reviewHint: args.review?.userFacingHint
|
|
1572
|
+
};
|
|
1573
|
+
return this.native.compactThread(request);
|
|
1574
|
+
}
|
|
1575
|
+
async fork(args) {
|
|
1576
|
+
if (!args.threadId) {
|
|
1577
|
+
throw new Error("threadId is required to fork a conversation");
|
|
1578
|
+
}
|
|
1579
|
+
const request = {
|
|
1580
|
+
threadId: args.threadId,
|
|
1581
|
+
nthUserMessage: args.nthUserMessage,
|
|
1582
|
+
model: args.model,
|
|
1583
|
+
oss: args.oss,
|
|
1584
|
+
sandboxMode: args.sandboxMode,
|
|
1585
|
+
approvalMode: args.approvalMode,
|
|
1586
|
+
workspaceWriteOptions: args.workspaceWriteOptions,
|
|
1587
|
+
workingDirectory: args.workingDirectory,
|
|
1588
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
1589
|
+
baseUrl: args.baseUrl,
|
|
1590
|
+
apiKey: args.apiKey,
|
|
1591
|
+
modelProvider: args.modelProvider,
|
|
1592
|
+
linuxSandboxPath: args.linuxSandboxPath,
|
|
1593
|
+
fullAuto: args.fullAuto
|
|
1594
|
+
};
|
|
1595
|
+
return this.native.forkThread(request);
|
|
1596
|
+
}
|
|
1597
|
+
async listConversations(request) {
|
|
1598
|
+
return this.native.listConversations(request);
|
|
1599
|
+
}
|
|
1600
|
+
async deleteConversation(request) {
|
|
1601
|
+
return this.native.deleteConversation(request);
|
|
1602
|
+
}
|
|
1603
|
+
async resumeConversationFromRollout(request) {
|
|
1604
|
+
return this.native.resumeConversationFromRollout(request);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
var AsyncQueue = class {
|
|
1608
|
+
buffer = [];
|
|
1609
|
+
waiters = [];
|
|
1610
|
+
ended = false;
|
|
1611
|
+
error;
|
|
1612
|
+
push(value) {
|
|
1613
|
+
if (this.ended) return;
|
|
1614
|
+
if (this.waiters.length > 0) {
|
|
1615
|
+
const waiter = this.waiters.shift();
|
|
1616
|
+
waiter.resolve({ value, done: false });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
this.buffer.push(value);
|
|
1620
|
+
}
|
|
1621
|
+
end() {
|
|
1622
|
+
if (this.ended) return;
|
|
1623
|
+
this.ended = true;
|
|
1624
|
+
const waiters = this.waiters;
|
|
1625
|
+
this.waiters = [];
|
|
1626
|
+
for (const waiter of waiters) {
|
|
1627
|
+
waiter.resolve({ value: void 0, done: true });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
fail(error) {
|
|
1631
|
+
if (this.ended) return;
|
|
1632
|
+
this.error = error;
|
|
1633
|
+
this.ended = true;
|
|
1634
|
+
const waiters = this.waiters;
|
|
1635
|
+
this.waiters = [];
|
|
1636
|
+
for (const waiter of waiters) {
|
|
1637
|
+
waiter.reject(error);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async next() {
|
|
1641
|
+
if (this.buffer.length > 0) {
|
|
1642
|
+
const value = this.buffer.shift();
|
|
1643
|
+
return { value, done: false };
|
|
1644
|
+
}
|
|
1645
|
+
if (this.error) {
|
|
1646
|
+
return Promise.reject(this.error);
|
|
1647
|
+
}
|
|
1648
|
+
if (this.ended) {
|
|
1649
|
+
return { value: void 0, done: true };
|
|
1650
|
+
}
|
|
1651
|
+
return new Promise((resolve5, reject) => {
|
|
1652
|
+
this.waiters.push({ resolve: resolve5, reject });
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
[Symbol.asyncIterator]() {
|
|
1656
|
+
return this;
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
function validateModel(model, oss) {
|
|
1660
|
+
if (!model) return;
|
|
1661
|
+
const trimmed = String(model).trim();
|
|
1662
|
+
if (oss) {
|
|
1663
|
+
if (!trimmed.startsWith("gpt-oss:")) {
|
|
1664
|
+
throw new Error(
|
|
1665
|
+
`Invalid model "${trimmed}" for OSS mode. Use models prefixed with "gpt-oss:", e.g. "gpt-oss:20b".`
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1671
|
+
// GPT models
|
|
1672
|
+
"gpt-5",
|
|
1673
|
+
"gpt-5-codex",
|
|
1674
|
+
"gpt-5-codex-mini",
|
|
1675
|
+
"gpt-5.1",
|
|
1676
|
+
"gpt-5.1-codex",
|
|
1677
|
+
"gpt-5.1-codex-mini",
|
|
1678
|
+
// Claude models
|
|
1679
|
+
"claude-sonnet-4-5-20250929",
|
|
1680
|
+
"claude-sonnet-4-20250514",
|
|
1681
|
+
"claude-opus-4-20250514"
|
|
1682
|
+
]);
|
|
1683
|
+
if (!allowed.has(trimmed) && !trimmed.startsWith("claude-") && !trimmed.startsWith("gpt-")) {
|
|
1684
|
+
throw new Error(
|
|
1685
|
+
`Invalid model "${trimmed}". Supported models: ${Array.from(allowed).map((m) => `"${m}"`).join(", ")}, or any model starting with "claude-" or "gpt-".`
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/reviewOptions.ts
|
|
1691
|
+
function buildReviewPrompt(target) {
|
|
1692
|
+
switch (target.type) {
|
|
1693
|
+
case "current_changes":
|
|
1694
|
+
return {
|
|
1695
|
+
prompt: "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.",
|
|
1696
|
+
hint: "current changes"
|
|
1697
|
+
};
|
|
1698
|
+
case "branch": {
|
|
1699
|
+
const branch = target.baseBranch;
|
|
1700
|
+
const prompt = `Review the code changes against the base branch '${branch}'. Start by finding the merge diff between the current branch and ${branch}'s upstream e.g. (\`git merge-base HEAD "$(git rev-parse --abbrev-ref "${branch}@{upstream}")"\`), then run \`git diff\` against that SHA to see what changes we would merge into the ${branch} branch. Provide prioritized, actionable findings.`;
|
|
1701
|
+
return {
|
|
1702
|
+
prompt,
|
|
1703
|
+
hint: `changes against '${branch}'`
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
case "commit": {
|
|
1707
|
+
const shortSha = target.sha.slice(0, 7);
|
|
1708
|
+
const subject = target.subject ?? target.sha;
|
|
1709
|
+
return {
|
|
1710
|
+
prompt: `Review the code changes introduced by commit ${target.sha} ("${subject}"). Provide prioritized, actionable findings.`,
|
|
1711
|
+
hint: `commit ${shortSha}`
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
case "custom": {
|
|
1715
|
+
const hint = target.hint ?? "custom review";
|
|
1716
|
+
return {
|
|
1717
|
+
prompt: target.prompt,
|
|
1718
|
+
hint
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
default: {
|
|
1722
|
+
const exhaustive = target;
|
|
1723
|
+
throw new Error(`Unsupported review target: ${String(exhaustive)}`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/codex.ts
|
|
1729
|
+
var Codex = class {
|
|
1730
|
+
exec;
|
|
1731
|
+
options;
|
|
1732
|
+
nativeBinding;
|
|
1733
|
+
lspForTools;
|
|
1734
|
+
constructor(options = {}) {
|
|
1735
|
+
const predefinedTools = options.tools ? [...options.tools] : [];
|
|
1736
|
+
const preserveRegisteredTools = options.preserveRegisteredTools === true;
|
|
1737
|
+
this.nativeBinding = getNativeBinding();
|
|
1738
|
+
this.options = { ...options, tools: [] };
|
|
1739
|
+
if (this.nativeBinding) {
|
|
1740
|
+
if (!preserveRegisteredTools && typeof this.nativeBinding.clearRegisteredTools === "function") {
|
|
1741
|
+
this.nativeBinding.clearRegisteredTools();
|
|
1742
|
+
}
|
|
1743
|
+
for (const tool2 of predefinedTools) {
|
|
1744
|
+
this.registerTool(tool2);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
this.lspForTools = this.createLspManagerForTools();
|
|
1748
|
+
if (this.lspForTools && this.nativeBinding) {
|
|
1749
|
+
this.registerDefaultReadFileInterceptor();
|
|
1750
|
+
}
|
|
1751
|
+
this.exec = new CodexExec();
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Register a tool for Codex. When `tool.name` matches a built-in Codex tool,
|
|
1755
|
+
* the native implementation is replaced for this Codex instance.
|
|
1756
|
+
*/
|
|
1757
|
+
registerTool(tool2) {
|
|
1758
|
+
if (!this.nativeBinding) {
|
|
1759
|
+
throw new Error("Native tool registration requires the NAPI binding");
|
|
1760
|
+
}
|
|
1761
|
+
if (typeof this.nativeBinding.registerTool !== "function") {
|
|
1762
|
+
console.warn("registerTool is not available in this build - tools feature may be incomplete");
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const { handler, ...info } = tool2;
|
|
1766
|
+
this.nativeBinding.registerTool(info, handler);
|
|
1767
|
+
if (!this.options.tools) {
|
|
1768
|
+
this.options.tools = [];
|
|
1769
|
+
}
|
|
1770
|
+
this.options.tools.push(tool2);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Register a tool interceptor for Codex. Interceptors can modify tool invocations
|
|
1774
|
+
* and results, and can call the built-in implementation.
|
|
1775
|
+
*/
|
|
1776
|
+
registerToolInterceptor(toolName, handler) {
|
|
1777
|
+
if (!this.nativeBinding) {
|
|
1778
|
+
throw new Error("Native tool interceptor registration requires the NAPI binding");
|
|
1779
|
+
}
|
|
1780
|
+
if (typeof this.nativeBinding.registerToolInterceptor !== "function" || typeof this.nativeBinding.callToolBuiltin !== "function") {
|
|
1781
|
+
console.warn("registerToolInterceptor is not available in this build - interceptor feature may be incomplete");
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
this.nativeBinding.registerToolInterceptor(toolName, async (...args) => {
|
|
1785
|
+
const context = args.length === 1 ? args[0] : args[1];
|
|
1786
|
+
if (!context || typeof context !== "object") {
|
|
1787
|
+
throw new Error("Native interceptor callback did not receive a context object");
|
|
1788
|
+
}
|
|
1789
|
+
const { invocation, token } = context;
|
|
1790
|
+
const callBuiltin = (override) => this.nativeBinding.callToolBuiltin(token, override ?? invocation);
|
|
1791
|
+
return handler({ invocation, callBuiltin });
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Clear all registered tools, restoring built-in defaults.
|
|
1796
|
+
*/
|
|
1797
|
+
clearTools() {
|
|
1798
|
+
if (!this.nativeBinding) {
|
|
1799
|
+
throw new Error("Native tool management requires the NAPI binding");
|
|
1800
|
+
}
|
|
1801
|
+
if (typeof this.nativeBinding.clearRegisteredTools === "function") {
|
|
1802
|
+
this.nativeBinding.clearRegisteredTools();
|
|
1803
|
+
}
|
|
1804
|
+
if (this.options.tools) {
|
|
1805
|
+
this.options.tools = [];
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
buildConversationConfig(options = {}) {
|
|
1809
|
+
return {
|
|
1810
|
+
model: options.model ?? this.options.defaultModel,
|
|
1811
|
+
modelProvider: options.modelProvider ?? this.options.modelProvider,
|
|
1812
|
+
oss: options.oss,
|
|
1813
|
+
sandboxMode: options.sandboxMode,
|
|
1814
|
+
approvalMode: options.approvalMode,
|
|
1815
|
+
workspaceWriteOptions: options.workspaceWriteOptions,
|
|
1816
|
+
workingDirectory: options.workingDirectory,
|
|
1817
|
+
skipGitRepoCheck: options.skipGitRepoCheck,
|
|
1818
|
+
reasoningEffort: options.reasoningEffort,
|
|
1819
|
+
reasoningSummary: options.reasoningSummary,
|
|
1820
|
+
fullAuto: options.fullAuto,
|
|
1821
|
+
baseUrl: this.options.baseUrl,
|
|
1822
|
+
apiKey: this.options.apiKey
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
createLspManagerForTools() {
|
|
1826
|
+
const cwd = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
1827
|
+
const options = {
|
|
1828
|
+
workingDirectory: cwd,
|
|
1829
|
+
waitForDiagnostics: true
|
|
1830
|
+
};
|
|
1831
|
+
try {
|
|
1832
|
+
return new LspManager(options);
|
|
1833
|
+
} catch {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
registerDefaultReadFileInterceptor() {
|
|
1838
|
+
if (!this.lspForTools) {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
this.registerToolInterceptor("read_file", async ({ invocation, callBuiltin }) => {
|
|
1843
|
+
let base;
|
|
1844
|
+
try {
|
|
1845
|
+
base = await callBuiltin();
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
return {
|
|
1848
|
+
success: false,
|
|
1849
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1850
|
+
output: void 0
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
if (!base.output || base.success === false) {
|
|
1854
|
+
return base;
|
|
1855
|
+
}
|
|
1856
|
+
let filePath;
|
|
1857
|
+
if (invocation.arguments) {
|
|
1858
|
+
try {
|
|
1859
|
+
const args = JSON.parse(invocation.arguments);
|
|
1860
|
+
const candidate = typeof args.file_path === "string" && args.file_path || typeof args.path === "string" && args.path || void 0;
|
|
1861
|
+
if (candidate && candidate.trim().length > 0) {
|
|
1862
|
+
filePath = candidate;
|
|
1863
|
+
}
|
|
1864
|
+
} catch {
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (!filePath) {
|
|
1868
|
+
return base;
|
|
1869
|
+
}
|
|
1870
|
+
let diagnosticsText = "";
|
|
1871
|
+
try {
|
|
1872
|
+
const results = await this.lspForTools.collectDiagnostics([filePath]);
|
|
1873
|
+
if (!results.length) {
|
|
1874
|
+
return base;
|
|
1875
|
+
}
|
|
1876
|
+
diagnosticsText = formatDiagnosticsForTool(results);
|
|
1877
|
+
} catch {
|
|
1878
|
+
return base;
|
|
1879
|
+
}
|
|
1880
|
+
if (!diagnosticsText) {
|
|
1881
|
+
return base;
|
|
1882
|
+
}
|
|
1883
|
+
const header = `LSP diagnostics for ${filePath}:
|
|
1884
|
+
${diagnosticsText}`;
|
|
1885
|
+
return prependSystemHintToToolResult(base, header);
|
|
1886
|
+
});
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Register a programmatic approval callback that Codex will call before executing
|
|
1892
|
+
* sensitive operations (e.g., shell commands, file writes).
|
|
1893
|
+
*/
|
|
1894
|
+
setApprovalCallback(handler) {
|
|
1895
|
+
if (!this.nativeBinding || typeof this.nativeBinding.registerApprovalCallback !== "function") {
|
|
1896
|
+
console.warn("Approval callback is not available in this build");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
this.nativeBinding.registerApprovalCallback(handler);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Starts a new conversation with an agent.
|
|
1903
|
+
* @returns A new thread instance.
|
|
1904
|
+
*/
|
|
1905
|
+
startThread(options = {}) {
|
|
1906
|
+
const threadOptions = {
|
|
1907
|
+
...options,
|
|
1908
|
+
model: options.model ?? this.options.defaultModel
|
|
1909
|
+
};
|
|
1910
|
+
return new Thread(this.exec, this.options, threadOptions);
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Resumes a conversation with an agent based on the thread id.
|
|
1914
|
+
* Threads are persisted in ~/.codex/sessions.
|
|
1915
|
+
*
|
|
1916
|
+
* @param id The id of the thread to resume.
|
|
1917
|
+
* @returns A new thread instance.
|
|
1918
|
+
*/
|
|
1919
|
+
resumeThread(id, options = {}) {
|
|
1920
|
+
const threadOptions = {
|
|
1921
|
+
...options,
|
|
1922
|
+
model: options.model ?? this.options.defaultModel
|
|
1923
|
+
};
|
|
1924
|
+
return new Thread(this.exec, this.options, threadOptions, id);
|
|
1925
|
+
}
|
|
1926
|
+
async listConversations(options = {}) {
|
|
1927
|
+
const request = {
|
|
1928
|
+
config: this.buildConversationConfig(options),
|
|
1929
|
+
pageSize: options.pageSize,
|
|
1930
|
+
cursor: options.cursor,
|
|
1931
|
+
modelProviders: options.modelProviders
|
|
1932
|
+
};
|
|
1933
|
+
return this.exec.listConversations(request);
|
|
1934
|
+
}
|
|
1935
|
+
async deleteConversation(id, options = {}) {
|
|
1936
|
+
const result = await this.exec.deleteConversation({
|
|
1937
|
+
id,
|
|
1938
|
+
config: this.buildConversationConfig(options)
|
|
1939
|
+
});
|
|
1940
|
+
return result.deleted;
|
|
1941
|
+
}
|
|
1942
|
+
async resumeConversationFromRollout(rolloutPath, options = {}) {
|
|
1943
|
+
const result = await this.exec.resumeConversationFromRollout({
|
|
1944
|
+
rolloutPath,
|
|
1945
|
+
config: this.buildConversationConfig(options)
|
|
1946
|
+
});
|
|
1947
|
+
const threadOptions = {
|
|
1948
|
+
...options,
|
|
1949
|
+
model: options.model ?? this.options.defaultModel
|
|
1950
|
+
};
|
|
1951
|
+
return new Thread(this.exec, this.options, threadOptions, result.threadId);
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Starts a review task using the built-in Codex review flow.
|
|
1955
|
+
*/
|
|
1956
|
+
async review(options) {
|
|
1957
|
+
const generator = this.reviewStreamedInternal(options);
|
|
1958
|
+
const items = [];
|
|
1959
|
+
let finalResponse = "";
|
|
1960
|
+
let usage = null;
|
|
1961
|
+
let turnFailure = null;
|
|
1962
|
+
for await (const event of generator) {
|
|
1963
|
+
if (event === null) continue;
|
|
1964
|
+
if (event.type === "item.completed") {
|
|
1965
|
+
if (event.item.type === "agent_message") {
|
|
1966
|
+
finalResponse = event.item.text;
|
|
1967
|
+
}
|
|
1968
|
+
items.push(event.item);
|
|
1969
|
+
} else if (event.type === "exited_review_mode") {
|
|
1970
|
+
if (event.review_output) {
|
|
1971
|
+
const reviewOutput = event.review_output;
|
|
1972
|
+
let reviewText = "";
|
|
1973
|
+
if (reviewOutput.overall_explanation) {
|
|
1974
|
+
reviewText += reviewOutput.overall_explanation;
|
|
1975
|
+
}
|
|
1976
|
+
if (reviewOutput.findings && reviewOutput.findings.length > 0) {
|
|
1977
|
+
if (reviewText) reviewText += "\n\n";
|
|
1978
|
+
reviewText += "## Review Findings\n\n";
|
|
1979
|
+
reviewOutput.findings.forEach((finding, index) => {
|
|
1980
|
+
reviewText += `### ${index + 1}. ${finding.title}
|
|
1981
|
+
`;
|
|
1982
|
+
reviewText += `${finding.body}
|
|
1983
|
+
`;
|
|
1984
|
+
reviewText += `**Priority:** ${finding.priority} | **Confidence:** ${finding.confidence_score}
|
|
1985
|
+
`;
|
|
1986
|
+
reviewText += `**Location:** ${finding.code_location.absolute_file_path}:${finding.code_location.line_range.start}-${finding.code_location.line_range.end}
|
|
1987
|
+
|
|
1988
|
+
`;
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
finalResponse = reviewText;
|
|
1992
|
+
}
|
|
1993
|
+
} else if (event.type === "turn.completed") {
|
|
1994
|
+
usage = event.usage;
|
|
1995
|
+
} else if (event.type === "turn.failed") {
|
|
1996
|
+
turnFailure = event.error;
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (turnFailure) {
|
|
2001
|
+
throw new Error(turnFailure.message);
|
|
2002
|
+
}
|
|
2003
|
+
return { items, finalResponse, usage };
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Starts a review task and returns the event stream.
|
|
2007
|
+
*/
|
|
2008
|
+
async reviewStreamed(options) {
|
|
2009
|
+
return { events: this.reviewStreamedInternal(options) };
|
|
2010
|
+
}
|
|
2011
|
+
async *reviewStreamedInternal(options) {
|
|
2012
|
+
const { target, threadOptions = {}, turnOptions = {} } = options;
|
|
2013
|
+
const { prompt, hint } = buildReviewPrompt(target);
|
|
2014
|
+
const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema);
|
|
2015
|
+
const needsSchemaFile = this.exec.requiresOutputSchemaFile();
|
|
2016
|
+
const schemaFile = needsSchemaFile ? await createOutputSchemaFile(normalizedSchema) : { schemaPath: void 0, cleanup: async () => {
|
|
2017
|
+
} };
|
|
2018
|
+
const generator = this.exec.run({
|
|
2019
|
+
input: prompt,
|
|
2020
|
+
baseUrl: this.options.baseUrl,
|
|
2021
|
+
apiKey: this.options.apiKey,
|
|
2022
|
+
model: threadOptions.model,
|
|
2023
|
+
modelProvider: threadOptions.modelProvider ?? this.options.modelProvider,
|
|
2024
|
+
oss: threadOptions.oss,
|
|
2025
|
+
sandboxMode: threadOptions.sandboxMode,
|
|
2026
|
+
approvalMode: threadOptions.approvalMode,
|
|
2027
|
+
workspaceWriteOptions: threadOptions.workspaceWriteOptions,
|
|
2028
|
+
workingDirectory: threadOptions.workingDirectory,
|
|
2029
|
+
skipGitRepoCheck: threadOptions.skipGitRepoCheck,
|
|
2030
|
+
outputSchemaFile: schemaFile.schemaPath,
|
|
2031
|
+
outputSchema: normalizedSchema,
|
|
2032
|
+
fullAuto: threadOptions.fullAuto,
|
|
2033
|
+
review: {
|
|
2034
|
+
userFacingHint: hint
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
try {
|
|
2038
|
+
for await (const item of generator) {
|
|
2039
|
+
let parsed;
|
|
2040
|
+
try {
|
|
2041
|
+
parsed = JSON.parse(item);
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
|
2044
|
+
}
|
|
2045
|
+
yield parsed;
|
|
2046
|
+
}
|
|
2047
|
+
} finally {
|
|
2048
|
+
await schemaFile.cleanup();
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
function prependSystemHintToToolResult(base, hint) {
|
|
2053
|
+
const trimmedHint = hint.trim();
|
|
2054
|
+
if (!trimmedHint) {
|
|
2055
|
+
return base;
|
|
2056
|
+
}
|
|
2057
|
+
const existing = base.output ?? "";
|
|
2058
|
+
const separator = existing.length === 0 || existing.startsWith("\n") ? "\n\n" : "\n\n";
|
|
2059
|
+
const output = existing.length === 0 ? `[SYSTEM_HINT]
|
|
2060
|
+
${trimmedHint}` : `[SYSTEM_HINT]
|
|
2061
|
+
${trimmedHint}${separator}${existing}`;
|
|
2062
|
+
return {
|
|
2063
|
+
...base,
|
|
2064
|
+
output
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/agents/toolRegistry.ts
|
|
2069
|
+
var executors = /* @__PURE__ */ new Map();
|
|
2070
|
+
function registerCodexToolExecutor(name, executor) {
|
|
2071
|
+
executors.set(name, executor);
|
|
2072
|
+
}
|
|
2073
|
+
function getCodexToolExecutor(name) {
|
|
2074
|
+
return executors.get(name);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/agents/CodexProvider.ts
|
|
2078
|
+
var fs6 = __toESM(require("fs"));
|
|
2079
|
+
var path9 = __toESM(require("path"));
|
|
2080
|
+
var os2 = __toESM(require("os"));
|
|
2081
|
+
|
|
2082
|
+
// src/agents/types.ts
|
|
2083
|
+
var import_agents_core = require("@openai/agents-core");
|
|
2084
|
+
|
|
2085
|
+
// src/agents/CodexProvider.ts
|
|
2086
|
+
var CodexProvider = class {
|
|
2087
|
+
codex = null;
|
|
2088
|
+
options;
|
|
2089
|
+
constructor(options = {}) {
|
|
2090
|
+
this.options = {
|
|
2091
|
+
workingDirectory: options.workingDirectory || process.cwd(),
|
|
2092
|
+
skipGitRepoCheck: options.skipGitRepoCheck ?? false,
|
|
2093
|
+
...options
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Lazy initialization of Codex instance
|
|
2098
|
+
*/
|
|
2099
|
+
getCodex() {
|
|
2100
|
+
if (!this.codex) {
|
|
2101
|
+
try {
|
|
2102
|
+
this.codex = new Codex({
|
|
2103
|
+
apiKey: this.options.apiKey,
|
|
2104
|
+
baseUrl: this.options.baseUrl
|
|
2105
|
+
});
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
throw new Error(
|
|
2108
|
+
`Failed to initialize Codex: ${error instanceof Error ? error.message : String(error)}`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return this.codex;
|
|
2113
|
+
}
|
|
2114
|
+
getModel(modelName) {
|
|
2115
|
+
const model = modelName || this.options.defaultModel;
|
|
2116
|
+
return new CodexModel(this.getCodex(), model, this.options);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Register a programmatic approval callback on the underlying Codex instance.
|
|
2120
|
+
*/
|
|
2121
|
+
setApprovalCallback(callback) {
|
|
2122
|
+
this.getCodex().setApprovalCallback(callback);
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
var CodexModel = class {
|
|
2126
|
+
codex;
|
|
2127
|
+
modelName;
|
|
2128
|
+
thread = null;
|
|
2129
|
+
options;
|
|
2130
|
+
registeredTools = /* @__PURE__ */ new Set();
|
|
2131
|
+
toolExecutors = /* @__PURE__ */ new Map();
|
|
2132
|
+
tempImageFiles = /* @__PURE__ */ new Set();
|
|
2133
|
+
streamedTurnItems = [];
|
|
2134
|
+
lastStreamedMessage = null;
|
|
2135
|
+
detachDiagnostics;
|
|
2136
|
+
diagnosticsThread;
|
|
2137
|
+
constructor(codex, modelName, options) {
|
|
2138
|
+
this.codex = codex;
|
|
2139
|
+
this.modelName = modelName;
|
|
2140
|
+
this.options = options;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Cleanup temporary image files created during request processing
|
|
2144
|
+
*/
|
|
2145
|
+
async cleanupTempFiles() {
|
|
2146
|
+
for (const filepath of this.tempImageFiles) {
|
|
2147
|
+
try {
|
|
2148
|
+
await fs6.promises.unlink(filepath);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
this.tempImageFiles.clear();
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Get or create the thread for this model instance
|
|
2156
|
+
*/
|
|
2157
|
+
getThread(conversationId) {
|
|
2158
|
+
if (conversationId) {
|
|
2159
|
+
if (!this.thread || this.thread.id !== conversationId) {
|
|
2160
|
+
this.detachDiagnostics?.();
|
|
2161
|
+
this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions());
|
|
2162
|
+
this.diagnosticsThread = null;
|
|
2163
|
+
}
|
|
2164
|
+
} else if (!this.thread) {
|
|
2165
|
+
this.detachDiagnostics?.();
|
|
2166
|
+
this.thread = this.codex.startThread(this.getThreadOptions());
|
|
2167
|
+
this.diagnosticsThread = null;
|
|
2168
|
+
}
|
|
2169
|
+
const thread = this.thread;
|
|
2170
|
+
if (!thread) {
|
|
2171
|
+
throw new Error("Thread initialization failed");
|
|
2172
|
+
}
|
|
2173
|
+
this.ensureDiagnosticsBridge(thread);
|
|
2174
|
+
return thread;
|
|
2175
|
+
}
|
|
2176
|
+
ensureDiagnosticsBridge(thread) {
|
|
2177
|
+
if (this.options.enableLsp === false) {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (this.diagnosticsThread === thread && this.detachDiagnostics) {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
this.detachDiagnostics?.();
|
|
2184
|
+
this.diagnosticsThread = thread;
|
|
2185
|
+
this.detachDiagnostics = attachLspDiagnostics(thread, {
|
|
2186
|
+
workingDirectory: this.options.workingDirectory ?? process.cwd(),
|
|
2187
|
+
waitForDiagnostics: true
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
getThreadOptions() {
|
|
2191
|
+
return {
|
|
2192
|
+
model: this.modelName,
|
|
2193
|
+
// When a custom baseUrl is provided (e.g., test proxy), do not enable OSS mode,
|
|
2194
|
+
// since the backend is not Ollama in that case.
|
|
2195
|
+
oss: this.options.baseUrl ? false : this.options.oss,
|
|
2196
|
+
workingDirectory: this.options.workingDirectory,
|
|
2197
|
+
skipGitRepoCheck: this.options.skipGitRepoCheck,
|
|
2198
|
+
sandboxMode: this.options.sandboxMode ?? "danger-full-access",
|
|
2199
|
+
approvalMode: this.options.approvalMode,
|
|
2200
|
+
reasoningEffort: this.options.reasoningEffort,
|
|
2201
|
+
reasoningSummary: this.options.reasoningSummary
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
async getResponse(request) {
|
|
2205
|
+
try {
|
|
2206
|
+
const thread = this.getThread(request.conversationId || request.previousResponseId);
|
|
2207
|
+
if (request.tools && request.tools.length > 0) {
|
|
2208
|
+
this.registerRequestTools(request.tools);
|
|
2209
|
+
}
|
|
2210
|
+
const input = await this.convertRequestToInput(request);
|
|
2211
|
+
const turn = await thread.run(input, {
|
|
2212
|
+
outputSchema: normalizeAgentsOutputType(request.outputType),
|
|
2213
|
+
oss: this.options.oss
|
|
2214
|
+
});
|
|
2215
|
+
const planItem = turn.items.filter((item) => item.type === "todo_list").slice(-1)[0];
|
|
2216
|
+
const response = {
|
|
2217
|
+
usage: this.convertUsage(turn.usage),
|
|
2218
|
+
output: this.convertItemsToOutput(turn.items, turn.finalResponse),
|
|
2219
|
+
responseId: thread.id || void 0
|
|
2220
|
+
};
|
|
2221
|
+
if (planItem) {
|
|
2222
|
+
response.plan = { items: planItem.items };
|
|
2223
|
+
}
|
|
2224
|
+
return response;
|
|
2225
|
+
} finally {
|
|
2226
|
+
await this.cleanupTempFiles();
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
async *getStreamedResponse(request) {
|
|
2230
|
+
const MAX_ACCUMULATED_SIZE = 1e7;
|
|
2231
|
+
try {
|
|
2232
|
+
const thread = this.getThread(request.conversationId || request.previousResponseId);
|
|
2233
|
+
if (request.tools && request.tools.length > 0) {
|
|
2234
|
+
this.registerRequestTools(request.tools);
|
|
2235
|
+
}
|
|
2236
|
+
const input = await this.convertRequestToInput(request);
|
|
2237
|
+
const { events } = await thread.runStreamed(input, {
|
|
2238
|
+
outputSchema: normalizeAgentsOutputType(request.outputType),
|
|
2239
|
+
oss: this.options.oss
|
|
2240
|
+
});
|
|
2241
|
+
const textAccumulator = /* @__PURE__ */ new Map();
|
|
2242
|
+
for await (const event of events) {
|
|
2243
|
+
let totalSize = 0;
|
|
2244
|
+
for (const text of textAccumulator.values()) {
|
|
2245
|
+
totalSize += text.length;
|
|
2246
|
+
}
|
|
2247
|
+
if (totalSize > MAX_ACCUMULATED_SIZE) {
|
|
2248
|
+
throw new Error(`Accumulated text exceeded maximum size limit (${MAX_ACCUMULATED_SIZE} bytes)`);
|
|
2249
|
+
}
|
|
2250
|
+
const streamEvents = this.convertCodexEventToStreamEvent(event, textAccumulator);
|
|
2251
|
+
for (const streamEvent of streamEvents) {
|
|
2252
|
+
yield streamEvent;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
} finally {
|
|
2256
|
+
await this.cleanupTempFiles();
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Register tools from ModelRequest with the Codex instance
|
|
2261
|
+
*
|
|
2262
|
+
* Converts SerializedTool format (OpenAI Agents) to NativeToolDefinition format (Codex)
|
|
2263
|
+
* and registers them with the Codex instance for bidirectional tool execution.
|
|
2264
|
+
*/
|
|
2265
|
+
registerRequestTools(tools) {
|
|
2266
|
+
this.toolExecutors.clear();
|
|
2267
|
+
for (const tool2 of tools) {
|
|
2268
|
+
if (tool2.type !== "function") {
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
if (this.registeredTools.has(tool2.name)) {
|
|
2272
|
+
const executor = this.resolveToolExecutor(tool2.name);
|
|
2273
|
+
if (executor) {
|
|
2274
|
+
this.toolExecutors.set(tool2.name, executor);
|
|
2275
|
+
}
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
try {
|
|
2279
|
+
const executor = this.resolveToolExecutor(tool2.name);
|
|
2280
|
+
if (executor) {
|
|
2281
|
+
this.toolExecutors.set(tool2.name, executor);
|
|
2282
|
+
}
|
|
2283
|
+
const nativeToolDef = {
|
|
2284
|
+
name: tool2.name,
|
|
2285
|
+
description: tool2.description,
|
|
2286
|
+
parameters: tool2.parameters,
|
|
2287
|
+
// The handler is called when Codex wants to execute this tool
|
|
2288
|
+
handler: async (invocation) => {
|
|
2289
|
+
return await this.executeToolViaFramework(invocation);
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
this.codex.registerTool(nativeToolDef);
|
|
2293
|
+
this.registeredTools.add(tool2.name);
|
|
2294
|
+
console.log(`Registered tool with Codex: ${tool2.name}`);
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
const errorMessage = `Failed to register tool ${tool2.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
2297
|
+
console.error(errorMessage);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
resolveToolExecutor(toolName) {
|
|
2302
|
+
return getCodexToolExecutor(toolName);
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Execute a tool via the OpenAI Agents framework
|
|
2306
|
+
*
|
|
2307
|
+
* This is the bridge between Codex's tool execution and the framework's tool handlers.
|
|
2308
|
+
*
|
|
2309
|
+
* FRAMEWORK INTEGRATION NOTE:
|
|
2310
|
+
* This method currently returns a placeholder result because the actual execution
|
|
2311
|
+
* requires integration with the OpenAI Agents framework's tool execution loop.
|
|
2312
|
+
*
|
|
2313
|
+
* In a full implementation, this would:
|
|
2314
|
+
* 1. Emit a "tool_call_requested" event that the framework can listen to
|
|
2315
|
+
* 2. Wait for the framework to execute the tool and provide the result
|
|
2316
|
+
* 3. Return that result to Codex
|
|
2317
|
+
*
|
|
2318
|
+
* For now, this creates a promise that could be resolved by framework code,
|
|
2319
|
+
* but the framework integration is not yet complete.
|
|
2320
|
+
*/
|
|
2321
|
+
async executeToolViaFramework(invocation) {
|
|
2322
|
+
if (!invocation) {
|
|
2323
|
+
console.warn("Codex requested a tool execution without invocation data.");
|
|
2324
|
+
return {
|
|
2325
|
+
output: JSON.stringify({
|
|
2326
|
+
message: "Tool invocation payload missing",
|
|
2327
|
+
note: "Codex returned null invocation data so the tool was not executed."
|
|
2328
|
+
}),
|
|
2329
|
+
success: false,
|
|
2330
|
+
error: "Missing tool invocation data from Codex"
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
console.log(
|
|
2334
|
+
`Tool execution requested by Codex: ${invocation.toolName} (callId: ${invocation.callId})`
|
|
2335
|
+
);
|
|
2336
|
+
const executor = this.toolExecutors.get(invocation.toolName) ?? getCodexToolExecutor(invocation.toolName);
|
|
2337
|
+
if (!executor) {
|
|
2338
|
+
const message = `No Codex executor registered for tool '${invocation.toolName}'. Use codexTool() or provide a codexExecute handler.`;
|
|
2339
|
+
console.warn(message);
|
|
2340
|
+
return {
|
|
2341
|
+
success: false,
|
|
2342
|
+
error: message,
|
|
2343
|
+
output: void 0
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
let parsedArguments = {};
|
|
2347
|
+
if (invocation.arguments) {
|
|
2348
|
+
try {
|
|
2349
|
+
parsedArguments = JSON.parse(invocation.arguments);
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
return {
|
|
2352
|
+
success: false,
|
|
2353
|
+
error: `Failed to parse tool arguments: ${error instanceof Error ? error.message : String(error)}`
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const context = {
|
|
2358
|
+
name: invocation.toolName,
|
|
2359
|
+
callId: invocation.callId,
|
|
2360
|
+
arguments: parsedArguments,
|
|
2361
|
+
rawInvocation: invocation
|
|
2362
|
+
};
|
|
2363
|
+
try {
|
|
2364
|
+
const result = await executor(context);
|
|
2365
|
+
return this.normalizeToolResult(result);
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
return {
|
|
2368
|
+
success: false,
|
|
2369
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Handle image input by converting to local file path
|
|
2375
|
+
* Supports: base64 data URLs, HTTP(S) URLs, and file IDs (not yet implemented)
|
|
2376
|
+
*/
|
|
2377
|
+
normalizeToolResult(result) {
|
|
2378
|
+
if (result === void 0 || result === null) {
|
|
2379
|
+
return { success: true };
|
|
2380
|
+
}
|
|
2381
|
+
if (typeof result === "string") {
|
|
2382
|
+
return { success: true, output: result };
|
|
2383
|
+
}
|
|
2384
|
+
if (typeof result === "object" && ("output" in result || "error" in result || "success" in result)) {
|
|
2385
|
+
return {
|
|
2386
|
+
success: result.success ?? !result.error,
|
|
2387
|
+
output: result.output,
|
|
2388
|
+
error: result.error
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
return {
|
|
2392
|
+
success: true,
|
|
2393
|
+
output: JSON.stringify(result)
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
async handleImageInput(item) {
|
|
2397
|
+
const imageValue = item.image;
|
|
2398
|
+
if (typeof imageValue === "string") {
|
|
2399
|
+
if (imageValue.startsWith("data:image/")) {
|
|
2400
|
+
return await this.saveBase64Image(imageValue);
|
|
2401
|
+
} else if (imageValue.startsWith("http://") || imageValue.startsWith("https://")) {
|
|
2402
|
+
return await this.downloadImage(imageValue);
|
|
2403
|
+
} else if (fs6.existsSync(imageValue)) {
|
|
2404
|
+
return imageValue;
|
|
2405
|
+
} else {
|
|
2406
|
+
throw new Error(`Invalid image format: ${imageValue.substring(0, 50)}...`);
|
|
2407
|
+
}
|
|
2408
|
+
} else if (typeof imageValue === "object" && "url" in imageValue) {
|
|
2409
|
+
return await this.downloadImage(imageValue.url);
|
|
2410
|
+
} else if (typeof imageValue === "object" && "fileId" in imageValue) {
|
|
2411
|
+
throw new Error(
|
|
2412
|
+
`Image fileId references are not yet supported. File IDs would need to be downloaded from the service first.`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Save base64-encoded image to temporary file
|
|
2419
|
+
*/
|
|
2420
|
+
async saveBase64Image(dataUrl) {
|
|
2421
|
+
const matches = dataUrl.match(/^data:image\/([^;]+);base64,(.+)$/);
|
|
2422
|
+
if (!matches) {
|
|
2423
|
+
throw new Error("Invalid base64 image data URL");
|
|
2424
|
+
}
|
|
2425
|
+
const mediaType = matches[1];
|
|
2426
|
+
const base64Data = matches[2];
|
|
2427
|
+
if (!base64Data) {
|
|
2428
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2429
|
+
}
|
|
2430
|
+
const sanitizedBase64 = base64Data.replace(/\s/g, "");
|
|
2431
|
+
if (sanitizedBase64.length === 0) {
|
|
2432
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2433
|
+
}
|
|
2434
|
+
if (!/^[A-Za-z0-9+/=_-]+$/.test(sanitizedBase64)) {
|
|
2435
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2436
|
+
}
|
|
2437
|
+
const normalizedBase64 = sanitizedBase64.replace(/-/g, "+").replace(/_/g, "/");
|
|
2438
|
+
let buffer;
|
|
2439
|
+
try {
|
|
2440
|
+
buffer = Buffer.from(normalizedBase64, "base64");
|
|
2441
|
+
} catch {
|
|
2442
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2443
|
+
}
|
|
2444
|
+
if (buffer.length === 0) {
|
|
2445
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2446
|
+
}
|
|
2447
|
+
const reencoded = buffer.toString("base64").replace(/=+$/, "");
|
|
2448
|
+
const normalizedInput = normalizedBase64.replace(/=+$/, "");
|
|
2449
|
+
if (reencoded !== normalizedInput) {
|
|
2450
|
+
throw new Error("Invalid base64 data in image URL");
|
|
2451
|
+
}
|
|
2452
|
+
const extension = this.getExtensionFromMediaType(mediaType, "png");
|
|
2453
|
+
const tempDir = os2.tmpdir();
|
|
2454
|
+
const filename = `codex-image-${Date.now()}.${extension}`;
|
|
2455
|
+
const filepath = path9.join(tempDir, filename);
|
|
2456
|
+
await fs6.promises.writeFile(filepath, buffer);
|
|
2457
|
+
this.tempImageFiles.add(filepath);
|
|
2458
|
+
return filepath;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Download image from URL to temporary file
|
|
2462
|
+
*/
|
|
2463
|
+
async downloadImage(url) {
|
|
2464
|
+
const response = await fetch(url);
|
|
2465
|
+
if (!response.ok) {
|
|
2466
|
+
throw new Error(`Failed to download image from ${url}: ${response.statusText}`);
|
|
2467
|
+
}
|
|
2468
|
+
const buffer = await response.arrayBuffer();
|
|
2469
|
+
const contentType = response.headers.get("content-type") || "image/png";
|
|
2470
|
+
const mediaTypePart = contentType.split(";")[0]?.trim() || "image/png";
|
|
2471
|
+
const mediaType = mediaTypePart.split("/")[1] || "png";
|
|
2472
|
+
const extension = this.getExtensionFromMediaType(mediaType, "png");
|
|
2473
|
+
const tempDir = os2.tmpdir();
|
|
2474
|
+
const filename = `codex-image-${Date.now()}.${extension}`;
|
|
2475
|
+
const filepath = path9.join(tempDir, filename);
|
|
2476
|
+
await fs6.promises.writeFile(filepath, Buffer.from(buffer));
|
|
2477
|
+
this.tempImageFiles.add(filepath);
|
|
2478
|
+
return filepath;
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Convert media type to file extension
|
|
2482
|
+
* Handles special cases like "jpeg" -> "jpg", "svg+xml" -> "svg"
|
|
2483
|
+
*/
|
|
2484
|
+
getExtensionFromMediaType(mediaType, defaultExt) {
|
|
2485
|
+
if (!mediaType) {
|
|
2486
|
+
return defaultExt;
|
|
2487
|
+
}
|
|
2488
|
+
const normalized = mediaType.toLowerCase().trim();
|
|
2489
|
+
const extensionMap = {
|
|
2490
|
+
"jpeg": "jpg",
|
|
2491
|
+
"svg+xml": "svg",
|
|
2492
|
+
"vnd.microsoft.icon": "ico",
|
|
2493
|
+
"x-icon": "ico"
|
|
2494
|
+
};
|
|
2495
|
+
if (extensionMap[normalized]) {
|
|
2496
|
+
return extensionMap[normalized];
|
|
2497
|
+
}
|
|
2498
|
+
const simpleExtension = normalized.split("+")[0];
|
|
2499
|
+
if (simpleExtension && /^[a-z0-9]+$/.test(simpleExtension)) {
|
|
2500
|
+
return simpleExtension;
|
|
2501
|
+
}
|
|
2502
|
+
return defaultExt;
|
|
2503
|
+
}
|
|
2504
|
+
async convertRequestToInput(request) {
|
|
2505
|
+
const parts = [];
|
|
2506
|
+
if (request.systemInstructions) {
|
|
2507
|
+
parts.push({
|
|
2508
|
+
type: "text",
|
|
2509
|
+
text: `<system>
|
|
2510
|
+
${request.systemInstructions}
|
|
2511
|
+
</system>
|
|
2512
|
+
|
|
2513
|
+
`
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
if (typeof request.input === "string") {
|
|
2517
|
+
parts.push({ type: "text", text: request.input });
|
|
2518
|
+
} else {
|
|
2519
|
+
for (const item of request.input) {
|
|
2520
|
+
if ("file" in item && "type" in item) {
|
|
2521
|
+
throw new Error(
|
|
2522
|
+
`CodexProvider does not yet support input_file type. File handling needs to be implemented based on file type and format.`
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
if ("audio" in item && "type" in item) {
|
|
2526
|
+
throw new Error(
|
|
2527
|
+
`CodexProvider does not yet support input_audio type. Audio handling needs to be implemented.`
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
if ("image" in item && "type" in item && item.type !== "message") {
|
|
2531
|
+
const imageItem = item;
|
|
2532
|
+
const imagePath = await this.handleImageInput(imageItem);
|
|
2533
|
+
if (imagePath) {
|
|
2534
|
+
parts.push({ type: "local_image", path: imagePath });
|
|
2535
|
+
}
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (item.type === "function_call_result") {
|
|
2539
|
+
if ("name" in item && "result" in item) {
|
|
2540
|
+
parts.push({
|
|
2541
|
+
type: "text",
|
|
2542
|
+
text: `[Tool ${item.name} returned: ${item.result}]`
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
} else if (item.type === "reasoning") {
|
|
2546
|
+
let text = "";
|
|
2547
|
+
if ("content" in item && typeof item.content === "string") {
|
|
2548
|
+
text = item.content;
|
|
2549
|
+
} else if ("reasoning" in item && typeof item.reasoning === "string") {
|
|
2550
|
+
text = item.reasoning;
|
|
2551
|
+
}
|
|
2552
|
+
if (text) {
|
|
2553
|
+
parts.push({
|
|
2554
|
+
type: "text",
|
|
2555
|
+
text: `[Reasoning: ${text}]`
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
} else if ((item.type === "message" || item.type === void 0) && "role" in item) {
|
|
2559
|
+
if (!("content" in item)) continue;
|
|
2560
|
+
const content = item.content;
|
|
2561
|
+
if (typeof content === "string") {
|
|
2562
|
+
parts.push({ type: "text", text: content });
|
|
2563
|
+
} else if (Array.isArray(content)) {
|
|
2564
|
+
for (const contentItem of content) {
|
|
2565
|
+
if (contentItem.type === "input_text") {
|
|
2566
|
+
parts.push({ type: "text", text: contentItem.text });
|
|
2567
|
+
} else if (contentItem.type === "input_image") {
|
|
2568
|
+
const imagePath = await this.handleImageInput(contentItem);
|
|
2569
|
+
if (imagePath) {
|
|
2570
|
+
parts.push({ type: "local_image", path: imagePath });
|
|
2571
|
+
}
|
|
2572
|
+
} else if (contentItem.type === "input_file") {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`CodexProvider does not yet support input_file type. File handling needs to be implemented based on file type and format.`
|
|
2575
|
+
);
|
|
2576
|
+
} else if (contentItem.type === "audio") {
|
|
2577
|
+
throw new Error(
|
|
2578
|
+
`CodexProvider does not yet support audio type. Audio handling needs to be implemented.`
|
|
2579
|
+
);
|
|
2580
|
+
} else if (contentItem.type === "refusal") {
|
|
2581
|
+
parts.push({
|
|
2582
|
+
type: "text",
|
|
2583
|
+
text: `[Refusal: ${contentItem.refusal}]`
|
|
2584
|
+
});
|
|
2585
|
+
} else if (contentItem.type === "output_text") {
|
|
2586
|
+
parts.push({ type: "text", text: contentItem.text });
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (parts.length === 1 && parts[0].type === "text") {
|
|
2594
|
+
return parts[0].text;
|
|
2595
|
+
}
|
|
2596
|
+
return parts;
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Convert Codex Usage to ModelResponse Usage
|
|
2600
|
+
*/
|
|
2601
|
+
convertUsage(usage) {
|
|
2602
|
+
if (!usage) {
|
|
2603
|
+
return new import_agents_core.Usage();
|
|
2604
|
+
}
|
|
2605
|
+
const converted = new import_agents_core.Usage({
|
|
2606
|
+
requests: 1,
|
|
2607
|
+
inputTokens: usage.input_tokens,
|
|
2608
|
+
outputTokens: usage.output_tokens,
|
|
2609
|
+
totalTokens: usage.input_tokens + usage.output_tokens
|
|
2610
|
+
});
|
|
2611
|
+
if (usage.cached_input_tokens) {
|
|
2612
|
+
converted.inputTokensDetails = [{ cachedTokens: usage.cached_input_tokens }];
|
|
2613
|
+
}
|
|
2614
|
+
return converted;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Convert Codex ThreadItems to AgentOutputItems
|
|
2618
|
+
*/
|
|
2619
|
+
convertItemsToOutput(items, finalResponse) {
|
|
2620
|
+
const output = [];
|
|
2621
|
+
for (const item of items) {
|
|
2622
|
+
switch (item.type) {
|
|
2623
|
+
case "agent_message": {
|
|
2624
|
+
const content = [
|
|
2625
|
+
{
|
|
2626
|
+
type: "output_text",
|
|
2627
|
+
text: item.text
|
|
2628
|
+
}
|
|
2629
|
+
];
|
|
2630
|
+
output.push({
|
|
2631
|
+
type: "message",
|
|
2632
|
+
role: "assistant",
|
|
2633
|
+
status: "completed",
|
|
2634
|
+
content
|
|
2635
|
+
});
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
// For final output, omit internal "reasoning" items. Streaming already surfaces reasoning events.
|
|
2639
|
+
case "reasoning":
|
|
2640
|
+
break;
|
|
2641
|
+
// Codex handles tools internally, so we don't expose them as function calls
|
|
2642
|
+
// The results are already incorporated into the agent_message
|
|
2643
|
+
case "command_execution":
|
|
2644
|
+
case "file_change":
|
|
2645
|
+
case "mcp_tool_call":
|
|
2646
|
+
break;
|
|
2647
|
+
default:
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (output.length === 0 && finalResponse) {
|
|
2652
|
+
output.push({
|
|
2653
|
+
type: "message",
|
|
2654
|
+
role: "assistant",
|
|
2655
|
+
status: "completed",
|
|
2656
|
+
content: [
|
|
2657
|
+
{
|
|
2658
|
+
type: "output_text",
|
|
2659
|
+
text: finalResponse
|
|
2660
|
+
}
|
|
2661
|
+
]
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
return output;
|
|
2665
|
+
}
|
|
2666
|
+
buildStreamResponse(usage, responseId, items, lastMessage) {
|
|
2667
|
+
const messageItems = items.filter(
|
|
2668
|
+
(item) => item.type === "agent_message"
|
|
2669
|
+
);
|
|
2670
|
+
const output = this.convertItemsToOutput(messageItems, lastMessage ?? "");
|
|
2671
|
+
const usageData = {
|
|
2672
|
+
requests: usage.requests,
|
|
2673
|
+
inputTokens: usage.inputTokens,
|
|
2674
|
+
outputTokens: usage.outputTokens,
|
|
2675
|
+
totalTokens: usage.totalTokens,
|
|
2676
|
+
inputTokensDetails: usage.inputTokensDetails?.[0],
|
|
2677
|
+
outputTokensDetails: usage.outputTokensDetails?.[0]
|
|
2678
|
+
};
|
|
2679
|
+
const latestPlan = items.filter((item) => item.type === "todo_list").slice(-1)[0];
|
|
2680
|
+
const response = {
|
|
2681
|
+
id: responseId,
|
|
2682
|
+
responseId,
|
|
2683
|
+
usage: usageData,
|
|
2684
|
+
output
|
|
2685
|
+
};
|
|
2686
|
+
if (latestPlan) {
|
|
2687
|
+
response.plan = { items: latestPlan.items };
|
|
2688
|
+
}
|
|
2689
|
+
return response;
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Convert Codex ThreadEvent to OpenAI Agents StreamEvent
|
|
2693
|
+
*/
|
|
2694
|
+
convertCodexEventToStreamEvent(event, textAccumulator) {
|
|
2695
|
+
const events = [];
|
|
2696
|
+
switch (event.type) {
|
|
2697
|
+
case "thread.started": {
|
|
2698
|
+
events.push({ type: "response_started" });
|
|
2699
|
+
const responseId = this.thread?.id ?? "codex-stream-response";
|
|
2700
|
+
events.push({
|
|
2701
|
+
type: "response.created",
|
|
2702
|
+
response: { id: responseId }
|
|
2703
|
+
});
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
case "turn.started":
|
|
2707
|
+
this.streamedTurnItems = [];
|
|
2708
|
+
this.lastStreamedMessage = null;
|
|
2709
|
+
break;
|
|
2710
|
+
case "item.started":
|
|
2711
|
+
if (event.item.type === "agent_message" || event.item.type === "reasoning") {
|
|
2712
|
+
const itemKey = `${event.item.type}`;
|
|
2713
|
+
textAccumulator.set(itemKey, "");
|
|
2714
|
+
}
|
|
2715
|
+
break;
|
|
2716
|
+
case "background_event":
|
|
2717
|
+
events.push({
|
|
2718
|
+
type: "model",
|
|
2719
|
+
event: {
|
|
2720
|
+
type: "background_event",
|
|
2721
|
+
message: event.message
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2724
|
+
break;
|
|
2725
|
+
case "item.updated":
|
|
2726
|
+
if (event.item.type === "agent_message") {
|
|
2727
|
+
const itemKey = "agent_message";
|
|
2728
|
+
const previousText = textAccumulator.get(itemKey) || "";
|
|
2729
|
+
const currentText = event.item.text;
|
|
2730
|
+
if (currentText.length < previousText.length) {
|
|
2731
|
+
console.warn("Received backwards update for text - ignoring delta");
|
|
2732
|
+
break;
|
|
2733
|
+
}
|
|
2734
|
+
if (currentText.length > previousText.length) {
|
|
2735
|
+
const delta = currentText.slice(previousText.length);
|
|
2736
|
+
textAccumulator.set(itemKey, currentText);
|
|
2737
|
+
events.push({
|
|
2738
|
+
type: "output_text_delta",
|
|
2739
|
+
delta
|
|
2740
|
+
});
|
|
2741
|
+
events.push({
|
|
2742
|
+
type: "response.output_text.delta",
|
|
2743
|
+
delta
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
} else if (event.item.type === "reasoning") {
|
|
2747
|
+
const itemKey = "reasoning";
|
|
2748
|
+
const previousText = textAccumulator.get(itemKey) || "";
|
|
2749
|
+
const currentText = event.item.text;
|
|
2750
|
+
if (currentText.length > previousText.length) {
|
|
2751
|
+
const delta = currentText.slice(previousText.length);
|
|
2752
|
+
textAccumulator.set(itemKey, currentText);
|
|
2753
|
+
events.push({
|
|
2754
|
+
type: "model",
|
|
2755
|
+
event: {
|
|
2756
|
+
type: "reasoning_delta",
|
|
2757
|
+
delta
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
break;
|
|
2763
|
+
case "item.completed":
|
|
2764
|
+
this.streamedTurnItems.push(event.item);
|
|
2765
|
+
if (event.item.type === "agent_message") {
|
|
2766
|
+
textAccumulator.delete("agent_message");
|
|
2767
|
+
this.lastStreamedMessage = event.item.text;
|
|
2768
|
+
} else if (event.item.type === "reasoning") {
|
|
2769
|
+
events.push({
|
|
2770
|
+
type: "model",
|
|
2771
|
+
event: {
|
|
2772
|
+
type: "reasoning_done",
|
|
2773
|
+
reasoning: event.item.text
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
textAccumulator.delete("reasoning");
|
|
2777
|
+
} else if (event.item.type === "todo_list") {
|
|
2778
|
+
events.push({
|
|
2779
|
+
type: "model",
|
|
2780
|
+
event: {
|
|
2781
|
+
type: "plan_update",
|
|
2782
|
+
items: event.item.items
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
break;
|
|
2787
|
+
case "turn.completed": {
|
|
2788
|
+
const usage = this.convertUsage(event.usage);
|
|
2789
|
+
const responseId = this.thread?.id ?? "codex-stream-response";
|
|
2790
|
+
const response = this.buildStreamResponse(
|
|
2791
|
+
usage,
|
|
2792
|
+
responseId,
|
|
2793
|
+
this.streamedTurnItems,
|
|
2794
|
+
this.lastStreamedMessage
|
|
2795
|
+
);
|
|
2796
|
+
this.streamedTurnItems = [];
|
|
2797
|
+
this.lastStreamedMessage = null;
|
|
2798
|
+
events.push({
|
|
2799
|
+
type: "response.completed",
|
|
2800
|
+
response: {
|
|
2801
|
+
id: response.id,
|
|
2802
|
+
usage: {
|
|
2803
|
+
input_tokens: usage.inputTokens,
|
|
2804
|
+
input_tokens_details: usage.inputTokensDetails?.[0] ?? null,
|
|
2805
|
+
output_tokens: usage.outputTokens,
|
|
2806
|
+
output_tokens_details: usage.outputTokensDetails?.[0] ?? null,
|
|
2807
|
+
total_tokens: usage.totalTokens
|
|
2808
|
+
},
|
|
2809
|
+
...response.output && response.output.length > 0 ? {
|
|
2810
|
+
output: response.output.map((item) => {
|
|
2811
|
+
if (item.type === "message" && item.role === "assistant") {
|
|
2812
|
+
return {
|
|
2813
|
+
id: item.id ?? "msg_1",
|
|
2814
|
+
role: item.role,
|
|
2815
|
+
content: item.content
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
return item;
|
|
2819
|
+
}),
|
|
2820
|
+
output_text: response.output.filter(
|
|
2821
|
+
(item) => item.type === "message" && item.role === "assistant"
|
|
2822
|
+
)[0]?.content?.find(
|
|
2823
|
+
(c) => c.type === "output_text"
|
|
2824
|
+
)?.text ?? (this.lastStreamedMessage ?? "")
|
|
2825
|
+
} : {}
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
events.push({
|
|
2829
|
+
type: "response_done",
|
|
2830
|
+
response
|
|
2831
|
+
});
|
|
2832
|
+
break;
|
|
2833
|
+
}
|
|
2834
|
+
case "turn.failed":
|
|
2835
|
+
events.push({
|
|
2836
|
+
type: "model",
|
|
2837
|
+
event: {
|
|
2838
|
+
type: "error",
|
|
2839
|
+
error: {
|
|
2840
|
+
message: event.error.message
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
break;
|
|
2845
|
+
case "error":
|
|
2846
|
+
events.push({
|
|
2847
|
+
type: "model",
|
|
2848
|
+
event: {
|
|
2849
|
+
type: "error",
|
|
2850
|
+
error: {
|
|
2851
|
+
message: event.message
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
break;
|
|
2856
|
+
case "raw_event":
|
|
2857
|
+
break;
|
|
2858
|
+
default:
|
|
2859
|
+
break;
|
|
2860
|
+
}
|
|
2861
|
+
if (event?.type !== "raw_event") {
|
|
2862
|
+
const rawEvent = {
|
|
2863
|
+
type: "raw_event",
|
|
2864
|
+
raw: event
|
|
2865
|
+
};
|
|
2866
|
+
if (events.length === 0) {
|
|
2867
|
+
return [rawEvent];
|
|
2868
|
+
}
|
|
2869
|
+
const result = [...events];
|
|
2870
|
+
const insertIndex = Math.min(1, result.length);
|
|
2871
|
+
result.splice(insertIndex, 0, rawEvent);
|
|
2872
|
+
return result;
|
|
2873
|
+
}
|
|
2874
|
+
return events;
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
function isObject(value) {
|
|
2878
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2879
|
+
}
|
|
2880
|
+
function normalizeAgentsOutputType(outputType) {
|
|
2881
|
+
if (!isObject(outputType)) return void 0;
|
|
2882
|
+
const outType = outputType;
|
|
2883
|
+
const t = typeof outType.type === "string" ? outType.type : void 0;
|
|
2884
|
+
if (t === "json_schema" || t === "json-schema") {
|
|
2885
|
+
const topLevelSchema = outType.schema;
|
|
2886
|
+
if (isObject(topLevelSchema)) {
|
|
2887
|
+
return topLevelSchema;
|
|
2888
|
+
}
|
|
2889
|
+
const nested = outType.json_schema;
|
|
2890
|
+
if (isObject(nested)) {
|
|
2891
|
+
const nestedSchema = nested.schema;
|
|
2892
|
+
if (isObject(nestedSchema)) {
|
|
2893
|
+
return nestedSchema;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
return void 0;
|
|
2897
|
+
}
|
|
2898
|
+
if ("schema" in outType && isObject(outType.schema)) {
|
|
2899
|
+
return outType.schema;
|
|
2900
|
+
}
|
|
2901
|
+
if ("type" in outType && outType.type === "object" || "properties" in outType || "required" in outType) {
|
|
2902
|
+
return outType;
|
|
2903
|
+
}
|
|
2904
|
+
return void 0;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/agents/codexTool.ts
|
|
2908
|
+
var import_agents = require("@openai/agents");
|
|
2909
|
+
function codexTool(options) {
|
|
2910
|
+
const { codexExecute, ...delegate } = options;
|
|
2911
|
+
const agentTool = (0, import_agents.tool)(delegate);
|
|
2912
|
+
const executor = createCodexExecutor(agentTool.name, codexExecute);
|
|
2913
|
+
registerCodexToolExecutor(agentTool.name, executor);
|
|
2914
|
+
return agentTool;
|
|
2915
|
+
}
|
|
2916
|
+
function createCodexExecutor(toolName, customExecutor) {
|
|
2917
|
+
return async ({ arguments: args }) => {
|
|
2918
|
+
const parsedArgs = args ?? {};
|
|
2919
|
+
try {
|
|
2920
|
+
const result = await customExecutor(parsedArgs);
|
|
2921
|
+
return result;
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
throw new Error(`Codex tool '${toolName}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// src/agents/streamFormatter.ts
|
|
2929
|
+
async function formatStream(stream, options = {}) {
|
|
2930
|
+
const state = {
|
|
2931
|
+
text: "",
|
|
2932
|
+
reasoning: "",
|
|
2933
|
+
toolCalls: [],
|
|
2934
|
+
usage: {
|
|
2935
|
+
inputTokens: 0,
|
|
2936
|
+
outputTokens: 0,
|
|
2937
|
+
totalTokens: 0
|
|
2938
|
+
},
|
|
2939
|
+
errors: []
|
|
2940
|
+
};
|
|
2941
|
+
for await (const event of stream) {
|
|
2942
|
+
switch (event.type) {
|
|
2943
|
+
case "response_started":
|
|
2944
|
+
options.onUpdate?.({ usage: state.usage });
|
|
2945
|
+
break;
|
|
2946
|
+
case "output_text_delta":
|
|
2947
|
+
state.text += event.delta;
|
|
2948
|
+
options.onUpdate?.({ text: state.text });
|
|
2949
|
+
break;
|
|
2950
|
+
case "model": {
|
|
2951
|
+
const e = event.event;
|
|
2952
|
+
if (e && typeof e === "object") {
|
|
2953
|
+
if (e.type === "reasoning_delta" && typeof e.delta === "string") {
|
|
2954
|
+
state.reasoning += e.delta;
|
|
2955
|
+
options.onUpdate?.({ reasoning: state.reasoning });
|
|
2956
|
+
} else if (e.type === "reasoning_done" && typeof e.reasoning === "string") {
|
|
2957
|
+
state.reasoning = e.reasoning || state.reasoning;
|
|
2958
|
+
options.onUpdate?.({ reasoning: state.reasoning });
|
|
2959
|
+
} else if (e.type === "error" && e.error && typeof e.error.message === "string") {
|
|
2960
|
+
state.errors.push({ message: e.error.message });
|
|
2961
|
+
options.onUpdate?.({ errors: state.errors.slice() });
|
|
2962
|
+
} else if (typeof e.type === "string" && e.type.startsWith("tool_")) {
|
|
2963
|
+
state.toolCalls.push({
|
|
2964
|
+
name: e.name,
|
|
2965
|
+
input: e.input,
|
|
2966
|
+
output: e.output,
|
|
2967
|
+
status: e.status === "started" || e.status === "completed" ? e.status : void 0
|
|
2968
|
+
});
|
|
2969
|
+
options.onUpdate?.({ toolCalls: state.toolCalls.slice() });
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
break;
|
|
2973
|
+
}
|
|
2974
|
+
case "response_done":
|
|
2975
|
+
state.responseId = event.response.id;
|
|
2976
|
+
{
|
|
2977
|
+
const u = event.response.usage;
|
|
2978
|
+
const mergeDetails = (arr) => {
|
|
2979
|
+
if (!arr || arr.length === 0) return void 0;
|
|
2980
|
+
const out = {};
|
|
2981
|
+
for (const rec of arr) {
|
|
2982
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
2983
|
+
out[k] = (out[k] ?? 0) + (typeof v === "number" ? v : 0);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return out;
|
|
2987
|
+
};
|
|
2988
|
+
const inputDetails = mergeDetails(u.inputTokensDetails);
|
|
2989
|
+
const outputDetails = mergeDetails(u.outputTokensDetails);
|
|
2990
|
+
state.usage = {
|
|
2991
|
+
requests: u.requests,
|
|
2992
|
+
inputTokens: u.inputTokens ?? 0,
|
|
2993
|
+
outputTokens: u.outputTokens ?? 0,
|
|
2994
|
+
totalTokens: u.totalTokens ?? 0,
|
|
2995
|
+
inputTokensDetails: inputDetails,
|
|
2996
|
+
outputTokensDetails: outputDetails
|
|
2997
|
+
};
|
|
2998
|
+
state.cachedTokens = inputDetails?.cachedTokens ?? state.cachedTokens;
|
|
2999
|
+
}
|
|
3000
|
+
if (event.response.providerData && typeof event.response.providerData === "object") {
|
|
3001
|
+
state.providerData = event.response.providerData;
|
|
3002
|
+
options.onUpdate?.({ providerData: state.providerData });
|
|
3003
|
+
}
|
|
3004
|
+
options.onUpdate?.({ responseId: state.responseId, usage: state.usage, cachedTokens: state.cachedTokens });
|
|
3005
|
+
break;
|
|
3006
|
+
default:
|
|
3007
|
+
break;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return state;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// src/agents/OpenCodeAgent.ts
|
|
3014
|
+
var import_node_net = __toESM(require("net"));
|
|
3015
|
+
var DEFAULT_MODEL = "anthropic/claude-sonnet-4-5-20250929";
|
|
3016
|
+
var DEFAULT_HOSTNAME = "127.0.0.1";
|
|
3017
|
+
var DEFAULT_PORT = 4096;
|
|
3018
|
+
var opencodeModulePromise = null;
|
|
3019
|
+
async function loadOpencodeModule() {
|
|
3020
|
+
if (!opencodeModulePromise) {
|
|
3021
|
+
opencodeModulePromise = import("@opencode-ai/sdk");
|
|
3022
|
+
}
|
|
3023
|
+
return opencodeModulePromise;
|
|
3024
|
+
}
|
|
3025
|
+
async function isPortAvailable(port, host) {
|
|
3026
|
+
return new Promise((resolve5) => {
|
|
3027
|
+
const tester = import_node_net.default.createServer().once("error", () => resolve5(false)).once("listening", () => tester.close(() => resolve5(true))).listen(port, host);
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
async function findAvailablePort(host, preferred) {
|
|
3031
|
+
if (preferred !== void 0 && await isPortAvailable(preferred, host)) {
|
|
3032
|
+
return preferred;
|
|
3033
|
+
}
|
|
3034
|
+
return new Promise((resolve5, reject) => {
|
|
3035
|
+
const server = import_node_net.default.createServer();
|
|
3036
|
+
server.once("error", reject);
|
|
3037
|
+
server.listen(0, host, () => {
|
|
3038
|
+
const address = server.address();
|
|
3039
|
+
if (!address || typeof address === "string") {
|
|
3040
|
+
server.close(() => reject(new Error("Failed to determine available port")));
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const { port } = address;
|
|
3044
|
+
server.close(() => resolve5(port));
|
|
3045
|
+
});
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
var OpenCodeAgent = class {
|
|
3049
|
+
options;
|
|
3050
|
+
approvalHandler;
|
|
3051
|
+
clientPromise;
|
|
3052
|
+
closeCallback;
|
|
3053
|
+
constructor(options = {}) {
|
|
3054
|
+
this.options = options;
|
|
3055
|
+
this.approvalHandler = options.onApprovalRequest;
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Cleanup method to shut down the OpenCode server if one was started.
|
|
3059
|
+
* Should be called when done using the agent to prevent zombie processes.
|
|
3060
|
+
*/
|
|
3061
|
+
async close() {
|
|
3062
|
+
if (this.closeCallback) {
|
|
3063
|
+
this.closeCallback();
|
|
3064
|
+
this.closeCallback = void 0;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
async delegate(task) {
|
|
3068
|
+
return this.executeTask(task);
|
|
3069
|
+
}
|
|
3070
|
+
async delegateStreaming(task, onEvent, sessionId) {
|
|
3071
|
+
return this.executeTask(task, { sessionId, onEvent });
|
|
3072
|
+
}
|
|
3073
|
+
async resume(sessionId, task) {
|
|
3074
|
+
return this.executeTask(task, { sessionId });
|
|
3075
|
+
}
|
|
3076
|
+
async workflow(steps) {
|
|
3077
|
+
const results = [];
|
|
3078
|
+
let sessionId;
|
|
3079
|
+
for (const step of steps) {
|
|
3080
|
+
const result = await this.executeTask(step, { sessionId });
|
|
3081
|
+
results.push(result);
|
|
3082
|
+
if (!result.success) {
|
|
3083
|
+
break;
|
|
3084
|
+
}
|
|
3085
|
+
sessionId = result.sessionId;
|
|
3086
|
+
}
|
|
3087
|
+
return results;
|
|
3088
|
+
}
|
|
3089
|
+
async executeTask(prompt, options) {
|
|
3090
|
+
let sessionId = options?.sessionId;
|
|
3091
|
+
try {
|
|
3092
|
+
const client = await this.ensureClient();
|
|
3093
|
+
sessionId = await this.ensureSession(client, sessionId, prompt);
|
|
3094
|
+
const shouldStream = Boolean(this.approvalHandler || options?.onEvent);
|
|
3095
|
+
const controller = new AbortController();
|
|
3096
|
+
const watcher = shouldStream ? this.watchEvents(client, sessionId, options?.onEvent, controller.signal).catch((error) => {
|
|
3097
|
+
if (!controller.signal.aborted) {
|
|
3098
|
+
throw error;
|
|
3099
|
+
}
|
|
3100
|
+
}) : null;
|
|
3101
|
+
try {
|
|
3102
|
+
const promptBody = {
|
|
3103
|
+
parts: [{ type: "text", text: prompt }]
|
|
3104
|
+
};
|
|
3105
|
+
const parsedModel = this.parseModel(this.options.model ?? DEFAULT_MODEL);
|
|
3106
|
+
if (parsedModel) {
|
|
3107
|
+
promptBody.model = parsedModel;
|
|
3108
|
+
}
|
|
3109
|
+
const response = await client.session.prompt({
|
|
3110
|
+
path: { id: sessionId },
|
|
3111
|
+
body: promptBody,
|
|
3112
|
+
query: { directory: this.getWorkingDirectory() }
|
|
3113
|
+
});
|
|
3114
|
+
const data = this.extractData(response);
|
|
3115
|
+
return {
|
|
3116
|
+
sessionId,
|
|
3117
|
+
threadId: sessionId,
|
|
3118
|
+
output: this.collectText(data),
|
|
3119
|
+
success: true,
|
|
3120
|
+
usage: this.toUsage(data)
|
|
3121
|
+
};
|
|
3122
|
+
} finally {
|
|
3123
|
+
if (watcher) {
|
|
3124
|
+
controller.abort();
|
|
3125
|
+
await watcher;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
} catch (error) {
|
|
3129
|
+
return {
|
|
3130
|
+
sessionId: sessionId ?? "",
|
|
3131
|
+
threadId: sessionId,
|
|
3132
|
+
output: "",
|
|
3133
|
+
success: false,
|
|
3134
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
async ensureClient() {
|
|
3139
|
+
if (this.clientPromise) {
|
|
3140
|
+
return this.clientPromise;
|
|
3141
|
+
}
|
|
3142
|
+
if (this.options.clientFactory) {
|
|
3143
|
+
this.clientPromise = this.options.clientFactory().then(({ client }) => client);
|
|
3144
|
+
return this.clientPromise;
|
|
3145
|
+
}
|
|
3146
|
+
if (this.options.baseUrl) {
|
|
3147
|
+
this.clientPromise = loadOpencodeModule().then(
|
|
3148
|
+
({ createOpencodeClient }) => createOpencodeClient({
|
|
3149
|
+
baseUrl: this.options.baseUrl
|
|
3150
|
+
})
|
|
3151
|
+
);
|
|
3152
|
+
return this.clientPromise;
|
|
3153
|
+
}
|
|
3154
|
+
this.clientPromise = loadOpencodeModule().then(async ({ createOpencode }) => {
|
|
3155
|
+
const hostname = this.options.hostname ?? DEFAULT_HOSTNAME;
|
|
3156
|
+
const port = await findAvailablePort(hostname, this.options.port ?? DEFAULT_PORT);
|
|
3157
|
+
const { client, server } = await createOpencode({ hostname, port, config: this.options.config });
|
|
3158
|
+
this.closeCallback = () => server.close();
|
|
3159
|
+
return client;
|
|
3160
|
+
});
|
|
3161
|
+
return this.clientPromise;
|
|
3162
|
+
}
|
|
3163
|
+
async ensureSession(client, existingId, prompt) {
|
|
3164
|
+
if (existingId) {
|
|
3165
|
+
return existingId;
|
|
3166
|
+
}
|
|
3167
|
+
const result = await client.session.create({
|
|
3168
|
+
body: {
|
|
3169
|
+
title: this.options.title ?? this.createSessionTitle(prompt)
|
|
3170
|
+
},
|
|
3171
|
+
query: { directory: this.getWorkingDirectory() }
|
|
3172
|
+
});
|
|
3173
|
+
const session = this.extractData(result);
|
|
3174
|
+
return session.id;
|
|
3175
|
+
}
|
|
3176
|
+
createSessionTitle(prompt) {
|
|
3177
|
+
const [firstLineRaw = ""] = prompt.trim().split(/\r?\n/);
|
|
3178
|
+
const firstLine = firstLineRaw || "OpenCode Session";
|
|
3179
|
+
return firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine;
|
|
3180
|
+
}
|
|
3181
|
+
parseModel(model) {
|
|
3182
|
+
if (!model) {
|
|
3183
|
+
return void 0;
|
|
3184
|
+
}
|
|
3185
|
+
if (model.includes("/")) {
|
|
3186
|
+
const [providerPart, modelPart] = model.split("/", 2);
|
|
3187
|
+
const providerID = providerPart || "anthropic";
|
|
3188
|
+
const modelID = modelPart || providerPart || model;
|
|
3189
|
+
return { providerID, modelID };
|
|
3190
|
+
}
|
|
3191
|
+
return { providerID: "anthropic", modelID: model };
|
|
3192
|
+
}
|
|
3193
|
+
collectText(response) {
|
|
3194
|
+
const texts = response.parts?.filter((part) => part.type === "text") ?? [];
|
|
3195
|
+
return texts.map((part) => part.text).join("\n").trim();
|
|
3196
|
+
}
|
|
3197
|
+
toUsage(response) {
|
|
3198
|
+
const tokens = response.info?.tokens;
|
|
3199
|
+
if (!tokens) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
return {
|
|
3203
|
+
input_tokens: tokens.input ?? 0,
|
|
3204
|
+
output_tokens: tokens.output ?? 0,
|
|
3205
|
+
cached_input_tokens: tokens.cache?.read ?? 0
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
extractData(result) {
|
|
3209
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
3210
|
+
const record = result;
|
|
3211
|
+
if (record.data !== void 0) {
|
|
3212
|
+
return record.data;
|
|
3213
|
+
}
|
|
3214
|
+
throw new Error(this.describeError(record.error));
|
|
3215
|
+
}
|
|
3216
|
+
return result;
|
|
3217
|
+
}
|
|
3218
|
+
describeError(error) {
|
|
3219
|
+
if (!error) {
|
|
3220
|
+
return "Unknown OpenCode error";
|
|
3221
|
+
}
|
|
3222
|
+
if (typeof error === "string") {
|
|
3223
|
+
return error;
|
|
3224
|
+
}
|
|
3225
|
+
if (error instanceof Error) {
|
|
3226
|
+
return error.message;
|
|
3227
|
+
}
|
|
3228
|
+
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
3229
|
+
return error.message;
|
|
3230
|
+
}
|
|
3231
|
+
return JSON.stringify(error);
|
|
3232
|
+
}
|
|
3233
|
+
async watchEvents(client, sessionId, onEvent, signal) {
|
|
3234
|
+
const { stream } = await client.event.subscribe({
|
|
3235
|
+
signal,
|
|
3236
|
+
query: { directory: this.getWorkingDirectory() }
|
|
3237
|
+
});
|
|
3238
|
+
const handledPermissions = /* @__PURE__ */ new Set();
|
|
3239
|
+
for await (const event of stream) {
|
|
3240
|
+
if (signal.aborted) {
|
|
3241
|
+
break;
|
|
3242
|
+
}
|
|
3243
|
+
const targetSessionId = this.extractSessionId(event);
|
|
3244
|
+
if (this.approvalHandler && event.type === "permission.updated") {
|
|
3245
|
+
const permission = event.properties;
|
|
3246
|
+
if (permission.sessionID === sessionId && !handledPermissions.has(permission.id)) {
|
|
3247
|
+
handledPermissions.add(permission.id);
|
|
3248
|
+
await this.respondToPermission(client, permission);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
if (onEvent && targetSessionId === sessionId) {
|
|
3252
|
+
onEvent(event);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
extractSessionId(event) {
|
|
3257
|
+
const properties = event.properties;
|
|
3258
|
+
if (!properties) {
|
|
3259
|
+
return void 0;
|
|
3260
|
+
}
|
|
3261
|
+
if (typeof properties.sessionID === "string") {
|
|
3262
|
+
return properties.sessionID;
|
|
3263
|
+
}
|
|
3264
|
+
if (typeof properties.info === "object" && properties.info !== null && "sessionID" in properties.info) {
|
|
3265
|
+
const value = properties.info.sessionID;
|
|
3266
|
+
return typeof value === "string" ? value : void 0;
|
|
3267
|
+
}
|
|
3268
|
+
return void 0;
|
|
3269
|
+
}
|
|
3270
|
+
async respondToPermission(client, permission) {
|
|
3271
|
+
if (!this.approvalHandler) {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
const decision = await this.approvalHandler({
|
|
3275
|
+
id: permission.id,
|
|
3276
|
+
type: permission.type,
|
|
3277
|
+
title: permission.title,
|
|
3278
|
+
sessionId: permission.sessionID,
|
|
3279
|
+
metadata: permission.metadata ?? {},
|
|
3280
|
+
pattern: Array.isArray(permission.pattern) ? permission.pattern.slice() : permission.pattern
|
|
3281
|
+
});
|
|
3282
|
+
const response = this.normalizeDecision(decision);
|
|
3283
|
+
await client.postSessionIdPermissionsPermissionId({
|
|
3284
|
+
path: {
|
|
3285
|
+
id: permission.sessionID,
|
|
3286
|
+
permissionID: permission.id
|
|
3287
|
+
},
|
|
3288
|
+
body: { response }
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
normalizeDecision(decision) {
|
|
3292
|
+
if (typeof decision === "boolean") {
|
|
3293
|
+
return decision ? "once" : "reject";
|
|
3294
|
+
}
|
|
3295
|
+
if (typeof decision === "string") {
|
|
3296
|
+
return decision;
|
|
3297
|
+
}
|
|
3298
|
+
return decision.response;
|
|
3299
|
+
}
|
|
3300
|
+
getWorkingDirectory() {
|
|
3301
|
+
return this.options.workingDirectory ?? process.cwd();
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
|
|
3305
|
+
// src/cloudTasks.ts
|
|
3306
|
+
var CloudTasks = class {
|
|
3307
|
+
constructor(options = {}) {
|
|
3308
|
+
this.options = options;
|
|
3309
|
+
}
|
|
3310
|
+
binding() {
|
|
3311
|
+
const b = getNativeBinding();
|
|
3312
|
+
if (!b) throw new Error("Native binding not available");
|
|
3313
|
+
return b;
|
|
3314
|
+
}
|
|
3315
|
+
async list(env) {
|
|
3316
|
+
const b = this.binding();
|
|
3317
|
+
if (!b.cloudTasksList) throw new Error("cloudTasksList is not available in this build");
|
|
3318
|
+
const json = await b.cloudTasksList(env, this.options.baseUrl, this.options.apiKey);
|
|
3319
|
+
return JSON.parse(json);
|
|
3320
|
+
}
|
|
3321
|
+
async getDiff(taskId) {
|
|
3322
|
+
const b = this.binding();
|
|
3323
|
+
if (!b.cloudTasksGetDiff) throw new Error("cloudTasksGetDiff is not available in this build");
|
|
3324
|
+
const json = await b.cloudTasksGetDiff(taskId, this.options.baseUrl, this.options.apiKey);
|
|
3325
|
+
const parsed = JSON.parse(json);
|
|
3326
|
+
return parsed.diff ?? null;
|
|
3327
|
+
}
|
|
3328
|
+
async applyPreflight(taskId, diffOverride) {
|
|
3329
|
+
const b = this.binding();
|
|
3330
|
+
if (!b.cloudTasksApplyPreflight) {
|
|
3331
|
+
throw new Error("cloudTasksApplyPreflight is not available in this build");
|
|
3332
|
+
}
|
|
3333
|
+
const json = await b.cloudTasksApplyPreflight(
|
|
3334
|
+
taskId,
|
|
3335
|
+
diffOverride,
|
|
3336
|
+
this.options.baseUrl,
|
|
3337
|
+
this.options.apiKey
|
|
3338
|
+
);
|
|
3339
|
+
return JSON.parse(json);
|
|
3340
|
+
}
|
|
3341
|
+
async apply(taskId, diffOverride) {
|
|
3342
|
+
const b = this.binding();
|
|
3343
|
+
if (!b.cloudTasksApply) throw new Error("cloudTasksApply is not available in this build");
|
|
3344
|
+
const json = await b.cloudTasksApply(
|
|
3345
|
+
taskId,
|
|
3346
|
+
diffOverride,
|
|
3347
|
+
this.options.baseUrl,
|
|
3348
|
+
this.options.apiKey
|
|
3349
|
+
);
|
|
3350
|
+
return JSON.parse(json);
|
|
3351
|
+
}
|
|
3352
|
+
async create(envId, prompt, opts) {
|
|
3353
|
+
const b = this.binding();
|
|
3354
|
+
if (!b.cloudTasksCreate) throw new Error("cloudTasksCreate is not available in this build");
|
|
3355
|
+
const json = await b.cloudTasksCreate(
|
|
3356
|
+
envId,
|
|
3357
|
+
prompt,
|
|
3358
|
+
opts?.gitRef,
|
|
3359
|
+
opts?.qaMode,
|
|
3360
|
+
opts?.bestOfN,
|
|
3361
|
+
this.options.baseUrl,
|
|
3362
|
+
this.options.apiKey
|
|
3363
|
+
);
|
|
3364
|
+
return JSON.parse(json);
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
// src/logging/types.ts
|
|
3369
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
3370
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
3371
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
3372
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
3373
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
3374
|
+
LogLevel2[LogLevel2["SILENT"] = 4] = "SILENT";
|
|
3375
|
+
return LogLevel2;
|
|
3376
|
+
})(LogLevel || {});
|
|
3377
|
+
|
|
3378
|
+
// src/logging/logger.ts
|
|
3379
|
+
var COLORS = {
|
|
3380
|
+
reset: "\x1B[0m",
|
|
3381
|
+
// Log levels
|
|
3382
|
+
debug: "\x1B[90m",
|
|
3383
|
+
// Gray
|
|
3384
|
+
info: "\x1B[36m",
|
|
3385
|
+
// Cyan
|
|
3386
|
+
warn: "\x1B[33m",
|
|
3387
|
+
// Yellow
|
|
3388
|
+
error: "\x1B[31m",
|
|
3389
|
+
// Red
|
|
3390
|
+
// Scopes
|
|
3391
|
+
thread: "\x1B[94m",
|
|
3392
|
+
// Bright blue
|
|
3393
|
+
merge: "\x1B[35m",
|
|
3394
|
+
// Magenta
|
|
3395
|
+
git: "\x1B[34m",
|
|
3396
|
+
// Blue
|
|
3397
|
+
coordinator: "\x1B[36m",
|
|
3398
|
+
// Cyan
|
|
3399
|
+
worker: "\x1B[33m",
|
|
3400
|
+
// Yellow
|
|
3401
|
+
supervisor: "\x1B[95m",
|
|
3402
|
+
// Bright magenta
|
|
3403
|
+
reviewer: "\x1B[32m",
|
|
3404
|
+
// Green
|
|
3405
|
+
validation: "\x1B[92m",
|
|
3406
|
+
// Bright green
|
|
3407
|
+
lsp: "\x1B[96m",
|
|
3408
|
+
// Bright cyan
|
|
3409
|
+
agent: "\x1B[93m",
|
|
3410
|
+
// Bright yellow
|
|
3411
|
+
provider: "\x1B[91m",
|
|
3412
|
+
// Bright red
|
|
3413
|
+
ci: "\x1B[35m",
|
|
3414
|
+
// Magenta
|
|
3415
|
+
test: "\x1B[32m",
|
|
3416
|
+
// Green
|
|
3417
|
+
system: "\x1B[37m"
|
|
3418
|
+
// White
|
|
3419
|
+
};
|
|
3420
|
+
var consoleOutput = {
|
|
3421
|
+
debug: (msg) => console.debug(msg),
|
|
3422
|
+
info: (msg) => console.log(msg),
|
|
3423
|
+
warn: (msg) => console.warn(msg),
|
|
3424
|
+
error: (msg) => console.error(msg)
|
|
3425
|
+
};
|
|
3426
|
+
var Logger = class _Logger {
|
|
3427
|
+
level;
|
|
3428
|
+
colors;
|
|
3429
|
+
timestamps;
|
|
3430
|
+
prefix;
|
|
3431
|
+
json;
|
|
3432
|
+
output;
|
|
3433
|
+
constructor(config = {}) {
|
|
3434
|
+
this.level = config.level ?? 1 /* INFO */;
|
|
3435
|
+
this.colors = config.colors ?? (typeof process !== "undefined" && process.stdout?.isTTY === true);
|
|
3436
|
+
this.timestamps = config.timestamps ?? false;
|
|
3437
|
+
this.prefix = config.prefix ?? "";
|
|
3438
|
+
this.json = config.json ?? false;
|
|
3439
|
+
this.output = config.output ?? consoleOutput;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Create a new logger with modified configuration
|
|
3443
|
+
*/
|
|
3444
|
+
configure(config) {
|
|
3445
|
+
return new _Logger({
|
|
3446
|
+
level: config.level ?? this.level,
|
|
3447
|
+
colors: config.colors ?? this.colors,
|
|
3448
|
+
timestamps: config.timestamps ?? this.timestamps,
|
|
3449
|
+
prefix: config.prefix ?? this.prefix,
|
|
3450
|
+
json: config.json ?? this.json,
|
|
3451
|
+
output: config.output ?? this.output
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Create a scoped logger
|
|
3456
|
+
*/
|
|
3457
|
+
scope(scope, subject) {
|
|
3458
|
+
return new ScopedLogger(this, scope, subject);
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Log a debug message
|
|
3462
|
+
*/
|
|
3463
|
+
debug(message, data) {
|
|
3464
|
+
this.log(0 /* DEBUG */, message, data);
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Log an info message
|
|
3468
|
+
*/
|
|
3469
|
+
info(message, data) {
|
|
3470
|
+
this.log(1 /* INFO */, message, data);
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Log a warning message
|
|
3474
|
+
*/
|
|
3475
|
+
warn(message, data) {
|
|
3476
|
+
this.log(2 /* WARN */, message, data);
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Log an error message
|
|
3480
|
+
*/
|
|
3481
|
+
error(message, data) {
|
|
3482
|
+
this.log(3 /* ERROR */, message, data);
|
|
3483
|
+
}
|
|
3484
|
+
/**
|
|
3485
|
+
* Internal log method
|
|
3486
|
+
*/
|
|
3487
|
+
log(level, message, data, scope, subject) {
|
|
3488
|
+
if (level < this.level) {
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
if (this.json) {
|
|
3492
|
+
this.logJson(level, message, data, scope, subject);
|
|
3493
|
+
} else {
|
|
3494
|
+
this.logFormatted(level, message, scope, subject);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Log in JSON format
|
|
3499
|
+
*/
|
|
3500
|
+
logJson(level, message, data, scope, subject) {
|
|
3501
|
+
const entry = {
|
|
3502
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3503
|
+
level: LogLevel[level],
|
|
3504
|
+
message,
|
|
3505
|
+
...scope && { scope },
|
|
3506
|
+
...subject && { subject },
|
|
3507
|
+
...data && { data }
|
|
3508
|
+
};
|
|
3509
|
+
const output = JSON.stringify(entry);
|
|
3510
|
+
this.output.info(output);
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Log in formatted text
|
|
3514
|
+
*/
|
|
3515
|
+
logFormatted(level, message, scope, subject) {
|
|
3516
|
+
const parts = [];
|
|
3517
|
+
if (this.timestamps) {
|
|
3518
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3519
|
+
parts.push(this.colors ? `\x1B[90m[${ts}]\x1B[0m` : `[${ts}]`);
|
|
3520
|
+
}
|
|
3521
|
+
const levelName = LogLevel[level];
|
|
3522
|
+
if (this.colors) {
|
|
3523
|
+
const color = COLORS[levelName.toLowerCase()] ?? COLORS.reset;
|
|
3524
|
+
parts.push(`${color}[${levelName}]${COLORS.reset}`);
|
|
3525
|
+
} else {
|
|
3526
|
+
parts.push(`[${levelName}]`);
|
|
3527
|
+
}
|
|
3528
|
+
if (scope) {
|
|
3529
|
+
const label = subject ? `${scope}:${subject}` : scope;
|
|
3530
|
+
if (this.colors) {
|
|
3531
|
+
const color = COLORS[scope] ?? COLORS.reset;
|
|
3532
|
+
parts.push(`${color}[${label}]${COLORS.reset}`);
|
|
3533
|
+
} else {
|
|
3534
|
+
parts.push(`[${label}]`);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
if (this.prefix) {
|
|
3538
|
+
parts.push(this.prefix);
|
|
3539
|
+
}
|
|
3540
|
+
parts.push(message);
|
|
3541
|
+
const formatted = parts.join(" ");
|
|
3542
|
+
switch (level) {
|
|
3543
|
+
case 0 /* DEBUG */:
|
|
3544
|
+
this.output.debug(formatted);
|
|
3545
|
+
break;
|
|
3546
|
+
case 1 /* INFO */:
|
|
3547
|
+
this.output.info(formatted);
|
|
3548
|
+
break;
|
|
3549
|
+
case 2 /* WARN */:
|
|
3550
|
+
this.output.warn(formatted);
|
|
3551
|
+
break;
|
|
3552
|
+
case 3 /* ERROR */:
|
|
3553
|
+
this.output.error(formatted);
|
|
3554
|
+
break;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Internal scoped log method (used by ScopedLogger)
|
|
3559
|
+
*/
|
|
3560
|
+
logScoped(level, message, scope, subject, data) {
|
|
3561
|
+
this.log(level, message, data, scope, subject);
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
var ScopedLogger = class {
|
|
3565
|
+
constructor(logger2, scope, subject) {
|
|
3566
|
+
this.logger = logger2;
|
|
3567
|
+
this.scope = scope;
|
|
3568
|
+
this.subject = subject;
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Log a debug message
|
|
3572
|
+
*/
|
|
3573
|
+
debug(message, data) {
|
|
3574
|
+
this.logger.logScoped(0 /* DEBUG */, message, this.scope, this.subject, data);
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Log an info message
|
|
3578
|
+
*/
|
|
3579
|
+
info(message, data) {
|
|
3580
|
+
this.logger.logScoped(1 /* INFO */, message, this.scope, this.subject, data);
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Log a warning message
|
|
3584
|
+
*/
|
|
3585
|
+
warn(message, data) {
|
|
3586
|
+
this.logger.logScoped(2 /* WARN */, message, this.scope, this.subject, data);
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Log an error message
|
|
3590
|
+
*/
|
|
3591
|
+
error(message, data) {
|
|
3592
|
+
this.logger.logScoped(3 /* ERROR */, message, this.scope, this.subject, data);
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Create a ThreadLoggingSink adapter
|
|
3596
|
+
*/
|
|
3597
|
+
asThreadSink() {
|
|
3598
|
+
return {
|
|
3599
|
+
info: (message) => this.info(message),
|
|
3600
|
+
warn: (message) => this.warn(message)
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
var logger = new Logger({
|
|
3605
|
+
level: process.env.CODEX_LOG_LEVEL ? LogLevel[process.env.CODEX_LOG_LEVEL] ?? 1 /* INFO */ : 1 /* INFO */,
|
|
3606
|
+
colors: process.env.CODEX_LOG_COLORS !== "false",
|
|
3607
|
+
timestamps: process.env.CODEX_LOG_TIMESTAMPS === "true",
|
|
3608
|
+
json: process.env.CODEX_LOG_JSON === "true"
|
|
3609
|
+
});
|
|
3610
|
+
|
|
3611
|
+
// src/logging/threadLogger.ts
|
|
3612
|
+
var THREAD_EVENT_TEXT_LIMIT = 400;
|
|
3613
|
+
function createThreadLogger(scopedLogger, onUsage) {
|
|
3614
|
+
return {
|
|
3615
|
+
info: (message) => scopedLogger.info(message),
|
|
3616
|
+
warn: (message) => scopedLogger.warn(message),
|
|
3617
|
+
recordUsage: onUsage
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
async function runThreadTurnWithLogs(thread, sink, prompt, turnOptions) {
|
|
3621
|
+
const unsubscribe = thread.onEvent((event) => logThreadEvent(event, sink));
|
|
3622
|
+
try {
|
|
3623
|
+
if (turnOptions) {
|
|
3624
|
+
return await thread.run(prompt, turnOptions);
|
|
3625
|
+
}
|
|
3626
|
+
return await thread.run(prompt);
|
|
3627
|
+
} finally {
|
|
3628
|
+
unsubscribe();
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
function logThreadEvent(event, sink) {
|
|
3632
|
+
switch (event.type) {
|
|
3633
|
+
case "thread.started":
|
|
3634
|
+
sink.info(`Thread started (id: ${event.thread_id})`);
|
|
3635
|
+
return;
|
|
3636
|
+
case "turn.started":
|
|
3637
|
+
sink.info("Turn started");
|
|
3638
|
+
return;
|
|
3639
|
+
case "turn.completed":
|
|
3640
|
+
sink.info(
|
|
3641
|
+
`Turn completed (input ${event.usage.input_tokens}, cached ${event.usage.cached_input_tokens}, output ${event.usage.output_tokens})`
|
|
3642
|
+
);
|
|
3643
|
+
if ("recordUsage" in sink && sink.recordUsage) {
|
|
3644
|
+
sink.recordUsage(event.usage);
|
|
3645
|
+
}
|
|
3646
|
+
return;
|
|
3647
|
+
case "turn.failed":
|
|
3648
|
+
sink.warn(`Turn failed: ${event.error.message}`);
|
|
3649
|
+
return;
|
|
3650
|
+
case "item.started":
|
|
3651
|
+
sink.info(`Item started: ${describeThreadItemForLog(event.item)}`);
|
|
3652
|
+
return;
|
|
3653
|
+
case "item.updated":
|
|
3654
|
+
sink.info(`Item updated: ${describeThreadItemForLog(event.item)}`);
|
|
3655
|
+
return;
|
|
3656
|
+
case "item.completed": {
|
|
3657
|
+
const message = `Item completed: ${describeThreadItemForLog(event.item)}`;
|
|
3658
|
+
if (event.item.type === "error") {
|
|
3659
|
+
sink.warn(message);
|
|
3660
|
+
} else {
|
|
3661
|
+
sink.info(message);
|
|
3662
|
+
}
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
case "background_event":
|
|
3666
|
+
sink.info(`Background: ${summarizeLogText(event.message)}`);
|
|
3667
|
+
return;
|
|
3668
|
+
case "exited_review_mode":
|
|
3669
|
+
sink.info("Exited review mode");
|
|
3670
|
+
return;
|
|
3671
|
+
case "error":
|
|
3672
|
+
sink.warn(`Stream error: ${event.message}`);
|
|
3673
|
+
return;
|
|
3674
|
+
case "raw_event":
|
|
3675
|
+
return;
|
|
3676
|
+
default:
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function describeThreadItemForLog(item) {
|
|
3681
|
+
switch (item.type) {
|
|
3682
|
+
case "agent_message":
|
|
3683
|
+
return `agent message \u2192 ${summarizeLogText(item.text)}`;
|
|
3684
|
+
case "reasoning":
|
|
3685
|
+
return `reasoning \u2192 ${summarizeLogText(item.text)}`;
|
|
3686
|
+
case "command_execution": {
|
|
3687
|
+
const exit = item.exit_code !== void 0 ? ` exit=${item.exit_code}` : "";
|
|
3688
|
+
return `command "${summarizeLogText(item.command)}" [${item.status}${exit}]`;
|
|
3689
|
+
}
|
|
3690
|
+
case "file_change": {
|
|
3691
|
+
const changeList = item.changes.map((change) => `${change.kind}:${change.path}`).join(", ");
|
|
3692
|
+
return `file change [${item.status}] ${summarizeLogText(changeList)}`;
|
|
3693
|
+
}
|
|
3694
|
+
case "mcp_tool_call":
|
|
3695
|
+
return `mcp ${item.server}.${item.tool} [${item.status}]`;
|
|
3696
|
+
case "web_search":
|
|
3697
|
+
return `web search "${summarizeLogText(item.query)}"`;
|
|
3698
|
+
case "todo_list": {
|
|
3699
|
+
const completed = item.items.filter((todo) => todo.completed).length;
|
|
3700
|
+
return `todo list ${completed}/${item.items.length}`;
|
|
3701
|
+
}
|
|
3702
|
+
case "error":
|
|
3703
|
+
return `error \u2192 ${summarizeLogText(item.message)}`;
|
|
3704
|
+
default: {
|
|
3705
|
+
const _exhaustive = item;
|
|
3706
|
+
return "unknown event";
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
function summarizeLogText(text, limit = THREAD_EVENT_TEXT_LIMIT) {
|
|
3711
|
+
if (!text) {
|
|
3712
|
+
return "";
|
|
3713
|
+
}
|
|
3714
|
+
const flattened = text.replace(/\s+/g, " ").trim();
|
|
3715
|
+
if (flattened.length <= limit) {
|
|
3716
|
+
return flattened;
|
|
3717
|
+
}
|
|
3718
|
+
return `${flattened.slice(0, limit)}\u2026`;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
// src/reverie/constants.ts
|
|
3722
|
+
var DEFAULT_REVERIE_LIMIT = 6;
|
|
3723
|
+
var DEFAULT_REVERIE_MAX_CANDIDATES = 80;
|
|
3724
|
+
var REVERIE_EMBED_MODEL = "BAAI/bge-large-en-v1.5";
|
|
3725
|
+
var REVERIE_RERANKER_MODEL = "rozgo/bge-reranker-v2-m3";
|
|
3726
|
+
var REVERIE_CANDIDATE_MULTIPLIER = 3;
|
|
3727
|
+
var REVERIE_LLM_GRADE_THRESHOLD = 0.7;
|
|
3728
|
+
var DEFAULT_RERANKER_TOP_K = 20;
|
|
3729
|
+
var DEFAULT_RERANKER_BATCH_SIZE = 8;
|
|
3730
|
+
|
|
3731
|
+
// src/reverie/quality.ts
|
|
3732
|
+
function isValidReverieExcerpt(excerpt) {
|
|
3733
|
+
if (!excerpt || excerpt.trim().length < 20) {
|
|
3734
|
+
return false;
|
|
3735
|
+
}
|
|
3736
|
+
const trimmed = excerpt.trim();
|
|
3737
|
+
const normalized = trimmed.toLowerCase();
|
|
3738
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3739
|
+
const rawTokens = trimmed.split(/\s+/).filter(Boolean);
|
|
3740
|
+
const tokens = rawTokens.map((token) => token.toLowerCase());
|
|
3741
|
+
if (rawTokens.length === 0) {
|
|
3742
|
+
return false;
|
|
3743
|
+
}
|
|
3744
|
+
const uppercaseTokens = rawTokens.filter((token) => {
|
|
3745
|
+
const alphabetic = token.replace(/[^a-z]/gi, "");
|
|
3746
|
+
return alphabetic.length >= 3 && alphabetic === alphabetic.toUpperCase();
|
|
3747
|
+
});
|
|
3748
|
+
const uppercaseRatio = uppercaseTokens.length / rawTokens.length;
|
|
3749
|
+
const snakeTokens = rawTokens.filter((token) => token.includes("_"));
|
|
3750
|
+
const underscoreRatio = snakeTokens.length / rawTokens.length;
|
|
3751
|
+
const headingLines = lines.filter((line) => /^#{1,6}\s/.test(line));
|
|
3752
|
+
const bulletLines = lines.filter((line) => /^\s*[\-\*]\s/.test(line));
|
|
3753
|
+
const numericBulletLines = lines.filter((line) => /^\s*\d+[\).]/.test(line));
|
|
3754
|
+
const colonLabelLines = lines.filter((line) => /^[A-Za-z0-9 _-]{1,24}:/.test(line));
|
|
3755
|
+
const headingRatio = headingLines.length / Math.max(lines.length, 1);
|
|
3756
|
+
const bulletRatio = bulletLines.length / Math.max(lines.length, 1);
|
|
3757
|
+
const colonLabelRatio = colonLabelLines.length / Math.max(lines.length, 1);
|
|
3758
|
+
const numericRatio = numericBulletLines.length / Math.max(lines.length, 1);
|
|
3759
|
+
const enumeratedRatio = (bulletLines.length + numericBulletLines.length) / Math.max(lines.length, 1);
|
|
3760
|
+
const initialTitleCaseRun = (() => {
|
|
3761
|
+
let run = 0;
|
|
3762
|
+
for (const token of rawTokens) {
|
|
3763
|
+
const cleaned = token.replace(/[^a-z]/gi, "");
|
|
3764
|
+
if (cleaned.length === 0) {
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
const rest = cleaned.slice(1);
|
|
3768
|
+
const isTitleCase = cleaned[0]?.toUpperCase() === cleaned[0] && rest === rest.toLowerCase();
|
|
3769
|
+
const isAllCaps = cleaned.length >= 2 && cleaned === cleaned.toUpperCase();
|
|
3770
|
+
if (isTitleCase || isAllCaps) {
|
|
3771
|
+
run += 1;
|
|
3772
|
+
} else {
|
|
3773
|
+
break;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
return run;
|
|
3777
|
+
})();
|
|
3778
|
+
const tokenFrequencies = tokens.reduce((map, token) => map.set(token, (map.get(token) ?? 0) + 1), /* @__PURE__ */ new Map());
|
|
3779
|
+
const frequencyValues = Array.from(tokenFrequencies.values());
|
|
3780
|
+
const mostCommonTokenCount = Math.max(...frequencyValues);
|
|
3781
|
+
const repeatedWordRatio = mostCommonTokenCount / tokens.length;
|
|
3782
|
+
if (snakeTokens.length >= 2 && underscoreRatio > 0.15) {
|
|
3783
|
+
return false;
|
|
3784
|
+
}
|
|
3785
|
+
if (headingRatio > 0.6 && lines.length <= 4) {
|
|
3786
|
+
return false;
|
|
3787
|
+
}
|
|
3788
|
+
if (initialTitleCaseRun >= 3 && rawTokens.length <= 20) {
|
|
3789
|
+
return false;
|
|
3790
|
+
}
|
|
3791
|
+
if (enumeratedRatio > 0.6 && lines.length >= 3) {
|
|
3792
|
+
return false;
|
|
3793
|
+
}
|
|
3794
|
+
const metadataScore = [
|
|
3795
|
+
uppercaseRatio > 0.45,
|
|
3796
|
+
underscoreRatio > 0.2,
|
|
3797
|
+
bulletRatio > 0.7,
|
|
3798
|
+
colonLabelRatio > 0.6 || lines.length <= 2 && colonLabelRatio > 0,
|
|
3799
|
+
initialTitleCaseRun >= 3,
|
|
3800
|
+
repeatedWordRatio > 0.45 && tokens.length > 15,
|
|
3801
|
+
rawTokens.length < 12 && colonLabelRatio > 0,
|
|
3802
|
+
numericRatio > 0.5
|
|
3803
|
+
].filter(Boolean).length;
|
|
3804
|
+
if (metadataScore >= 2) {
|
|
3805
|
+
return false;
|
|
3806
|
+
}
|
|
3807
|
+
const tagMatches = trimmed.match(/<[^>]+>/g) || [];
|
|
3808
|
+
if (tagMatches.length > 3) {
|
|
3809
|
+
return false;
|
|
3810
|
+
}
|
|
3811
|
+
const blockTagMatch = trimmed.match(/^<([a-z0-9_\-]+)>[\s\S]*<\/\1>$/i);
|
|
3812
|
+
if (blockTagMatch) {
|
|
3813
|
+
const tagName = blockTagMatch[1]?.toLowerCase() ?? "";
|
|
3814
|
+
const looksLikeSystem = tagName.includes("system") || tagName.includes("context") || tagName.includes("env");
|
|
3815
|
+
if (tagName.includes("_") || looksLikeSystem) {
|
|
3816
|
+
return false;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (/\(\d{2,3}%\)\s*$/.test(trimmed)) {
|
|
3820
|
+
return false;
|
|
3821
|
+
}
|
|
3822
|
+
const looksJsonLike = (/^\{[\s\S]*\}$/.test(trimmed) || /^\[[\s\S]*\]$/.test(trimmed)) && /"\w+"\s*:/.test(trimmed);
|
|
3823
|
+
if (looksJsonLike) {
|
|
3824
|
+
return false;
|
|
3825
|
+
}
|
|
3826
|
+
return true;
|
|
3827
|
+
}
|
|
3828
|
+
function deduplicateReverieInsights(insights) {
|
|
3829
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
3830
|
+
for (const insight of insights) {
|
|
3831
|
+
const fingerprint = insight.excerpt.slice(0, 100).toLowerCase().replace(/\s+/g, " ");
|
|
3832
|
+
const existing = fingerprintMap.get(fingerprint);
|
|
3833
|
+
if (!existing || insight.relevance > existing.relevance) {
|
|
3834
|
+
fingerprintMap.set(fingerprint, insight);
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
return Array.from(fingerprintMap.values()).sort((a, b) => b.relevance - a.relevance);
|
|
3838
|
+
}
|
|
3839
|
+
function applyQualityPipeline(insights, limit = 10) {
|
|
3840
|
+
const stats = {
|
|
3841
|
+
initial: insights.length,
|
|
3842
|
+
afterValidityFilter: 0,
|
|
3843
|
+
afterDeduplication: 0,
|
|
3844
|
+
final: 0
|
|
3845
|
+
};
|
|
3846
|
+
const validInsights = insights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
3847
|
+
stats.afterValidityFilter = validInsights.length;
|
|
3848
|
+
const deduplicated = deduplicateReverieInsights(validInsights);
|
|
3849
|
+
stats.afterDeduplication = deduplicated.length;
|
|
3850
|
+
const final = deduplicated.slice(0, limit);
|
|
3851
|
+
stats.final = final.length;
|
|
3852
|
+
return { insights: final, stats };
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// src/reverie/boilerplate.ts
|
|
3856
|
+
var DEFAULT_THRESHOLD = 0.8;
|
|
3857
|
+
var DEFAULT_MAX_EXCERPT_LENGTH = 512;
|
|
3858
|
+
var BOILERPLATE_SEEDS = [
|
|
3859
|
+
"<system>Focus on summarizing repo context and keep instructions short.",
|
|
3860
|
+
"<environment_context>Working directory: /repo/codex sandbox_mode: workspace-write network_access: disabled</environment_context>",
|
|
3861
|
+
"# AGENTS.md instructions for this task require you to enumerate files before running commands.",
|
|
3862
|
+
"Tool output: command completed successfully with exit code 0.",
|
|
3863
|
+
"You are coordinating multiple agents. Respond with JSON describing the plan.",
|
|
3864
|
+
"Sandbox env vars: CODEX_SANDBOX=seatbelt CODEX_SANDBOX_NETWORK_DISABLED=1",
|
|
3865
|
+
"1. Inspect repository status; 2. List directories; 3. Review README/AGENTS instructions before acting.",
|
|
3866
|
+
"1. Inventory tooling - run `just --list` for recipes. 2. Verify Rust toolchain. 3. Read AGENTS.md for repo-specific guidance before editing."
|
|
3867
|
+
];
|
|
3868
|
+
var seedVectorsPromise = null;
|
|
3869
|
+
var embeddingDisabled = false;
|
|
3870
|
+
var dot = (a, b) => a.reduce((sum, value, idx) => sum + value * (b[idx] ?? 0), 0);
|
|
3871
|
+
function truncateExcerpt(text, maxLength) {
|
|
3872
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3873
|
+
if (normalized.length <= maxLength) {
|
|
3874
|
+
return normalized;
|
|
3875
|
+
}
|
|
3876
|
+
return normalized.slice(0, maxLength);
|
|
3877
|
+
}
|
|
3878
|
+
async function embedTexts(inputs, projectRoot) {
|
|
3879
|
+
if (embeddingDisabled || inputs.length === 0) {
|
|
3880
|
+
return null;
|
|
3881
|
+
}
|
|
3882
|
+
try {
|
|
3883
|
+
const embeddings = await fastEmbedEmbed({
|
|
3884
|
+
inputs,
|
|
3885
|
+
projectRoot,
|
|
3886
|
+
normalize: true
|
|
3887
|
+
});
|
|
3888
|
+
return embeddings;
|
|
3889
|
+
} catch (error) {
|
|
3890
|
+
embeddingDisabled = true;
|
|
3891
|
+
console.warn(`\u26A0\uFE0F Reverie boilerplate filter disabled (fastEmbedEmbed unavailable: ${error.message ?? error})`);
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
async function getSeedVectors(projectRoot) {
|
|
3896
|
+
if (seedVectorsPromise) {
|
|
3897
|
+
return seedVectorsPromise;
|
|
3898
|
+
}
|
|
3899
|
+
seedVectorsPromise = embedTexts(BOILERPLATE_SEEDS, projectRoot);
|
|
3900
|
+
return seedVectorsPromise;
|
|
3901
|
+
}
|
|
3902
|
+
async function filterBoilerplateInsights(insights, options) {
|
|
3903
|
+
if (insights.length === 0) {
|
|
3904
|
+
return { kept: [], removed: 0 };
|
|
3905
|
+
}
|
|
3906
|
+
const projectRoot = options?.projectRoot;
|
|
3907
|
+
const threshold = options?.threshold ?? DEFAULT_THRESHOLD;
|
|
3908
|
+
const maxExcerpt = options?.maxExcerptLength ?? DEFAULT_MAX_EXCERPT_LENGTH;
|
|
3909
|
+
const seeds = await getSeedVectors(projectRoot);
|
|
3910
|
+
if (!seeds || seeds.length === 0) {
|
|
3911
|
+
return { kept: insights, removed: 0 };
|
|
3912
|
+
}
|
|
3913
|
+
const excerptBatch = insights.map((insight) => truncateExcerpt(insight.excerpt, maxExcerpt));
|
|
3914
|
+
const excerptVectors = await embedTexts(excerptBatch, projectRoot);
|
|
3915
|
+
if (!excerptVectors) {
|
|
3916
|
+
return { kept: insights, removed: 0 };
|
|
3917
|
+
}
|
|
3918
|
+
const kept = [];
|
|
3919
|
+
let removed = 0;
|
|
3920
|
+
for (let i = 0; i < insights.length; i += 1) {
|
|
3921
|
+
const vector = excerptVectors[i];
|
|
3922
|
+
if (!vector) {
|
|
3923
|
+
kept.push(insights[i]);
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
const maxSimilarity = seeds.reduce((currentMax, seedVec) => {
|
|
3927
|
+
const similarity = dot(vector, seedVec);
|
|
3928
|
+
return similarity > currentMax ? similarity : currentMax;
|
|
3929
|
+
}, -Infinity);
|
|
3930
|
+
if (Number.isFinite(maxSimilarity) && maxSimilarity >= threshold) {
|
|
3931
|
+
removed += 1;
|
|
3932
|
+
} else {
|
|
3933
|
+
kept.push(insights[i]);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
if (removed > 0) {
|
|
3937
|
+
console.log(`\u{1F9F9} Reverie boilerplate filter removed ${removed}/${insights.length} excerpts (threshold ${threshold.toFixed(2)})`);
|
|
3938
|
+
}
|
|
3939
|
+
return { kept, removed };
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
// src/reverie/logger.ts
|
|
3943
|
+
function logReverieSearch(query, context) {
|
|
3944
|
+
const contextStr = context ? ` (${context})` : "";
|
|
3945
|
+
console.log(`\u{1F50D} Reverie search${contextStr}: "${query}"`);
|
|
3946
|
+
}
|
|
3947
|
+
function logReverieFiltering(stats) {
|
|
3948
|
+
const { total, afterQuality, afterBoilerplate, afterScore, afterDedup, minScore = 0.7 } = stats;
|
|
3949
|
+
const qualityFiltered = total - afterQuality;
|
|
3950
|
+
const boilerplateStage = afterBoilerplate ?? afterQuality;
|
|
3951
|
+
const boilerplateFiltered = afterQuality - boilerplateStage;
|
|
3952
|
+
const scoreFiltered = boilerplateStage - afterScore;
|
|
3953
|
+
const duplicatesFiltered = afterScore - afterDedup;
|
|
3954
|
+
console.log(
|
|
3955
|
+
`\u{1F4CA} Reverie filtering: ${total} raw \u2192 ${afterQuality} valid \u2192 ${boilerplateStage} conversational \u2192 ${afterScore} high-scoring (\u2265${minScore}) \u2192 ${afterDedup} unique (filtered: ${qualityFiltered} low-quality, ${boilerplateFiltered} boilerplate, ${scoreFiltered} low-score, ${duplicatesFiltered} duplicates)`
|
|
3956
|
+
);
|
|
3957
|
+
}
|
|
3958
|
+
function logReverieInsights(insights, limit = 3) {
|
|
3959
|
+
if (insights.length === 0) {
|
|
3960
|
+
console.log("\u{1F4ED} No reverie insights found");
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
console.log(`\u2728 Top ${Math.min(limit, insights.length)} reverie insights:`);
|
|
3964
|
+
const topInsights = insights.slice(0, limit);
|
|
3965
|
+
for (let i = 0; i < topInsights.length; i++) {
|
|
3966
|
+
const insight = topInsights[i];
|
|
3967
|
+
if (!insight) continue;
|
|
3968
|
+
const score = `${Math.round(insight.relevance * 100)}%`;
|
|
3969
|
+
const excerpt = truncate(insight.excerpt, 150);
|
|
3970
|
+
const insightText = insight.insights.length > 0 ? truncate(insight.insights[0] ?? "", 100) : "";
|
|
3971
|
+
console.log(` ${i + 1}. [${score}] ${excerpt}`);
|
|
3972
|
+
if (insightText) {
|
|
3973
|
+
console.log(` \u2192 ${insightText}`);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
function logReverieHintQuality(stats) {
|
|
3978
|
+
const { totalRaw, afterQuality, afterDedup } = stats;
|
|
3979
|
+
const qualityFiltered = totalRaw - afterQuality;
|
|
3980
|
+
const duplicatesFiltered = afterQuality - afterDedup;
|
|
3981
|
+
if (totalRaw > 0) {
|
|
3982
|
+
console.log(
|
|
3983
|
+
`\u{1FA84} Reverie hint quality: ${totalRaw} raw \u2192 ${afterQuality} valid \u2192 ${afterDedup} unique (filtered ${qualityFiltered} low-quality, ${duplicatesFiltered} duplicates)`
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
function logLLMGrading(stats) {
|
|
3988
|
+
const { total, approved, rejected, minScore = 0.7 } = stats;
|
|
3989
|
+
const approvalRate = total > 0 ? Math.round(approved / total * 100) : 0;
|
|
3990
|
+
console.log(
|
|
3991
|
+
`\u{1F916} LLM grading: ${approved}/${total} approved (${approvalRate}%) [high-scoring \u2265${minScore}, rejected ${rejected}]`
|
|
3992
|
+
);
|
|
3993
|
+
}
|
|
3994
|
+
function logApprovedReveries(insights, maxToShow = 5) {
|
|
3995
|
+
if (insights.length === 0) {
|
|
3996
|
+
console.log(" No reveries passed LLM grading");
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3999
|
+
console.log(` ${insights.length} reveries approved by LLM:`);
|
|
4000
|
+
const toShow = insights.slice(0, maxToShow);
|
|
4001
|
+
for (let i = 0; i < toShow.length; i++) {
|
|
4002
|
+
const insight = toShow[i];
|
|
4003
|
+
if (!insight) continue;
|
|
4004
|
+
const score = insight.relevance.toFixed(2);
|
|
4005
|
+
const preview = truncate(insight.excerpt.replace(/\s+/g, " ").trim(), 200);
|
|
4006
|
+
const insightText = insight.insights[0] || "Context from past work";
|
|
4007
|
+
console.log(` ${i + 1}. [${score}] ${insightText}`);
|
|
4008
|
+
console.log(` "${preview}"`);
|
|
4009
|
+
}
|
|
4010
|
+
if (insights.length > maxToShow) {
|
|
4011
|
+
console.log(` ... and ${insights.length - maxToShow} more`);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
function truncate(text, maxLength) {
|
|
4015
|
+
if (!text) return "";
|
|
4016
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}\u2026` : text;
|
|
4017
|
+
}
|
|
4018
|
+
function logMultiLevelSearch(levels) {
|
|
4019
|
+
if (levels.length === 0) {
|
|
4020
|
+
console.log("\u{1F50D} Multi-level reverie search: (no levels specified)");
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
const levelIcons = {
|
|
4024
|
+
project: "\u{1F310}",
|
|
4025
|
+
branch: "\u{1F33F}",
|
|
4026
|
+
file: "\u{1F4C4}"
|
|
4027
|
+
};
|
|
4028
|
+
const levelLabels = levels.map((level) => `${levelIcons[level]} ${level}`).join(" \u2192 ");
|
|
4029
|
+
console.log(`\u{1F50D} Multi-level reverie search: ${levelLabels}`);
|
|
4030
|
+
}
|
|
4031
|
+
function logLevelResults(level, result) {
|
|
4032
|
+
const levelIcons = {
|
|
4033
|
+
project: "\u{1F310}",
|
|
4034
|
+
branch: "\u{1F33F}",
|
|
4035
|
+
file: "\u{1F4C4}"
|
|
4036
|
+
};
|
|
4037
|
+
const icon = levelIcons[level];
|
|
4038
|
+
const { stats, insights } = result;
|
|
4039
|
+
const filterRate = stats.total > 0 ? Math.round((stats.total - stats.final) / stats.total * 100) : 0;
|
|
4040
|
+
const levelName = level.charAt(0).toUpperCase() + level.slice(1);
|
|
4041
|
+
console.log(
|
|
4042
|
+
` ${icon} ${levelName} level: ${insights.length} insights (${stats.total} \u2192 ${stats.final}, ${filterRate}% filtered)`
|
|
4043
|
+
);
|
|
4044
|
+
if (stats.total > 0) {
|
|
4045
|
+
const qualityFiltered = stats.total - stats.afterQuality;
|
|
4046
|
+
const scoreFiltered = stats.afterQuality - stats.afterScore;
|
|
4047
|
+
const dedupFiltered = stats.afterScore - (stats.afterDedup || stats.afterScore);
|
|
4048
|
+
if (qualityFiltered > 0 || scoreFiltered > 0 || dedupFiltered > 0) {
|
|
4049
|
+
console.log(
|
|
4050
|
+
` \u21B3 Quality: -${qualityFiltered}, Score: -${scoreFiltered}, Dedup: -${dedupFiltered}`
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
function logMultiLevelSummary(results) {
|
|
4056
|
+
const totalInsights = Array.from(results.values()).reduce((sum, result) => sum + result.insights.length, 0);
|
|
4057
|
+
const totalProcessed = Array.from(results.values()).reduce((sum, result) => sum + result.stats.total, 0);
|
|
4058
|
+
console.log(
|
|
4059
|
+
`
|
|
4060
|
+
\u2728 Multi-level search complete: ${totalInsights} total insights (processed ${totalProcessed} candidates across ${results.size} levels)`
|
|
4061
|
+
);
|
|
4062
|
+
const levelCounts = [];
|
|
4063
|
+
for (const [level, result] of results) {
|
|
4064
|
+
levelCounts.push(`${level}: ${result.insights.length}`);
|
|
4065
|
+
}
|
|
4066
|
+
console.log(` Breakdown: ${levelCounts.join(", ")}`);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/reverie/symbols.ts
|
|
4070
|
+
function extractKeySymbols(diff) {
|
|
4071
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
4072
|
+
const functionMatch = diff.match(/(?:function|class|const|let|var|export|interface|type)\s+(\w+)/g);
|
|
4073
|
+
if (functionMatch) {
|
|
4074
|
+
for (const match of functionMatch) {
|
|
4075
|
+
const name = match.split(/\s+/).pop();
|
|
4076
|
+
if (name && name.length > 2 && !name.match(/^(true|false|null|undefined|const|let|var)$/)) {
|
|
4077
|
+
symbols.add(name);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
if (symbols.size === 0) {
|
|
4082
|
+
return "code changes";
|
|
4083
|
+
}
|
|
4084
|
+
return Array.from(symbols).slice(0, 5).join(", ");
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// src/reverie/episodes.ts
|
|
4088
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
4089
|
+
var import_node_path3 = __toESM(require("path"));
|
|
4090
|
+
var EPISODES_FILENAME = "reverie_episodes.json";
|
|
4091
|
+
async function readEpisodesFile(codexHome) {
|
|
4092
|
+
try {
|
|
4093
|
+
const file = await import_promises.default.readFile(import_node_path3.default.join(codexHome, EPISODES_FILENAME), "utf8");
|
|
4094
|
+
const parsed = JSON.parse(file);
|
|
4095
|
+
if (Array.isArray(parsed)) {
|
|
4096
|
+
return parsed;
|
|
4097
|
+
}
|
|
4098
|
+
return [];
|
|
4099
|
+
} catch (error) {
|
|
4100
|
+
if (error.code === "ENOENT") {
|
|
4101
|
+
return [];
|
|
4102
|
+
}
|
|
4103
|
+
throw error;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
async function searchEpisodeSummaries(codexHome, query, repo, limit = 20) {
|
|
4107
|
+
const summaries = await readEpisodesFile(codexHome);
|
|
4108
|
+
if (!summaries.length || !query.trim()) {
|
|
4109
|
+
return [];
|
|
4110
|
+
}
|
|
4111
|
+
const documents = summaries.map(
|
|
4112
|
+
(episode) => [episode.summary, ...episode.keyDecisions ?? []].join("\n")
|
|
4113
|
+
);
|
|
4114
|
+
const inputs = [query, ...documents];
|
|
4115
|
+
const embeddings = await fastEmbedEmbed({
|
|
4116
|
+
inputs,
|
|
4117
|
+
projectRoot: repo,
|
|
4118
|
+
normalize: true,
|
|
4119
|
+
cache: true
|
|
4120
|
+
});
|
|
4121
|
+
if (embeddings.length !== inputs.length) {
|
|
4122
|
+
return [];
|
|
4123
|
+
}
|
|
4124
|
+
const [queryVector, ...docVectors] = embeddings;
|
|
4125
|
+
if (!queryVector) {
|
|
4126
|
+
return [];
|
|
4127
|
+
}
|
|
4128
|
+
const scored = summaries.map((episode, idx) => {
|
|
4129
|
+
const vector = docVectors[idx] ?? [];
|
|
4130
|
+
return {
|
|
4131
|
+
episode,
|
|
4132
|
+
score: cosineSimilarity(queryVector, vector)
|
|
4133
|
+
};
|
|
4134
|
+
});
|
|
4135
|
+
scored.sort((a, b) => b.score - a.score);
|
|
4136
|
+
return scored.slice(0, limit).map(({ episode }) => episode);
|
|
4137
|
+
}
|
|
4138
|
+
function cosineSimilarity(a, b) {
|
|
4139
|
+
const length = Math.min(a.length, b.length);
|
|
4140
|
+
if (length === 0) {
|
|
4141
|
+
return 0;
|
|
4142
|
+
}
|
|
4143
|
+
let dot2 = 0;
|
|
4144
|
+
let magA = 0;
|
|
4145
|
+
let magB = 0;
|
|
4146
|
+
for (let i = 0; i < length; i += 1) {
|
|
4147
|
+
const av = a[i] ?? 0;
|
|
4148
|
+
const bv = b[i] ?? 0;
|
|
4149
|
+
dot2 += av * bv;
|
|
4150
|
+
magA += av * av;
|
|
4151
|
+
magB += bv * bv;
|
|
4152
|
+
}
|
|
4153
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
4154
|
+
return denom === 0 ? 0 : dot2 / denom;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// src/reverie/search.ts
|
|
4158
|
+
async function searchReveries(codexHome, text, repo, options) {
|
|
4159
|
+
const {
|
|
4160
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
4161
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
4162
|
+
useReranker = true,
|
|
4163
|
+
rerankerModel = REVERIE_RERANKER_MODEL,
|
|
4164
|
+
rerankerTopK = DEFAULT_RERANKER_TOP_K,
|
|
4165
|
+
rerankerBatchSize = DEFAULT_RERANKER_BATCH_SIZE,
|
|
4166
|
+
candidateMultiplier = REVERIE_CANDIDATE_MULTIPLIER
|
|
4167
|
+
} = options || {};
|
|
4168
|
+
const normalized = text.trim();
|
|
4169
|
+
if (!normalized) {
|
|
4170
|
+
return [];
|
|
4171
|
+
}
|
|
4172
|
+
const searchOptions = {
|
|
4173
|
+
projectRoot: repo,
|
|
4174
|
+
limit: maxCandidates * candidateMultiplier,
|
|
4175
|
+
// Get 3x candidates for heavy filtering
|
|
4176
|
+
maxCandidates: maxCandidates * candidateMultiplier,
|
|
4177
|
+
normalize: true,
|
|
4178
|
+
cache: true
|
|
4179
|
+
};
|
|
4180
|
+
if (useReranker) {
|
|
4181
|
+
searchOptions.rerankerModel = rerankerModel;
|
|
4182
|
+
searchOptions.rerankerTopK = rerankerTopK;
|
|
4183
|
+
searchOptions.rerankerBatchSize = rerankerBatchSize;
|
|
4184
|
+
}
|
|
4185
|
+
try {
|
|
4186
|
+
const regexMatches = looksLikeStructuredQuery(normalized) ? await reverieSearchConversations(codexHome, normalized, limit).catch(() => []) : [];
|
|
4187
|
+
const matches = await reverieSearchSemantic(codexHome, normalized, searchOptions);
|
|
4188
|
+
const combinedMatches = mergeSearchResults(regexMatches, matches);
|
|
4189
|
+
const insights = convertSearchResultsToInsights(combinedMatches);
|
|
4190
|
+
const validInsights = insights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
4191
|
+
const { kept: conversational } = await filterBoilerplateInsights(validInsights, {
|
|
4192
|
+
projectRoot: repo
|
|
4193
|
+
});
|
|
4194
|
+
const deduplicated = deduplicateReverieInsights(conversational);
|
|
4195
|
+
const episodeMatches = await searchEpisodeSummaries(codexHome, normalized, repo, limit * 4).catch(() => []);
|
|
4196
|
+
const episodeBoost = /* @__PURE__ */ new Map();
|
|
4197
|
+
for (const episode of episodeMatches) {
|
|
4198
|
+
episodeBoost.set(episode.conversationId, Math.max(episodeBoost.get(episode.conversationId) ?? 0, episode.importance ?? 0));
|
|
4199
|
+
}
|
|
4200
|
+
const ranked = deduplicated.map((insight) => {
|
|
4201
|
+
const bonus = episodeBoost.get(insight.conversationId) ?? 0;
|
|
4202
|
+
return {
|
|
4203
|
+
insight,
|
|
4204
|
+
score: insight.relevance + bonus / 10
|
|
4205
|
+
};
|
|
4206
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit).map(({ insight }) => insight);
|
|
4207
|
+
return ranked;
|
|
4208
|
+
} catch (error) {
|
|
4209
|
+
console.warn(
|
|
4210
|
+
`Reverie search failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4211
|
+
);
|
|
4212
|
+
return [];
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
function convertSearchResultsToInsights(results) {
|
|
4216
|
+
const flattened = [];
|
|
4217
|
+
for (const match of results) {
|
|
4218
|
+
const base = {
|
|
4219
|
+
conversationId: match.conversation?.id || "unknown",
|
|
4220
|
+
timestamp: match.conversation?.createdAt || match.conversation?.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4221
|
+
relevance: typeof match.relevanceScore === "number" ? match.relevanceScore : 0,
|
|
4222
|
+
excerpt: "",
|
|
4223
|
+
insights: Array.isArray(match.insights) ? match.insights : []
|
|
4224
|
+
};
|
|
4225
|
+
const excerpts = match.matchingExcerpts?.length ? match.matchingExcerpts : [""];
|
|
4226
|
+
for (const excerpt of excerpts) {
|
|
4227
|
+
if (!excerpt.trim()) {
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
flattened.push({ ...base, excerpt });
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
return flattened;
|
|
4234
|
+
}
|
|
4235
|
+
function mergeSearchResults(primary, secondary) {
|
|
4236
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4237
|
+
const merged = [];
|
|
4238
|
+
for (const list of [primary, secondary]) {
|
|
4239
|
+
for (const match of list) {
|
|
4240
|
+
const convoId = match.conversation?.id || "unknown";
|
|
4241
|
+
const excerptKey = match.matchingExcerpts?.[0] || String(match.relevanceScore ?? 0);
|
|
4242
|
+
const key = `${convoId}:${excerptKey}`;
|
|
4243
|
+
if (seen.has(key)) {
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
seen.add(key);
|
|
4247
|
+
merged.push(match);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return merged;
|
|
4251
|
+
}
|
|
4252
|
+
function looksLikeStructuredQuery(text) {
|
|
4253
|
+
if (!text) {
|
|
4254
|
+
return false;
|
|
4255
|
+
}
|
|
4256
|
+
const structuredPatterns = [
|
|
4257
|
+
/traceback \(most recent call last\)/i,
|
|
4258
|
+
// Python
|
|
4259
|
+
/exception in thread/i,
|
|
4260
|
+
/java\.lang\./i,
|
|
4261
|
+
/org\.junit/i,
|
|
4262
|
+
/at\s+org\./i,
|
|
4263
|
+
/AssertionError:/i,
|
|
4264
|
+
/panic!|thread '.+' panicked/i,
|
|
4265
|
+
/FAIL\s+\S+\s+\(/i,
|
|
4266
|
+
// Jest/Vitest
|
|
4267
|
+
/(?:error|fail|fatal):/i,
|
|
4268
|
+
/Caused by:/i,
|
|
4269
|
+
/\bundefined reference to\b/i
|
|
4270
|
+
];
|
|
4271
|
+
for (const pattern of structuredPatterns) {
|
|
4272
|
+
if (pattern.test(text)) {
|
|
4273
|
+
return true;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
const hashPattern = /\b[0-9a-f]{32,}\b/i;
|
|
4277
|
+
if (hashPattern.test(text)) {
|
|
4278
|
+
return true;
|
|
4279
|
+
}
|
|
4280
|
+
const uuidPattern = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i;
|
|
4281
|
+
if (uuidPattern.test(text)) {
|
|
4282
|
+
return true;
|
|
4283
|
+
}
|
|
4284
|
+
const stackFrameMatches = text.match(/\bat\s+[^\s]+\s*\(|\b\S+\.\w+:\d+/gi);
|
|
4285
|
+
if ((stackFrameMatches?.length ?? 0) >= 2) {
|
|
4286
|
+
return true;
|
|
4287
|
+
}
|
|
4288
|
+
const severityTokens = text.match(/\b(?:fail|error|panic|assert|fatal)\b/gi)?.length ?? 0;
|
|
4289
|
+
if (severityTokens >= 3 && text.length > 50) {
|
|
4290
|
+
return true;
|
|
4291
|
+
}
|
|
4292
|
+
return false;
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
// src/reverie/grader.ts
|
|
4296
|
+
var import_agents2 = require("@openai/agents");
|
|
4297
|
+
var REVERIE_GRADING_SCHEMA = {
|
|
4298
|
+
type: "object",
|
|
4299
|
+
properties: {
|
|
4300
|
+
is_relevant: {
|
|
4301
|
+
type: "boolean",
|
|
4302
|
+
description: "True if excerpt contains specific technical details relevant to the work context"
|
|
4303
|
+
},
|
|
4304
|
+
reasoning: {
|
|
4305
|
+
type: "string",
|
|
4306
|
+
description: "Brief explanation (1-2 sentences) of why the excerpt was approved or rejected"
|
|
4307
|
+
}
|
|
4308
|
+
},
|
|
4309
|
+
required: ["is_relevant", "reasoning"],
|
|
4310
|
+
additionalProperties: false
|
|
4311
|
+
};
|
|
4312
|
+
async function gradeReverieRelevance(runner, searchContext, insight) {
|
|
4313
|
+
const graderAgent = new import_agents2.Agent({
|
|
4314
|
+
name: "ReverieGrader",
|
|
4315
|
+
instructions: `You are a STRICT filter for conversation excerpts. Only approve excerpts with SPECIFIC technical details.
|
|
4316
|
+
|
|
4317
|
+
REJECT excerpts containing:
|
|
4318
|
+
- Greetings and pleasantries
|
|
4319
|
+
- Thinking markers (**, ##, <thinking>)
|
|
4320
|
+
- JSON objects or structured data dumps
|
|
4321
|
+
- Generic phrases ("Context from past work", "working on this", etc.)
|
|
4322
|
+
- Metadata and system information
|
|
4323
|
+
- Boilerplate text
|
|
4324
|
+
- Task or checklist instructions ("1.", "2.", "Plan:")
|
|
4325
|
+
- AGENTS.md guidance, sandbox instructions, or environment descriptions
|
|
4326
|
+
- Tool output summaries or command transcript blocks
|
|
4327
|
+
|
|
4328
|
+
APPROVE ONLY excerpts with:
|
|
4329
|
+
- Specific code/file references (file paths, function names, variable names)
|
|
4330
|
+
- Technical decisions and rationale
|
|
4331
|
+
- Error messages and debugging details
|
|
4332
|
+
- Implementation specifics and algorithms
|
|
4333
|
+
- Architecture patterns and design choices
|
|
4334
|
+
|
|
4335
|
+
Return a JSON object with:
|
|
4336
|
+
- is_relevant: boolean indicating if this excerpt should be kept
|
|
4337
|
+
- reasoning: brief 1-2 sentence explanation of your decision`,
|
|
4338
|
+
outputType: {
|
|
4339
|
+
type: "json_schema",
|
|
4340
|
+
schema: REVERIE_GRADING_SCHEMA,
|
|
4341
|
+
name: "ReverieGrading",
|
|
4342
|
+
strict: true
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
const prompt = `Context: ${searchContext}
|
|
4346
|
+
|
|
4347
|
+
Excerpt to grade:
|
|
4348
|
+
"""
|
|
4349
|
+
${insight.excerpt.slice(0, 400)}
|
|
4350
|
+
"""
|
|
4351
|
+
|
|
4352
|
+
Evaluate whether this excerpt contains specific technical details relevant to the work context.`;
|
|
4353
|
+
const result = await runner.run(graderAgent, prompt);
|
|
4354
|
+
if (result.finalOutput && typeof result.finalOutput === "object") {
|
|
4355
|
+
const grading = result.finalOutput;
|
|
4356
|
+
return grading.is_relevant;
|
|
4357
|
+
}
|
|
4358
|
+
console.warn("Reverie grading failed to return structured output, defaulting to reject");
|
|
4359
|
+
return false;
|
|
4360
|
+
}
|
|
4361
|
+
async function gradeReveriesInParallel(runner, context, insights, options) {
|
|
4362
|
+
const { minRelevanceForGrading = 0.7, parallel = true } = options || {};
|
|
4363
|
+
const highScoring = insights.filter((insight) => insight.relevance >= minRelevanceForGrading);
|
|
4364
|
+
const lowScoring = insights.filter((insight) => insight.relevance < minRelevanceForGrading);
|
|
4365
|
+
if (highScoring.length === 0) {
|
|
4366
|
+
return [];
|
|
4367
|
+
}
|
|
4368
|
+
if (parallel) {
|
|
4369
|
+
const gradingPromises = highScoring.map(
|
|
4370
|
+
(insight) => gradeReverieRelevance(runner, context, insight).then((isRelevant) => ({
|
|
4371
|
+
insight,
|
|
4372
|
+
isRelevant
|
|
4373
|
+
}))
|
|
4374
|
+
);
|
|
4375
|
+
const gradedResults = await Promise.all(gradingPromises);
|
|
4376
|
+
return gradedResults.filter((r) => r.isRelevant).map((r) => r.insight);
|
|
4377
|
+
} else {
|
|
4378
|
+
const approved = [];
|
|
4379
|
+
for (const insight of highScoring) {
|
|
4380
|
+
const isRelevant = await gradeReverieRelevance(runner, context, insight);
|
|
4381
|
+
if (isRelevant) {
|
|
4382
|
+
approved.push(insight);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
return approved;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
// src/reverie/context.ts
|
|
4390
|
+
function buildProjectContext(query, options) {
|
|
4391
|
+
return {
|
|
4392
|
+
level: "project",
|
|
4393
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
4394
|
+
query,
|
|
4395
|
+
filePatterns: options?.filePatterns
|
|
4396
|
+
};
|
|
4397
|
+
}
|
|
4398
|
+
function buildBranchContext(branch, changedFiles, options) {
|
|
4399
|
+
return {
|
|
4400
|
+
level: "branch",
|
|
4401
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
4402
|
+
branch,
|
|
4403
|
+
baseBranch: options?.baseBranch,
|
|
4404
|
+
changedFiles,
|
|
4405
|
+
recentCommits: options?.recentCommits
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4408
|
+
function buildFileContext(filePath, options) {
|
|
4409
|
+
const context = {
|
|
4410
|
+
level: "file",
|
|
4411
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
4412
|
+
filePath,
|
|
4413
|
+
diff: options?.diff
|
|
4414
|
+
};
|
|
4415
|
+
if (options?.extractSymbols && options?.diff) {
|
|
4416
|
+
const symbolsText = extractKeySymbols(options.diff);
|
|
4417
|
+
if (symbolsText) {
|
|
4418
|
+
context.symbols = symbolsText.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
return context;
|
|
4422
|
+
}
|
|
4423
|
+
function contextToQuery(context) {
|
|
4424
|
+
switch (context.level) {
|
|
4425
|
+
case "project": {
|
|
4426
|
+
let query = `Project-wide: ${context.query}`;
|
|
4427
|
+
if (context.filePatterns && context.filePatterns.length > 0) {
|
|
4428
|
+
query += `
|
|
4429
|
+
Scope: ${context.filePatterns.join(", ")}`;
|
|
4430
|
+
}
|
|
4431
|
+
return query;
|
|
4432
|
+
}
|
|
4433
|
+
case "branch": {
|
|
4434
|
+
let query = `Branch: ${context.branch}`;
|
|
4435
|
+
if (context.baseBranch) {
|
|
4436
|
+
query += ` (base: ${context.baseBranch})`;
|
|
4437
|
+
}
|
|
4438
|
+
query += `
|
|
4439
|
+
Files changed: ${context.changedFiles.join(", ")}`;
|
|
4440
|
+
if (context.recentCommits) {
|
|
4441
|
+
query += `
|
|
4442
|
+
Recent commits: ${context.recentCommits}`;
|
|
4443
|
+
}
|
|
4444
|
+
return query;
|
|
4445
|
+
}
|
|
4446
|
+
case "file": {
|
|
4447
|
+
let query = `File: ${context.filePath}`;
|
|
4448
|
+
if (context.symbols && context.symbols.length > 0) {
|
|
4449
|
+
query += `
|
|
4450
|
+
Symbols: ${context.symbols.join(", ")}`;
|
|
4451
|
+
}
|
|
4452
|
+
if (context.diff) {
|
|
4453
|
+
const truncatedDiff = context.diff.length > 500 ? context.diff.slice(0, 500) + "..." : context.diff;
|
|
4454
|
+
query += `
|
|
4455
|
+
Changes:
|
|
4456
|
+
${truncatedDiff}`;
|
|
4457
|
+
}
|
|
4458
|
+
return query;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
function formatFileList(files, maxFiles = 10) {
|
|
4463
|
+
if (files.length === 0) {
|
|
4464
|
+
return "(no files)";
|
|
4465
|
+
}
|
|
4466
|
+
if (files.length <= maxFiles) {
|
|
4467
|
+
return files.join(", ");
|
|
4468
|
+
}
|
|
4469
|
+
const shown = files.slice(0, maxFiles);
|
|
4470
|
+
const remaining = files.length - maxFiles;
|
|
4471
|
+
return `${shown.join(", ")} ... and ${remaining} more`;
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
// src/reverie/pipeline.ts
|
|
4475
|
+
async function applyReveriePipeline(codexHome, searchText, repo, runner, options) {
|
|
4476
|
+
const {
|
|
4477
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
4478
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
4479
|
+
minRelevanceForGrading = REVERIE_LLM_GRADE_THRESHOLD,
|
|
4480
|
+
skipLLMGrading = false,
|
|
4481
|
+
...searchOptions
|
|
4482
|
+
} = options || {};
|
|
4483
|
+
logReverieSearch(searchText, `repo: ${repo}`);
|
|
4484
|
+
const rawInsights = await searchReveries(codexHome, searchText, repo, {
|
|
4485
|
+
limit,
|
|
4486
|
+
maxCandidates,
|
|
4487
|
+
...searchOptions
|
|
4488
|
+
});
|
|
4489
|
+
const stats = {
|
|
4490
|
+
total: rawInsights.length,
|
|
4491
|
+
afterQuality: 0,
|
|
4492
|
+
afterBoilerplate: 0,
|
|
4493
|
+
afterScore: 0,
|
|
4494
|
+
afterDedup: 0,
|
|
4495
|
+
final: 0
|
|
4496
|
+
};
|
|
4497
|
+
const validInsights = rawInsights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
4498
|
+
stats.afterQuality = validInsights.length;
|
|
4499
|
+
const { kept: conversationalInsights } = await filterBoilerplateInsights(validInsights, {
|
|
4500
|
+
projectRoot: repo
|
|
4501
|
+
});
|
|
4502
|
+
stats.afterBoilerplate = conversationalInsights.length;
|
|
4503
|
+
const highScoring = conversationalInsights.filter((insight) => insight.relevance >= minRelevanceForGrading);
|
|
4504
|
+
const lowScoring = conversationalInsights.filter((insight) => insight.relevance < minRelevanceForGrading);
|
|
4505
|
+
stats.afterScore = highScoring.length;
|
|
4506
|
+
let gradedInsights;
|
|
4507
|
+
if (skipLLMGrading || !runner) {
|
|
4508
|
+
gradedInsights = highScoring;
|
|
4509
|
+
stats.afterLLMGrade = highScoring.length;
|
|
4510
|
+
} else {
|
|
4511
|
+
gradedInsights = await gradeReveriesInParallel(runner, searchText, highScoring, {
|
|
4512
|
+
minRelevanceForGrading,
|
|
4513
|
+
parallel: true
|
|
4514
|
+
});
|
|
4515
|
+
stats.afterLLMGrade = gradedInsights.length;
|
|
4516
|
+
logLLMGrading({
|
|
4517
|
+
total: highScoring.length,
|
|
4518
|
+
approved: gradedInsights.length,
|
|
4519
|
+
rejected: highScoring.length - gradedInsights.length,
|
|
4520
|
+
minScore: minRelevanceForGrading
|
|
4521
|
+
});
|
|
4522
|
+
if (gradedInsights.length > 0) {
|
|
4523
|
+
logApprovedReveries(gradedInsights);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
const deduplicated = deduplicateReverieInsights(gradedInsights);
|
|
4527
|
+
stats.afterDedup = deduplicated.length;
|
|
4528
|
+
const finalInsights = deduplicated.slice(0, limit);
|
|
4529
|
+
stats.final = finalInsights.length;
|
|
4530
|
+
logReverieFiltering({
|
|
4531
|
+
total: stats.total,
|
|
4532
|
+
afterQuality: stats.afterQuality,
|
|
4533
|
+
afterBoilerplate: stats.afterBoilerplate,
|
|
4534
|
+
afterScore: stats.afterScore,
|
|
4535
|
+
afterDedup: stats.afterDedup,
|
|
4536
|
+
minScore: minRelevanceForGrading
|
|
4537
|
+
});
|
|
4538
|
+
return {
|
|
4539
|
+
insights: finalInsights,
|
|
4540
|
+
stats
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
async function applyFileReveriePipeline(codexHome, filePath, fileContext, repo, runner, options) {
|
|
4544
|
+
const {
|
|
4545
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
4546
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
4547
|
+
...restOptions
|
|
4548
|
+
} = options || {};
|
|
4549
|
+
const fileOptions = {
|
|
4550
|
+
...restOptions,
|
|
4551
|
+
maxCandidates: Math.floor(maxCandidates / 2),
|
|
4552
|
+
limit
|
|
4553
|
+
};
|
|
4554
|
+
return applyReveriePipeline(codexHome, fileContext, repo, runner, fileOptions);
|
|
4555
|
+
}
|
|
4556
|
+
async function searchMultiLevel(codexHome, contexts, runner, options) {
|
|
4557
|
+
const levels = contexts.map((ctx) => ctx.level);
|
|
4558
|
+
logMultiLevelSearch(levels);
|
|
4559
|
+
const results = /* @__PURE__ */ new Map();
|
|
4560
|
+
for (const context of contexts) {
|
|
4561
|
+
let result;
|
|
4562
|
+
switch (context.level) {
|
|
4563
|
+
case "project":
|
|
4564
|
+
result = await searchProjectLevel(codexHome, context, runner, options);
|
|
4565
|
+
break;
|
|
4566
|
+
case "branch":
|
|
4567
|
+
result = await searchBranchLevel(codexHome, context, runner, options);
|
|
4568
|
+
break;
|
|
4569
|
+
case "file":
|
|
4570
|
+
result = await searchFileLevel(codexHome, context, runner, options);
|
|
4571
|
+
break;
|
|
4572
|
+
}
|
|
4573
|
+
results.set(context.level, result);
|
|
4574
|
+
logLevelResults(context.level, result);
|
|
4575
|
+
}
|
|
4576
|
+
return results;
|
|
4577
|
+
}
|
|
4578
|
+
async function searchProjectLevel(codexHome, context, runner, options) {
|
|
4579
|
+
const searchQuery = contextToQuery(context);
|
|
4580
|
+
const projectOptions = {
|
|
4581
|
+
...options,
|
|
4582
|
+
maxCandidates: (options?.maxCandidates || DEFAULT_REVERIE_MAX_CANDIDATES) * 1.5
|
|
4583
|
+
};
|
|
4584
|
+
return applyReveriePipeline(
|
|
4585
|
+
codexHome,
|
|
4586
|
+
searchQuery,
|
|
4587
|
+
context.repoPath,
|
|
4588
|
+
runner,
|
|
4589
|
+
projectOptions
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
async function searchBranchLevel(codexHome, context, runner, options) {
|
|
4593
|
+
const searchQuery = contextToQuery(context);
|
|
4594
|
+
return applyReveriePipeline(
|
|
4595
|
+
codexHome,
|
|
4596
|
+
searchQuery,
|
|
4597
|
+
context.repoPath,
|
|
4598
|
+
runner,
|
|
4599
|
+
options
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
async function searchFileLevel(codexHome, context, runner, options) {
|
|
4603
|
+
const searchQuery = contextToQuery(context);
|
|
4604
|
+
return applyFileReveriePipeline(
|
|
4605
|
+
codexHome,
|
|
4606
|
+
context.filePath,
|
|
4607
|
+
searchQuery,
|
|
4608
|
+
context.repoPath,
|
|
4609
|
+
runner,
|
|
4610
|
+
options
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
// src/index.ts
|
|
4615
|
+
function evCompleted(id) {
|
|
4616
|
+
const binding = getNativeBinding();
|
|
4617
|
+
if (!binding) throw new Error("Native binding not available");
|
|
4618
|
+
return binding.evCompleted(id);
|
|
4619
|
+
}
|
|
4620
|
+
function evResponseCreated(id) {
|
|
4621
|
+
const binding = getNativeBinding();
|
|
4622
|
+
if (!binding) throw new Error("Native binding not available");
|
|
4623
|
+
return binding.evResponseCreated(id);
|
|
4624
|
+
}
|
|
4625
|
+
function evAssistantMessage(id, text) {
|
|
4626
|
+
const binding = getNativeBinding();
|
|
4627
|
+
if (!binding) throw new Error("Native binding not available");
|
|
4628
|
+
return binding.evAssistantMessage(id, text);
|
|
4629
|
+
}
|
|
4630
|
+
function evFunctionCall(callId, name, args) {
|
|
4631
|
+
const binding = getNativeBinding();
|
|
4632
|
+
if (!binding) throw new Error("Native binding not available");
|
|
4633
|
+
return binding.evFunctionCall(callId, name, args);
|
|
4634
|
+
}
|
|
4635
|
+
function sse(events) {
|
|
4636
|
+
const binding = getNativeBinding();
|
|
4637
|
+
if (!binding) throw new Error("Native binding not available");
|
|
4638
|
+
return binding.sse(events);
|
|
4639
|
+
}
|
|
4640
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4641
|
+
0 && (module.exports = {
|
|
4642
|
+
CloudTasks,
|
|
4643
|
+
Codex,
|
|
4644
|
+
CodexProvider,
|
|
4645
|
+
DEFAULT_RERANKER_BATCH_SIZE,
|
|
4646
|
+
DEFAULT_RERANKER_TOP_K,
|
|
4647
|
+
DEFAULT_REVERIE_LIMIT,
|
|
4648
|
+
DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
4649
|
+
DEFAULT_SERVERS,
|
|
4650
|
+
LogLevel,
|
|
4651
|
+
Logger,
|
|
4652
|
+
LspDiagnosticsBridge,
|
|
4653
|
+
LspManager,
|
|
4654
|
+
OpenCodeAgent,
|
|
4655
|
+
REVERIE_CANDIDATE_MULTIPLIER,
|
|
4656
|
+
REVERIE_EMBED_MODEL,
|
|
4657
|
+
REVERIE_LLM_GRADE_THRESHOLD,
|
|
4658
|
+
REVERIE_RERANKER_MODEL,
|
|
4659
|
+
ScopedLogger,
|
|
4660
|
+
Thread,
|
|
4661
|
+
applyFileReveriePipeline,
|
|
4662
|
+
applyQualityPipeline,
|
|
4663
|
+
applyReveriePipeline,
|
|
4664
|
+
attachLspDiagnostics,
|
|
4665
|
+
buildBranchContext,
|
|
4666
|
+
buildFileContext,
|
|
4667
|
+
buildProjectContext,
|
|
4668
|
+
codexTool,
|
|
4669
|
+
collectRepoDiffSummary,
|
|
4670
|
+
contextToQuery,
|
|
4671
|
+
createThreadLogger,
|
|
4672
|
+
deduplicateReverieInsights,
|
|
4673
|
+
encodeToToon,
|
|
4674
|
+
evAssistantMessage,
|
|
4675
|
+
evCompleted,
|
|
4676
|
+
evFunctionCall,
|
|
4677
|
+
evResponseCreated,
|
|
4678
|
+
extractKeySymbols,
|
|
4679
|
+
fastEmbedEmbed,
|
|
4680
|
+
fastEmbedInit,
|
|
4681
|
+
filterBySeverity,
|
|
4682
|
+
findServerForFile,
|
|
4683
|
+
formatDiagnosticsForBackgroundEvent,
|
|
4684
|
+
formatDiagnosticsForTool,
|
|
4685
|
+
formatDiagnosticsWithSummary,
|
|
4686
|
+
formatFileList,
|
|
4687
|
+
formatStream,
|
|
4688
|
+
gradeReverieRelevance,
|
|
4689
|
+
gradeReveriesInParallel,
|
|
4690
|
+
isValidReverieExcerpt,
|
|
4691
|
+
logApprovedReveries,
|
|
4692
|
+
logLLMGrading,
|
|
4693
|
+
logLevelResults,
|
|
4694
|
+
logMultiLevelSearch,
|
|
4695
|
+
logMultiLevelSummary,
|
|
4696
|
+
logReverieFiltering,
|
|
4697
|
+
logReverieHintQuality,
|
|
4698
|
+
logReverieInsights,
|
|
4699
|
+
logReverieSearch,
|
|
4700
|
+
logger,
|
|
4701
|
+
resolveWorkspaceRoot,
|
|
4702
|
+
reverieGetConversationInsights,
|
|
4703
|
+
reverieIndexSemantic,
|
|
4704
|
+
reverieListConversations,
|
|
4705
|
+
reverieSearchConversations,
|
|
4706
|
+
reverieSearchSemantic,
|
|
4707
|
+
runThreadTurnWithLogs,
|
|
4708
|
+
runTui,
|
|
4709
|
+
searchBranchLevel,
|
|
4710
|
+
searchFileLevel,
|
|
4711
|
+
searchMultiLevel,
|
|
4712
|
+
searchProjectLevel,
|
|
4713
|
+
searchReveries,
|
|
4714
|
+
sse,
|
|
4715
|
+
startTui,
|
|
4716
|
+
summarizeDiagnostics,
|
|
4717
|
+
tokenizerCount,
|
|
4718
|
+
tokenizerDecode,
|
|
4719
|
+
tokenizerEncode,
|
|
4720
|
+
truncateText
|
|
4721
|
+
});
|
|
4722
|
+
//# sourceMappingURL=index.cjs.map
|