@codex-native/sdk 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,387 +1,50 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __esm = (fn, res) => function __init() {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- };
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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/outputSchemaFile.ts
23
- import { promises as fs } from "fs";
24
- import os from "os";
25
- import path from "path";
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
- var init_outputSchemaFile = __esm({
54
- "src/outputSchemaFile.ts"() {
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/codex.ts
317
- var codex_exports = {};
318
- __export(codex_exports, {
319
- Codex: () => Codex
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/index.ts
383
- init_thread();
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
- const { Codex: CodexClass } = (init_codex(), __toCommonJS(codex_exports));
403
- this.codex = new CodexClass({
404
- apiKey: this.options.apiKey,
405
- baseUrl: this.options.baseUrl
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 && !this.thread) {
430
- this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions());
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
- return this.thread;
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
- const thread = this.getThread(request.conversationId || request.previousResponseId);
445
- const input = this.convertRequestToInput(request);
446
- const turn = await thread.run(input, {
447
- outputSchema: request.outputType?.schema
448
- });
449
- return {
450
- usage: this.convertUsage(turn.usage),
451
- output: this.convertItemsToOutput(turn.items, turn.finalResponse),
452
- responseId: thread.id || void 0
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 thread = this.getThread(request.conversationId || request.previousResponseId);
457
- const input = this.convertRequestToInput(request);
458
- const { events } = await thread.runStreamed(input, {
459
- outputSchema: request.outputType?.schema
460
- });
461
- let accumulatedText = "";
462
- for await (const event of events) {
463
- const streamEvents = this.convertCodexEventToStreamEvent(event, accumulatedText);
464
- if (event.type === "item.completed" && event.item.type === "agent_message") {
465
- accumulatedText = event.item.text;
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
- for (const streamEvent of streamEvents) {
468
- yield streamEvent;
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 ModelRequest to Codex Input format
445
+ * Convert media type to file extension
446
+ * Handles special cases like "jpeg" -> "jpg", "svg+xml" -> "svg"
474
447
  */
475
- convertRequestToInput(request) {
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.type === "input_text") {
492
- parts.push({ type: "text", text: item.text });
493
- } else if (item.type === "input_image") {
494
- if (typeof item.image === "string") {
495
- continue;
496
- } else if ("url" in item.image) {
497
- continue;
498
- } else if ("fileId" in item.image) {
499
- continue;
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 inputTokensDetails = usage.cached_input_tokens ? [{ cachedTokens: usage.cached_input_tokens }] : void 0;
521
- return {
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
- inputTokensDetails
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
- case "reasoning": {
551
- output.push({
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, previousText) {
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: "output_text_done",
599
- text: event.item.text
734
+ type: "model",
735
+ event: {
736
+ type: "reasoning_done",
737
+ reasoning: event.item.text
738
+ }
600
739
  });
601
- } else if (event.item.type === "reasoning") {
740
+ textAccumulator.delete("reasoning");
741
+ } else if (event.item.type === "todo_list") {
602
742
  events.push({
603
- type: "reasoning_done",
604
- reasoning: event.item.text
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: "response_done",
763
+ type: "response.completed",
611
764
  response: {
612
- usage: this.convertUsage(event.usage),
613
- output: [],
614
- // Items were already emitted
615
- responseId: this.thread?.id || void 0
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: "error",
622
- error: {
623
- message: event.error.message
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
- Thread
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