@messenger-agent/codex-agent 0.24.0-alpha.2 → 0.24.0-alpha.4

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/codex.js CHANGED
@@ -1,461 +1,3 @@
1
- import { mkdirSync, existsSync } from "node:fs";
2
- import { appServerClient } from "./app-server-client.js";
3
- import { dynamicToolSpecs } from "./dynamic-tools.js";
4
- import { loadAllSessions, persistSession } from "./db.js";
5
- import { appConfig } from "./config.js";
6
- import { logger } from "@messenger-agent/shared/logger";
7
- import { resolveAgentWorkdir } from "@messenger-agent/shared/agent-workspaces";
8
- import { codexPlatformInstructions } from "./platform-instructions.js";
9
- export class WorkdirNotFoundError extends Error {
10
- path;
11
- constructor(path) {
12
- super(`Workdir not found: ${path}`);
13
- this.path = path;
14
- }
15
- }
16
- export class CodexThread {
17
- id;
18
- model;
19
- workingDirectory;
20
- constructor(id, model, workingDirectory) {
21
- this.id = id;
22
- this.model = model;
23
- this.workingDirectory = workingDirectory;
24
- }
25
- async run(prompt, options) {
26
- const streamed = await this.runStreamed(prompt, options);
27
- const items = new Map();
28
- const completedItems = new Set();
29
- let usage = null;
30
- for await (const event of streamed.events) {
31
- if (event.type === "item.started") {
32
- if (!completedItems.has(event.item.id)) {
33
- items.set(event.item.id, event.item);
34
- }
35
- }
36
- else if (event.type === "item.completed") {
37
- completedItems.add(event.item.id);
38
- items.set(event.item.id, event.item);
39
- }
40
- else if (event.type === "thread.tokenUsage.updated") {
41
- usage = event.usage;
42
- }
43
- else if (event.type === "turn.failed") {
44
- throw new Error(event.error.message);
45
- }
46
- }
47
- const itemList = Array.from(items.values());
48
- const finalResponse = itemList
49
- .filter((item) => item.type === "agentMessage")
50
- .map((item) => item.text)
51
- .join("\n");
52
- return { items: itemList, finalResponse: finalResponse || undefined, usage };
53
- }
54
- async runStreamed(_prompt, options) {
55
- const response = await appServerClient.request("turn/start", this.turnStartParams(_prompt, options));
56
- const turnId = response.turn.id;
57
- return { turnId, events: this.eventsForTurn(turnId, response.turn, options?.signal) };
58
- }
59
- async steerStreamed(prompt, expectedTurnId, options) {
60
- const params = {
61
- threadId: this.id,
62
- input: [{ type: "text", text: prompt }],
63
- expectedTurnId,
64
- };
65
- const response = await appServerClient.request("turn/steer", params);
66
- return {
67
- turnId: response.turnId,
68
- events: this.eventsForTurn(response.turnId, undefined, options?.signal, undefined, true),
69
- };
70
- }
71
- async continueWithUserInput(itemId, response, options) {
72
- const turnId = appServerClient.pendingUserInputTurnId(this.id, itemId);
73
- if (!turnId) {
74
- await this.interruptStaleTurn(options?.sourceTurnId);
75
- return this.runStreamed(userInputResponsePrompt(itemId, response), options);
76
- }
77
- return {
78
- turnId,
79
- events: this.eventsForTurn(turnId, undefined, options?.signal, async () => {
80
- const accepted = await appServerClient.respondToUserInput(this.id, itemId, response);
81
- if (!accepted) {
82
- throw new Error(`No pending user input request found for item: ${itemId}`);
83
- }
84
- }),
85
- };
86
- }
87
- async submitPendingUserInput(itemId, response) {
88
- const turnId = appServerClient.pendingUserInputTurnId(this.id, itemId);
89
- if (!turnId)
90
- return undefined;
91
- const accepted = await appServerClient.respondToUserInput(this.id, itemId, response);
92
- if (!accepted)
93
- return undefined;
94
- return turnId;
95
- }
96
- async interrupt(turnId) {
97
- await appServerClient.request("turn/interrupt", { threadId: this.id, turnId });
98
- }
99
- async interruptPendingUserInput(turnId, itemId) {
100
- await this.interrupt(turnId);
101
- appServerClient.discardPendingUserInput(this.id, itemId);
102
- }
103
- async interruptStaleTurn(turnId) {
104
- if (!turnId)
105
- return;
106
- const response = await appServerClient.request("thread/read", {
107
- threadId: this.id,
108
- includeTurns: true,
109
- });
110
- const turn = response.thread.turns?.find((candidate) => candidate.id === turnId);
111
- if (turn?.status === "inProgress") {
112
- await this.interrupt(turnId);
113
- }
114
- }
115
- turnStartParams(prompt, options) {
116
- return {
117
- threadId: this.id,
118
- input: [{ type: "text", text: prompt, text_elements: [] }],
119
- cwd: this.workingDirectory,
120
- approvalPolicy: "never",
121
- model: this.model,
122
- ...(options?.collaborationMode ? { collaborationMode: options.collaborationMode } : null),
123
- };
124
- }
125
- async *eventsForTurn(turnId, initialTurn, signal, beforeWait, emitStarted = false) {
126
- const queue = [
127
- ...(initialTurn || emitStarted
128
- ? [
129
- { type: "thread.started", thread_id: this.id },
130
- { type: "turn.started", turn_id: turnId },
131
- ]
132
- : []),
133
- ...(initialTurn ? eventsFromInitialTurn(initialTurn) : []),
134
- ];
135
- let done = initialTurn?.status === "completed" || initialTurn?.status === "failed" || initialTurn?.status === "interrupted";
136
- let failure;
137
- let stopped = false;
138
- let wake = () => { };
139
- const onNotification = ({ method, params }) => {
140
- if (stopped)
141
- return;
142
- const event = notificationToEvent(method, params, this.id, turnId);
143
- if (!event)
144
- return;
145
- queue.push(event);
146
- if (event.type === "turn.completed" || event.type === "turn.failed") {
147
- done = true;
148
- }
149
- wake();
150
- };
151
- const onExit = (err) => {
152
- failure = err;
153
- done = true;
154
- wake();
155
- };
156
- const onAbort = () => {
157
- stopped = true;
158
- queue.length = 0;
159
- done = true;
160
- wake();
161
- };
162
- appServerClient.on("notification", onNotification);
163
- appServerClient.on("exit", onExit);
164
- signal?.addEventListener("abort", onAbort, { once: true });
165
- if (signal?.aborted)
166
- onAbort();
167
- try {
168
- await beforeWait?.();
169
- while (!done || queue.length > 0) {
170
- while (queue.length > 0) {
171
- yield queue.shift();
172
- }
173
- if (done)
174
- break;
175
- await new Promise((resolve) => {
176
- const timer = setTimeout(() => {
177
- wake = () => { };
178
- resolve();
179
- }, 50);
180
- wake = () => {
181
- clearTimeout(timer);
182
- wake = () => { };
183
- resolve();
184
- };
185
- });
186
- }
187
- if (failure) {
188
- yield { type: "error", message: failure.message };
189
- }
190
- }
191
- finally {
192
- appServerClient.off("notification", onNotification);
193
- appServerClient.off("exit", onExit);
194
- signal?.removeEventListener("abort", onAbort);
195
- }
196
- }
197
- }
198
- function userInputResponsePrompt(itemId, response) {
199
- return [
200
- `The user answered a previous request_user_input prompt (${itemId}), but the original pending tool request is no longer active.`,
201
- "Continue the task using this answer:",
202
- JSON.stringify(response, null, 2),
203
- ].join("\n");
204
- }
205
- class SessionManager {
206
- sessions = new Map();
207
- constructor() {
208
- for (const row of loadAllSessions()) {
209
- this.sessions.set(row.sessionId, {
210
- workdir: row.workdir,
211
- botName: row.botName ?? "main",
212
- llmProxyBinding: row.llmProxyBinding,
213
- });
214
- }
215
- logger.info(`SessionManager: loaded ${this.sessions.size} persisted sessions`);
216
- }
217
- async getOrCreate(sessionId, botName, requestedWorkdir, requestedWorkspaceId, model, sandboxMode, llmProxyBinding) {
218
- const targetWorkdir = this.resolveWorkdir(requestedWorkdir, requestedWorkspaceId);
219
- if (sessionId) {
220
- const stored = this.sessions.get(sessionId);
221
- if (stored && stored.workdir === targetWorkdir) {
222
- const effectiveLlmProxyBinding = stored.llmProxyBinding ?? llmProxyBinding;
223
- const response = await appServerClient.request("thread/resume", this.threadResumeParams(sessionId, model, targetWorkdir, sandboxMode, effectiveLlmProxyBinding));
224
- if (!stored.llmProxyBinding && effectiveLlmProxyBinding) {
225
- const updated = { ...stored, llmProxyBinding: effectiveLlmProxyBinding };
226
- this.sessions.set(sessionId, updated);
227
- persistSession({
228
- sessionId,
229
- botName: updated.botName,
230
- workdir: updated.workdir,
231
- createdAt: Date.now(),
232
- llmProxyBinding: effectiveLlmProxyBinding,
233
- });
234
- }
235
- return { thread: new CodexThread(response.thread.id, model, targetWorkdir), sessionId, isNewSession: false };
236
- }
237
- }
238
- if (requestedWorkdir && !existsSync(targetWorkdir)) {
239
- throw new WorkdirNotFoundError(targetWorkdir);
240
- }
241
- mkdirSync(targetWorkdir, { recursive: true });
242
- const response = await appServerClient.request("thread/start", this.threadStartParams(model, targetWorkdir, sandboxMode, llmProxyBinding));
243
- return {
244
- thread: new CodexThread(response.thread.id, model, targetWorkdir),
245
- sessionId: undefined,
246
- isNewSession: true,
247
- };
248
- }
249
- async fork(source, botName, requestedWorkdir, requestedWorkspaceId, model, sandboxMode, llmProxyBinding) {
250
- const targetWorkdir = this.resolveWorkdir(requestedWorkdir, requestedWorkspaceId);
251
- if (requestedWorkdir && !existsSync(targetWorkdir)) {
252
- throw new WorkdirNotFoundError(targetWorkdir);
253
- }
254
- mkdirSync(targetWorkdir, { recursive: true });
255
- const forkResponse = await appServerClient.request("thread/fork", this.threadForkParams(source.threadId, source.turnId, model, targetWorkdir, sandboxMode, llmProxyBinding));
256
- const threadId = forkResponse.thread.id;
257
- this.persist(threadId, botName, requestedWorkdir, requestedWorkspaceId, llmProxyBinding);
258
- return {
259
- thread: new CodexThread(threadId, model, targetWorkdir),
260
- sessionId: threadId,
261
- isNewSession: false,
262
- };
263
- }
264
- persist(sessionId, botName, workdir, workspaceId, llmProxyBinding) {
265
- const resolvedBotName = botName ?? "main";
266
- const resolvedWorkdir = this.resolveWorkdir(workdir, workspaceId);
267
- this.sessions.set(sessionId, { botName: resolvedBotName, workdir: resolvedWorkdir, llmProxyBinding });
268
- persistSession({
269
- sessionId,
270
- botName: resolvedBotName,
271
- workdir: resolvedWorkdir,
272
- createdAt: Date.now(),
273
- llmProxyBinding,
274
- });
275
- }
276
- bindingFor(sessionId) {
277
- return sessionId ? this.sessions.get(sessionId)?.llmProxyBinding : undefined;
278
- }
279
- workdirFor(sessionId) {
280
- return sessionId ? this.sessions.get(sessionId)?.workdir : undefined;
281
- }
282
- hasSession(sessionId) {
283
- return !!sessionId && this.sessions.has(sessionId);
284
- }
285
- resolveWorkdir(subpath, workspaceId) {
286
- return resolveAgentWorkdir(appConfig, workspaceId, subpath);
287
- }
288
- threadStartParams(model, cwd, sandbox, llmProxyBinding) {
289
- return {
290
- model,
291
- modelProvider: appConfig.codex.baseUrl ? "custom" : null,
292
- cwd,
293
- approvalPolicy: "never",
294
- sandbox,
295
- config: this.codexConfig(llmProxyBinding),
296
- developerInstructions: codexPlatformInstructions,
297
- dynamicTools: dynamicToolSpecs(),
298
- experimentalRawEvents: false,
299
- persistExtendedHistory: false,
300
- };
301
- }
302
- threadResumeParams(threadId, model, cwd, sandbox, llmProxyBinding) {
303
- return {
304
- threadId,
305
- model,
306
- modelProvider: appConfig.codex.baseUrl ? "custom" : null,
307
- cwd,
308
- approvalPolicy: "never",
309
- sandbox,
310
- config: this.codexConfig(llmProxyBinding),
311
- developerInstructions: codexPlatformInstructions,
312
- excludeTurns: true,
313
- persistExtendedHistory: false,
314
- };
315
- }
316
- threadForkParams(threadId, lastTurnId, model, cwd, sandbox, llmProxyBinding) {
317
- return {
318
- threadId,
319
- lastTurnId,
320
- model,
321
- modelProvider: appConfig.codex.baseUrl ? "custom" : null,
322
- cwd,
323
- approvalPolicy: "never",
324
- sandbox,
325
- config: this.codexConfig(llmProxyBinding),
326
- developerInstructions: codexPlatformInstructions,
327
- ephemeral: false,
328
- excludeTurns: true,
329
- };
330
- }
331
- codexConfig(llmProxyBinding) {
332
- return {
333
- approval_policy: "never",
334
- ...(appConfig.codex.baseUrl
335
- ? {
336
- model_provider: "custom",
337
- model_reasoning_effort: "medium",
338
- disable_response_storage: true,
339
- model_providers: {
340
- custom: {
341
- name: "custom",
342
- base_url: appConfig.codex.baseUrl,
343
- wire_api: "responses",
344
- ...(appConfig.codex.apiKey ? { env_key: "OPENAI_API_KEY" } : null),
345
- ...(llmProxyBinding ? { http_headers: { "X-Elevo-LLM-Binding": llmProxyBinding } } : null),
346
- },
347
- },
348
- "features.default_mode_request_user_input": true,
349
- }
350
- : null),
351
- };
352
- }
353
- }
354
- if (appConfig.codex.apiKey && !process.env.OPENAI_API_KEY) {
355
- process.env.OPENAI_API_KEY = appConfig.codex.apiKey;
356
- }
357
- function eventsFromInitialTurn(turn) {
358
- const events = [];
359
- for (const item of turn.items ?? []) {
360
- events.push({ type: "item.completed", item });
361
- }
362
- if (turn.status === "completed") {
363
- events.push({ type: "turn.completed" });
364
- }
365
- else if (turn.status === "failed") {
366
- events.push({ type: "turn.failed", error: { message: turn.error?.message ?? "Codex turn failed" } });
367
- }
368
- return events;
369
- }
370
- function notificationToEvent(method, params, expectedThreadId, expectedTurnId) {
371
- const record = params;
372
- if (!record)
373
- return undefined;
374
- const threadId = record.threadId;
375
- const turnId = record.turnId;
376
- if (typeof threadId === "string" && threadId !== expectedThreadId)
377
- return undefined;
378
- if (typeof turnId === "string" && turnId !== expectedTurnId)
379
- return undefined;
380
- if (method === "item/started" && isThreadItem(record.item)) {
381
- return { type: "item.started", item: record.item };
382
- }
383
- if (method === "item/completed" && isThreadItem(record.item)) {
384
- return { type: "item.completed", item: record.item };
385
- }
386
- if (method === "item/agentMessage/delta") {
387
- return deltaEvent(record, "agentMessage");
388
- }
389
- if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
390
- return deltaEvent(record, "reasoning");
391
- }
392
- if (method === "item/plan/delta") {
393
- return deltaEvent(record, "plan");
394
- }
395
- if (method === "item/tool/requestUserInput") {
396
- return { type: "item.tool.requestUserInput", params: record };
397
- }
398
- if (method === "turn/diff/updated" && typeof record.diff === "string") {
399
- return { type: "turn.diff.updated", diff: record.diff };
400
- }
401
- if (method === "thread/tokenUsage/updated") {
402
- return { type: "thread.tokenUsage.updated", usage: usageFromTokenUsageUpdate(record) };
403
- }
404
- if (method === "turn/completed") {
405
- const turn = record.turn;
406
- if (turn?.status === "failed") {
407
- return { type: "turn.failed", error: { message: turn.error?.message ?? "Codex turn failed" } };
408
- }
409
- return { type: "turn.completed" };
410
- }
411
- if (method === "error") {
412
- console.error("Received error notification from app-server:", record);
413
- return { type: "error", message: errorMessageFromRecord(record) };
414
- }
415
- return undefined;
416
- }
417
- function errorMessageFromRecord(record) {
418
- const nestedError = record.error;
419
- const message = nestedError && typeof nestedError === "object" && "message" in nestedError
420
- ? nestedError.message
421
- : undefined;
422
- const details = record.additionalDetails;
423
- const messageText = typeof message === "string" && message.trim() ? message : "Codex app-server error";
424
- if (typeof details === "string" && details.trim()) {
425
- return `${messageText}: ${details}`;
426
- }
427
- return messageText;
428
- }
429
- function deltaEvent(record, itemType) {
430
- if (typeof record.itemId !== "string" || typeof record.delta !== "string")
431
- return undefined;
432
- return { type: "item.delta", item_id: record.itemId, item_type: itemType, delta: record.delta };
433
- }
434
- function usageFromTokenUsageUpdate(record) {
435
- const tokenUsage = record.tokenUsage;
436
- if (!tokenUsage || typeof tokenUsage !== "object")
437
- return null;
438
- const usage = tokenUsage;
439
- const total = usage.total;
440
- const last = usage.last;
441
- if (!total || typeof total !== "object" || !last || typeof last !== "object")
442
- return null;
443
- const breakdown = (value) => {
444
- const fields = value;
445
- return {
446
- input_tokens: typeof fields.inputTokens === "number" ? fields.inputTokens : undefined,
447
- output_tokens: typeof fields.outputTokens === "number" ? fields.outputTokens : undefined,
448
- total_tokens: typeof fields.totalTokens === "number" ? fields.totalTokens : undefined,
449
- };
450
- };
451
- const modelContextWindow = usage.modelContextWindow;
452
- return {
453
- total: breakdown(total),
454
- last: breakdown(last),
455
- model_context_window: typeof modelContextWindow === "number" ? modelContextWindow : undefined,
456
- };
457
- }
458
- function isThreadItem(item) {
459
- return Boolean(item && typeof item === "object" && "type" in item && "id" in item);
460
- }
461
- export const sessionManager = new SessionManager();
1
+ import{mkdirSync as b,existsSync as _}from"node:fs";import{appServerClient as l}from"./app-server-client.js";import{dynamicToolSpecs as S}from"./dynamic-tools.js";import{loadAllSessions as U,persistSession as I}from"./db.js";import{appConfig as f}from"./config.js";import{logger as E}from"@messenger-agent/shared/logger";import{resolveAgentWorkdir as N}from"@messenger-agent/shared/agent-workspaces";import{codexPlatformInstructions as v}from"./platform-instructions.js";class P extends Error{path;constructor(e){super(`Workdir not found: ${e}`),this.path=e}}class k{id;model;workingDirectory;constructor(e,r,n){this.id=e,this.model=r,this.workingDirectory=n}async run(e,r){const n=await this.runStreamed(e,r),t=new Map,i=new Set;let a=null;for await(const u of n.events)if(u.type==="item.started")i.has(u.item.id)||t.set(u.item.id,u.item);else if(u.type==="item.completed")i.add(u.item.id),t.set(u.item.id,u.item);else if(u.type==="thread.tokenUsage.updated")a=u.usage;else if(u.type==="turn.failed")throw new Error(u.error.message);const d=Array.from(t.values()),o=d.filter(u=>u.type==="agentMessage").map(u=>u.text).join(`
2
+ `);return{items:d,finalResponse:o||void 0,usage:a}}async runStreamed(e,r){const n=await l.request("turn/start",this.turnStartParams(e,r)),t=n.turn.id;return{turnId:t,events:this.eventsForTurn(t,n.turn,r?.signal)}}async steerStreamed(e,r,n){const t={threadId:this.id,input:[{type:"text",text:e}],expectedTurnId:r},i=await l.request("turn/steer",t);return{turnId:i.turnId,events:this.eventsForTurn(i.turnId,void 0,n?.signal,void 0,!0)}}async continueWithUserInput(e,r,n){const t=l.pendingUserInputTurnId(this.id,e);return t?{turnId:t,events:this.eventsForTurn(t,void 0,n?.signal,async()=>{if(!await l.respondToUserInput(this.id,e,r))throw new Error(`No pending user input request found for item: ${e}`)})}:(await this.interruptStaleTurn(n?.sourceTurnId),this.runStreamed(C(e,r),n))}async submitPendingUserInput(e,r){const n=l.pendingUserInputTurnId(this.id,e);if(!(!n||!await l.respondToUserInput(this.id,e,r)))return n}async interrupt(e){await l.request("turn/interrupt",{threadId:this.id,turnId:e})}async interruptPendingUserInput(e,r){await this.interrupt(e),l.discardPendingUserInput(this.id,r)}async interruptStaleTurn(e){if(!e)return;(await l.request("thread/read",{threadId:this.id,includeTurns:!0})).thread.turns?.find(t=>t.id===e)?.status==="inProgress"&&await this.interrupt(e)}turnStartParams(e,r){return{threadId:this.id,input:[{type:"text",text:e,text_elements:[]}],cwd:this.workingDirectory,approvalPolicy:"never",model:this.model,...r?.collaborationMode?{collaborationMode:r.collaborationMode}:null}}async*eventsForTurn(e,r,n,t,i=!1){const a=[...r||i?[{type:"thread.started",thread_id:this.id},{type:"turn.started",turn_id:e}]:[],...r?F(r):[]];let d=r?.status==="completed"||r?.status==="failed"||r?.status==="interrupted",o,u=!1,p=()=>{};const c=({method:h,params:w})=>{if(u)return;const y=M(h,w,this.id,e);y&&(a.push(y),(y.type==="turn.completed"||y.type==="turn.failed")&&(d=!0),p())},g=h=>{o=h,d=!0,p()},m=()=>{u=!0,a.length=0,d=!0,p()};l.on("notification",c),l.on("exit",g),n?.addEventListener("abort",m,{once:!0}),n?.aborted&&m();try{for(await t?.();!d||a.length>0;){for(;a.length>0;)yield a.shift();if(d)break;await new Promise(h=>{const w=setTimeout(()=>{p=()=>{},h()},50);p=()=>{clearTimeout(w),p=()=>{},h()}})}o&&(yield{type:"error",message:o.message})}finally{l.off("notification",c),l.off("exit",g),n?.removeEventListener("abort",m)}}}function C(s,e){return[`The user answered a previous request_user_input prompt (${s}), but the original pending tool request is no longer active.`,"Continue the task using this answer:",JSON.stringify(e,null,2)].join(`
3
+ `)}class A{sessions=new Map;constructor(){for(const e of U())this.sessions.set(e.sessionId,{workdir:e.workdir,botName:e.botName??"main",llmProxyBinding:e.llmProxyBinding});E.info(`SessionManager: loaded ${this.sessions.size} persisted sessions`)}async getOrCreate(e,r,n,t,i,a,d){const o=this.resolveWorkdir(n,t);if(e){const p=this.sessions.get(e);if(p&&p.workdir===o){const c=p.llmProxyBinding??d,g=await l.request("thread/resume",this.threadResumeParams(e,i,o,a,c));if(!p.llmProxyBinding&&c){const m={...p,llmProxyBinding:c};this.sessions.set(e,m),I({sessionId:e,botName:m.botName,workdir:m.workdir,createdAt:Date.now(),llmProxyBinding:c})}return{thread:new k(g.thread.id,i,o),sessionId:e,isNewSession:!1}}}if(n&&!_(o))throw new P(o);b(o,{recursive:!0});const u=await l.request("thread/start",this.threadStartParams(i,o,a,d));return{thread:new k(u.thread.id,i,o),sessionId:void 0,isNewSession:!0}}async fork(e,r,n,t,i,a,d){const o=this.resolveWorkdir(n,t);if(n&&!_(o))throw new P(o);b(o,{recursive:!0});const p=(await l.request("thread/fork",this.threadForkParams(e.threadId,e.turnId,i,o,a,d))).thread.id;return this.persist(p,r,n,t,d),{thread:new k(p,i,o),sessionId:p,isNewSession:!1}}persist(e,r,n,t,i){const a=r??"main",d=this.resolveWorkdir(n,t);this.sessions.set(e,{botName:a,workdir:d,llmProxyBinding:i}),I({sessionId:e,botName:a,workdir:d,createdAt:Date.now(),llmProxyBinding:i})}bindingFor(e){return e?this.sessions.get(e)?.llmProxyBinding:void 0}workdirFor(e){return e?this.sessions.get(e)?.workdir:void 0}hasSession(e){return!!e&&this.sessions.has(e)}resolveWorkdir(e,r){return N(f,r,e)}threadStartParams(e,r,n,t){return{model:e,modelProvider:f.codex.baseUrl?"custom":null,cwd:r,approvalPolicy:"never",sandbox:n,config:this.codexConfig(t),developerInstructions:v,dynamicTools:S(),experimentalRawEvents:!1,persistExtendedHistory:!1}}threadResumeParams(e,r,n,t,i){return{threadId:e,model:r,modelProvider:f.codex.baseUrl?"custom":null,cwd:n,approvalPolicy:"never",sandbox:t,config:this.codexConfig(i),developerInstructions:v,excludeTurns:!0,persistExtendedHistory:!1}}threadForkParams(e,r,n,t,i,a){return{threadId:e,lastTurnId:r,model:n,modelProvider:f.codex.baseUrl?"custom":null,cwd:t,approvalPolicy:"never",sandbox:i,config:this.codexConfig(a),developerInstructions:v,ephemeral:!1,excludeTurns:!0}}codexConfig(e){return{approval_policy:"never",...f.codex.baseUrl?{model_provider:"custom",model_reasoning_effort:"medium",disable_response_storage:!0,model_providers:{custom:{name:"custom",base_url:f.codex.baseUrl,wire_api:"responses",...f.codex.apiKey?{env_key:"OPENAI_API_KEY"}:null,...e?{http_headers:{"X-Elevo-LLM-Binding":e}}:null}},"features.default_mode_request_user_input":!0}:null}}}f.codex.apiKey&&!process.env.OPENAI_API_KEY&&(process.env.OPENAI_API_KEY=f.codex.apiKey);function F(s){const e=[];for(const r of s.items??[])e.push({type:"item.completed",item:r});return s.status==="completed"?e.push({type:"turn.completed"}):s.status==="failed"&&e.push({type:"turn.failed",error:{message:s.error?.message??"Codex turn failed"}}),e}function M(s,e,r,n){const t=e;if(!t)return;const i=t.threadId,a=t.turnId;if(!(typeof i=="string"&&i!==r)&&!(typeof a=="string"&&a!==n)){if(s==="item/started"&&T(t.item))return{type:"item.started",item:t.item};if(s==="item/completed"&&T(t.item))return{type:"item.completed",item:t.item};if(s==="item/agentMessage/delta")return x(t,"agentMessage");if(s==="item/reasoning/summaryTextDelta"||s==="item/reasoning/textDelta")return x(t,"reasoning");if(s==="item/plan/delta")return x(t,"plan");if(s==="item/tool/requestUserInput")return{type:"item.tool.requestUserInput",params:t};if(s==="turn/diff/updated"&&typeof t.diff=="string")return{type:"turn.diff.updated",diff:t.diff};if(s==="thread/tokenUsage/updated")return{type:"thread.tokenUsage.updated",usage:R(t)};if(s==="turn/completed"){const d=t.turn;return d?.status==="failed"?{type:"turn.failed",error:{message:d.error?.message??"Codex turn failed"}}:{type:"turn.completed"}}if(s==="error")return console.error("Received error notification from app-server:",t),{type:"error",message:q(t)}}}function q(s){const e=s.error,r=e&&typeof e=="object"&&"message"in e?e.message:void 0,n=s.additionalDetails,t=typeof r=="string"&&r.trim()?r:"Codex app-server error";return typeof n=="string"&&n.trim()?`${t}: ${n}`:t}function x(s,e){if(!(typeof s.itemId!="string"||typeof s.delta!="string"))return{type:"item.delta",item_id:s.itemId,item_type:e,delta:s.delta}}function R(s){const e=s.tokenUsage;if(!e||typeof e!="object")return null;const r=e,n=r.total,t=r.last;if(!n||typeof n!="object"||!t||typeof t!="object")return null;const i=d=>{const o=d;return{input_tokens:typeof o.inputTokens=="number"?o.inputTokens:void 0,output_tokens:typeof o.outputTokens=="number"?o.outputTokens:void 0,total_tokens:typeof o.totalTokens=="number"?o.totalTokens:void 0}},a=r.modelContextWindow;return{total:i(n),last:i(t),model_context_window:typeof a=="number"?a:void 0}}function T(s){return!!(s&&typeof s=="object"&&"type"in s&&"id"in s)}const Y=new A;export{k as CodexThread,P as WorkdirNotFoundError,Y as sessionManager};
package/dist/config.js CHANGED
@@ -1,154 +1 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { parse } from "yaml";
5
- import { z } from "zod";
6
- import { normalizeOptional, parseAgentAuthConfig, parseAgentWorkspacesConfig, parseGitInitConfig, } from "@messenger-agent/shared/agent-config";
7
- import { logger, normalizeLogLevel, setLogFile, setLogLevel } from "@messenger-agent/shared/logger";
8
- const defaultTunnelServerUrl = "wss://m.elevo.vip/agent-bridge/tunnel";
9
- const defaultCodexBaseUrl = "https://m.elevo.vip/agent-bridge/llm/v1";
10
- const RawConfigSchema = z
11
- .object({
12
- log_level: z.string().optional(),
13
- port: z.coerce.number().int().positive().optional(),
14
- auth_tokens: z.record(z.string(), z.string()).optional(),
15
- workspaces: z
16
- .array(z.object({
17
- id: z.string().optional(),
18
- name: z.string().optional(),
19
- path: z.string().optional(),
20
- }))
21
- .optional(),
22
- file_uploads: z
23
- .object({
24
- temp_dir: z.string().optional(),
25
- })
26
- .optional(),
27
- data_dir: z.string().optional(),
28
- codex: z
29
- .object({
30
- port: z.coerce.number().int().positive().optional(),
31
- data_dir: z.string().optional(),
32
- file_uploads: z.object({ temp_dir: z.string().optional() }).optional(),
33
- api_key: z.string().optional(),
34
- base_url: z.string().optional(),
35
- })
36
- .optional(),
37
- openai: z
38
- .object({
39
- api_key: z.string().optional(),
40
- base_url: z.string().optional(),
41
- image_model: z.string().optional(),
42
- })
43
- .optional(),
44
- gitlab: z
45
- .object({
46
- ca_cert_path: z.string().optional(),
47
- host: z.string().optional(),
48
- })
49
- .optional(),
50
- git: z
51
- .object({
52
- co_authors: z.array(z.string()).optional(),
53
- })
54
- .optional(),
55
- tunnel: z
56
- .object({
57
- enabled: z.boolean().optional(),
58
- server_url: z.string().optional(),
59
- tunnel_id: z.string().optional(),
60
- token: z.string().optional(),
61
- reconnect_initial_ms: z.number().int().positive().optional(),
62
- reconnect_max_ms: z.number().int().positive().optional(),
63
- heartbeat_interval_ms: z.number().int().positive().optional(),
64
- })
65
- .optional(),
66
- })
67
- .loose();
68
- function loadConfig() {
69
- const configPath = process.env.AGENT_CONFIG_PATH ?? "./agent-config.yaml";
70
- if (!existsSync(configPath)) {
71
- const dataDir = "./data";
72
- const logFile = join(dataDir, "logs", "codex-agent.log");
73
- setLogLevel("info");
74
- setLogFile(logFile);
75
- logger.warn(`Config file not found at ${configPath}`);
76
- return {
77
- configPath,
78
- logLevel: "info",
79
- logFile,
80
- port: 3000,
81
- fileUploads: { tempDir: join(tmpdir(), "codex-agent-uploads") },
82
- dataDir,
83
- sessionsDbPath: join(dataDir, "sessions.db"),
84
- codex: {},
85
- openai: { imageModel: "gpt-image-2" },
86
- tunnel: {
87
- enabled: false,
88
- reconnectInitialMs: 1000,
89
- reconnectMaxMs: 30000,
90
- heartbeatIntervalMs: 30000,
91
- },
92
- ...parseAgentAuthConfig({}),
93
- ...parseAgentWorkspacesConfig({}),
94
- ...parseGitInitConfig({}),
95
- };
96
- }
97
- let parsedYaml;
98
- try {
99
- parsedYaml = parse(readFileSync(configPath, "utf-8"));
100
- }
101
- catch (err) {
102
- logger.error(`Failed to parse config file at ${configPath}:`, err);
103
- parsedYaml = {};
104
- }
105
- const raw = RawConfigSchema.safeParse(parsedYaml ?? {});
106
- if (!raw.success) {
107
- logger.error(`Invalid config format at ${configPath}:`, raw.error.issues);
108
- }
109
- const data = raw.success ? raw.data : {};
110
- const logLevel = normalizeLogLevel(data.log_level);
111
- const dataDir = normalizeOptional(data.codex?.data_dir) ?? normalizeOptional(data.data_dir) ?? "./data";
112
- const logFile = join(dataDir, "logs", "codex-agent.log");
113
- const tunnelEnabled = data.tunnel?.enabled ?? false;
114
- const tunnelToken = normalizeOptional(data.tunnel?.token);
115
- setLogLevel(logLevel);
116
- setLogFile(logFile);
117
- const config = {
118
- configPath,
119
- logLevel,
120
- logFile,
121
- port: data.codex?.port ?? data.port ?? 3000,
122
- fileUploads: {
123
- tempDir: normalizeOptional(data.codex?.file_uploads?.temp_dir) ??
124
- normalizeOptional(data.file_uploads?.temp_dir) ??
125
- join(tmpdir(), "codex-agent-uploads"),
126
- },
127
- dataDir,
128
- sessionsDbPath: join(dataDir, "sessions.db"),
129
- codex: {
130
- apiKey: normalizeOptional(data.codex?.api_key) ?? (tunnelEnabled ? tunnelToken : undefined),
131
- baseUrl: normalizeOptional(data.codex?.base_url) ?? (tunnelEnabled ? defaultCodexBaseUrl : undefined),
132
- },
133
- openai: {
134
- apiKey: normalizeOptional(data.openai?.api_key) ?? (tunnelEnabled ? tunnelToken : undefined),
135
- baseUrl: normalizeOptional(data.openai?.base_url) ?? (tunnelEnabled ? defaultCodexBaseUrl : undefined),
136
- imageModel: normalizeOptional(data.openai?.image_model) ?? "gpt-image-2",
137
- },
138
- tunnel: {
139
- enabled: tunnelEnabled,
140
- serverUrl: normalizeOptional(data.tunnel?.server_url) ?? (tunnelEnabled ? defaultTunnelServerUrl : undefined),
141
- tunnelId: normalizeOptional(data.tunnel?.tunnel_id),
142
- token: tunnelToken,
143
- reconnectInitialMs: data.tunnel?.reconnect_initial_ms ?? 1000,
144
- reconnectMaxMs: data.tunnel?.reconnect_max_ms ?? 30000,
145
- heartbeatIntervalMs: data.tunnel?.heartbeat_interval_ms ?? 30000,
146
- },
147
- ...parseAgentAuthConfig(data),
148
- ...parseAgentWorkspacesConfig(data),
149
- ...parseGitInitConfig(data),
150
- };
151
- logger.info(`Loaded agent config from ${configPath}`);
152
- return config;
153
- }
154
- export const appConfig = loadConfig();
1
+ import{existsSync as j,readFileSync as M}from"node:fs";import{tmpdir as f}from"node:os";import{join as a}from"node:path";import{parse as C}from"yaml";import{z as e}from"zod";import{normalizeOptional as n,parseAgentAuthConfig as _,parseAgentWorkspacesConfig as b,parseGitInitConfig as v}from"@messenger-agent/shared/agent-config";import{logger as s,normalizeLogLevel as I,setLogFile as x,setLogLevel as h}from"@messenger-agent/shared/logger";const L="wss://m.elevo.vip/agent-bridge/tunnel",y="https://m.elevo.vip/agent-bridge/llm/v1",U=e.object({log_level:e.string().optional(),port:e.coerce.number().int().positive().optional(),auth_tokens:e.record(e.string(),e.string()).optional(),workspaces:e.array(e.object({id:e.string().optional(),name:e.string().optional(),path:e.string().optional()})).optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),data_dir:e.string().optional(),codex:e.object({port:e.coerce.number().int().positive().optional(),data_dir:e.string().optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),api_key:e.string().optional(),base_url:e.string().optional()}).optional(),openai:e.object({api_key:e.string().optional(),base_url:e.string().optional(),image_model:e.string().optional()}).optional(),gitlab:e.object({ca_cert_path:e.string().optional(),host:e.string().optional()}).optional(),git:e.object({co_authors:e.array(e.string()).optional()}).optional(),tunnel:e.object({enabled:e.boolean().optional(),server_url:e.string().optional(),tunnel_id:e.string().optional(),token:e.string().optional(),reconnect_initial_ms:e.number().int().positive().optional(),reconnect_max_ms:e.number().int().positive().optional(),heartbeat_interval_ms:e.number().int().positive().optional()}).optional()}).loose();function D(){const t=process.env.AGENT_CONFIG_PATH??"./agent-config.yaml";if(!j(t)){const r="./data",u=a(r,"logs","codex-agent.log");return h("info"),x(u),s.warn(`Config file not found at ${t}`),{configPath:t,logLevel:"info",logFile:u,port:3e3,fileUploads:{tempDir:a(f(),"codex-agent-uploads")},dataDir:r,sessionsDbPath:a(r,"sessions.db"),codex:{},openai:{imageModel:"gpt-image-2"},tunnel:{enabled:!1,reconnectInitialMs:1e3,reconnectMaxMs:3e4,heartbeatIntervalMs:3e4},..._({}),...b({}),...v({})}}let p;try{p=C(M(t,"utf-8"))}catch(r){s.error(`Failed to parse config file at ${t}:`,r),p={}}const l=U.safeParse(p??{});l.success||s.error(`Invalid config format at ${t}:`,l.error.issues);const o=l.success?l.data:{},g=I(o.log_level),c=n(o.codex?.data_dir)??n(o.data_dir)??"./data",m=a(c,"logs","codex-agent.log"),i=o.tunnel?.enabled??!1,d=n(o.tunnel?.token);h(g),x(m);const k={configPath:t,logLevel:g,logFile:m,port:o.codex?.port??o.port??3e3,fileUploads:{tempDir:n(o.codex?.file_uploads?.temp_dir)??n(o.file_uploads?.temp_dir)??a(f(),"codex-agent-uploads")},dataDir:c,sessionsDbPath:a(c,"sessions.db"),codex:{apiKey:n(o.codex?.api_key)??(i?d:void 0),baseUrl:n(o.codex?.base_url)??(i?y:void 0)},openai:{apiKey:n(o.openai?.api_key)??(i?d:void 0),baseUrl:n(o.openai?.base_url)??(i?y:void 0),imageModel:n(o.openai?.image_model)??"gpt-image-2"},tunnel:{enabled:i,serverUrl:n(o.tunnel?.server_url)??(i?L:void 0),tunnelId:n(o.tunnel?.tunnel_id),token:d,reconnectInitialMs:o.tunnel?.reconnect_initial_ms??1e3,reconnectMaxMs:o.tunnel?.reconnect_max_ms??3e4,heartbeatIntervalMs:o.tunnel?.heartbeat_interval_ms??3e4},..._(o),...b(o),...v(o)};return s.info(`Loaded agent config from ${t}`),k}const z=D();export{z as appConfig};
package/dist/db.js CHANGED
@@ -1,11 +1,4 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- import { mkdirSync } from "node:fs";
3
- import { dirname } from "node:path";
4
- import { appConfig } from "./config.js";
5
- const DB_PATH = appConfig.sessionsDbPath;
6
- mkdirSync(dirname(DB_PATH), { recursive: true });
7
- const db = new DatabaseSync(DB_PATH);
8
- db.exec(`
1
+ import{DatabaseSync as o}from"node:sqlite";import{mkdirSync as i}from"node:fs";import{dirname as t}from"node:path";import{appConfig as r}from"./config.js";const n=r.sessionsDbPath;i(t(n),{recursive:!0});const e=new o(n);e.exec(`
9
2
  CREATE TABLE IF NOT EXISTS sessions (
10
3
  sessionId TEXT PRIMARY KEY,
11
4
  workdir TEXT NOT NULL,
@@ -13,20 +6,4 @@ db.exec(`
13
6
  botName TEXT NOT NULL DEFAULT 'main',
14
7
  llmProxyBinding TEXT
15
8
  )
16
- `);
17
- const sessionColumns = db.prepare("PRAGMA table_info(sessions)").all();
18
- if (!sessionColumns.some((column) => column.name === "llmProxyBinding")) {
19
- db.exec("ALTER TABLE sessions ADD COLUMN llmProxyBinding TEXT");
20
- }
21
- const stmtInsert = db.prepare(`INSERT OR REPLACE INTO sessions (sessionId, workdir, createdAt, botName, llmProxyBinding) VALUES (?, ?, ?, ?, ?)`);
22
- const stmtGetAll = db.prepare(`SELECT * FROM sessions`);
23
- const stmtGet = db.prepare(`SELECT * FROM sessions WHERE sessionId = ?`);
24
- export function persistSession(row) {
25
- stmtInsert.run(row.sessionId, row.workdir, row.createdAt, row.botName, row.llmProxyBinding ?? null);
26
- }
27
- export function loadAllSessions() {
28
- return stmtGetAll.all();
29
- }
30
- export function getSession(sessionId) {
31
- return stmtGet.get(sessionId);
32
- }
9
+ `);const T=e.prepare("PRAGMA table_info(sessions)").all();T.some(s=>s.name==="llmProxyBinding")||e.exec("ALTER TABLE sessions ADD COLUMN llmProxyBinding TEXT");const m=e.prepare("INSERT OR REPLACE INTO sessions (sessionId, workdir, createdAt, botName, llmProxyBinding) VALUES (?, ?, ?, ?, ?)"),E=e.prepare("SELECT * FROM sessions"),l=e.prepare("SELECT * FROM sessions WHERE sessionId = ?");function A(s){m.run(s.sessionId,s.workdir,s.createdAt,s.botName,s.llmProxyBinding??null)}function L(){return E.all()}function N(s){return l.get(s)}export{N as getSession,L as loadAllSessions,A as persistSession};