@messenger-agent/codex-agent 0.24.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-server-client.d.ts +32 -0
- package/dist/app-server-client.js +316 -0
- package/dist/app-server-protocol.d.ts +325 -0
- package/dist/app-server-protocol.js +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +15 -0
- package/dist/assets/codex-home/AGENTS.md +50 -0
- package/dist/codex.d.ts +63 -0
- package/dist/codex.js +461 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +154 -0
- package/dist/db.d.ts +10 -0
- package/dist/db.js +32 -0
- package/dist/dynamic-tools.d.ts +14 -0
- package/dist/dynamic-tools.js +172 -0
- package/dist/git-init.d.ts +1 -0
- package/dist/git-init.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -0
- package/dist/platform-instructions.d.ts +2 -0
- package/dist/platform-instructions.js +21 -0
- package/dist/prompts/plan-mode-system-reminder.txt +26 -0
- package/dist/prompts/question-tool-description.txt +12 -0
- package/dist/routes/chat.d.ts +27 -0
- package/dist/routes/chat.js +909 -0
- package/dist/routes/shared.d.ts +37 -0
- package/dist/routes/shared.js +218 -0
- package/dist/schemas.d.ts +112 -0
- package/dist/schemas.js +72 -0
- package/dist/tunnel-client.d.ts +15 -0
- package/dist/tunnel-client.js +232 -0
- package/package.json +36 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Merge Request and Pull Request Workflow
|
|
2
|
+
|
|
3
|
+
- When code changes are complete, commit the changes, push a `feat/*` or `fix/*` branch, and create the corresponding merge request or pull request by default.
|
|
4
|
+
- After creating a merge request or pull request, include the created MR/PR link in the response.
|
|
5
|
+
- For GitLab merge request titles and descriptions, use the user's original language.
|
|
6
|
+
- For GitHub pull request titles and descriptions, always use English.
|
|
7
|
+
|
|
8
|
+
## Response Style
|
|
9
|
+
|
|
10
|
+
- Reply in the same language the user used in their message. If the user writes in Chinese, answer in Chinese; if in English, answer in English.
|
|
11
|
+
- When referencing workspace files or directories in a response, you may use Markdown links whose targets are absolute paths. The Matrix client opens these links in the current room workspace.
|
|
12
|
+
|
|
13
|
+
## Hosting Tools
|
|
14
|
+
|
|
15
|
+
- Repositories hosted on `git.easyops.local` are internal GitLab projects. Use `glab` for merge requests and other GitLab operations.
|
|
16
|
+
- Repositories hosted on `github.com` are GitHub projects. Use `gh` for pull requests and other GitHub operations.
|
|
17
|
+
- When creating or updating merge requests or pull requests with multiline Markdown bodies, do not pass the body directly inside a shell double-quoted argument. Markdown backticks and `$...` can be interpreted by the shell and corrupt the description. Write the body to a temporary file with a single-quoted heredoc first. For `gh`, pass the file with `--body-file <file>`. For the currently installed `glab`, which does not support `--description-file`, pass the file contents through a variable or command substitution, for example `body=$(cat "$file"); glab mr create --description "$body"`; command substitution output is not re-parsed for Markdown backticks. When a temporary file is used for a merge request or pull request description, delete it after the create or update operation completes successfully.
|
|
18
|
+
|
|
19
|
+
## Git Conventions
|
|
20
|
+
|
|
21
|
+
- Use English Conventional Commits for commit messages.
|
|
22
|
+
- When making a git commit for a Matrix chat request, set the commit author to the last user who asked for the commit-triggering work. If there is no known mapping from that Matrix user ID to a git author name and email, ask the user for the author identity before committing.
|
|
23
|
+
- Unless the user explicitly asks for it, do not proactively run code formatting; commit hooks handle formatting automatically. If related files are formatted after a commit, that is expected behavior.
|
|
24
|
+
|
|
25
|
+
### Matrix Git Authors
|
|
26
|
+
|
|
27
|
+
By default, map git author from the Matrix user localpart, and always use `@easyops.cn` as the email domain suffix.
|
|
28
|
+
|
|
29
|
+
Example: `@tom:m.elevo.vip` -> `tom <tom@easyops.cn>`.
|
|
30
|
+
|
|
31
|
+
## Task Management
|
|
32
|
+
|
|
33
|
+
- Managed task operations require explicit task-management capabilities from the agent runtime.
|
|
34
|
+
- Unless the user explicitly requests another tracking method, use managed task tools by default to track and manage problems and requirements.
|
|
35
|
+
- Prefer existing task records over assumptions before updating or continuing a managed task.
|
|
36
|
+
- Do not invent missing requirements, plans, decisions, or completion status. Ask the user when task intent or state is ambiguous.
|
|
37
|
+
- When creating or updating managed tasks, write the task title and task documents in the user's original language.
|
|
38
|
+
- When the user asks an Agent assistant to plan or implement a managed task, set that task's `assignee` to the Agent assistant itself.
|
|
39
|
+
- When starting execution for a managed task, update the task's `workdir` to the workspace directory where the implementation is being performed.
|
|
40
|
+
- When continuing a managed task that already has `workdir`, work in that directory by default.
|
|
41
|
+
- Keep task titles concise; put detailed requirements, plans, notes, and results in task documents when the runtime supports them.
|
|
42
|
+
|
|
43
|
+
Supported task statuses:
|
|
44
|
+
|
|
45
|
+
- `backlog`
|
|
46
|
+
- `planned`
|
|
47
|
+
- `in_progress`
|
|
48
|
+
- `completed`
|
|
49
|
+
|
|
50
|
+
Only mark a task as `completed` after the related merge request or pull request has been merged. Completing code changes and opening an MR/PR is not enough to mark the task as completed.
|
package/dist/codex.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AppServerEvent, SandboxMode, ThreadItem, ToolRequestUserInputResponse, TurnStartParams, Usage } from "./app-server-protocol.js";
|
|
2
|
+
type SessionResult = {
|
|
3
|
+
thread: CodexThread;
|
|
4
|
+
sessionId: string | undefined;
|
|
5
|
+
isNewSession: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type StreamedTurn = {
|
|
8
|
+
turnId: string;
|
|
9
|
+
events: AsyncIterable<AppServerEvent>;
|
|
10
|
+
};
|
|
11
|
+
type TurnRunOptions = {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
collaborationMode?: TurnStartParams["collaborationMode"];
|
|
14
|
+
};
|
|
15
|
+
export declare class WorkdirNotFoundError extends Error {
|
|
16
|
+
path: string;
|
|
17
|
+
constructor(path: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class CodexThread {
|
|
20
|
+
id: string;
|
|
21
|
+
private model;
|
|
22
|
+
private workingDirectory;
|
|
23
|
+
constructor(id: string, model: string, workingDirectory: string);
|
|
24
|
+
run(prompt: string, options?: TurnRunOptions): Promise<{
|
|
25
|
+
items: ThreadItem[];
|
|
26
|
+
finalResponse?: string;
|
|
27
|
+
usage?: Usage | null;
|
|
28
|
+
}>;
|
|
29
|
+
runStreamed(_prompt: string, options?: TurnRunOptions): Promise<StreamedTurn>;
|
|
30
|
+
steerStreamed(prompt: string, expectedTurnId: string, options?: {
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}): Promise<StreamedTurn>;
|
|
33
|
+
continueWithUserInput(itemId: string, response: ToolRequestUserInputResponse, options?: {
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
sourceTurnId?: string;
|
|
36
|
+
}): Promise<StreamedTurn>;
|
|
37
|
+
submitPendingUserInput(itemId: string, response: ToolRequestUserInputResponse): Promise<string | undefined>;
|
|
38
|
+
interrupt(turnId: string): Promise<void>;
|
|
39
|
+
interruptPendingUserInput(turnId: string, itemId: string): Promise<void>;
|
|
40
|
+
private interruptStaleTurn;
|
|
41
|
+
private turnStartParams;
|
|
42
|
+
private eventsForTurn;
|
|
43
|
+
}
|
|
44
|
+
declare class SessionManager {
|
|
45
|
+
private sessions;
|
|
46
|
+
constructor();
|
|
47
|
+
getOrCreate(sessionId: string | undefined, botName: string | undefined | null, requestedWorkdir: string | undefined | null, requestedWorkspaceId: string | undefined | null, model: string, sandboxMode: SandboxMode, llmProxyBinding?: string): Promise<SessionResult>;
|
|
48
|
+
fork(source: {
|
|
49
|
+
threadId: string;
|
|
50
|
+
turnId: string;
|
|
51
|
+
}, botName: string | undefined | null, requestedWorkdir: string | undefined | null, requestedWorkspaceId: string | undefined | null, model: string, sandboxMode: SandboxMode, llmProxyBinding?: string): Promise<SessionResult>;
|
|
52
|
+
persist(sessionId: string, botName: string | undefined | null, workdir: string | undefined | null, workspaceId?: string | null, llmProxyBinding?: string): void;
|
|
53
|
+
bindingFor(sessionId: string | undefined): string | undefined;
|
|
54
|
+
workdirFor(sessionId: string | undefined): string | undefined;
|
|
55
|
+
hasSession(sessionId: string | undefined): boolean;
|
|
56
|
+
private resolveWorkdir;
|
|
57
|
+
private threadStartParams;
|
|
58
|
+
private threadResumeParams;
|
|
59
|
+
private threadForkParams;
|
|
60
|
+
private codexConfig;
|
|
61
|
+
}
|
|
62
|
+
export declare const sessionManager: SessionManager;
|
|
63
|
+
export {};
|
package/dist/codex.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
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();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AgentAuthConfig, type AgentWorkspacesConfig, type GitInitConfig } from "@messenger-agent/shared/agent-config";
|
|
2
|
+
import { type LogLevel } from "@messenger-agent/shared/logger";
|
|
3
|
+
export type AppConfig = {
|
|
4
|
+
configPath: string;
|
|
5
|
+
logLevel: LogLevel;
|
|
6
|
+
logFile?: string;
|
|
7
|
+
port: number;
|
|
8
|
+
fileUploads: {
|
|
9
|
+
tempDir: string;
|
|
10
|
+
};
|
|
11
|
+
dataDir: string;
|
|
12
|
+
sessionsDbPath: string;
|
|
13
|
+
codex: {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
};
|
|
17
|
+
openai: {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
imageModel: string;
|
|
21
|
+
};
|
|
22
|
+
tunnel: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
serverUrl?: string;
|
|
25
|
+
tunnelId?: string;
|
|
26
|
+
token?: string;
|
|
27
|
+
reconnectInitialMs: number;
|
|
28
|
+
reconnectMaxMs: number;
|
|
29
|
+
heartbeatIntervalMs: number;
|
|
30
|
+
};
|
|
31
|
+
} & GitInitConfig & AgentAuthConfig & AgentWorkspacesConfig;
|
|
32
|
+
export declare const appConfig: AppConfig;
|