@codex-native/sdk 0.0.1 → 0.0.2
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 +3507 -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 +1658 -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/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 +56 -20
package/dist/index.mjs
CHANGED
|
@@ -1,387 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import {
|
|
2
|
+
Codex,
|
|
3
|
+
DEFAULT_SERVERS,
|
|
4
|
+
LspDiagnosticsBridge,
|
|
5
|
+
LspManager,
|
|
6
|
+
Thread,
|
|
7
|
+
attachLspDiagnostics,
|
|
8
|
+
collectRepoDiffSummary,
|
|
9
|
+
encodeToToon,
|
|
10
|
+
fastEmbedEmbed,
|
|
11
|
+
fastEmbedInit,
|
|
12
|
+
filterBySeverity,
|
|
13
|
+
findServerForFile,
|
|
14
|
+
formatDiagnosticsForBackgroundEvent,
|
|
15
|
+
formatDiagnosticsForTool,
|
|
16
|
+
formatDiagnosticsWithSummary,
|
|
17
|
+
getNativeBinding,
|
|
18
|
+
resolveWorkspaceRoot,
|
|
19
|
+
reverieGetConversationInsights,
|
|
20
|
+
reverieIndexSemantic,
|
|
21
|
+
reverieListConversations,
|
|
22
|
+
reverieSearchConversations,
|
|
23
|
+
reverieSearchSemantic,
|
|
24
|
+
runTui,
|
|
25
|
+
startTui,
|
|
26
|
+
summarizeDiagnostics,
|
|
27
|
+
tokenizerCount,
|
|
28
|
+
tokenizerDecode,
|
|
29
|
+
tokenizerEncode
|
|
30
|
+
} from "./chunk-ZTUGAPWF.mjs";
|
|
21
31
|
|
|
22
|
-
// src/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async function createOutputSchemaFile(schema) {
|
|
27
|
-
if (schema === void 0) {
|
|
28
|
-
return { cleanup: async () => {
|
|
29
|
-
} };
|
|
30
|
-
}
|
|
31
|
-
if (!isJsonObject(schema)) {
|
|
32
|
-
throw new Error("outputSchema must be a plain JSON object");
|
|
33
|
-
}
|
|
34
|
-
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
|
35
|
-
const schemaPath = path.join(schemaDir, "schema.json");
|
|
36
|
-
const cleanup = async () => {
|
|
37
|
-
try {
|
|
38
|
-
await fs.rm(schemaDir, { recursive: true, force: true });
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
try {
|
|
43
|
-
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
|
44
|
-
return { schemaPath, cleanup };
|
|
45
|
-
} catch (error) {
|
|
46
|
-
await cleanup();
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function isJsonObject(value) {
|
|
51
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
// src/agents/toolRegistry.ts
|
|
33
|
+
var executors = /* @__PURE__ */ new Map();
|
|
34
|
+
function registerCodexToolExecutor(name, executor) {
|
|
35
|
+
executors.set(name, executor);
|
|
52
36
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"use strict";
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// src/thread.ts
|
|
60
|
-
function normalizeInput(input) {
|
|
61
|
-
if (typeof input === "string") {
|
|
62
|
-
return { prompt: input, images: [] };
|
|
63
|
-
}
|
|
64
|
-
const promptParts = [];
|
|
65
|
-
const images = [];
|
|
66
|
-
for (const item of input) {
|
|
67
|
-
if (item.type === "text") {
|
|
68
|
-
promptParts.push(item.text);
|
|
69
|
-
} else if (item.type === "local_image") {
|
|
70
|
-
images.push(item.path);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { prompt: promptParts.join("\n\n"), images };
|
|
74
|
-
}
|
|
75
|
-
var Thread;
|
|
76
|
-
var init_thread = __esm({
|
|
77
|
-
"src/thread.ts"() {
|
|
78
|
-
"use strict";
|
|
79
|
-
init_outputSchemaFile();
|
|
80
|
-
Thread = class {
|
|
81
|
-
_exec;
|
|
82
|
-
_options;
|
|
83
|
-
_id;
|
|
84
|
-
_threadOptions;
|
|
85
|
-
/** Returns the ID of the thread. Populated after the first turn starts. */
|
|
86
|
-
get id() {
|
|
87
|
-
return this._id;
|
|
88
|
-
}
|
|
89
|
-
/* @internal */
|
|
90
|
-
constructor(exec, options, threadOptions, id = null) {
|
|
91
|
-
this._exec = exec;
|
|
92
|
-
this._options = options;
|
|
93
|
-
this._id = id;
|
|
94
|
-
this._threadOptions = threadOptions;
|
|
95
|
-
}
|
|
96
|
-
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
|
97
|
-
async runStreamed(input, turnOptions = {}) {
|
|
98
|
-
return { events: this.runStreamedInternal(input, turnOptions) };
|
|
99
|
-
}
|
|
100
|
-
async *runStreamedInternal(input, turnOptions = {}) {
|
|
101
|
-
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
|
|
102
|
-
const options = this._threadOptions;
|
|
103
|
-
const { prompt, images } = normalizeInput(input);
|
|
104
|
-
const generator = this._exec.run({
|
|
105
|
-
input: prompt,
|
|
106
|
-
baseUrl: this._options.baseUrl,
|
|
107
|
-
apiKey: this._options.apiKey,
|
|
108
|
-
threadId: this._id,
|
|
109
|
-
images,
|
|
110
|
-
model: options?.model,
|
|
111
|
-
sandboxMode: options?.sandboxMode,
|
|
112
|
-
workingDirectory: options?.workingDirectory,
|
|
113
|
-
skipGitRepoCheck: options?.skipGitRepoCheck,
|
|
114
|
-
outputSchemaFile: schemaPath,
|
|
115
|
-
outputSchema: turnOptions.outputSchema
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
for await (const item of generator) {
|
|
119
|
-
let parsed;
|
|
120
|
-
try {
|
|
121
|
-
parsed = JSON.parse(item);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
|
124
|
-
}
|
|
125
|
-
if (parsed.type === "thread.started") {
|
|
126
|
-
this._id = parsed.thread_id;
|
|
127
|
-
}
|
|
128
|
-
yield parsed;
|
|
129
|
-
}
|
|
130
|
-
} finally {
|
|
131
|
-
await cleanup();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Provides the input to the agent and returns the completed turn. */
|
|
135
|
-
async run(input, turnOptions = {}) {
|
|
136
|
-
const generator = this.runStreamedInternal(input, turnOptions);
|
|
137
|
-
const items = [];
|
|
138
|
-
let finalResponse = "";
|
|
139
|
-
let usage = null;
|
|
140
|
-
let turnFailure = null;
|
|
141
|
-
for await (const event of generator) {
|
|
142
|
-
if (event.type === "item.completed") {
|
|
143
|
-
if (event.item.type === "agent_message") {
|
|
144
|
-
finalResponse = event.item.text;
|
|
145
|
-
}
|
|
146
|
-
items.push(event.item);
|
|
147
|
-
} else if (event.type === "turn.completed") {
|
|
148
|
-
usage = event.usage;
|
|
149
|
-
} else if (event.type === "turn.failed") {
|
|
150
|
-
turnFailure = event.error;
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (turnFailure) {
|
|
155
|
-
throw new Error(turnFailure.message);
|
|
156
|
-
}
|
|
157
|
-
return { items, finalResponse, usage };
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// src/nativeBinding.ts
|
|
164
|
-
import { createRequire } from "module";
|
|
165
|
-
function getNativeBinding() {
|
|
166
|
-
if (cachedBinding !== void 0) {
|
|
167
|
-
return cachedBinding;
|
|
168
|
-
}
|
|
169
|
-
const require2 = createRequire(import.meta.url);
|
|
170
|
-
try {
|
|
171
|
-
const binding = require2("../index.js");
|
|
172
|
-
cachedBinding = binding;
|
|
173
|
-
return cachedBinding;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.warn("Failed to load native NAPI binding:", error);
|
|
176
|
-
cachedBinding = null;
|
|
177
|
-
return cachedBinding;
|
|
178
|
-
}
|
|
37
|
+
function getCodexToolExecutor(name) {
|
|
38
|
+
return executors.get(name);
|
|
179
39
|
}
|
|
180
|
-
var cachedBinding;
|
|
181
|
-
var init_nativeBinding = __esm({
|
|
182
|
-
"src/nativeBinding.ts"() {
|
|
183
|
-
"use strict";
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// src/exec.ts
|
|
188
|
-
var CodexExec, AsyncQueue;
|
|
189
|
-
var init_exec = __esm({
|
|
190
|
-
"src/exec.ts"() {
|
|
191
|
-
"use strict";
|
|
192
|
-
init_nativeBinding();
|
|
193
|
-
CodexExec = class {
|
|
194
|
-
native;
|
|
195
|
-
constructor() {
|
|
196
|
-
const nativeBinding = getNativeBinding();
|
|
197
|
-
if (!nativeBinding) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
"Native NAPI binding not available. Make sure @openai/codex-native is properly installed and built."
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
this.native = nativeBinding;
|
|
203
|
-
}
|
|
204
|
-
async *run(args) {
|
|
205
|
-
const binding = this.native;
|
|
206
|
-
const queue = new AsyncQueue();
|
|
207
|
-
const request = {
|
|
208
|
-
prompt: args.input,
|
|
209
|
-
threadId: args.threadId ?? void 0,
|
|
210
|
-
images: args.images && args.images.length > 0 ? args.images : void 0,
|
|
211
|
-
model: args.model,
|
|
212
|
-
sandboxMode: args.sandboxMode,
|
|
213
|
-
workingDirectory: args.workingDirectory,
|
|
214
|
-
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
215
|
-
outputSchema: args.outputSchema,
|
|
216
|
-
baseUrl: args.baseUrl,
|
|
217
|
-
apiKey: args.apiKey
|
|
218
|
-
};
|
|
219
|
-
let runPromise;
|
|
220
|
-
try {
|
|
221
|
-
runPromise = binding.runThreadStream(request, (err, eventJson) => {
|
|
222
|
-
if (err) {
|
|
223
|
-
queue.fail(err);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
queue.push(eventJson ?? "null");
|
|
228
|
-
} catch (error) {
|
|
229
|
-
queue.fail(error);
|
|
230
|
-
}
|
|
231
|
-
}).then(
|
|
232
|
-
() => {
|
|
233
|
-
queue.end();
|
|
234
|
-
},
|
|
235
|
-
(error) => {
|
|
236
|
-
queue.fail(error);
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
queue.fail(error);
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
let loopError;
|
|
244
|
-
try {
|
|
245
|
-
for await (const value of queue) {
|
|
246
|
-
yield value;
|
|
247
|
-
}
|
|
248
|
-
await runPromise;
|
|
249
|
-
} catch (error) {
|
|
250
|
-
loopError = error;
|
|
251
|
-
throw error;
|
|
252
|
-
} finally {
|
|
253
|
-
queue.end();
|
|
254
|
-
if (loopError) {
|
|
255
|
-
await runPromise.catch(() => {
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
AsyncQueue = class {
|
|
262
|
-
buffer = [];
|
|
263
|
-
waiters = [];
|
|
264
|
-
ended = false;
|
|
265
|
-
error;
|
|
266
|
-
push(value) {
|
|
267
|
-
if (this.ended) return;
|
|
268
|
-
if (this.waiters.length > 0) {
|
|
269
|
-
const waiter = this.waiters.shift();
|
|
270
|
-
waiter.resolve({ value, done: false });
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
this.buffer.push(value);
|
|
274
|
-
}
|
|
275
|
-
end() {
|
|
276
|
-
if (this.ended) return;
|
|
277
|
-
this.ended = true;
|
|
278
|
-
const waiters = this.waiters;
|
|
279
|
-
this.waiters = [];
|
|
280
|
-
for (const waiter of waiters) {
|
|
281
|
-
waiter.resolve({ value: void 0, done: true });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
fail(error) {
|
|
285
|
-
if (this.ended) return;
|
|
286
|
-
this.error = error;
|
|
287
|
-
this.ended = true;
|
|
288
|
-
const waiters = this.waiters;
|
|
289
|
-
this.waiters = [];
|
|
290
|
-
for (const waiter of waiters) {
|
|
291
|
-
waiter.reject(error);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
async next() {
|
|
295
|
-
if (this.buffer.length > 0) {
|
|
296
|
-
const value = this.buffer.shift();
|
|
297
|
-
return { value, done: false };
|
|
298
|
-
}
|
|
299
|
-
if (this.error) {
|
|
300
|
-
return Promise.reject(this.error);
|
|
301
|
-
}
|
|
302
|
-
if (this.ended) {
|
|
303
|
-
return { value: void 0, done: true };
|
|
304
|
-
}
|
|
305
|
-
return new Promise((resolve, reject) => {
|
|
306
|
-
this.waiters.push({ resolve, reject });
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
[Symbol.asyncIterator]() {
|
|
310
|
-
return this;
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
40
|
|
|
316
|
-
// src/
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
});
|
|
321
|
-
var Codex;
|
|
322
|
-
var init_codex = __esm({
|
|
323
|
-
"src/codex.ts"() {
|
|
324
|
-
"use strict";
|
|
325
|
-
init_exec();
|
|
326
|
-
init_nativeBinding();
|
|
327
|
-
init_thread();
|
|
328
|
-
Codex = class {
|
|
329
|
-
exec;
|
|
330
|
-
options;
|
|
331
|
-
nativeBinding;
|
|
332
|
-
constructor(options = {}) {
|
|
333
|
-
const predefinedTools = options.tools ? [...options.tools] : [];
|
|
334
|
-
this.nativeBinding = getNativeBinding();
|
|
335
|
-
this.options = { ...options, tools: [] };
|
|
336
|
-
if (this.nativeBinding) {
|
|
337
|
-
if (typeof this.nativeBinding.clearRegisteredTools === "function") {
|
|
338
|
-
this.nativeBinding.clearRegisteredTools();
|
|
339
|
-
}
|
|
340
|
-
for (const tool of predefinedTools) {
|
|
341
|
-
this.registerTool(tool);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
this.exec = new CodexExec();
|
|
345
|
-
}
|
|
346
|
-
registerTool(tool) {
|
|
347
|
-
if (!this.nativeBinding) {
|
|
348
|
-
throw new Error("Native tool registration requires the NAPI binding");
|
|
349
|
-
}
|
|
350
|
-
if (typeof this.nativeBinding.registerTool !== "function") {
|
|
351
|
-
console.warn("registerTool is not available in this build - tools feature may be incomplete");
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const { handler, ...info } = tool;
|
|
355
|
-
this.nativeBinding.registerTool(info, handler);
|
|
356
|
-
if (!this.options.tools) {
|
|
357
|
-
this.options.tools = [];
|
|
358
|
-
}
|
|
359
|
-
this.options.tools.push(tool);
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Starts a new conversation with an agent.
|
|
363
|
-
* @returns A new thread instance.
|
|
364
|
-
*/
|
|
365
|
-
startThread(options = {}) {
|
|
366
|
-
return new Thread(this.exec, this.options, options);
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Resumes a conversation with an agent based on the thread id.
|
|
370
|
-
* Threads are persisted in ~/.codex/sessions.
|
|
371
|
-
*
|
|
372
|
-
* @param id The id of the thread to resume.
|
|
373
|
-
* @returns A new thread instance.
|
|
374
|
-
*/
|
|
375
|
-
resumeThread(id, options = {}) {
|
|
376
|
-
return new Thread(this.exec, this.options, options, id);
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
});
|
|
41
|
+
// src/agents/CodexProvider.ts
|
|
42
|
+
import * as fs from "fs";
|
|
43
|
+
import * as path from "path";
|
|
44
|
+
import * as os from "os";
|
|
381
45
|
|
|
382
|
-
// src/
|
|
383
|
-
|
|
384
|
-
init_codex();
|
|
46
|
+
// src/agents/types.ts
|
|
47
|
+
import { Usage } from "@openai/agents-core";
|
|
385
48
|
|
|
386
49
|
// src/agents/CodexProvider.ts
|
|
387
50
|
var CodexProvider = class {
|
|
@@ -399,11 +62,16 @@ var CodexProvider = class {
|
|
|
399
62
|
*/
|
|
400
63
|
getCodex() {
|
|
401
64
|
if (!this.codex) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
65
|
+
try {
|
|
66
|
+
this.codex = new Codex({
|
|
67
|
+
apiKey: this.options.apiKey,
|
|
68
|
+
baseUrl: this.options.baseUrl
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Failed to initialize Codex: ${error instanceof Error ? error.message : String(error)}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
407
75
|
}
|
|
408
76
|
return this.codex;
|
|
409
77
|
}
|
|
@@ -411,68 +79,393 @@ var CodexProvider = class {
|
|
|
411
79
|
const model = modelName || this.options.defaultModel;
|
|
412
80
|
return new CodexModel(this.getCodex(), model, this.options);
|
|
413
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Register a programmatic approval callback on the underlying Codex instance.
|
|
84
|
+
*/
|
|
85
|
+
setApprovalCallback(callback) {
|
|
86
|
+
this.getCodex().setApprovalCallback(callback);
|
|
87
|
+
}
|
|
414
88
|
};
|
|
415
89
|
var CodexModel = class {
|
|
416
90
|
codex;
|
|
417
91
|
modelName;
|
|
418
92
|
thread = null;
|
|
419
93
|
options;
|
|
94
|
+
registeredTools = /* @__PURE__ */ new Set();
|
|
95
|
+
toolExecutors = /* @__PURE__ */ new Map();
|
|
96
|
+
tempImageFiles = /* @__PURE__ */ new Set();
|
|
97
|
+
streamedTurnItems = [];
|
|
98
|
+
lastStreamedMessage = null;
|
|
99
|
+
detachDiagnostics;
|
|
100
|
+
diagnosticsThread;
|
|
420
101
|
constructor(codex, modelName, options) {
|
|
421
102
|
this.codex = codex;
|
|
422
103
|
this.modelName = modelName;
|
|
423
104
|
this.options = options;
|
|
424
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Cleanup temporary image files created during request processing
|
|
108
|
+
*/
|
|
109
|
+
async cleanupTempFiles() {
|
|
110
|
+
for (const filepath of this.tempImageFiles) {
|
|
111
|
+
try {
|
|
112
|
+
await fs.promises.unlink(filepath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.tempImageFiles.clear();
|
|
117
|
+
}
|
|
425
118
|
/**
|
|
426
119
|
* Get or create the thread for this model instance
|
|
427
120
|
*/
|
|
428
121
|
getThread(conversationId) {
|
|
429
|
-
if (conversationId
|
|
430
|
-
this.thread
|
|
122
|
+
if (conversationId) {
|
|
123
|
+
if (!this.thread || this.thread.id !== conversationId) {
|
|
124
|
+
this.detachDiagnostics?.();
|
|
125
|
+
this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions());
|
|
126
|
+
this.diagnosticsThread = null;
|
|
127
|
+
}
|
|
431
128
|
} else if (!this.thread) {
|
|
129
|
+
this.detachDiagnostics?.();
|
|
432
130
|
this.thread = this.codex.startThread(this.getThreadOptions());
|
|
131
|
+
this.diagnosticsThread = null;
|
|
132
|
+
}
|
|
133
|
+
const thread = this.thread;
|
|
134
|
+
if (!thread) {
|
|
135
|
+
throw new Error("Thread initialization failed");
|
|
433
136
|
}
|
|
434
|
-
|
|
137
|
+
this.ensureDiagnosticsBridge(thread);
|
|
138
|
+
return thread;
|
|
139
|
+
}
|
|
140
|
+
ensureDiagnosticsBridge(thread) {
|
|
141
|
+
if (this.options.enableLsp === false) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.diagnosticsThread === thread && this.detachDiagnostics) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.detachDiagnostics?.();
|
|
148
|
+
this.diagnosticsThread = thread;
|
|
149
|
+
this.detachDiagnostics = attachLspDiagnostics(thread, {
|
|
150
|
+
workingDirectory: this.options.workingDirectory ?? process.cwd(),
|
|
151
|
+
waitForDiagnostics: true
|
|
152
|
+
});
|
|
435
153
|
}
|
|
436
154
|
getThreadOptions() {
|
|
437
155
|
return {
|
|
438
156
|
model: this.modelName,
|
|
157
|
+
// When a custom baseUrl is provided (e.g., test proxy), do not enable OSS mode,
|
|
158
|
+
// since the backend is not Ollama in that case.
|
|
159
|
+
oss: this.options.baseUrl ? false : this.options.oss,
|
|
439
160
|
workingDirectory: this.options.workingDirectory,
|
|
440
|
-
skipGitRepoCheck: this.options.skipGitRepoCheck
|
|
161
|
+
skipGitRepoCheck: this.options.skipGitRepoCheck,
|
|
162
|
+
sandboxMode: this.options.sandboxMode ?? "danger-full-access",
|
|
163
|
+
approvalMode: this.options.approvalMode,
|
|
164
|
+
reasoningEffort: this.options.reasoningEffort,
|
|
165
|
+
reasoningSummary: this.options.reasoningSummary
|
|
441
166
|
};
|
|
442
167
|
}
|
|
443
168
|
async getResponse(request) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
169
|
+
try {
|
|
170
|
+
const thread = this.getThread(request.conversationId || request.previousResponseId);
|
|
171
|
+
if (request.tools && request.tools.length > 0) {
|
|
172
|
+
this.registerRequestTools(request.tools);
|
|
173
|
+
}
|
|
174
|
+
const input = await this.convertRequestToInput(request);
|
|
175
|
+
const turn = await thread.run(input, {
|
|
176
|
+
outputSchema: normalizeAgentsOutputType(request.outputType),
|
|
177
|
+
oss: this.options.oss
|
|
178
|
+
});
|
|
179
|
+
const planItem = turn.items.filter((item) => item.type === "todo_list").slice(-1)[0];
|
|
180
|
+
const response = {
|
|
181
|
+
usage: this.convertUsage(turn.usage),
|
|
182
|
+
output: this.convertItemsToOutput(turn.items, turn.finalResponse),
|
|
183
|
+
responseId: thread.id || void 0
|
|
184
|
+
};
|
|
185
|
+
if (planItem) {
|
|
186
|
+
response.plan = { items: planItem.items };
|
|
187
|
+
}
|
|
188
|
+
return response;
|
|
189
|
+
} finally {
|
|
190
|
+
await this.cleanupTempFiles();
|
|
191
|
+
}
|
|
454
192
|
}
|
|
455
193
|
async *getStreamedResponse(request) {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
194
|
+
const MAX_ACCUMULATED_SIZE = 1e7;
|
|
195
|
+
try {
|
|
196
|
+
const thread = this.getThread(request.conversationId || request.previousResponseId);
|
|
197
|
+
if (request.tools && request.tools.length > 0) {
|
|
198
|
+
this.registerRequestTools(request.tools);
|
|
199
|
+
}
|
|
200
|
+
const input = await this.convertRequestToInput(request);
|
|
201
|
+
const { events } = await thread.runStreamed(input, {
|
|
202
|
+
outputSchema: normalizeAgentsOutputType(request.outputType),
|
|
203
|
+
oss: this.options.oss
|
|
204
|
+
});
|
|
205
|
+
const textAccumulator = /* @__PURE__ */ new Map();
|
|
206
|
+
for await (const event of events) {
|
|
207
|
+
let totalSize = 0;
|
|
208
|
+
for (const text of textAccumulator.values()) {
|
|
209
|
+
totalSize += text.length;
|
|
210
|
+
}
|
|
211
|
+
if (totalSize > MAX_ACCUMULATED_SIZE) {
|
|
212
|
+
throw new Error(`Accumulated text exceeded maximum size limit (${MAX_ACCUMULATED_SIZE} bytes)`);
|
|
213
|
+
}
|
|
214
|
+
const streamEvents = this.convertCodexEventToStreamEvent(event, textAccumulator);
|
|
215
|
+
for (const streamEvent of streamEvents) {
|
|
216
|
+
yield streamEvent;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
await this.cleanupTempFiles();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Register tools from ModelRequest with the Codex instance
|
|
225
|
+
*
|
|
226
|
+
* Converts SerializedTool format (OpenAI Agents) to NativeToolDefinition format (Codex)
|
|
227
|
+
* and registers them with the Codex instance for bidirectional tool execution.
|
|
228
|
+
*/
|
|
229
|
+
registerRequestTools(tools) {
|
|
230
|
+
this.toolExecutors.clear();
|
|
231
|
+
for (const tool2 of tools) {
|
|
232
|
+
if (tool2.type !== "function") {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (this.registeredTools.has(tool2.name)) {
|
|
236
|
+
const executor = this.resolveToolExecutor(tool2.name);
|
|
237
|
+
if (executor) {
|
|
238
|
+
this.toolExecutors.set(tool2.name, executor);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const executor = this.resolveToolExecutor(tool2.name);
|
|
244
|
+
if (executor) {
|
|
245
|
+
this.toolExecutors.set(tool2.name, executor);
|
|
246
|
+
}
|
|
247
|
+
const nativeToolDef = {
|
|
248
|
+
name: tool2.name,
|
|
249
|
+
description: tool2.description,
|
|
250
|
+
parameters: tool2.parameters,
|
|
251
|
+
// The handler is called when Codex wants to execute this tool
|
|
252
|
+
handler: async (invocation) => {
|
|
253
|
+
return await this.executeToolViaFramework(invocation);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
this.codex.registerTool(nativeToolDef);
|
|
257
|
+
this.registeredTools.add(tool2.name);
|
|
258
|
+
console.log(`Registered tool with Codex: ${tool2.name}`);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const errorMessage = `Failed to register tool ${tool2.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
261
|
+
console.error(errorMessage);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
resolveToolExecutor(toolName) {
|
|
266
|
+
return getCodexToolExecutor(toolName);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Execute a tool via the OpenAI Agents framework
|
|
270
|
+
*
|
|
271
|
+
* This is the bridge between Codex's tool execution and the framework's tool handlers.
|
|
272
|
+
*
|
|
273
|
+
* FRAMEWORK INTEGRATION NOTE:
|
|
274
|
+
* This method currently returns a placeholder result because the actual execution
|
|
275
|
+
* requires integration with the OpenAI Agents framework's tool execution loop.
|
|
276
|
+
*
|
|
277
|
+
* In a full implementation, this would:
|
|
278
|
+
* 1. Emit a "tool_call_requested" event that the framework can listen to
|
|
279
|
+
* 2. Wait for the framework to execute the tool and provide the result
|
|
280
|
+
* 3. Return that result to Codex
|
|
281
|
+
*
|
|
282
|
+
* For now, this creates a promise that could be resolved by framework code,
|
|
283
|
+
* but the framework integration is not yet complete.
|
|
284
|
+
*/
|
|
285
|
+
async executeToolViaFramework(invocation) {
|
|
286
|
+
if (!invocation) {
|
|
287
|
+
console.warn("Codex requested a tool execution without invocation data.");
|
|
288
|
+
return {
|
|
289
|
+
output: JSON.stringify({
|
|
290
|
+
message: "Tool invocation payload missing",
|
|
291
|
+
note: "Codex returned null invocation data so the tool was not executed."
|
|
292
|
+
}),
|
|
293
|
+
success: false,
|
|
294
|
+
error: "Missing tool invocation data from Codex"
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
console.log(
|
|
298
|
+
`Tool execution requested by Codex: ${invocation.toolName} (callId: ${invocation.callId})`
|
|
299
|
+
);
|
|
300
|
+
const executor = this.toolExecutors.get(invocation.toolName) ?? getCodexToolExecutor(invocation.toolName);
|
|
301
|
+
if (!executor) {
|
|
302
|
+
const message = `No Codex executor registered for tool '${invocation.toolName}'. Use codexTool() or provide a codexExecute handler.`;
|
|
303
|
+
console.warn(message);
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: message,
|
|
307
|
+
output: void 0
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
let parsedArguments = {};
|
|
311
|
+
if (invocation.arguments) {
|
|
312
|
+
try {
|
|
313
|
+
parsedArguments = JSON.parse(invocation.arguments);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
error: `Failed to parse tool arguments: ${error instanceof Error ? error.message : String(error)}`
|
|
318
|
+
};
|
|
466
319
|
}
|
|
467
|
-
|
|
468
|
-
|
|
320
|
+
}
|
|
321
|
+
const context = {
|
|
322
|
+
name: invocation.toolName,
|
|
323
|
+
callId: invocation.callId,
|
|
324
|
+
arguments: parsedArguments,
|
|
325
|
+
rawInvocation: invocation
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
const result = await executor(context);
|
|
329
|
+
return this.normalizeToolResult(result);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
error: error instanceof Error ? error.message : String(error)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle image input by converting to local file path
|
|
339
|
+
* Supports: base64 data URLs, HTTP(S) URLs, and file IDs (not yet implemented)
|
|
340
|
+
*/
|
|
341
|
+
normalizeToolResult(result) {
|
|
342
|
+
if (result === void 0 || result === null) {
|
|
343
|
+
return { success: true };
|
|
344
|
+
}
|
|
345
|
+
if (typeof result === "string") {
|
|
346
|
+
return { success: true, output: result };
|
|
347
|
+
}
|
|
348
|
+
if (typeof result === "object" && ("output" in result || "error" in result || "success" in result)) {
|
|
349
|
+
return {
|
|
350
|
+
success: result.success ?? !result.error,
|
|
351
|
+
output: result.output,
|
|
352
|
+
error: result.error
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
output: JSON.stringify(result)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async handleImageInput(item) {
|
|
361
|
+
const imageValue = item.image;
|
|
362
|
+
if (typeof imageValue === "string") {
|
|
363
|
+
if (imageValue.startsWith("data:image/")) {
|
|
364
|
+
return await this.saveBase64Image(imageValue);
|
|
365
|
+
} else if (imageValue.startsWith("http://") || imageValue.startsWith("https://")) {
|
|
366
|
+
return await this.downloadImage(imageValue);
|
|
367
|
+
} else if (fs.existsSync(imageValue)) {
|
|
368
|
+
return imageValue;
|
|
369
|
+
} else {
|
|
370
|
+
throw new Error(`Invalid image format: ${imageValue.substring(0, 50)}...`);
|
|
469
371
|
}
|
|
372
|
+
} else if (typeof imageValue === "object" && "url" in imageValue) {
|
|
373
|
+
return await this.downloadImage(imageValue.url);
|
|
374
|
+
} else if (typeof imageValue === "object" && "fileId" in imageValue) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Image fileId references are not yet supported. File IDs would need to be downloaded from the service first.`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Save base64-encoded image to temporary file
|
|
383
|
+
*/
|
|
384
|
+
async saveBase64Image(dataUrl) {
|
|
385
|
+
const matches = dataUrl.match(/^data:image\/([^;]+);base64,(.+)$/);
|
|
386
|
+
if (!matches) {
|
|
387
|
+
throw new Error("Invalid base64 image data URL");
|
|
388
|
+
}
|
|
389
|
+
const mediaType = matches[1];
|
|
390
|
+
const base64Data = matches[2];
|
|
391
|
+
if (!base64Data) {
|
|
392
|
+
throw new Error("Invalid base64 data in image URL");
|
|
393
|
+
}
|
|
394
|
+
const sanitizedBase64 = base64Data.replace(/\s/g, "");
|
|
395
|
+
if (sanitizedBase64.length === 0) {
|
|
396
|
+
throw new Error("Invalid base64 data in image URL");
|
|
397
|
+
}
|
|
398
|
+
if (!/^[A-Za-z0-9+/=_-]+$/.test(sanitizedBase64)) {
|
|
399
|
+
throw new Error("Invalid base64 data in image URL");
|
|
400
|
+
}
|
|
401
|
+
const normalizedBase64 = sanitizedBase64.replace(/-/g, "+").replace(/_/g, "/");
|
|
402
|
+
let buffer;
|
|
403
|
+
try {
|
|
404
|
+
buffer = Buffer.from(normalizedBase64, "base64");
|
|
405
|
+
} catch {
|
|
406
|
+
throw new Error("Invalid base64 data in image URL");
|
|
407
|
+
}
|
|
408
|
+
if (buffer.length === 0) {
|
|
409
|
+
throw new Error("Invalid base64 data in image URL");
|
|
410
|
+
}
|
|
411
|
+
const reencoded = buffer.toString("base64").replace(/=+$/, "");
|
|
412
|
+
const normalizedInput = normalizedBase64.replace(/=+$/, "");
|
|
413
|
+
if (reencoded !== normalizedInput) {
|
|
414
|
+
throw new Error("Invalid base64 data in image URL");
|
|
415
|
+
}
|
|
416
|
+
const extension = this.getExtensionFromMediaType(mediaType, "png");
|
|
417
|
+
const tempDir = os.tmpdir();
|
|
418
|
+
const filename = `codex-image-${Date.now()}.${extension}`;
|
|
419
|
+
const filepath = path.join(tempDir, filename);
|
|
420
|
+
await fs.promises.writeFile(filepath, buffer);
|
|
421
|
+
this.tempImageFiles.add(filepath);
|
|
422
|
+
return filepath;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Download image from URL to temporary file
|
|
426
|
+
*/
|
|
427
|
+
async downloadImage(url) {
|
|
428
|
+
const response = await fetch(url);
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
throw new Error(`Failed to download image from ${url}: ${response.statusText}`);
|
|
470
431
|
}
|
|
432
|
+
const buffer = await response.arrayBuffer();
|
|
433
|
+
const contentType = response.headers.get("content-type") || "image/png";
|
|
434
|
+
const mediaTypePart = contentType.split(";")[0]?.trim() || "image/png";
|
|
435
|
+
const mediaType = mediaTypePart.split("/")[1] || "png";
|
|
436
|
+
const extension = this.getExtensionFromMediaType(mediaType, "png");
|
|
437
|
+
const tempDir = os.tmpdir();
|
|
438
|
+
const filename = `codex-image-${Date.now()}.${extension}`;
|
|
439
|
+
const filepath = path.join(tempDir, filename);
|
|
440
|
+
await fs.promises.writeFile(filepath, Buffer.from(buffer));
|
|
441
|
+
this.tempImageFiles.add(filepath);
|
|
442
|
+
return filepath;
|
|
471
443
|
}
|
|
472
444
|
/**
|
|
473
|
-
* Convert
|
|
445
|
+
* Convert media type to file extension
|
|
446
|
+
* Handles special cases like "jpeg" -> "jpg", "svg+xml" -> "svg"
|
|
474
447
|
*/
|
|
475
|
-
|
|
448
|
+
getExtensionFromMediaType(mediaType, defaultExt) {
|
|
449
|
+
if (!mediaType) {
|
|
450
|
+
return defaultExt;
|
|
451
|
+
}
|
|
452
|
+
const normalized = mediaType.toLowerCase().trim();
|
|
453
|
+
const extensionMap = {
|
|
454
|
+
"jpeg": "jpg",
|
|
455
|
+
"svg+xml": "svg",
|
|
456
|
+
"vnd.microsoft.icon": "ico",
|
|
457
|
+
"x-icon": "ico"
|
|
458
|
+
};
|
|
459
|
+
if (extensionMap[normalized]) {
|
|
460
|
+
return extensionMap[normalized];
|
|
461
|
+
}
|
|
462
|
+
const simpleExtension = normalized.split("+")[0];
|
|
463
|
+
if (simpleExtension && /^[a-z0-9]+$/.test(simpleExtension)) {
|
|
464
|
+
return simpleExtension;
|
|
465
|
+
}
|
|
466
|
+
return defaultExt;
|
|
467
|
+
}
|
|
468
|
+
async convertRequestToInput(request) {
|
|
476
469
|
const parts = [];
|
|
477
470
|
if (request.systemInstructions) {
|
|
478
471
|
parts.push({
|
|
@@ -488,15 +481,75 @@ ${request.systemInstructions}
|
|
|
488
481
|
parts.push({ type: "text", text: request.input });
|
|
489
482
|
} else {
|
|
490
483
|
for (const item of request.input) {
|
|
491
|
-
if (item
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
484
|
+
if ("file" in item && "type" in item) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`CodexProvider does not yet support input_file type. File handling needs to be implemented based on file type and format.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if ("audio" in item && "type" in item) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`CodexProvider does not yet support input_audio type. Audio handling needs to be implemented.`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if ("image" in item && "type" in item && item.type !== "message") {
|
|
495
|
+
const imageItem = item;
|
|
496
|
+
const imagePath = await this.handleImageInput(imageItem);
|
|
497
|
+
if (imagePath) {
|
|
498
|
+
parts.push({ type: "local_image", path: imagePath });
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (item.type === "function_call_result") {
|
|
503
|
+
if ("name" in item && "result" in item) {
|
|
504
|
+
parts.push({
|
|
505
|
+
type: "text",
|
|
506
|
+
text: `[Tool ${item.name} returned: ${item.result}]`
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
} else if (item.type === "reasoning") {
|
|
510
|
+
let text = "";
|
|
511
|
+
if ("content" in item && typeof item.content === "string") {
|
|
512
|
+
text = item.content;
|
|
513
|
+
} else if ("reasoning" in item && typeof item.reasoning === "string") {
|
|
514
|
+
text = item.reasoning;
|
|
515
|
+
}
|
|
516
|
+
if (text) {
|
|
517
|
+
parts.push({
|
|
518
|
+
type: "text",
|
|
519
|
+
text: `[Reasoning: ${text}]`
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
} else if ((item.type === "message" || item.type === void 0) && "role" in item) {
|
|
523
|
+
if (!("content" in item)) continue;
|
|
524
|
+
const content = item.content;
|
|
525
|
+
if (typeof content === "string") {
|
|
526
|
+
parts.push({ type: "text", text: content });
|
|
527
|
+
} else if (Array.isArray(content)) {
|
|
528
|
+
for (const contentItem of content) {
|
|
529
|
+
if (contentItem.type === "input_text") {
|
|
530
|
+
parts.push({ type: "text", text: contentItem.text });
|
|
531
|
+
} else if (contentItem.type === "input_image") {
|
|
532
|
+
const imagePath = await this.handleImageInput(contentItem);
|
|
533
|
+
if (imagePath) {
|
|
534
|
+
parts.push({ type: "local_image", path: imagePath });
|
|
535
|
+
}
|
|
536
|
+
} else if (contentItem.type === "input_file") {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`CodexProvider does not yet support input_file type. File handling needs to be implemented based on file type and format.`
|
|
539
|
+
);
|
|
540
|
+
} else if (contentItem.type === "audio") {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`CodexProvider does not yet support audio type. Audio handling needs to be implemented.`
|
|
543
|
+
);
|
|
544
|
+
} else if (contentItem.type === "refusal") {
|
|
545
|
+
parts.push({
|
|
546
|
+
type: "text",
|
|
547
|
+
text: `[Refusal: ${contentItem.refusal}]`
|
|
548
|
+
});
|
|
549
|
+
} else if (contentItem.type === "output_text") {
|
|
550
|
+
parts.push({ type: "text", text: contentItem.text });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
500
553
|
}
|
|
501
554
|
}
|
|
502
555
|
}
|
|
@@ -511,19 +564,18 @@ ${request.systemInstructions}
|
|
|
511
564
|
*/
|
|
512
565
|
convertUsage(usage) {
|
|
513
566
|
if (!usage) {
|
|
514
|
-
return
|
|
515
|
-
inputTokens: 0,
|
|
516
|
-
outputTokens: 0,
|
|
517
|
-
totalTokens: 0
|
|
518
|
-
};
|
|
567
|
+
return new Usage();
|
|
519
568
|
}
|
|
520
|
-
const
|
|
521
|
-
|
|
569
|
+
const converted = new Usage({
|
|
570
|
+
requests: 1,
|
|
522
571
|
inputTokens: usage.input_tokens,
|
|
523
572
|
outputTokens: usage.output_tokens,
|
|
524
|
-
totalTokens: usage.input_tokens + usage.output_tokens
|
|
525
|
-
|
|
526
|
-
|
|
573
|
+
totalTokens: usage.input_tokens + usage.output_tokens
|
|
574
|
+
});
|
|
575
|
+
if (usage.cached_input_tokens) {
|
|
576
|
+
converted.inputTokensDetails = [{ cachedTokens: usage.cached_input_tokens }];
|
|
577
|
+
}
|
|
578
|
+
return converted;
|
|
527
579
|
}
|
|
528
580
|
/**
|
|
529
581
|
* Convert Codex ThreadItems to AgentOutputItems
|
|
@@ -547,13 +599,9 @@ ${request.systemInstructions}
|
|
|
547
599
|
});
|
|
548
600
|
break;
|
|
549
601
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
type: "reasoning",
|
|
553
|
-
reasoning: item.text
|
|
554
|
-
});
|
|
602
|
+
// For final output, omit internal "reasoning" items. Streaming already surfaces reasoning events.
|
|
603
|
+
case "reasoning":
|
|
555
604
|
break;
|
|
556
|
-
}
|
|
557
605
|
// Codex handles tools internally, so we don't expose them as function calls
|
|
558
606
|
// The results are already incorporated into the agent_message
|
|
559
607
|
case "command_execution":
|
|
@@ -579,60 +627,2059 @@ ${request.systemInstructions}
|
|
|
579
627
|
}
|
|
580
628
|
return output;
|
|
581
629
|
}
|
|
630
|
+
buildStreamResponse(usage, responseId, items, lastMessage) {
|
|
631
|
+
const messageItems = items.filter(
|
|
632
|
+
(item) => item.type === "agent_message"
|
|
633
|
+
);
|
|
634
|
+
const output = this.convertItemsToOutput(messageItems, lastMessage ?? "");
|
|
635
|
+
const usageData = {
|
|
636
|
+
requests: usage.requests,
|
|
637
|
+
inputTokens: usage.inputTokens,
|
|
638
|
+
outputTokens: usage.outputTokens,
|
|
639
|
+
totalTokens: usage.totalTokens,
|
|
640
|
+
inputTokensDetails: usage.inputTokensDetails?.[0],
|
|
641
|
+
outputTokensDetails: usage.outputTokensDetails?.[0]
|
|
642
|
+
};
|
|
643
|
+
const latestPlan = items.filter((item) => item.type === "todo_list").slice(-1)[0];
|
|
644
|
+
const response = {
|
|
645
|
+
id: responseId,
|
|
646
|
+
responseId,
|
|
647
|
+
usage: usageData,
|
|
648
|
+
output
|
|
649
|
+
};
|
|
650
|
+
if (latestPlan) {
|
|
651
|
+
response.plan = { items: latestPlan.items };
|
|
652
|
+
}
|
|
653
|
+
return response;
|
|
654
|
+
}
|
|
582
655
|
/**
|
|
583
656
|
* Convert Codex ThreadEvent to OpenAI Agents StreamEvent
|
|
584
657
|
*/
|
|
585
|
-
convertCodexEventToStreamEvent(event,
|
|
658
|
+
convertCodexEventToStreamEvent(event, textAccumulator) {
|
|
586
659
|
const events = [];
|
|
587
660
|
switch (event.type) {
|
|
588
|
-
case "thread.started":
|
|
661
|
+
case "thread.started": {
|
|
589
662
|
events.push({ type: "response_started" });
|
|
663
|
+
const responseId = this.thread?.id ?? "codex-stream-response";
|
|
664
|
+
events.push({
|
|
665
|
+
type: "response.created",
|
|
666
|
+
response: { id: responseId }
|
|
667
|
+
});
|
|
590
668
|
break;
|
|
669
|
+
}
|
|
591
670
|
case "turn.started":
|
|
671
|
+
this.streamedTurnItems = [];
|
|
672
|
+
this.lastStreamedMessage = null;
|
|
592
673
|
break;
|
|
593
674
|
case "item.started":
|
|
675
|
+
if (event.item.type === "agent_message" || event.item.type === "reasoning") {
|
|
676
|
+
const itemKey = `${event.item.type}`;
|
|
677
|
+
textAccumulator.set(itemKey, "");
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
case "background_event":
|
|
681
|
+
events.push({
|
|
682
|
+
type: "model",
|
|
683
|
+
event: {
|
|
684
|
+
type: "background_event",
|
|
685
|
+
message: event.message
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
break;
|
|
689
|
+
case "item.updated":
|
|
690
|
+
if (event.item.type === "agent_message") {
|
|
691
|
+
const itemKey = "agent_message";
|
|
692
|
+
const previousText = textAccumulator.get(itemKey) || "";
|
|
693
|
+
const currentText = event.item.text;
|
|
694
|
+
if (currentText.length < previousText.length) {
|
|
695
|
+
console.warn("Received backwards update for text - ignoring delta");
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
if (currentText.length > previousText.length) {
|
|
699
|
+
const delta = currentText.slice(previousText.length);
|
|
700
|
+
textAccumulator.set(itemKey, currentText);
|
|
701
|
+
events.push({
|
|
702
|
+
type: "output_text_delta",
|
|
703
|
+
delta
|
|
704
|
+
});
|
|
705
|
+
events.push({
|
|
706
|
+
type: "response.output_text.delta",
|
|
707
|
+
delta
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
} else if (event.item.type === "reasoning") {
|
|
711
|
+
const itemKey = "reasoning";
|
|
712
|
+
const previousText = textAccumulator.get(itemKey) || "";
|
|
713
|
+
const currentText = event.item.text;
|
|
714
|
+
if (currentText.length > previousText.length) {
|
|
715
|
+
const delta = currentText.slice(previousText.length);
|
|
716
|
+
textAccumulator.set(itemKey, currentText);
|
|
717
|
+
events.push({
|
|
718
|
+
type: "model",
|
|
719
|
+
event: {
|
|
720
|
+
type: "reasoning_delta",
|
|
721
|
+
delta
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
594
726
|
break;
|
|
595
727
|
case "item.completed":
|
|
728
|
+
this.streamedTurnItems.push(event.item);
|
|
596
729
|
if (event.item.type === "agent_message") {
|
|
730
|
+
textAccumulator.delete("agent_message");
|
|
731
|
+
this.lastStreamedMessage = event.item.text;
|
|
732
|
+
} else if (event.item.type === "reasoning") {
|
|
597
733
|
events.push({
|
|
598
|
-
type: "
|
|
599
|
-
|
|
734
|
+
type: "model",
|
|
735
|
+
event: {
|
|
736
|
+
type: "reasoning_done",
|
|
737
|
+
reasoning: event.item.text
|
|
738
|
+
}
|
|
600
739
|
});
|
|
601
|
-
|
|
740
|
+
textAccumulator.delete("reasoning");
|
|
741
|
+
} else if (event.item.type === "todo_list") {
|
|
602
742
|
events.push({
|
|
603
|
-
type: "
|
|
604
|
-
|
|
743
|
+
type: "model",
|
|
744
|
+
event: {
|
|
745
|
+
type: "plan_update",
|
|
746
|
+
items: event.item.items
|
|
747
|
+
}
|
|
605
748
|
});
|
|
606
749
|
}
|
|
607
750
|
break;
|
|
608
|
-
case "turn.completed":
|
|
751
|
+
case "turn.completed": {
|
|
752
|
+
const usage = this.convertUsage(event.usage);
|
|
753
|
+
const responseId = this.thread?.id ?? "codex-stream-response";
|
|
754
|
+
const response = this.buildStreamResponse(
|
|
755
|
+
usage,
|
|
756
|
+
responseId,
|
|
757
|
+
this.streamedTurnItems,
|
|
758
|
+
this.lastStreamedMessage
|
|
759
|
+
);
|
|
760
|
+
this.streamedTurnItems = [];
|
|
761
|
+
this.lastStreamedMessage = null;
|
|
609
762
|
events.push({
|
|
610
|
-
type: "
|
|
763
|
+
type: "response.completed",
|
|
611
764
|
response: {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
765
|
+
id: response.id,
|
|
766
|
+
usage: {
|
|
767
|
+
input_tokens: usage.inputTokens,
|
|
768
|
+
input_tokens_details: usage.inputTokensDetails?.[0] ?? null,
|
|
769
|
+
output_tokens: usage.outputTokens,
|
|
770
|
+
output_tokens_details: usage.outputTokensDetails?.[0] ?? null,
|
|
771
|
+
total_tokens: usage.totalTokens
|
|
772
|
+
},
|
|
773
|
+
...response.output && response.output.length > 0 ? {
|
|
774
|
+
output: response.output.map((item) => {
|
|
775
|
+
if (item.type === "message" && item.role === "assistant") {
|
|
776
|
+
return {
|
|
777
|
+
id: item.id ?? "msg_1",
|
|
778
|
+
role: item.role,
|
|
779
|
+
content: item.content
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return item;
|
|
783
|
+
}),
|
|
784
|
+
output_text: response.output.filter(
|
|
785
|
+
(item) => item.type === "message" && item.role === "assistant"
|
|
786
|
+
)[0]?.content?.find(
|
|
787
|
+
(c) => c.type === "output_text"
|
|
788
|
+
)?.text ?? (this.lastStreamedMessage ?? "")
|
|
789
|
+
} : {}
|
|
616
790
|
}
|
|
617
791
|
});
|
|
792
|
+
events.push({
|
|
793
|
+
type: "response_done",
|
|
794
|
+
response
|
|
795
|
+
});
|
|
618
796
|
break;
|
|
797
|
+
}
|
|
619
798
|
case "turn.failed":
|
|
620
799
|
events.push({
|
|
621
|
-
type: "
|
|
622
|
-
|
|
623
|
-
|
|
800
|
+
type: "model",
|
|
801
|
+
event: {
|
|
802
|
+
type: "error",
|
|
803
|
+
error: {
|
|
804
|
+
message: event.error.message
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
break;
|
|
809
|
+
case "error":
|
|
810
|
+
events.push({
|
|
811
|
+
type: "model",
|
|
812
|
+
event: {
|
|
813
|
+
type: "error",
|
|
814
|
+
error: {
|
|
815
|
+
message: event.message
|
|
816
|
+
}
|
|
624
817
|
}
|
|
625
818
|
});
|
|
626
819
|
break;
|
|
820
|
+
case "raw_event":
|
|
821
|
+
break;
|
|
627
822
|
default:
|
|
628
823
|
break;
|
|
629
824
|
}
|
|
825
|
+
if (event?.type !== "raw_event") {
|
|
826
|
+
const rawEvent = {
|
|
827
|
+
type: "raw_event",
|
|
828
|
+
raw: event
|
|
829
|
+
};
|
|
830
|
+
if (events.length === 0) {
|
|
831
|
+
return [rawEvent];
|
|
832
|
+
}
|
|
833
|
+
const result = [...events];
|
|
834
|
+
const insertIndex = Math.min(1, result.length);
|
|
835
|
+
result.splice(insertIndex, 0, rawEvent);
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
630
838
|
return events;
|
|
631
839
|
}
|
|
632
840
|
};
|
|
841
|
+
function isObject(value) {
|
|
842
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
843
|
+
}
|
|
844
|
+
function normalizeAgentsOutputType(outputType) {
|
|
845
|
+
if (!isObject(outputType)) return void 0;
|
|
846
|
+
const outType = outputType;
|
|
847
|
+
const t = typeof outType.type === "string" ? outType.type : void 0;
|
|
848
|
+
if (t === "json_schema" || t === "json-schema") {
|
|
849
|
+
const topLevelSchema = outType.schema;
|
|
850
|
+
if (isObject(topLevelSchema)) {
|
|
851
|
+
return topLevelSchema;
|
|
852
|
+
}
|
|
853
|
+
const nested = outType.json_schema;
|
|
854
|
+
if (isObject(nested)) {
|
|
855
|
+
const nestedSchema = nested.schema;
|
|
856
|
+
if (isObject(nestedSchema)) {
|
|
857
|
+
return nestedSchema;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return void 0;
|
|
861
|
+
}
|
|
862
|
+
if ("schema" in outType && isObject(outType.schema)) {
|
|
863
|
+
return outType.schema;
|
|
864
|
+
}
|
|
865
|
+
if ("type" in outType && outType.type === "object" || "properties" in outType || "required" in outType) {
|
|
866
|
+
return outType;
|
|
867
|
+
}
|
|
868
|
+
return void 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/agents/codexTool.ts
|
|
872
|
+
import { tool } from "@openai/agents";
|
|
873
|
+
function codexTool(options) {
|
|
874
|
+
const { codexExecute, ...delegate } = options;
|
|
875
|
+
const agentTool = tool(delegate);
|
|
876
|
+
const executor = createCodexExecutor(agentTool.name, codexExecute);
|
|
877
|
+
registerCodexToolExecutor(agentTool.name, executor);
|
|
878
|
+
return agentTool;
|
|
879
|
+
}
|
|
880
|
+
function createCodexExecutor(toolName, customExecutor) {
|
|
881
|
+
return async ({ arguments: args }) => {
|
|
882
|
+
const parsedArgs = args ?? {};
|
|
883
|
+
try {
|
|
884
|
+
const result = await customExecutor(parsedArgs);
|
|
885
|
+
return result;
|
|
886
|
+
} catch (error) {
|
|
887
|
+
throw new Error(`Codex tool '${toolName}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/agents/streamFormatter.ts
|
|
893
|
+
async function formatStream(stream, options = {}) {
|
|
894
|
+
const state = {
|
|
895
|
+
text: "",
|
|
896
|
+
reasoning: "",
|
|
897
|
+
toolCalls: [],
|
|
898
|
+
usage: {
|
|
899
|
+
inputTokens: 0,
|
|
900
|
+
outputTokens: 0,
|
|
901
|
+
totalTokens: 0
|
|
902
|
+
},
|
|
903
|
+
errors: []
|
|
904
|
+
};
|
|
905
|
+
for await (const event of stream) {
|
|
906
|
+
switch (event.type) {
|
|
907
|
+
case "response_started":
|
|
908
|
+
options.onUpdate?.({ usage: state.usage });
|
|
909
|
+
break;
|
|
910
|
+
case "output_text_delta":
|
|
911
|
+
state.text += event.delta;
|
|
912
|
+
options.onUpdate?.({ text: state.text });
|
|
913
|
+
break;
|
|
914
|
+
case "model": {
|
|
915
|
+
const e = event.event;
|
|
916
|
+
if (e && typeof e === "object") {
|
|
917
|
+
if (e.type === "reasoning_delta" && typeof e.delta === "string") {
|
|
918
|
+
state.reasoning += e.delta;
|
|
919
|
+
options.onUpdate?.({ reasoning: state.reasoning });
|
|
920
|
+
} else if (e.type === "reasoning_done" && typeof e.reasoning === "string") {
|
|
921
|
+
state.reasoning = e.reasoning || state.reasoning;
|
|
922
|
+
options.onUpdate?.({ reasoning: state.reasoning });
|
|
923
|
+
} else if (e.type === "error" && e.error && typeof e.error.message === "string") {
|
|
924
|
+
state.errors.push({ message: e.error.message });
|
|
925
|
+
options.onUpdate?.({ errors: state.errors.slice() });
|
|
926
|
+
} else if (typeof e.type === "string" && e.type.startsWith("tool_")) {
|
|
927
|
+
state.toolCalls.push({
|
|
928
|
+
name: e.name,
|
|
929
|
+
input: e.input,
|
|
930
|
+
output: e.output,
|
|
931
|
+
status: e.status === "started" || e.status === "completed" ? e.status : void 0
|
|
932
|
+
});
|
|
933
|
+
options.onUpdate?.({ toolCalls: state.toolCalls.slice() });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
case "response_done":
|
|
939
|
+
state.responseId = event.response.id;
|
|
940
|
+
{
|
|
941
|
+
const u = event.response.usage;
|
|
942
|
+
const mergeDetails = (arr) => {
|
|
943
|
+
if (!arr || arr.length === 0) return void 0;
|
|
944
|
+
const out = {};
|
|
945
|
+
for (const rec of arr) {
|
|
946
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
947
|
+
out[k] = (out[k] ?? 0) + (typeof v === "number" ? v : 0);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return out;
|
|
951
|
+
};
|
|
952
|
+
const inputDetails = mergeDetails(u.inputTokensDetails);
|
|
953
|
+
const outputDetails = mergeDetails(u.outputTokensDetails);
|
|
954
|
+
state.usage = {
|
|
955
|
+
requests: u.requests,
|
|
956
|
+
inputTokens: u.inputTokens ?? 0,
|
|
957
|
+
outputTokens: u.outputTokens ?? 0,
|
|
958
|
+
totalTokens: u.totalTokens ?? 0,
|
|
959
|
+
inputTokensDetails: inputDetails,
|
|
960
|
+
outputTokensDetails: outputDetails
|
|
961
|
+
};
|
|
962
|
+
state.cachedTokens = inputDetails?.cachedTokens ?? state.cachedTokens;
|
|
963
|
+
}
|
|
964
|
+
if (event.response.providerData && typeof event.response.providerData === "object") {
|
|
965
|
+
state.providerData = event.response.providerData;
|
|
966
|
+
options.onUpdate?.({ providerData: state.providerData });
|
|
967
|
+
}
|
|
968
|
+
options.onUpdate?.({ responseId: state.responseId, usage: state.usage, cachedTokens: state.cachedTokens });
|
|
969
|
+
break;
|
|
970
|
+
default:
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return state;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/agents/OpenCodeAgent.ts
|
|
978
|
+
import net from "net";
|
|
979
|
+
var DEFAULT_MODEL = "anthropic/claude-sonnet-4-5-20250929";
|
|
980
|
+
var DEFAULT_HOSTNAME = "127.0.0.1";
|
|
981
|
+
var DEFAULT_PORT = 4096;
|
|
982
|
+
var opencodeModulePromise = null;
|
|
983
|
+
async function loadOpencodeModule() {
|
|
984
|
+
if (!opencodeModulePromise) {
|
|
985
|
+
opencodeModulePromise = import("@opencode-ai/sdk");
|
|
986
|
+
}
|
|
987
|
+
return opencodeModulePromise;
|
|
988
|
+
}
|
|
989
|
+
async function isPortAvailable(port, host) {
|
|
990
|
+
return new Promise((resolve) => {
|
|
991
|
+
const tester = net.createServer().once("error", () => resolve(false)).once("listening", () => tester.close(() => resolve(true))).listen(port, host);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
async function findAvailablePort(host, preferred) {
|
|
995
|
+
if (preferred !== void 0 && await isPortAvailable(preferred, host)) {
|
|
996
|
+
return preferred;
|
|
997
|
+
}
|
|
998
|
+
return new Promise((resolve, reject) => {
|
|
999
|
+
const server = net.createServer();
|
|
1000
|
+
server.once("error", reject);
|
|
1001
|
+
server.listen(0, host, () => {
|
|
1002
|
+
const address = server.address();
|
|
1003
|
+
if (!address || typeof address === "string") {
|
|
1004
|
+
server.close(() => reject(new Error("Failed to determine available port")));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const { port } = address;
|
|
1008
|
+
server.close(() => resolve(port));
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
var OpenCodeAgent = class {
|
|
1013
|
+
options;
|
|
1014
|
+
approvalHandler;
|
|
1015
|
+
clientPromise;
|
|
1016
|
+
closeCallback;
|
|
1017
|
+
constructor(options = {}) {
|
|
1018
|
+
this.options = options;
|
|
1019
|
+
this.approvalHandler = options.onApprovalRequest;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Cleanup method to shut down the OpenCode server if one was started.
|
|
1023
|
+
* Should be called when done using the agent to prevent zombie processes.
|
|
1024
|
+
*/
|
|
1025
|
+
async close() {
|
|
1026
|
+
if (this.closeCallback) {
|
|
1027
|
+
this.closeCallback();
|
|
1028
|
+
this.closeCallback = void 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async delegate(task) {
|
|
1032
|
+
return this.executeTask(task);
|
|
1033
|
+
}
|
|
1034
|
+
async delegateStreaming(task, onEvent, sessionId) {
|
|
1035
|
+
return this.executeTask(task, { sessionId, onEvent });
|
|
1036
|
+
}
|
|
1037
|
+
async resume(sessionId, task) {
|
|
1038
|
+
return this.executeTask(task, { sessionId });
|
|
1039
|
+
}
|
|
1040
|
+
async workflow(steps) {
|
|
1041
|
+
const results = [];
|
|
1042
|
+
let sessionId;
|
|
1043
|
+
for (const step of steps) {
|
|
1044
|
+
const result = await this.executeTask(step, { sessionId });
|
|
1045
|
+
results.push(result);
|
|
1046
|
+
if (!result.success) {
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
sessionId = result.sessionId;
|
|
1050
|
+
}
|
|
1051
|
+
return results;
|
|
1052
|
+
}
|
|
1053
|
+
async executeTask(prompt, options) {
|
|
1054
|
+
let sessionId = options?.sessionId;
|
|
1055
|
+
try {
|
|
1056
|
+
const client = await this.ensureClient();
|
|
1057
|
+
sessionId = await this.ensureSession(client, sessionId, prompt);
|
|
1058
|
+
const shouldStream = Boolean(this.approvalHandler || options?.onEvent);
|
|
1059
|
+
const controller = new AbortController();
|
|
1060
|
+
const watcher = shouldStream ? this.watchEvents(client, sessionId, options?.onEvent, controller.signal).catch((error) => {
|
|
1061
|
+
if (!controller.signal.aborted) {
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
}) : null;
|
|
1065
|
+
try {
|
|
1066
|
+
const promptBody = {
|
|
1067
|
+
parts: [{ type: "text", text: prompt }]
|
|
1068
|
+
};
|
|
1069
|
+
const parsedModel = this.parseModel(this.options.model ?? DEFAULT_MODEL);
|
|
1070
|
+
if (parsedModel) {
|
|
1071
|
+
promptBody.model = parsedModel;
|
|
1072
|
+
}
|
|
1073
|
+
const response = await client.session.prompt({
|
|
1074
|
+
path: { id: sessionId },
|
|
1075
|
+
body: promptBody,
|
|
1076
|
+
query: { directory: this.getWorkingDirectory() }
|
|
1077
|
+
});
|
|
1078
|
+
const data = this.extractData(response);
|
|
1079
|
+
return {
|
|
1080
|
+
sessionId,
|
|
1081
|
+
threadId: sessionId,
|
|
1082
|
+
output: this.collectText(data),
|
|
1083
|
+
success: true,
|
|
1084
|
+
usage: this.toUsage(data)
|
|
1085
|
+
};
|
|
1086
|
+
} finally {
|
|
1087
|
+
if (watcher) {
|
|
1088
|
+
controller.abort();
|
|
1089
|
+
await watcher;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
return {
|
|
1094
|
+
sessionId: sessionId ?? "",
|
|
1095
|
+
threadId: sessionId,
|
|
1096
|
+
output: "",
|
|
1097
|
+
success: false,
|
|
1098
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async ensureClient() {
|
|
1103
|
+
if (this.clientPromise) {
|
|
1104
|
+
return this.clientPromise;
|
|
1105
|
+
}
|
|
1106
|
+
if (this.options.clientFactory) {
|
|
1107
|
+
this.clientPromise = this.options.clientFactory().then(({ client }) => client);
|
|
1108
|
+
return this.clientPromise;
|
|
1109
|
+
}
|
|
1110
|
+
if (this.options.baseUrl) {
|
|
1111
|
+
this.clientPromise = loadOpencodeModule().then(
|
|
1112
|
+
({ createOpencodeClient }) => createOpencodeClient({
|
|
1113
|
+
baseUrl: this.options.baseUrl
|
|
1114
|
+
})
|
|
1115
|
+
);
|
|
1116
|
+
return this.clientPromise;
|
|
1117
|
+
}
|
|
1118
|
+
this.clientPromise = loadOpencodeModule().then(async ({ createOpencode }) => {
|
|
1119
|
+
const hostname = this.options.hostname ?? DEFAULT_HOSTNAME;
|
|
1120
|
+
const port = await findAvailablePort(hostname, this.options.port ?? DEFAULT_PORT);
|
|
1121
|
+
const { client, server } = await createOpencode({ hostname, port, config: this.options.config });
|
|
1122
|
+
this.closeCallback = () => server.close();
|
|
1123
|
+
return client;
|
|
1124
|
+
});
|
|
1125
|
+
return this.clientPromise;
|
|
1126
|
+
}
|
|
1127
|
+
async ensureSession(client, existingId, prompt) {
|
|
1128
|
+
if (existingId) {
|
|
1129
|
+
return existingId;
|
|
1130
|
+
}
|
|
1131
|
+
const result = await client.session.create({
|
|
1132
|
+
body: {
|
|
1133
|
+
title: this.options.title ?? this.createSessionTitle(prompt)
|
|
1134
|
+
},
|
|
1135
|
+
query: { directory: this.getWorkingDirectory() }
|
|
1136
|
+
});
|
|
1137
|
+
const session = this.extractData(result);
|
|
1138
|
+
return session.id;
|
|
1139
|
+
}
|
|
1140
|
+
createSessionTitle(prompt) {
|
|
1141
|
+
const [firstLineRaw = ""] = prompt.trim().split(/\r?\n/);
|
|
1142
|
+
const firstLine = firstLineRaw || "OpenCode Session";
|
|
1143
|
+
return firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine;
|
|
1144
|
+
}
|
|
1145
|
+
parseModel(model) {
|
|
1146
|
+
if (!model) {
|
|
1147
|
+
return void 0;
|
|
1148
|
+
}
|
|
1149
|
+
if (model.includes("/")) {
|
|
1150
|
+
const [providerPart, modelPart] = model.split("/", 2);
|
|
1151
|
+
const providerID = providerPart || "anthropic";
|
|
1152
|
+
const modelID = modelPart || providerPart || model;
|
|
1153
|
+
return { providerID, modelID };
|
|
1154
|
+
}
|
|
1155
|
+
return { providerID: "anthropic", modelID: model };
|
|
1156
|
+
}
|
|
1157
|
+
collectText(response) {
|
|
1158
|
+
const texts = response.parts?.filter((part) => part.type === "text") ?? [];
|
|
1159
|
+
return texts.map((part) => part.text).join("\n").trim();
|
|
1160
|
+
}
|
|
1161
|
+
toUsage(response) {
|
|
1162
|
+
const tokens = response.info?.tokens;
|
|
1163
|
+
if (!tokens) {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
input_tokens: tokens.input ?? 0,
|
|
1168
|
+
output_tokens: tokens.output ?? 0,
|
|
1169
|
+
cached_input_tokens: tokens.cache?.read ?? 0
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
extractData(result) {
|
|
1173
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
1174
|
+
const record = result;
|
|
1175
|
+
if (record.data !== void 0) {
|
|
1176
|
+
return record.data;
|
|
1177
|
+
}
|
|
1178
|
+
throw new Error(this.describeError(record.error));
|
|
1179
|
+
}
|
|
1180
|
+
return result;
|
|
1181
|
+
}
|
|
1182
|
+
describeError(error) {
|
|
1183
|
+
if (!error) {
|
|
1184
|
+
return "Unknown OpenCode error";
|
|
1185
|
+
}
|
|
1186
|
+
if (typeof error === "string") {
|
|
1187
|
+
return error;
|
|
1188
|
+
}
|
|
1189
|
+
if (error instanceof Error) {
|
|
1190
|
+
return error.message;
|
|
1191
|
+
}
|
|
1192
|
+
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
1193
|
+
return error.message;
|
|
1194
|
+
}
|
|
1195
|
+
return JSON.stringify(error);
|
|
1196
|
+
}
|
|
1197
|
+
async watchEvents(client, sessionId, onEvent, signal) {
|
|
1198
|
+
const { stream } = await client.event.subscribe({
|
|
1199
|
+
signal,
|
|
1200
|
+
query: { directory: this.getWorkingDirectory() }
|
|
1201
|
+
});
|
|
1202
|
+
const handledPermissions = /* @__PURE__ */ new Set();
|
|
1203
|
+
for await (const event of stream) {
|
|
1204
|
+
if (signal.aborted) {
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
const targetSessionId = this.extractSessionId(event);
|
|
1208
|
+
if (this.approvalHandler && event.type === "permission.updated") {
|
|
1209
|
+
const permission = event.properties;
|
|
1210
|
+
if (permission.sessionID === sessionId && !handledPermissions.has(permission.id)) {
|
|
1211
|
+
handledPermissions.add(permission.id);
|
|
1212
|
+
await this.respondToPermission(client, permission);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (onEvent && targetSessionId === sessionId) {
|
|
1216
|
+
onEvent(event);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
extractSessionId(event) {
|
|
1221
|
+
const properties = event.properties;
|
|
1222
|
+
if (!properties) {
|
|
1223
|
+
return void 0;
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof properties.sessionID === "string") {
|
|
1226
|
+
return properties.sessionID;
|
|
1227
|
+
}
|
|
1228
|
+
if (typeof properties.info === "object" && properties.info !== null && "sessionID" in properties.info) {
|
|
1229
|
+
const value = properties.info.sessionID;
|
|
1230
|
+
return typeof value === "string" ? value : void 0;
|
|
1231
|
+
}
|
|
1232
|
+
return void 0;
|
|
1233
|
+
}
|
|
1234
|
+
async respondToPermission(client, permission) {
|
|
1235
|
+
if (!this.approvalHandler) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const decision = await this.approvalHandler({
|
|
1239
|
+
id: permission.id,
|
|
1240
|
+
type: permission.type,
|
|
1241
|
+
title: permission.title,
|
|
1242
|
+
sessionId: permission.sessionID,
|
|
1243
|
+
metadata: permission.metadata ?? {},
|
|
1244
|
+
pattern: Array.isArray(permission.pattern) ? permission.pattern.slice() : permission.pattern
|
|
1245
|
+
});
|
|
1246
|
+
const response = this.normalizeDecision(decision);
|
|
1247
|
+
await client.postSessionIdPermissionsPermissionId({
|
|
1248
|
+
path: {
|
|
1249
|
+
id: permission.sessionID,
|
|
1250
|
+
permissionID: permission.id
|
|
1251
|
+
},
|
|
1252
|
+
body: { response }
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
normalizeDecision(decision) {
|
|
1256
|
+
if (typeof decision === "boolean") {
|
|
1257
|
+
return decision ? "once" : "reject";
|
|
1258
|
+
}
|
|
1259
|
+
if (typeof decision === "string") {
|
|
1260
|
+
return decision;
|
|
1261
|
+
}
|
|
1262
|
+
return decision.response;
|
|
1263
|
+
}
|
|
1264
|
+
getWorkingDirectory() {
|
|
1265
|
+
return this.options.workingDirectory ?? process.cwd();
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// src/cloudTasks.ts
|
|
1270
|
+
var CloudTasks = class {
|
|
1271
|
+
constructor(options = {}) {
|
|
1272
|
+
this.options = options;
|
|
1273
|
+
}
|
|
1274
|
+
binding() {
|
|
1275
|
+
const b = getNativeBinding();
|
|
1276
|
+
if (!b) throw new Error("Native binding not available");
|
|
1277
|
+
return b;
|
|
1278
|
+
}
|
|
1279
|
+
async list(env) {
|
|
1280
|
+
const b = this.binding();
|
|
1281
|
+
if (!b.cloudTasksList) throw new Error("cloudTasksList is not available in this build");
|
|
1282
|
+
const json = await b.cloudTasksList(env, this.options.baseUrl, this.options.apiKey);
|
|
1283
|
+
return JSON.parse(json);
|
|
1284
|
+
}
|
|
1285
|
+
async getDiff(taskId) {
|
|
1286
|
+
const b = this.binding();
|
|
1287
|
+
if (!b.cloudTasksGetDiff) throw new Error("cloudTasksGetDiff is not available in this build");
|
|
1288
|
+
const json = await b.cloudTasksGetDiff(taskId, this.options.baseUrl, this.options.apiKey);
|
|
1289
|
+
const parsed = JSON.parse(json);
|
|
1290
|
+
return parsed.diff ?? null;
|
|
1291
|
+
}
|
|
1292
|
+
async applyPreflight(taskId, diffOverride) {
|
|
1293
|
+
const b = this.binding();
|
|
1294
|
+
if (!b.cloudTasksApplyPreflight) {
|
|
1295
|
+
throw new Error("cloudTasksApplyPreflight is not available in this build");
|
|
1296
|
+
}
|
|
1297
|
+
const json = await b.cloudTasksApplyPreflight(
|
|
1298
|
+
taskId,
|
|
1299
|
+
diffOverride,
|
|
1300
|
+
this.options.baseUrl,
|
|
1301
|
+
this.options.apiKey
|
|
1302
|
+
);
|
|
1303
|
+
return JSON.parse(json);
|
|
1304
|
+
}
|
|
1305
|
+
async apply(taskId, diffOverride) {
|
|
1306
|
+
const b = this.binding();
|
|
1307
|
+
if (!b.cloudTasksApply) throw new Error("cloudTasksApply is not available in this build");
|
|
1308
|
+
const json = await b.cloudTasksApply(
|
|
1309
|
+
taskId,
|
|
1310
|
+
diffOverride,
|
|
1311
|
+
this.options.baseUrl,
|
|
1312
|
+
this.options.apiKey
|
|
1313
|
+
);
|
|
1314
|
+
return JSON.parse(json);
|
|
1315
|
+
}
|
|
1316
|
+
async create(envId, prompt, opts) {
|
|
1317
|
+
const b = this.binding();
|
|
1318
|
+
if (!b.cloudTasksCreate) throw new Error("cloudTasksCreate is not available in this build");
|
|
1319
|
+
const json = await b.cloudTasksCreate(
|
|
1320
|
+
envId,
|
|
1321
|
+
prompt,
|
|
1322
|
+
opts?.gitRef,
|
|
1323
|
+
opts?.qaMode,
|
|
1324
|
+
opts?.bestOfN,
|
|
1325
|
+
this.options.baseUrl,
|
|
1326
|
+
this.options.apiKey
|
|
1327
|
+
);
|
|
1328
|
+
return JSON.parse(json);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// src/logging/types.ts
|
|
1333
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
1334
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
1335
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
1336
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
1337
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
1338
|
+
LogLevel2[LogLevel2["SILENT"] = 4] = "SILENT";
|
|
1339
|
+
return LogLevel2;
|
|
1340
|
+
})(LogLevel || {});
|
|
1341
|
+
|
|
1342
|
+
// src/logging/logger.ts
|
|
1343
|
+
var COLORS = {
|
|
1344
|
+
reset: "\x1B[0m",
|
|
1345
|
+
// Log levels
|
|
1346
|
+
debug: "\x1B[90m",
|
|
1347
|
+
// Gray
|
|
1348
|
+
info: "\x1B[36m",
|
|
1349
|
+
// Cyan
|
|
1350
|
+
warn: "\x1B[33m",
|
|
1351
|
+
// Yellow
|
|
1352
|
+
error: "\x1B[31m",
|
|
1353
|
+
// Red
|
|
1354
|
+
// Scopes
|
|
1355
|
+
thread: "\x1B[94m",
|
|
1356
|
+
// Bright blue
|
|
1357
|
+
merge: "\x1B[35m",
|
|
1358
|
+
// Magenta
|
|
1359
|
+
git: "\x1B[34m",
|
|
1360
|
+
// Blue
|
|
1361
|
+
coordinator: "\x1B[36m",
|
|
1362
|
+
// Cyan
|
|
1363
|
+
worker: "\x1B[33m",
|
|
1364
|
+
// Yellow
|
|
1365
|
+
supervisor: "\x1B[95m",
|
|
1366
|
+
// Bright magenta
|
|
1367
|
+
reviewer: "\x1B[32m",
|
|
1368
|
+
// Green
|
|
1369
|
+
validation: "\x1B[92m",
|
|
1370
|
+
// Bright green
|
|
1371
|
+
lsp: "\x1B[96m",
|
|
1372
|
+
// Bright cyan
|
|
1373
|
+
agent: "\x1B[93m",
|
|
1374
|
+
// Bright yellow
|
|
1375
|
+
provider: "\x1B[91m",
|
|
1376
|
+
// Bright red
|
|
1377
|
+
ci: "\x1B[35m",
|
|
1378
|
+
// Magenta
|
|
1379
|
+
test: "\x1B[32m",
|
|
1380
|
+
// Green
|
|
1381
|
+
system: "\x1B[37m"
|
|
1382
|
+
// White
|
|
1383
|
+
};
|
|
1384
|
+
var consoleOutput = {
|
|
1385
|
+
debug: (msg) => console.debug(msg),
|
|
1386
|
+
info: (msg) => console.log(msg),
|
|
1387
|
+
warn: (msg) => console.warn(msg),
|
|
1388
|
+
error: (msg) => console.error(msg)
|
|
1389
|
+
};
|
|
1390
|
+
var Logger = class _Logger {
|
|
1391
|
+
level;
|
|
1392
|
+
colors;
|
|
1393
|
+
timestamps;
|
|
1394
|
+
prefix;
|
|
1395
|
+
json;
|
|
1396
|
+
output;
|
|
1397
|
+
constructor(config = {}) {
|
|
1398
|
+
this.level = config.level ?? 1 /* INFO */;
|
|
1399
|
+
this.colors = config.colors ?? (typeof process !== "undefined" && process.stdout?.isTTY === true);
|
|
1400
|
+
this.timestamps = config.timestamps ?? false;
|
|
1401
|
+
this.prefix = config.prefix ?? "";
|
|
1402
|
+
this.json = config.json ?? false;
|
|
1403
|
+
this.output = config.output ?? consoleOutput;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Create a new logger with modified configuration
|
|
1407
|
+
*/
|
|
1408
|
+
configure(config) {
|
|
1409
|
+
return new _Logger({
|
|
1410
|
+
level: config.level ?? this.level,
|
|
1411
|
+
colors: config.colors ?? this.colors,
|
|
1412
|
+
timestamps: config.timestamps ?? this.timestamps,
|
|
1413
|
+
prefix: config.prefix ?? this.prefix,
|
|
1414
|
+
json: config.json ?? this.json,
|
|
1415
|
+
output: config.output ?? this.output
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Create a scoped logger
|
|
1420
|
+
*/
|
|
1421
|
+
scope(scope, subject) {
|
|
1422
|
+
return new ScopedLogger(this, scope, subject);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Log a debug message
|
|
1426
|
+
*/
|
|
1427
|
+
debug(message, data) {
|
|
1428
|
+
this.log(0 /* DEBUG */, message, data);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Log an info message
|
|
1432
|
+
*/
|
|
1433
|
+
info(message, data) {
|
|
1434
|
+
this.log(1 /* INFO */, message, data);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Log a warning message
|
|
1438
|
+
*/
|
|
1439
|
+
warn(message, data) {
|
|
1440
|
+
this.log(2 /* WARN */, message, data);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Log an error message
|
|
1444
|
+
*/
|
|
1445
|
+
error(message, data) {
|
|
1446
|
+
this.log(3 /* ERROR */, message, data);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Internal log method
|
|
1450
|
+
*/
|
|
1451
|
+
log(level, message, data, scope, subject) {
|
|
1452
|
+
if (level < this.level) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (this.json) {
|
|
1456
|
+
this.logJson(level, message, data, scope, subject);
|
|
1457
|
+
} else {
|
|
1458
|
+
this.logFormatted(level, message, scope, subject);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Log in JSON format
|
|
1463
|
+
*/
|
|
1464
|
+
logJson(level, message, data, scope, subject) {
|
|
1465
|
+
const entry = {
|
|
1466
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1467
|
+
level: LogLevel[level],
|
|
1468
|
+
message,
|
|
1469
|
+
...scope && { scope },
|
|
1470
|
+
...subject && { subject },
|
|
1471
|
+
...data && { data }
|
|
1472
|
+
};
|
|
1473
|
+
const output = JSON.stringify(entry);
|
|
1474
|
+
this.output.info(output);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Log in formatted text
|
|
1478
|
+
*/
|
|
1479
|
+
logFormatted(level, message, scope, subject) {
|
|
1480
|
+
const parts = [];
|
|
1481
|
+
if (this.timestamps) {
|
|
1482
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1483
|
+
parts.push(this.colors ? `\x1B[90m[${ts}]\x1B[0m` : `[${ts}]`);
|
|
1484
|
+
}
|
|
1485
|
+
const levelName = LogLevel[level];
|
|
1486
|
+
if (this.colors) {
|
|
1487
|
+
const color = COLORS[levelName.toLowerCase()] ?? COLORS.reset;
|
|
1488
|
+
parts.push(`${color}[${levelName}]${COLORS.reset}`);
|
|
1489
|
+
} else {
|
|
1490
|
+
parts.push(`[${levelName}]`);
|
|
1491
|
+
}
|
|
1492
|
+
if (scope) {
|
|
1493
|
+
const label = subject ? `${scope}:${subject}` : scope;
|
|
1494
|
+
if (this.colors) {
|
|
1495
|
+
const color = COLORS[scope] ?? COLORS.reset;
|
|
1496
|
+
parts.push(`${color}[${label}]${COLORS.reset}`);
|
|
1497
|
+
} else {
|
|
1498
|
+
parts.push(`[${label}]`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (this.prefix) {
|
|
1502
|
+
parts.push(this.prefix);
|
|
1503
|
+
}
|
|
1504
|
+
parts.push(message);
|
|
1505
|
+
const formatted = parts.join(" ");
|
|
1506
|
+
switch (level) {
|
|
1507
|
+
case 0 /* DEBUG */:
|
|
1508
|
+
this.output.debug(formatted);
|
|
1509
|
+
break;
|
|
1510
|
+
case 1 /* INFO */:
|
|
1511
|
+
this.output.info(formatted);
|
|
1512
|
+
break;
|
|
1513
|
+
case 2 /* WARN */:
|
|
1514
|
+
this.output.warn(formatted);
|
|
1515
|
+
break;
|
|
1516
|
+
case 3 /* ERROR */:
|
|
1517
|
+
this.output.error(formatted);
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Internal scoped log method (used by ScopedLogger)
|
|
1523
|
+
*/
|
|
1524
|
+
logScoped(level, message, scope, subject, data) {
|
|
1525
|
+
this.log(level, message, data, scope, subject);
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
var ScopedLogger = class {
|
|
1529
|
+
constructor(logger2, scope, subject) {
|
|
1530
|
+
this.logger = logger2;
|
|
1531
|
+
this.scope = scope;
|
|
1532
|
+
this.subject = subject;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Log a debug message
|
|
1536
|
+
*/
|
|
1537
|
+
debug(message, data) {
|
|
1538
|
+
this.logger.logScoped(0 /* DEBUG */, message, this.scope, this.subject, data);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Log an info message
|
|
1542
|
+
*/
|
|
1543
|
+
info(message, data) {
|
|
1544
|
+
this.logger.logScoped(1 /* INFO */, message, this.scope, this.subject, data);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Log a warning message
|
|
1548
|
+
*/
|
|
1549
|
+
warn(message, data) {
|
|
1550
|
+
this.logger.logScoped(2 /* WARN */, message, this.scope, this.subject, data);
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Log an error message
|
|
1554
|
+
*/
|
|
1555
|
+
error(message, data) {
|
|
1556
|
+
this.logger.logScoped(3 /* ERROR */, message, this.scope, this.subject, data);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Create a ThreadLoggingSink adapter
|
|
1560
|
+
*/
|
|
1561
|
+
asThreadSink() {
|
|
1562
|
+
return {
|
|
1563
|
+
info: (message) => this.info(message),
|
|
1564
|
+
warn: (message) => this.warn(message)
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
var logger = new Logger({
|
|
1569
|
+
level: process.env.CODEX_LOG_LEVEL ? LogLevel[process.env.CODEX_LOG_LEVEL] ?? 1 /* INFO */ : 1 /* INFO */,
|
|
1570
|
+
colors: process.env.CODEX_LOG_COLORS !== "false",
|
|
1571
|
+
timestamps: process.env.CODEX_LOG_TIMESTAMPS === "true",
|
|
1572
|
+
json: process.env.CODEX_LOG_JSON === "true"
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
// src/logging/threadLogger.ts
|
|
1576
|
+
var THREAD_EVENT_TEXT_LIMIT = 400;
|
|
1577
|
+
function createThreadLogger(scopedLogger, onUsage) {
|
|
1578
|
+
return {
|
|
1579
|
+
info: (message) => scopedLogger.info(message),
|
|
1580
|
+
warn: (message) => scopedLogger.warn(message),
|
|
1581
|
+
recordUsage: onUsage
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
async function runThreadTurnWithLogs(thread, sink, prompt, turnOptions) {
|
|
1585
|
+
const unsubscribe = thread.onEvent((event) => logThreadEvent(event, sink));
|
|
1586
|
+
try {
|
|
1587
|
+
if (turnOptions) {
|
|
1588
|
+
return await thread.run(prompt, turnOptions);
|
|
1589
|
+
}
|
|
1590
|
+
return await thread.run(prompt);
|
|
1591
|
+
} finally {
|
|
1592
|
+
unsubscribe();
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function logThreadEvent(event, sink) {
|
|
1596
|
+
switch (event.type) {
|
|
1597
|
+
case "thread.started":
|
|
1598
|
+
sink.info(`Thread started (id: ${event.thread_id})`);
|
|
1599
|
+
return;
|
|
1600
|
+
case "turn.started":
|
|
1601
|
+
sink.info("Turn started");
|
|
1602
|
+
return;
|
|
1603
|
+
case "turn.completed":
|
|
1604
|
+
sink.info(
|
|
1605
|
+
`Turn completed (input ${event.usage.input_tokens}, cached ${event.usage.cached_input_tokens}, output ${event.usage.output_tokens})`
|
|
1606
|
+
);
|
|
1607
|
+
if ("recordUsage" in sink && sink.recordUsage) {
|
|
1608
|
+
sink.recordUsage(event.usage);
|
|
1609
|
+
}
|
|
1610
|
+
return;
|
|
1611
|
+
case "turn.failed":
|
|
1612
|
+
sink.warn(`Turn failed: ${event.error.message}`);
|
|
1613
|
+
return;
|
|
1614
|
+
case "item.started":
|
|
1615
|
+
sink.info(`Item started: ${describeThreadItemForLog(event.item)}`);
|
|
1616
|
+
return;
|
|
1617
|
+
case "item.updated":
|
|
1618
|
+
sink.info(`Item updated: ${describeThreadItemForLog(event.item)}`);
|
|
1619
|
+
return;
|
|
1620
|
+
case "item.completed": {
|
|
1621
|
+
const message = `Item completed: ${describeThreadItemForLog(event.item)}`;
|
|
1622
|
+
if (event.item.type === "error") {
|
|
1623
|
+
sink.warn(message);
|
|
1624
|
+
} else {
|
|
1625
|
+
sink.info(message);
|
|
1626
|
+
}
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
case "background_event":
|
|
1630
|
+
sink.info(`Background: ${summarizeLogText(event.message)}`);
|
|
1631
|
+
return;
|
|
1632
|
+
case "exited_review_mode":
|
|
1633
|
+
sink.info("Exited review mode");
|
|
1634
|
+
return;
|
|
1635
|
+
case "error":
|
|
1636
|
+
sink.warn(`Stream error: ${event.message}`);
|
|
1637
|
+
return;
|
|
1638
|
+
case "raw_event":
|
|
1639
|
+
return;
|
|
1640
|
+
default:
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function describeThreadItemForLog(item) {
|
|
1645
|
+
switch (item.type) {
|
|
1646
|
+
case "agent_message":
|
|
1647
|
+
return `agent message \u2192 ${summarizeLogText(item.text)}`;
|
|
1648
|
+
case "reasoning":
|
|
1649
|
+
return `reasoning \u2192 ${summarizeLogText(item.text)}`;
|
|
1650
|
+
case "command_execution": {
|
|
1651
|
+
const exit = item.exit_code !== void 0 ? ` exit=${item.exit_code}` : "";
|
|
1652
|
+
return `command "${summarizeLogText(item.command)}" [${item.status}${exit}]`;
|
|
1653
|
+
}
|
|
1654
|
+
case "file_change": {
|
|
1655
|
+
const changeList = item.changes.map((change) => `${change.kind}:${change.path}`).join(", ");
|
|
1656
|
+
return `file change [${item.status}] ${summarizeLogText(changeList)}`;
|
|
1657
|
+
}
|
|
1658
|
+
case "mcp_tool_call":
|
|
1659
|
+
return `mcp ${item.server}.${item.tool} [${item.status}]`;
|
|
1660
|
+
case "web_search":
|
|
1661
|
+
return `web search "${summarizeLogText(item.query)}"`;
|
|
1662
|
+
case "todo_list": {
|
|
1663
|
+
const completed = item.items.filter((todo) => todo.completed).length;
|
|
1664
|
+
return `todo list ${completed}/${item.items.length}`;
|
|
1665
|
+
}
|
|
1666
|
+
case "error":
|
|
1667
|
+
return `error \u2192 ${summarizeLogText(item.message)}`;
|
|
1668
|
+
default: {
|
|
1669
|
+
const _exhaustive = item;
|
|
1670
|
+
return "unknown event";
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function summarizeLogText(text, limit = THREAD_EVENT_TEXT_LIMIT) {
|
|
1675
|
+
if (!text) {
|
|
1676
|
+
return "";
|
|
1677
|
+
}
|
|
1678
|
+
const flattened = text.replace(/\s+/g, " ").trim();
|
|
1679
|
+
if (flattened.length <= limit) {
|
|
1680
|
+
return flattened;
|
|
1681
|
+
}
|
|
1682
|
+
return `${flattened.slice(0, limit)}\u2026`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/reverie/constants.ts
|
|
1686
|
+
var DEFAULT_REVERIE_LIMIT = 6;
|
|
1687
|
+
var DEFAULT_REVERIE_MAX_CANDIDATES = 80;
|
|
1688
|
+
var REVERIE_EMBED_MODEL = "BAAI/bge-large-en-v1.5";
|
|
1689
|
+
var REVERIE_RERANKER_MODEL = "rozgo/bge-reranker-v2-m3";
|
|
1690
|
+
var REVERIE_CANDIDATE_MULTIPLIER = 3;
|
|
1691
|
+
var REVERIE_LLM_GRADE_THRESHOLD = 0.7;
|
|
1692
|
+
var DEFAULT_RERANKER_TOP_K = 20;
|
|
1693
|
+
var DEFAULT_RERANKER_BATCH_SIZE = 8;
|
|
1694
|
+
|
|
1695
|
+
// src/reverie/quality.ts
|
|
1696
|
+
function isValidReverieExcerpt(excerpt) {
|
|
1697
|
+
if (!excerpt || excerpt.trim().length < 20) {
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
const trimmed = excerpt.trim();
|
|
1701
|
+
const normalized = trimmed.toLowerCase();
|
|
1702
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1703
|
+
const rawTokens = trimmed.split(/\s+/).filter(Boolean);
|
|
1704
|
+
const tokens = rawTokens.map((token) => token.toLowerCase());
|
|
1705
|
+
if (rawTokens.length === 0) {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
const uppercaseTokens = rawTokens.filter((token) => {
|
|
1709
|
+
const alphabetic = token.replace(/[^a-z]/gi, "");
|
|
1710
|
+
return alphabetic.length >= 3 && alphabetic === alphabetic.toUpperCase();
|
|
1711
|
+
});
|
|
1712
|
+
const uppercaseRatio = uppercaseTokens.length / rawTokens.length;
|
|
1713
|
+
const snakeTokens = rawTokens.filter((token) => token.includes("_"));
|
|
1714
|
+
const underscoreRatio = snakeTokens.length / rawTokens.length;
|
|
1715
|
+
const headingLines = lines.filter((line) => /^#{1,6}\s/.test(line));
|
|
1716
|
+
const bulletLines = lines.filter((line) => /^\s*[\-\*]\s/.test(line));
|
|
1717
|
+
const numericBulletLines = lines.filter((line) => /^\s*\d+[\).]/.test(line));
|
|
1718
|
+
const colonLabelLines = lines.filter((line) => /^[A-Za-z0-9 _-]{1,24}:/.test(line));
|
|
1719
|
+
const headingRatio = headingLines.length / Math.max(lines.length, 1);
|
|
1720
|
+
const bulletRatio = bulletLines.length / Math.max(lines.length, 1);
|
|
1721
|
+
const colonLabelRatio = colonLabelLines.length / Math.max(lines.length, 1);
|
|
1722
|
+
const numericRatio = numericBulletLines.length / Math.max(lines.length, 1);
|
|
1723
|
+
const enumeratedRatio = (bulletLines.length + numericBulletLines.length) / Math.max(lines.length, 1);
|
|
1724
|
+
const initialTitleCaseRun = (() => {
|
|
1725
|
+
let run = 0;
|
|
1726
|
+
for (const token of rawTokens) {
|
|
1727
|
+
const cleaned = token.replace(/[^a-z]/gi, "");
|
|
1728
|
+
if (cleaned.length === 0) {
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
const rest = cleaned.slice(1);
|
|
1732
|
+
const isTitleCase = cleaned[0]?.toUpperCase() === cleaned[0] && rest === rest.toLowerCase();
|
|
1733
|
+
const isAllCaps = cleaned.length >= 2 && cleaned === cleaned.toUpperCase();
|
|
1734
|
+
if (isTitleCase || isAllCaps) {
|
|
1735
|
+
run += 1;
|
|
1736
|
+
} else {
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return run;
|
|
1741
|
+
})();
|
|
1742
|
+
const tokenFrequencies = tokens.reduce((map, token) => map.set(token, (map.get(token) ?? 0) + 1), /* @__PURE__ */ new Map());
|
|
1743
|
+
const frequencyValues = Array.from(tokenFrequencies.values());
|
|
1744
|
+
const mostCommonTokenCount = Math.max(...frequencyValues);
|
|
1745
|
+
const repeatedWordRatio = mostCommonTokenCount / tokens.length;
|
|
1746
|
+
if (snakeTokens.length >= 2 && underscoreRatio > 0.15) {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
if (headingRatio > 0.6 && lines.length <= 4) {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
if (initialTitleCaseRun >= 3 && rawTokens.length <= 20) {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
if (enumeratedRatio > 0.6 && lines.length >= 3) {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
const metadataScore = [
|
|
1759
|
+
uppercaseRatio > 0.45,
|
|
1760
|
+
underscoreRatio > 0.2,
|
|
1761
|
+
bulletRatio > 0.7,
|
|
1762
|
+
colonLabelRatio > 0.6 || lines.length <= 2 && colonLabelRatio > 0,
|
|
1763
|
+
initialTitleCaseRun >= 3,
|
|
1764
|
+
repeatedWordRatio > 0.45 && tokens.length > 15,
|
|
1765
|
+
rawTokens.length < 12 && colonLabelRatio > 0,
|
|
1766
|
+
numericRatio > 0.5
|
|
1767
|
+
].filter(Boolean).length;
|
|
1768
|
+
if (metadataScore >= 2) {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
const tagMatches = trimmed.match(/<[^>]+>/g) || [];
|
|
1772
|
+
if (tagMatches.length > 3) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
const blockTagMatch = trimmed.match(/^<([a-z0-9_\-]+)>[\s\S]*<\/\1>$/i);
|
|
1776
|
+
if (blockTagMatch) {
|
|
1777
|
+
const tagName = blockTagMatch[1]?.toLowerCase() ?? "";
|
|
1778
|
+
const looksLikeSystem = tagName.includes("system") || tagName.includes("context") || tagName.includes("env");
|
|
1779
|
+
if (tagName.includes("_") || looksLikeSystem) {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (/\(\d{2,3}%\)\s*$/.test(trimmed)) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
const looksJsonLike = (/^\{[\s\S]*\}$/.test(trimmed) || /^\[[\s\S]*\]$/.test(trimmed)) && /"\w+"\s*:/.test(trimmed);
|
|
1787
|
+
if (looksJsonLike) {
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
function deduplicateReverieInsights(insights) {
|
|
1793
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
1794
|
+
for (const insight of insights) {
|
|
1795
|
+
const fingerprint = insight.excerpt.slice(0, 100).toLowerCase().replace(/\s+/g, " ");
|
|
1796
|
+
const existing = fingerprintMap.get(fingerprint);
|
|
1797
|
+
if (!existing || insight.relevance > existing.relevance) {
|
|
1798
|
+
fingerprintMap.set(fingerprint, insight);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return Array.from(fingerprintMap.values()).sort((a, b) => b.relevance - a.relevance);
|
|
1802
|
+
}
|
|
1803
|
+
function applyQualityPipeline(insights, limit = 10) {
|
|
1804
|
+
const stats = {
|
|
1805
|
+
initial: insights.length,
|
|
1806
|
+
afterValidityFilter: 0,
|
|
1807
|
+
afterDeduplication: 0,
|
|
1808
|
+
final: 0
|
|
1809
|
+
};
|
|
1810
|
+
const validInsights = insights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
1811
|
+
stats.afterValidityFilter = validInsights.length;
|
|
1812
|
+
const deduplicated = deduplicateReverieInsights(validInsights);
|
|
1813
|
+
stats.afterDeduplication = deduplicated.length;
|
|
1814
|
+
const final = deduplicated.slice(0, limit);
|
|
1815
|
+
stats.final = final.length;
|
|
1816
|
+
return { insights: final, stats };
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/reverie/boilerplate.ts
|
|
1820
|
+
var DEFAULT_THRESHOLD = 0.8;
|
|
1821
|
+
var DEFAULT_MAX_EXCERPT_LENGTH = 512;
|
|
1822
|
+
var BOILERPLATE_SEEDS = [
|
|
1823
|
+
"<system>Focus on summarizing repo context and keep instructions short.",
|
|
1824
|
+
"<environment_context>Working directory: /repo/codex sandbox_mode: workspace-write network_access: disabled</environment_context>",
|
|
1825
|
+
"# AGENTS.md instructions for this task require you to enumerate files before running commands.",
|
|
1826
|
+
"Tool output: command completed successfully with exit code 0.",
|
|
1827
|
+
"You are coordinating multiple agents. Respond with JSON describing the plan.",
|
|
1828
|
+
"Sandbox env vars: CODEX_SANDBOX=seatbelt CODEX_SANDBOX_NETWORK_DISABLED=1",
|
|
1829
|
+
"1. Inspect repository status; 2. List directories; 3. Review README/AGENTS instructions before acting.",
|
|
1830
|
+
"1. Inventory tooling - run `just --list` for recipes. 2. Verify Rust toolchain. 3. Read AGENTS.md for repo-specific guidance before editing."
|
|
1831
|
+
];
|
|
1832
|
+
var seedVectorsPromise = null;
|
|
1833
|
+
var embeddingDisabled = false;
|
|
1834
|
+
var dot = (a, b) => a.reduce((sum, value, idx) => sum + value * (b[idx] ?? 0), 0);
|
|
1835
|
+
function truncateExcerpt(text, maxLength) {
|
|
1836
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1837
|
+
if (normalized.length <= maxLength) {
|
|
1838
|
+
return normalized;
|
|
1839
|
+
}
|
|
1840
|
+
return normalized.slice(0, maxLength);
|
|
1841
|
+
}
|
|
1842
|
+
async function embedTexts(inputs, projectRoot) {
|
|
1843
|
+
if (embeddingDisabled || inputs.length === 0) {
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
try {
|
|
1847
|
+
const embeddings = await fastEmbedEmbed({
|
|
1848
|
+
inputs,
|
|
1849
|
+
projectRoot,
|
|
1850
|
+
normalize: true
|
|
1851
|
+
});
|
|
1852
|
+
return embeddings;
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
embeddingDisabled = true;
|
|
1855
|
+
console.warn(`\u26A0\uFE0F Reverie boilerplate filter disabled (fastEmbedEmbed unavailable: ${error.message ?? error})`);
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async function getSeedVectors(projectRoot) {
|
|
1860
|
+
if (seedVectorsPromise) {
|
|
1861
|
+
return seedVectorsPromise;
|
|
1862
|
+
}
|
|
1863
|
+
seedVectorsPromise = embedTexts(BOILERPLATE_SEEDS, projectRoot);
|
|
1864
|
+
return seedVectorsPromise;
|
|
1865
|
+
}
|
|
1866
|
+
async function filterBoilerplateInsights(insights, options) {
|
|
1867
|
+
if (insights.length === 0) {
|
|
1868
|
+
return { kept: [], removed: 0 };
|
|
1869
|
+
}
|
|
1870
|
+
const projectRoot = options?.projectRoot;
|
|
1871
|
+
const threshold = options?.threshold ?? DEFAULT_THRESHOLD;
|
|
1872
|
+
const maxExcerpt = options?.maxExcerptLength ?? DEFAULT_MAX_EXCERPT_LENGTH;
|
|
1873
|
+
const seeds = await getSeedVectors(projectRoot);
|
|
1874
|
+
if (!seeds || seeds.length === 0) {
|
|
1875
|
+
return { kept: insights, removed: 0 };
|
|
1876
|
+
}
|
|
1877
|
+
const excerptBatch = insights.map((insight) => truncateExcerpt(insight.excerpt, maxExcerpt));
|
|
1878
|
+
const excerptVectors = await embedTexts(excerptBatch, projectRoot);
|
|
1879
|
+
if (!excerptVectors) {
|
|
1880
|
+
return { kept: insights, removed: 0 };
|
|
1881
|
+
}
|
|
1882
|
+
const kept = [];
|
|
1883
|
+
let removed = 0;
|
|
1884
|
+
for (let i = 0; i < insights.length; i += 1) {
|
|
1885
|
+
const vector = excerptVectors[i];
|
|
1886
|
+
if (!vector) {
|
|
1887
|
+
kept.push(insights[i]);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
const maxSimilarity = seeds.reduce((currentMax, seedVec) => {
|
|
1891
|
+
const similarity = dot(vector, seedVec);
|
|
1892
|
+
return similarity > currentMax ? similarity : currentMax;
|
|
1893
|
+
}, -Infinity);
|
|
1894
|
+
if (Number.isFinite(maxSimilarity) && maxSimilarity >= threshold) {
|
|
1895
|
+
removed += 1;
|
|
1896
|
+
} else {
|
|
1897
|
+
kept.push(insights[i]);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (removed > 0) {
|
|
1901
|
+
console.log(`\u{1F9F9} Reverie boilerplate filter removed ${removed}/${insights.length} excerpts (threshold ${threshold.toFixed(2)})`);
|
|
1902
|
+
}
|
|
1903
|
+
return { kept, removed };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/reverie/logger.ts
|
|
1907
|
+
function logReverieSearch(query, context) {
|
|
1908
|
+
const contextStr = context ? ` (${context})` : "";
|
|
1909
|
+
console.log(`\u{1F50D} Reverie search${contextStr}: "${query}"`);
|
|
1910
|
+
}
|
|
1911
|
+
function logReverieFiltering(stats) {
|
|
1912
|
+
const { total, afterQuality, afterBoilerplate, afterScore, afterDedup, minScore = 0.7 } = stats;
|
|
1913
|
+
const qualityFiltered = total - afterQuality;
|
|
1914
|
+
const boilerplateStage = afterBoilerplate ?? afterQuality;
|
|
1915
|
+
const boilerplateFiltered = afterQuality - boilerplateStage;
|
|
1916
|
+
const scoreFiltered = boilerplateStage - afterScore;
|
|
1917
|
+
const duplicatesFiltered = afterScore - afterDedup;
|
|
1918
|
+
console.log(
|
|
1919
|
+
`\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)`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
function logReverieInsights(insights, limit = 3) {
|
|
1923
|
+
if (insights.length === 0) {
|
|
1924
|
+
console.log("\u{1F4ED} No reverie insights found");
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
console.log(`\u2728 Top ${Math.min(limit, insights.length)} reverie insights:`);
|
|
1928
|
+
const topInsights = insights.slice(0, limit);
|
|
1929
|
+
for (let i = 0; i < topInsights.length; i++) {
|
|
1930
|
+
const insight = topInsights[i];
|
|
1931
|
+
if (!insight) continue;
|
|
1932
|
+
const score = `${Math.round(insight.relevance * 100)}%`;
|
|
1933
|
+
const excerpt = truncate(insight.excerpt, 150);
|
|
1934
|
+
const insightText = insight.insights.length > 0 ? truncate(insight.insights[0] ?? "", 100) : "";
|
|
1935
|
+
console.log(` ${i + 1}. [${score}] ${excerpt}`);
|
|
1936
|
+
if (insightText) {
|
|
1937
|
+
console.log(` \u2192 ${insightText}`);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function logReverieHintQuality(stats) {
|
|
1942
|
+
const { totalRaw, afterQuality, afterDedup } = stats;
|
|
1943
|
+
const qualityFiltered = totalRaw - afterQuality;
|
|
1944
|
+
const duplicatesFiltered = afterQuality - afterDedup;
|
|
1945
|
+
if (totalRaw > 0) {
|
|
1946
|
+
console.log(
|
|
1947
|
+
`\u{1FA84} Reverie hint quality: ${totalRaw} raw \u2192 ${afterQuality} valid \u2192 ${afterDedup} unique (filtered ${qualityFiltered} low-quality, ${duplicatesFiltered} duplicates)`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function logLLMGrading(stats) {
|
|
1952
|
+
const { total, approved, rejected, minScore = 0.7 } = stats;
|
|
1953
|
+
const approvalRate = total > 0 ? Math.round(approved / total * 100) : 0;
|
|
1954
|
+
console.log(
|
|
1955
|
+
`\u{1F916} LLM grading: ${approved}/${total} approved (${approvalRate}%) [high-scoring \u2265${minScore}, rejected ${rejected}]`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
function logApprovedReveries(insights, maxToShow = 5) {
|
|
1959
|
+
if (insights.length === 0) {
|
|
1960
|
+
console.log(" No reveries passed LLM grading");
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
console.log(` ${insights.length} reveries approved by LLM:`);
|
|
1964
|
+
const toShow = insights.slice(0, maxToShow);
|
|
1965
|
+
for (let i = 0; i < toShow.length; i++) {
|
|
1966
|
+
const insight = toShow[i];
|
|
1967
|
+
if (!insight) continue;
|
|
1968
|
+
const score = insight.relevance.toFixed(2);
|
|
1969
|
+
const preview = truncate(insight.excerpt.replace(/\s+/g, " ").trim(), 200);
|
|
1970
|
+
const insightText = insight.insights[0] || "Context from past work";
|
|
1971
|
+
console.log(` ${i + 1}. [${score}] ${insightText}`);
|
|
1972
|
+
console.log(` "${preview}"`);
|
|
1973
|
+
}
|
|
1974
|
+
if (insights.length > maxToShow) {
|
|
1975
|
+
console.log(` ... and ${insights.length - maxToShow} more`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
function truncate(text, maxLength) {
|
|
1979
|
+
if (!text) return "";
|
|
1980
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}\u2026` : text;
|
|
1981
|
+
}
|
|
1982
|
+
function logMultiLevelSearch(levels) {
|
|
1983
|
+
if (levels.length === 0) {
|
|
1984
|
+
console.log("\u{1F50D} Multi-level reverie search: (no levels specified)");
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const levelIcons = {
|
|
1988
|
+
project: "\u{1F310}",
|
|
1989
|
+
branch: "\u{1F33F}",
|
|
1990
|
+
file: "\u{1F4C4}"
|
|
1991
|
+
};
|
|
1992
|
+
const levelLabels = levels.map((level) => `${levelIcons[level]} ${level}`).join(" \u2192 ");
|
|
1993
|
+
console.log(`\u{1F50D} Multi-level reverie search: ${levelLabels}`);
|
|
1994
|
+
}
|
|
1995
|
+
function logLevelResults(level, result) {
|
|
1996
|
+
const levelIcons = {
|
|
1997
|
+
project: "\u{1F310}",
|
|
1998
|
+
branch: "\u{1F33F}",
|
|
1999
|
+
file: "\u{1F4C4}"
|
|
2000
|
+
};
|
|
2001
|
+
const icon = levelIcons[level];
|
|
2002
|
+
const { stats, insights } = result;
|
|
2003
|
+
const filterRate = stats.total > 0 ? Math.round((stats.total - stats.final) / stats.total * 100) : 0;
|
|
2004
|
+
const levelName = level.charAt(0).toUpperCase() + level.slice(1);
|
|
2005
|
+
console.log(
|
|
2006
|
+
` ${icon} ${levelName} level: ${insights.length} insights (${stats.total} \u2192 ${stats.final}, ${filterRate}% filtered)`
|
|
2007
|
+
);
|
|
2008
|
+
if (stats.total > 0) {
|
|
2009
|
+
const qualityFiltered = stats.total - stats.afterQuality;
|
|
2010
|
+
const scoreFiltered = stats.afterQuality - stats.afterScore;
|
|
2011
|
+
const dedupFiltered = stats.afterScore - (stats.afterDedup || stats.afterScore);
|
|
2012
|
+
if (qualityFiltered > 0 || scoreFiltered > 0 || dedupFiltered > 0) {
|
|
2013
|
+
console.log(
|
|
2014
|
+
` \u21B3 Quality: -${qualityFiltered}, Score: -${scoreFiltered}, Dedup: -${dedupFiltered}`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
function logMultiLevelSummary(results) {
|
|
2020
|
+
const totalInsights = Array.from(results.values()).reduce((sum, result) => sum + result.insights.length, 0);
|
|
2021
|
+
const totalProcessed = Array.from(results.values()).reduce((sum, result) => sum + result.stats.total, 0);
|
|
2022
|
+
console.log(
|
|
2023
|
+
`
|
|
2024
|
+
\u2728 Multi-level search complete: ${totalInsights} total insights (processed ${totalProcessed} candidates across ${results.size} levels)`
|
|
2025
|
+
);
|
|
2026
|
+
const levelCounts = [];
|
|
2027
|
+
for (const [level, result] of results) {
|
|
2028
|
+
levelCounts.push(`${level}: ${result.insights.length}`);
|
|
2029
|
+
}
|
|
2030
|
+
console.log(` Breakdown: ${levelCounts.join(", ")}`);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/reverie/symbols.ts
|
|
2034
|
+
function extractKeySymbols(diff) {
|
|
2035
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
2036
|
+
const functionMatch = diff.match(/(?:function|class|const|let|var|export|interface|type)\s+(\w+)/g);
|
|
2037
|
+
if (functionMatch) {
|
|
2038
|
+
for (const match of functionMatch) {
|
|
2039
|
+
const name = match.split(/\s+/).pop();
|
|
2040
|
+
if (name && name.length > 2 && !name.match(/^(true|false|null|undefined|const|let|var)$/)) {
|
|
2041
|
+
symbols.add(name);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (symbols.size === 0) {
|
|
2046
|
+
return "code changes";
|
|
2047
|
+
}
|
|
2048
|
+
return Array.from(symbols).slice(0, 5).join(", ");
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// src/reverie/episodes.ts
|
|
2052
|
+
import fs2 from "fs/promises";
|
|
2053
|
+
import path2 from "path";
|
|
2054
|
+
var EPISODES_FILENAME = "reverie_episodes.json";
|
|
2055
|
+
async function readEpisodesFile(codexHome) {
|
|
2056
|
+
try {
|
|
2057
|
+
const file = await fs2.readFile(path2.join(codexHome, EPISODES_FILENAME), "utf8");
|
|
2058
|
+
const parsed = JSON.parse(file);
|
|
2059
|
+
if (Array.isArray(parsed)) {
|
|
2060
|
+
return parsed;
|
|
2061
|
+
}
|
|
2062
|
+
return [];
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
if (error.code === "ENOENT") {
|
|
2065
|
+
return [];
|
|
2066
|
+
}
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function searchEpisodeSummaries(codexHome, query, repo, limit = 20) {
|
|
2071
|
+
const summaries = await readEpisodesFile(codexHome);
|
|
2072
|
+
if (!summaries.length || !query.trim()) {
|
|
2073
|
+
return [];
|
|
2074
|
+
}
|
|
2075
|
+
const documents = summaries.map(
|
|
2076
|
+
(episode) => [episode.summary, ...episode.keyDecisions ?? []].join("\n")
|
|
2077
|
+
);
|
|
2078
|
+
const inputs = [query, ...documents];
|
|
2079
|
+
const embeddings = await fastEmbedEmbed({
|
|
2080
|
+
inputs,
|
|
2081
|
+
projectRoot: repo,
|
|
2082
|
+
normalize: true,
|
|
2083
|
+
cache: true
|
|
2084
|
+
});
|
|
2085
|
+
if (embeddings.length !== inputs.length) {
|
|
2086
|
+
return [];
|
|
2087
|
+
}
|
|
2088
|
+
const [queryVector, ...docVectors] = embeddings;
|
|
2089
|
+
if (!queryVector) {
|
|
2090
|
+
return [];
|
|
2091
|
+
}
|
|
2092
|
+
const scored = summaries.map((episode, idx) => {
|
|
2093
|
+
const vector = docVectors[idx] ?? [];
|
|
2094
|
+
return {
|
|
2095
|
+
episode,
|
|
2096
|
+
score: cosineSimilarity(queryVector, vector)
|
|
2097
|
+
};
|
|
2098
|
+
});
|
|
2099
|
+
scored.sort((a, b) => b.score - a.score);
|
|
2100
|
+
return scored.slice(0, limit).map(({ episode }) => episode);
|
|
2101
|
+
}
|
|
2102
|
+
function cosineSimilarity(a, b) {
|
|
2103
|
+
const length = Math.min(a.length, b.length);
|
|
2104
|
+
if (length === 0) {
|
|
2105
|
+
return 0;
|
|
2106
|
+
}
|
|
2107
|
+
let dot2 = 0;
|
|
2108
|
+
let magA = 0;
|
|
2109
|
+
let magB = 0;
|
|
2110
|
+
for (let i = 0; i < length; i += 1) {
|
|
2111
|
+
const av = a[i] ?? 0;
|
|
2112
|
+
const bv = b[i] ?? 0;
|
|
2113
|
+
dot2 += av * bv;
|
|
2114
|
+
magA += av * av;
|
|
2115
|
+
magB += bv * bv;
|
|
2116
|
+
}
|
|
2117
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
2118
|
+
return denom === 0 ? 0 : dot2 / denom;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/reverie/search.ts
|
|
2122
|
+
async function searchReveries(codexHome, text, repo, options) {
|
|
2123
|
+
const {
|
|
2124
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
2125
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
2126
|
+
useReranker = true,
|
|
2127
|
+
rerankerModel = REVERIE_RERANKER_MODEL,
|
|
2128
|
+
rerankerTopK = DEFAULT_RERANKER_TOP_K,
|
|
2129
|
+
rerankerBatchSize = DEFAULT_RERANKER_BATCH_SIZE,
|
|
2130
|
+
candidateMultiplier = REVERIE_CANDIDATE_MULTIPLIER
|
|
2131
|
+
} = options || {};
|
|
2132
|
+
const normalized = text.trim();
|
|
2133
|
+
if (!normalized) {
|
|
2134
|
+
return [];
|
|
2135
|
+
}
|
|
2136
|
+
const searchOptions = {
|
|
2137
|
+
projectRoot: repo,
|
|
2138
|
+
limit: maxCandidates * candidateMultiplier,
|
|
2139
|
+
// Get 3x candidates for heavy filtering
|
|
2140
|
+
maxCandidates: maxCandidates * candidateMultiplier,
|
|
2141
|
+
normalize: true,
|
|
2142
|
+
cache: true
|
|
2143
|
+
};
|
|
2144
|
+
if (useReranker) {
|
|
2145
|
+
searchOptions.rerankerModel = rerankerModel;
|
|
2146
|
+
searchOptions.rerankerTopK = rerankerTopK;
|
|
2147
|
+
searchOptions.rerankerBatchSize = rerankerBatchSize;
|
|
2148
|
+
}
|
|
2149
|
+
try {
|
|
2150
|
+
const regexMatches = looksLikeStructuredQuery(normalized) ? await reverieSearchConversations(codexHome, normalized, limit).catch(() => []) : [];
|
|
2151
|
+
const matches = await reverieSearchSemantic(codexHome, normalized, searchOptions);
|
|
2152
|
+
const combinedMatches = mergeSearchResults(regexMatches, matches);
|
|
2153
|
+
const insights = convertSearchResultsToInsights(combinedMatches);
|
|
2154
|
+
const validInsights = insights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
2155
|
+
const { kept: conversational } = await filterBoilerplateInsights(validInsights, {
|
|
2156
|
+
projectRoot: repo
|
|
2157
|
+
});
|
|
2158
|
+
const deduplicated = deduplicateReverieInsights(conversational);
|
|
2159
|
+
const episodeMatches = await searchEpisodeSummaries(codexHome, normalized, repo, limit * 4).catch(() => []);
|
|
2160
|
+
const episodeBoost = /* @__PURE__ */ new Map();
|
|
2161
|
+
for (const episode of episodeMatches) {
|
|
2162
|
+
episodeBoost.set(episode.conversationId, Math.max(episodeBoost.get(episode.conversationId) ?? 0, episode.importance ?? 0));
|
|
2163
|
+
}
|
|
2164
|
+
const ranked = deduplicated.map((insight) => {
|
|
2165
|
+
const bonus = episodeBoost.get(insight.conversationId) ?? 0;
|
|
2166
|
+
return {
|
|
2167
|
+
insight,
|
|
2168
|
+
score: insight.relevance + bonus / 10
|
|
2169
|
+
};
|
|
2170
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit).map(({ insight }) => insight);
|
|
2171
|
+
return ranked;
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
console.warn(
|
|
2174
|
+
`Reverie search failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2175
|
+
);
|
|
2176
|
+
return [];
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
function convertSearchResultsToInsights(results) {
|
|
2180
|
+
const flattened = [];
|
|
2181
|
+
for (const match of results) {
|
|
2182
|
+
const base = {
|
|
2183
|
+
conversationId: match.conversation?.id || "unknown",
|
|
2184
|
+
timestamp: match.conversation?.createdAt || match.conversation?.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2185
|
+
relevance: typeof match.relevanceScore === "number" ? match.relevanceScore : 0,
|
|
2186
|
+
excerpt: "",
|
|
2187
|
+
insights: Array.isArray(match.insights) ? match.insights : []
|
|
2188
|
+
};
|
|
2189
|
+
const excerpts = match.matchingExcerpts?.length ? match.matchingExcerpts : [""];
|
|
2190
|
+
for (const excerpt of excerpts) {
|
|
2191
|
+
if (!excerpt.trim()) {
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
flattened.push({ ...base, excerpt });
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return flattened;
|
|
2198
|
+
}
|
|
2199
|
+
function mergeSearchResults(primary, secondary) {
|
|
2200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2201
|
+
const merged = [];
|
|
2202
|
+
for (const list of [primary, secondary]) {
|
|
2203
|
+
for (const match of list) {
|
|
2204
|
+
const convoId = match.conversation?.id || "unknown";
|
|
2205
|
+
const excerptKey = match.matchingExcerpts?.[0] || String(match.relevanceScore ?? 0);
|
|
2206
|
+
const key = `${convoId}:${excerptKey}`;
|
|
2207
|
+
if (seen.has(key)) {
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
seen.add(key);
|
|
2211
|
+
merged.push(match);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return merged;
|
|
2215
|
+
}
|
|
2216
|
+
function looksLikeStructuredQuery(text) {
|
|
2217
|
+
if (!text) {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
const structuredPatterns = [
|
|
2221
|
+
/traceback \(most recent call last\)/i,
|
|
2222
|
+
// Python
|
|
2223
|
+
/exception in thread/i,
|
|
2224
|
+
/java\.lang\./i,
|
|
2225
|
+
/org\.junit/i,
|
|
2226
|
+
/at\s+org\./i,
|
|
2227
|
+
/AssertionError:/i,
|
|
2228
|
+
/panic!|thread '.+' panicked/i,
|
|
2229
|
+
/FAIL\s+\S+\s+\(/i,
|
|
2230
|
+
// Jest/Vitest
|
|
2231
|
+
/(?:error|fail|fatal):/i,
|
|
2232
|
+
/Caused by:/i,
|
|
2233
|
+
/\bundefined reference to\b/i
|
|
2234
|
+
];
|
|
2235
|
+
for (const pattern of structuredPatterns) {
|
|
2236
|
+
if (pattern.test(text)) {
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const hashPattern = /\b[0-9a-f]{32,}\b/i;
|
|
2241
|
+
if (hashPattern.test(text)) {
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
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;
|
|
2245
|
+
if (uuidPattern.test(text)) {
|
|
2246
|
+
return true;
|
|
2247
|
+
}
|
|
2248
|
+
const stackFrameMatches = text.match(/\bat\s+[^\s]+\s*\(|\b\S+\.\w+:\d+/gi);
|
|
2249
|
+
if ((stackFrameMatches?.length ?? 0) >= 2) {
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
const severityTokens = text.match(/\b(?:fail|error|panic|assert|fatal)\b/gi)?.length ?? 0;
|
|
2253
|
+
if (severityTokens >= 3 && text.length > 50) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/reverie/grader.ts
|
|
2260
|
+
import { Agent } from "@openai/agents";
|
|
2261
|
+
var REVERIE_GRADING_SCHEMA = {
|
|
2262
|
+
type: "object",
|
|
2263
|
+
properties: {
|
|
2264
|
+
is_relevant: {
|
|
2265
|
+
type: "boolean",
|
|
2266
|
+
description: "True if excerpt contains specific technical details relevant to the work context"
|
|
2267
|
+
},
|
|
2268
|
+
reasoning: {
|
|
2269
|
+
type: "string",
|
|
2270
|
+
description: "Brief explanation (1-2 sentences) of why the excerpt was approved or rejected"
|
|
2271
|
+
}
|
|
2272
|
+
},
|
|
2273
|
+
required: ["is_relevant", "reasoning"],
|
|
2274
|
+
additionalProperties: false
|
|
2275
|
+
};
|
|
2276
|
+
async function gradeReverieRelevance(runner, searchContext, insight) {
|
|
2277
|
+
const graderAgent = new Agent({
|
|
2278
|
+
name: "ReverieGrader",
|
|
2279
|
+
instructions: `You are a STRICT filter for conversation excerpts. Only approve excerpts with SPECIFIC technical details.
|
|
2280
|
+
|
|
2281
|
+
REJECT excerpts containing:
|
|
2282
|
+
- Greetings and pleasantries
|
|
2283
|
+
- Thinking markers (**, ##, <thinking>)
|
|
2284
|
+
- JSON objects or structured data dumps
|
|
2285
|
+
- Generic phrases ("Context from past work", "working on this", etc.)
|
|
2286
|
+
- Metadata and system information
|
|
2287
|
+
- Boilerplate text
|
|
2288
|
+
- Task or checklist instructions ("1.", "2.", "Plan:")
|
|
2289
|
+
- AGENTS.md guidance, sandbox instructions, or environment descriptions
|
|
2290
|
+
- Tool output summaries or command transcript blocks
|
|
2291
|
+
|
|
2292
|
+
APPROVE ONLY excerpts with:
|
|
2293
|
+
- Specific code/file references (file paths, function names, variable names)
|
|
2294
|
+
- Technical decisions and rationale
|
|
2295
|
+
- Error messages and debugging details
|
|
2296
|
+
- Implementation specifics and algorithms
|
|
2297
|
+
- Architecture patterns and design choices
|
|
2298
|
+
|
|
2299
|
+
Return a JSON object with:
|
|
2300
|
+
- is_relevant: boolean indicating if this excerpt should be kept
|
|
2301
|
+
- reasoning: brief 1-2 sentence explanation of your decision`,
|
|
2302
|
+
outputType: {
|
|
2303
|
+
type: "json_schema",
|
|
2304
|
+
schema: REVERIE_GRADING_SCHEMA,
|
|
2305
|
+
name: "ReverieGrading",
|
|
2306
|
+
strict: true
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
const prompt = `Context: ${searchContext}
|
|
2310
|
+
|
|
2311
|
+
Excerpt to grade:
|
|
2312
|
+
"""
|
|
2313
|
+
${insight.excerpt.slice(0, 400)}
|
|
2314
|
+
"""
|
|
2315
|
+
|
|
2316
|
+
Evaluate whether this excerpt contains specific technical details relevant to the work context.`;
|
|
2317
|
+
const result = await runner.run(graderAgent, prompt);
|
|
2318
|
+
if (result.finalOutput && typeof result.finalOutput === "object") {
|
|
2319
|
+
const grading = result.finalOutput;
|
|
2320
|
+
return grading.is_relevant;
|
|
2321
|
+
}
|
|
2322
|
+
console.warn("Reverie grading failed to return structured output, defaulting to reject");
|
|
2323
|
+
return false;
|
|
2324
|
+
}
|
|
2325
|
+
async function gradeReveriesInParallel(runner, context, insights, options) {
|
|
2326
|
+
const { minRelevanceForGrading = 0.7, parallel = true } = options || {};
|
|
2327
|
+
const highScoring = insights.filter((insight) => insight.relevance >= minRelevanceForGrading);
|
|
2328
|
+
const lowScoring = insights.filter((insight) => insight.relevance < minRelevanceForGrading);
|
|
2329
|
+
if (highScoring.length === 0) {
|
|
2330
|
+
return [];
|
|
2331
|
+
}
|
|
2332
|
+
if (parallel) {
|
|
2333
|
+
const gradingPromises = highScoring.map(
|
|
2334
|
+
(insight) => gradeReverieRelevance(runner, context, insight).then((isRelevant) => ({
|
|
2335
|
+
insight,
|
|
2336
|
+
isRelevant
|
|
2337
|
+
}))
|
|
2338
|
+
);
|
|
2339
|
+
const gradedResults = await Promise.all(gradingPromises);
|
|
2340
|
+
return gradedResults.filter((r) => r.isRelevant).map((r) => r.insight);
|
|
2341
|
+
} else {
|
|
2342
|
+
const approved = [];
|
|
2343
|
+
for (const insight of highScoring) {
|
|
2344
|
+
const isRelevant = await gradeReverieRelevance(runner, context, insight);
|
|
2345
|
+
if (isRelevant) {
|
|
2346
|
+
approved.push(insight);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return approved;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/reverie/context.ts
|
|
2354
|
+
function buildProjectContext(query, options) {
|
|
2355
|
+
return {
|
|
2356
|
+
level: "project",
|
|
2357
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
2358
|
+
query,
|
|
2359
|
+
filePatterns: options?.filePatterns
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
function buildBranchContext(branch, changedFiles, options) {
|
|
2363
|
+
return {
|
|
2364
|
+
level: "branch",
|
|
2365
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
2366
|
+
branch,
|
|
2367
|
+
baseBranch: options?.baseBranch,
|
|
2368
|
+
changedFiles,
|
|
2369
|
+
recentCommits: options?.recentCommits
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function buildFileContext(filePath, options) {
|
|
2373
|
+
const context = {
|
|
2374
|
+
level: "file",
|
|
2375
|
+
repoPath: options?.repoPath || process.cwd(),
|
|
2376
|
+
filePath,
|
|
2377
|
+
diff: options?.diff
|
|
2378
|
+
};
|
|
2379
|
+
if (options?.extractSymbols && options?.diff) {
|
|
2380
|
+
const symbolsText = extractKeySymbols(options.diff);
|
|
2381
|
+
if (symbolsText) {
|
|
2382
|
+
context.symbols = symbolsText.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return context;
|
|
2386
|
+
}
|
|
2387
|
+
function contextToQuery(context) {
|
|
2388
|
+
switch (context.level) {
|
|
2389
|
+
case "project": {
|
|
2390
|
+
let query = `Project-wide: ${context.query}`;
|
|
2391
|
+
if (context.filePatterns && context.filePatterns.length > 0) {
|
|
2392
|
+
query += `
|
|
2393
|
+
Scope: ${context.filePatterns.join(", ")}`;
|
|
2394
|
+
}
|
|
2395
|
+
return query;
|
|
2396
|
+
}
|
|
2397
|
+
case "branch": {
|
|
2398
|
+
let query = `Branch: ${context.branch}`;
|
|
2399
|
+
if (context.baseBranch) {
|
|
2400
|
+
query += ` (base: ${context.baseBranch})`;
|
|
2401
|
+
}
|
|
2402
|
+
query += `
|
|
2403
|
+
Files changed: ${context.changedFiles.join(", ")}`;
|
|
2404
|
+
if (context.recentCommits) {
|
|
2405
|
+
query += `
|
|
2406
|
+
Recent commits: ${context.recentCommits}`;
|
|
2407
|
+
}
|
|
2408
|
+
return query;
|
|
2409
|
+
}
|
|
2410
|
+
case "file": {
|
|
2411
|
+
let query = `File: ${context.filePath}`;
|
|
2412
|
+
if (context.symbols && context.symbols.length > 0) {
|
|
2413
|
+
query += `
|
|
2414
|
+
Symbols: ${context.symbols.join(", ")}`;
|
|
2415
|
+
}
|
|
2416
|
+
if (context.diff) {
|
|
2417
|
+
const truncatedDiff = context.diff.length > 500 ? context.diff.slice(0, 500) + "..." : context.diff;
|
|
2418
|
+
query += `
|
|
2419
|
+
Changes:
|
|
2420
|
+
${truncatedDiff}`;
|
|
2421
|
+
}
|
|
2422
|
+
return query;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
function formatFileList(files, maxFiles = 10) {
|
|
2427
|
+
if (files.length === 0) {
|
|
2428
|
+
return "(no files)";
|
|
2429
|
+
}
|
|
2430
|
+
if (files.length <= maxFiles) {
|
|
2431
|
+
return files.join(", ");
|
|
2432
|
+
}
|
|
2433
|
+
const shown = files.slice(0, maxFiles);
|
|
2434
|
+
const remaining = files.length - maxFiles;
|
|
2435
|
+
return `${shown.join(", ")} ... and ${remaining} more`;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/reverie/pipeline.ts
|
|
2439
|
+
async function applyReveriePipeline(codexHome, searchText, repo, runner, options) {
|
|
2440
|
+
const {
|
|
2441
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
2442
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
2443
|
+
minRelevanceForGrading = REVERIE_LLM_GRADE_THRESHOLD,
|
|
2444
|
+
skipLLMGrading = false,
|
|
2445
|
+
...searchOptions
|
|
2446
|
+
} = options || {};
|
|
2447
|
+
logReverieSearch(searchText, `repo: ${repo}`);
|
|
2448
|
+
const rawInsights = await searchReveries(codexHome, searchText, repo, {
|
|
2449
|
+
limit,
|
|
2450
|
+
maxCandidates,
|
|
2451
|
+
...searchOptions
|
|
2452
|
+
});
|
|
2453
|
+
const stats = {
|
|
2454
|
+
total: rawInsights.length,
|
|
2455
|
+
afterQuality: 0,
|
|
2456
|
+
afterBoilerplate: 0,
|
|
2457
|
+
afterScore: 0,
|
|
2458
|
+
afterDedup: 0,
|
|
2459
|
+
final: 0
|
|
2460
|
+
};
|
|
2461
|
+
const validInsights = rawInsights.filter((insight) => isValidReverieExcerpt(insight.excerpt));
|
|
2462
|
+
stats.afterQuality = validInsights.length;
|
|
2463
|
+
const { kept: conversationalInsights } = await filterBoilerplateInsights(validInsights, {
|
|
2464
|
+
projectRoot: repo
|
|
2465
|
+
});
|
|
2466
|
+
stats.afterBoilerplate = conversationalInsights.length;
|
|
2467
|
+
const highScoring = conversationalInsights.filter((insight) => insight.relevance >= minRelevanceForGrading);
|
|
2468
|
+
const lowScoring = conversationalInsights.filter((insight) => insight.relevance < minRelevanceForGrading);
|
|
2469
|
+
stats.afterScore = highScoring.length;
|
|
2470
|
+
let gradedInsights;
|
|
2471
|
+
if (skipLLMGrading || !runner) {
|
|
2472
|
+
gradedInsights = highScoring;
|
|
2473
|
+
stats.afterLLMGrade = highScoring.length;
|
|
2474
|
+
} else {
|
|
2475
|
+
gradedInsights = await gradeReveriesInParallel(runner, searchText, highScoring, {
|
|
2476
|
+
minRelevanceForGrading,
|
|
2477
|
+
parallel: true
|
|
2478
|
+
});
|
|
2479
|
+
stats.afterLLMGrade = gradedInsights.length;
|
|
2480
|
+
logLLMGrading({
|
|
2481
|
+
total: highScoring.length,
|
|
2482
|
+
approved: gradedInsights.length,
|
|
2483
|
+
rejected: highScoring.length - gradedInsights.length,
|
|
2484
|
+
minScore: minRelevanceForGrading
|
|
2485
|
+
});
|
|
2486
|
+
if (gradedInsights.length > 0) {
|
|
2487
|
+
logApprovedReveries(gradedInsights);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const deduplicated = deduplicateReverieInsights(gradedInsights);
|
|
2491
|
+
stats.afterDedup = deduplicated.length;
|
|
2492
|
+
const finalInsights = deduplicated.slice(0, limit);
|
|
2493
|
+
stats.final = finalInsights.length;
|
|
2494
|
+
logReverieFiltering({
|
|
2495
|
+
total: stats.total,
|
|
2496
|
+
afterQuality: stats.afterQuality,
|
|
2497
|
+
afterBoilerplate: stats.afterBoilerplate,
|
|
2498
|
+
afterScore: stats.afterScore,
|
|
2499
|
+
afterDedup: stats.afterDedup,
|
|
2500
|
+
minScore: minRelevanceForGrading
|
|
2501
|
+
});
|
|
2502
|
+
return {
|
|
2503
|
+
insights: finalInsights,
|
|
2504
|
+
stats
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
async function applyFileReveriePipeline(codexHome, filePath, fileContext, repo, runner, options) {
|
|
2508
|
+
const {
|
|
2509
|
+
maxCandidates = DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
2510
|
+
limit = DEFAULT_REVERIE_LIMIT,
|
|
2511
|
+
...restOptions
|
|
2512
|
+
} = options || {};
|
|
2513
|
+
const fileOptions = {
|
|
2514
|
+
...restOptions,
|
|
2515
|
+
maxCandidates: Math.floor(maxCandidates / 2),
|
|
2516
|
+
limit
|
|
2517
|
+
};
|
|
2518
|
+
return applyReveriePipeline(codexHome, fileContext, repo, runner, fileOptions);
|
|
2519
|
+
}
|
|
2520
|
+
async function searchMultiLevel(codexHome, contexts, runner, options) {
|
|
2521
|
+
const levels = contexts.map((ctx) => ctx.level);
|
|
2522
|
+
logMultiLevelSearch(levels);
|
|
2523
|
+
const results = /* @__PURE__ */ new Map();
|
|
2524
|
+
for (const context of contexts) {
|
|
2525
|
+
let result;
|
|
2526
|
+
switch (context.level) {
|
|
2527
|
+
case "project":
|
|
2528
|
+
result = await searchProjectLevel(codexHome, context, runner, options);
|
|
2529
|
+
break;
|
|
2530
|
+
case "branch":
|
|
2531
|
+
result = await searchBranchLevel(codexHome, context, runner, options);
|
|
2532
|
+
break;
|
|
2533
|
+
case "file":
|
|
2534
|
+
result = await searchFileLevel(codexHome, context, runner, options);
|
|
2535
|
+
break;
|
|
2536
|
+
}
|
|
2537
|
+
results.set(context.level, result);
|
|
2538
|
+
logLevelResults(context.level, result);
|
|
2539
|
+
}
|
|
2540
|
+
return results;
|
|
2541
|
+
}
|
|
2542
|
+
async function searchProjectLevel(codexHome, context, runner, options) {
|
|
2543
|
+
const searchQuery = contextToQuery(context);
|
|
2544
|
+
const projectOptions = {
|
|
2545
|
+
...options,
|
|
2546
|
+
maxCandidates: (options?.maxCandidates || DEFAULT_REVERIE_MAX_CANDIDATES) * 1.5
|
|
2547
|
+
};
|
|
2548
|
+
return applyReveriePipeline(
|
|
2549
|
+
codexHome,
|
|
2550
|
+
searchQuery,
|
|
2551
|
+
context.repoPath,
|
|
2552
|
+
runner,
|
|
2553
|
+
projectOptions
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
async function searchBranchLevel(codexHome, context, runner, options) {
|
|
2557
|
+
const searchQuery = contextToQuery(context);
|
|
2558
|
+
return applyReveriePipeline(
|
|
2559
|
+
codexHome,
|
|
2560
|
+
searchQuery,
|
|
2561
|
+
context.repoPath,
|
|
2562
|
+
runner,
|
|
2563
|
+
options
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
async function searchFileLevel(codexHome, context, runner, options) {
|
|
2567
|
+
const searchQuery = contextToQuery(context);
|
|
2568
|
+
return applyFileReveriePipeline(
|
|
2569
|
+
codexHome,
|
|
2570
|
+
context.filePath,
|
|
2571
|
+
searchQuery,
|
|
2572
|
+
context.repoPath,
|
|
2573
|
+
runner,
|
|
2574
|
+
options
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/index.ts
|
|
2579
|
+
function evCompleted(id) {
|
|
2580
|
+
const binding = getNativeBinding();
|
|
2581
|
+
if (!binding) throw new Error("Native binding not available");
|
|
2582
|
+
return binding.evCompleted(id);
|
|
2583
|
+
}
|
|
2584
|
+
function evResponseCreated(id) {
|
|
2585
|
+
const binding = getNativeBinding();
|
|
2586
|
+
if (!binding) throw new Error("Native binding not available");
|
|
2587
|
+
return binding.evResponseCreated(id);
|
|
2588
|
+
}
|
|
2589
|
+
function evAssistantMessage(id, text) {
|
|
2590
|
+
const binding = getNativeBinding();
|
|
2591
|
+
if (!binding) throw new Error("Native binding not available");
|
|
2592
|
+
return binding.evAssistantMessage(id, text);
|
|
2593
|
+
}
|
|
2594
|
+
function evFunctionCall(callId, name, args) {
|
|
2595
|
+
const binding = getNativeBinding();
|
|
2596
|
+
if (!binding) throw new Error("Native binding not available");
|
|
2597
|
+
return binding.evFunctionCall(callId, name, args);
|
|
2598
|
+
}
|
|
2599
|
+
function sse(events) {
|
|
2600
|
+
const binding = getNativeBinding();
|
|
2601
|
+
if (!binding) throw new Error("Native binding not available");
|
|
2602
|
+
return binding.sse(events);
|
|
2603
|
+
}
|
|
633
2604
|
export {
|
|
2605
|
+
CloudTasks,
|
|
634
2606
|
Codex,
|
|
635
2607
|
CodexProvider,
|
|
636
|
-
|
|
2608
|
+
DEFAULT_RERANKER_BATCH_SIZE,
|
|
2609
|
+
DEFAULT_RERANKER_TOP_K,
|
|
2610
|
+
DEFAULT_REVERIE_LIMIT,
|
|
2611
|
+
DEFAULT_REVERIE_MAX_CANDIDATES,
|
|
2612
|
+
DEFAULT_SERVERS,
|
|
2613
|
+
LogLevel,
|
|
2614
|
+
Logger,
|
|
2615
|
+
LspDiagnosticsBridge,
|
|
2616
|
+
LspManager,
|
|
2617
|
+
OpenCodeAgent,
|
|
2618
|
+
REVERIE_CANDIDATE_MULTIPLIER,
|
|
2619
|
+
REVERIE_EMBED_MODEL,
|
|
2620
|
+
REVERIE_LLM_GRADE_THRESHOLD,
|
|
2621
|
+
REVERIE_RERANKER_MODEL,
|
|
2622
|
+
ScopedLogger,
|
|
2623
|
+
Thread,
|
|
2624
|
+
applyFileReveriePipeline,
|
|
2625
|
+
applyQualityPipeline,
|
|
2626
|
+
applyReveriePipeline,
|
|
2627
|
+
attachLspDiagnostics,
|
|
2628
|
+
buildBranchContext,
|
|
2629
|
+
buildFileContext,
|
|
2630
|
+
buildProjectContext,
|
|
2631
|
+
codexTool,
|
|
2632
|
+
collectRepoDiffSummary,
|
|
2633
|
+
contextToQuery,
|
|
2634
|
+
createThreadLogger,
|
|
2635
|
+
deduplicateReverieInsights,
|
|
2636
|
+
encodeToToon,
|
|
2637
|
+
evAssistantMessage,
|
|
2638
|
+
evCompleted,
|
|
2639
|
+
evFunctionCall,
|
|
2640
|
+
evResponseCreated,
|
|
2641
|
+
extractKeySymbols,
|
|
2642
|
+
fastEmbedEmbed,
|
|
2643
|
+
fastEmbedInit,
|
|
2644
|
+
filterBySeverity,
|
|
2645
|
+
findServerForFile,
|
|
2646
|
+
formatDiagnosticsForBackgroundEvent,
|
|
2647
|
+
formatDiagnosticsForTool,
|
|
2648
|
+
formatDiagnosticsWithSummary,
|
|
2649
|
+
formatFileList,
|
|
2650
|
+
formatStream,
|
|
2651
|
+
gradeReverieRelevance,
|
|
2652
|
+
gradeReveriesInParallel,
|
|
2653
|
+
isValidReverieExcerpt,
|
|
2654
|
+
logApprovedReveries,
|
|
2655
|
+
logLLMGrading,
|
|
2656
|
+
logLevelResults,
|
|
2657
|
+
logMultiLevelSearch,
|
|
2658
|
+
logMultiLevelSummary,
|
|
2659
|
+
logReverieFiltering,
|
|
2660
|
+
logReverieHintQuality,
|
|
2661
|
+
logReverieInsights,
|
|
2662
|
+
logReverieSearch,
|
|
2663
|
+
logger,
|
|
2664
|
+
resolveWorkspaceRoot,
|
|
2665
|
+
reverieGetConversationInsights,
|
|
2666
|
+
reverieIndexSemantic,
|
|
2667
|
+
reverieListConversations,
|
|
2668
|
+
reverieSearchConversations,
|
|
2669
|
+
reverieSearchSemantic,
|
|
2670
|
+
runThreadTurnWithLogs,
|
|
2671
|
+
runTui,
|
|
2672
|
+
searchBranchLevel,
|
|
2673
|
+
searchFileLevel,
|
|
2674
|
+
searchMultiLevel,
|
|
2675
|
+
searchProjectLevel,
|
|
2676
|
+
searchReveries,
|
|
2677
|
+
sse,
|
|
2678
|
+
startTui,
|
|
2679
|
+
summarizeDiagnostics,
|
|
2680
|
+
tokenizerCount,
|
|
2681
|
+
tokenizerDecode,
|
|
2682
|
+
tokenizerEncode,
|
|
2683
|
+
truncate as truncateText
|
|
637
2684
|
};
|
|
638
2685
|
//# sourceMappingURL=index.mjs.map
|