agent-anywhere-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/bin/agent-anywhere-gateway.js +13 -0
- package/config/cloudflare.gateway.env.example +9 -0
- package/package.json +29 -0
- package/src/adapters/local-agent-adapter.js +131 -0
- package/src/gateway/client.js +422 -0
- package/src/gateway/main.js +224 -0
- package/src/gateway/providers.js +28 -0
- package/src/gateway/runner.js +337 -0
- package/src/gateway.js +7 -0
- package/src/lib/capabilities.js +1 -0
- package/src/lib/local-discovery.js +322 -0
- package/src/lib/path-policy.js +1 -0
- package/src/runtimes/claude-code-headless-runtime.js +547 -0
- package/src/runtimes/claude-code-runtime.js +984 -0
- package/src/runtimes/codex-app-server-client.js +157 -0
- package/src/runtimes/codex-app-server-runtime.js +790 -0
- package/src/runtimes/codex-runtime.js +418 -0
- package/src/runtimes/mock-runtime.js +140 -0
- package/src/shared/capabilities.js +175 -0
- package/src/shared/gateway-protocol.js +26 -0
- package/src/shared/http-utils.js +78 -0
- package/src/shared/image-attachments.js +269 -0
- package/src/shared/path-policy.js +110 -0
- package/src/shared/project-files.js +119 -0
- package/src/shared/providers.js +27 -0
- package/src/shared/runtime-environment.js +32 -0
- package/src/shared/websocket.js +258 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
const { EventEmitter } = require("node:events");
|
|
2
|
+
const { buildCapabilities, codexPolicyForMode } = require("../shared/capabilities");
|
|
3
|
+
const { CodexAppServerClient } = require("./codex-app-server-client");
|
|
4
|
+
|
|
5
|
+
class AsyncEventQueue {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.items = [];
|
|
8
|
+
this.waiters = [];
|
|
9
|
+
this.closed = false;
|
|
10
|
+
this.error = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
push(item) {
|
|
14
|
+
if (this.closed) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const waiter = this.waiters.shift();
|
|
18
|
+
if (waiter) {
|
|
19
|
+
waiter.resolve({ value: item, done: false });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.items.push(item);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fail(error) {
|
|
26
|
+
if (this.closed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.closed = true;
|
|
30
|
+
this.error = error;
|
|
31
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
32
|
+
waiter.reject(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
complete() {
|
|
37
|
+
if (this.closed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.closed = true;
|
|
41
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
42
|
+
waiter.resolve({ done: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
next() {
|
|
47
|
+
if (this.items.length) {
|
|
48
|
+
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
49
|
+
}
|
|
50
|
+
if (this.error) {
|
|
51
|
+
return Promise.reject(this.error);
|
|
52
|
+
}
|
|
53
|
+
if (this.closed) {
|
|
54
|
+
return Promise.resolve({ done: true });
|
|
55
|
+
}
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
this.waiters.push({ resolve, reject });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[Symbol.asyncIterator]() {
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function appServerApprovalPolicy(settings = {}) {
|
|
67
|
+
return settings.approval_policy || "on-request";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appServerApprovalsReviewer(settings = {}) {
|
|
71
|
+
return settings.mode === "auto-review" ? "auto_review" : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appServerSandboxMode(mode) {
|
|
75
|
+
const policy = codexPolicyForMode(mode);
|
|
76
|
+
if (policy.sandbox === "read-only") {
|
|
77
|
+
return "read-only";
|
|
78
|
+
}
|
|
79
|
+
if (policy.sandbox === "danger-full-access") {
|
|
80
|
+
return "danger-full-access";
|
|
81
|
+
}
|
|
82
|
+
return "workspace-write";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appServerSandboxPolicy({ projectPath, settings }) {
|
|
86
|
+
const policy = codexPolicyForMode(settings.mode);
|
|
87
|
+
if (policy.sandbox === "read-only") {
|
|
88
|
+
return {
|
|
89
|
+
type: "readOnly",
|
|
90
|
+
networkAccess: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (policy.sandbox === "danger-full-access") {
|
|
94
|
+
return { type: "dangerFullAccess" };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
type: "workspaceWrite",
|
|
98
|
+
writableRoots: [projectPath],
|
|
99
|
+
networkAccess: Boolean(policy.networkAccess),
|
|
100
|
+
excludeTmpdirEnvVar: false,
|
|
101
|
+
excludeSlashTmp: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function appServerTextInput(text) {
|
|
106
|
+
return {
|
|
107
|
+
type: "text",
|
|
108
|
+
text: String(text || ""),
|
|
109
|
+
text_elements: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function appServerLocalImageInput(image = {}) {
|
|
114
|
+
return {
|
|
115
|
+
type: "localImage",
|
|
116
|
+
path: String(image.path || "")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function appServerInputItems(message, attachments = []) {
|
|
121
|
+
const items = [appServerTextInput(message)];
|
|
122
|
+
for (const image of attachments || []) {
|
|
123
|
+
if (image?.path) {
|
|
124
|
+
items.push(appServerLocalImageInput(image));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return items;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildThreadStartParams({ projectPath, settings }) {
|
|
131
|
+
const approvalsReviewer = appServerApprovalsReviewer(settings);
|
|
132
|
+
return {
|
|
133
|
+
model: settings.model,
|
|
134
|
+
cwd: projectPath,
|
|
135
|
+
approvalPolicy: appServerApprovalPolicy(settings),
|
|
136
|
+
...(approvalsReviewer ? { approvalsReviewer } : {}),
|
|
137
|
+
sandbox: appServerSandboxMode(settings.mode),
|
|
138
|
+
serviceName: "agent_anywhere"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildThreadResumeParams({ runtimeSessionId, projectPath, settings }) {
|
|
143
|
+
const approvalsReviewer = appServerApprovalsReviewer(settings);
|
|
144
|
+
return {
|
|
145
|
+
threadId: runtimeSessionId,
|
|
146
|
+
model: settings.model,
|
|
147
|
+
cwd: projectPath,
|
|
148
|
+
approvalPolicy: appServerApprovalPolicy(settings),
|
|
149
|
+
...(approvalsReviewer ? { approvalsReviewer } : {}),
|
|
150
|
+
sandbox: appServerSandboxMode(settings.mode),
|
|
151
|
+
serviceName: "agent_anywhere"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildTurnStartParams({ threadId, projectPath, message, attachments = [], settings }) {
|
|
156
|
+
const approvalsReviewer = appServerApprovalsReviewer(settings);
|
|
157
|
+
return {
|
|
158
|
+
threadId,
|
|
159
|
+
input: appServerInputItems(message, attachments),
|
|
160
|
+
cwd: projectPath,
|
|
161
|
+
approvalPolicy: appServerApprovalPolicy(settings),
|
|
162
|
+
...(approvalsReviewer ? { approvalsReviewer } : {}),
|
|
163
|
+
sandboxPolicy: appServerSandboxPolicy({ projectPath, settings }),
|
|
164
|
+
model: settings.model,
|
|
165
|
+
effort: settings.reasoning_effort
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderValue(value) {
|
|
170
|
+
if (value === undefined || value === null) {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "string") {
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
return value.map((item) => renderValue(item)).filter(Boolean).join("\n");
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === "object") {
|
|
180
|
+
if (typeof value.text === "string") {
|
|
181
|
+
return value.text;
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(value.content)) {
|
|
184
|
+
return renderValue(value.content);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return JSON.stringify(value, null, 2);
|
|
188
|
+
} catch {
|
|
189
|
+
return String(value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractThreadId(result) {
|
|
196
|
+
return result?.thread?.id || result?.threadId || result?.id || null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractTurnId(result) {
|
|
200
|
+
return result?.turn?.id || result?.turnId || result?.id || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function extractTurnStatus(params = {}) {
|
|
204
|
+
return params.turn?.status || params.status || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function settleThreadHistory(client, threadId) {
|
|
208
|
+
if (!threadId) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await client.request("thread/read", {
|
|
213
|
+
threadId,
|
|
214
|
+
includeTurns: true
|
|
215
|
+
});
|
|
216
|
+
} catch {
|
|
217
|
+
// Best-effort: the live turn already completed, so history settling must not
|
|
218
|
+
// turn a successful user-visible run into a failure.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractTurnError(params = {}) {
|
|
223
|
+
return params.turn?.error?.message || params.error?.message || params.error || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function convertThreadItem(item = {}, lifecycle) {
|
|
227
|
+
const itemId = item.id || null;
|
|
228
|
+
const itemType = item.type;
|
|
229
|
+
const status = item.status || (lifecycle === "completed" ? "completed" : "inProgress");
|
|
230
|
+
|
|
231
|
+
if (itemType === "agentMessage") {
|
|
232
|
+
if (lifecycle !== "completed") {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const text = String(item.text || "");
|
|
236
|
+
return text ? [{ type: "delta", payload: { text } }] : [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (itemType === "reasoning") {
|
|
240
|
+
const text = renderValue(item.summary || item.content);
|
|
241
|
+
return text ? [{ type: "activity", payload: { message: text, kind: "agent" } }] : [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (itemType === "commandExecution") {
|
|
245
|
+
if (lifecycle === "started") {
|
|
246
|
+
return [{
|
|
247
|
+
type: "tool_use",
|
|
248
|
+
payload: {
|
|
249
|
+
tool_name: "exec_command",
|
|
250
|
+
tool_input: { cmd: item.command || "", cwd: item.cwd || null },
|
|
251
|
+
tool_use_id: itemId
|
|
252
|
+
}
|
|
253
|
+
}];
|
|
254
|
+
}
|
|
255
|
+
if (lifecycle === "completed") {
|
|
256
|
+
return [{
|
|
257
|
+
type: "tool_result",
|
|
258
|
+
payload: {
|
|
259
|
+
tool_use_id: itemId,
|
|
260
|
+
content: String(item.aggregatedOutput || ""),
|
|
261
|
+
is_error: status === "failed" || (Number.isInteger(item.exitCode) && item.exitCode !== 0)
|
|
262
|
+
}
|
|
263
|
+
}];
|
|
264
|
+
}
|
|
265
|
+
return [{ type: "activity", payload: { message: "命令执行中", kind: "tool_progress" } }];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (itemType === "mcpToolCall" || itemType === "dynamicToolCall") {
|
|
269
|
+
const toolName = item.tool || itemType;
|
|
270
|
+
if (lifecycle === "started") {
|
|
271
|
+
return [{
|
|
272
|
+
type: "tool_use",
|
|
273
|
+
payload: {
|
|
274
|
+
tool_name: toolName,
|
|
275
|
+
tool_input: item.arguments || {},
|
|
276
|
+
tool_use_id: itemId
|
|
277
|
+
}
|
|
278
|
+
}];
|
|
279
|
+
}
|
|
280
|
+
if (lifecycle === "completed") {
|
|
281
|
+
return [{
|
|
282
|
+
type: "tool_result",
|
|
283
|
+
payload: {
|
|
284
|
+
tool_use_id: itemId,
|
|
285
|
+
content: item.error?.message || renderValue(item.result?.content || item.contentItems || item.result),
|
|
286
|
+
is_error: status === "failed" || Boolean(item.error) || item.success === false
|
|
287
|
+
}
|
|
288
|
+
}];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (itemType === "webSearch") {
|
|
293
|
+
if (lifecycle === "started") {
|
|
294
|
+
return [{
|
|
295
|
+
type: "tool_use",
|
|
296
|
+
payload: {
|
|
297
|
+
tool_name: "web_search",
|
|
298
|
+
tool_input: { query: item.query || "" },
|
|
299
|
+
tool_use_id: itemId
|
|
300
|
+
}
|
|
301
|
+
}];
|
|
302
|
+
}
|
|
303
|
+
if (lifecycle === "completed") {
|
|
304
|
+
return [{
|
|
305
|
+
type: "tool_result",
|
|
306
|
+
payload: {
|
|
307
|
+
tool_use_id: itemId,
|
|
308
|
+
content: `搜索完成:${item.query || ""}`,
|
|
309
|
+
is_error: false
|
|
310
|
+
}
|
|
311
|
+
}];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (itemType === "fileChange") {
|
|
316
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
317
|
+
const summary = changes
|
|
318
|
+
.map((change) => `${change.type || "update"} ${change.path || change.move_path || ""}`.trim())
|
|
319
|
+
.filter(Boolean)
|
|
320
|
+
.join(", ");
|
|
321
|
+
return [{
|
|
322
|
+
type: "activity",
|
|
323
|
+
payload: {
|
|
324
|
+
message: status === "failed"
|
|
325
|
+
? `文件修改失败${summary ? `:${summary}` : ""}`
|
|
326
|
+
: `文件修改${lifecycle === "started" ? "开始" : "完成"}${summary ? `:${summary}` : ""}`,
|
|
327
|
+
kind: "tool_progress"
|
|
328
|
+
}
|
|
329
|
+
}];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function convertAppServerNotification(message, state = {}) {
|
|
336
|
+
const method = message?.method;
|
|
337
|
+
const params = message?.params || {};
|
|
338
|
+
if (method === "thread/started") {
|
|
339
|
+
const threadId = params.thread?.id || params.threadId || null;
|
|
340
|
+
return [{
|
|
341
|
+
type: "runtime_session",
|
|
342
|
+
payload: {
|
|
343
|
+
runtime_session_id: threadId,
|
|
344
|
+
working_directory: params.thread?.cwd || params.cwd || ""
|
|
345
|
+
}
|
|
346
|
+
}];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (method === "turn/started") {
|
|
350
|
+
return [{ type: "activity", payload: { message: "Codex app-server 开始处理任务", kind: "status" } }];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (method === "item/agentMessage/delta") {
|
|
354
|
+
if (params.itemId) {
|
|
355
|
+
state.deltaItemIds?.add(params.itemId);
|
|
356
|
+
}
|
|
357
|
+
return [{ type: "delta", payload: { text: params.delta || "" } }];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (method === "item/started") {
|
|
361
|
+
return convertThreadItem(params.item, "started");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (method === "item/completed") {
|
|
365
|
+
if (params.item?.type === "agentMessage" && state.deltaItemIds?.has(params.item.id)) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
return convertThreadItem(params.item, "completed");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (method === "item/commandExecution/outputDelta" || method === "item/fileChange/outputDelta") {
|
|
372
|
+
return params.delta
|
|
373
|
+
? [{
|
|
374
|
+
type: "activity",
|
|
375
|
+
payload: {
|
|
376
|
+
message: params.delta,
|
|
377
|
+
kind: "tool_progress",
|
|
378
|
+
tool_use_id: params.itemId || params.item?.id || null
|
|
379
|
+
}
|
|
380
|
+
}]
|
|
381
|
+
: [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (method === "turn/completed") {
|
|
385
|
+
const events = [];
|
|
386
|
+
if (params.usage || params.turn?.usage) {
|
|
387
|
+
events.push({ type: "usage", payload: { usage: params.usage || params.turn.usage } });
|
|
388
|
+
}
|
|
389
|
+
const status = extractTurnStatus(params);
|
|
390
|
+
events.push({
|
|
391
|
+
type: status === "interrupted" ? "cancelled" : "complete",
|
|
392
|
+
payload: {
|
|
393
|
+
message: status === "interrupted" ? "Codex 执行已取消。" : "Codex 执行完成。",
|
|
394
|
+
status: status || "completed"
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return events;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (method === "turn/failed" || method === "error") {
|
|
401
|
+
return [{ type: "error", payload: { message: extractTurnError(params) || params.message || "Codex app-server 执行失败" } }];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (method === "serverRequest/resolved") {
|
|
405
|
+
return [{ type: "activity", payload: { message: "权限请求已处理", kind: "status" } }];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (method === "thread/status/changed") {
|
|
409
|
+
const status = params.status || params.thread?.status || "unknown";
|
|
410
|
+
return [{ type: "activity", payload: { message: `Codex thread 状态:${status}`, kind: "status" } }];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function convertApprovalRequest(message) {
|
|
417
|
+
const params = message?.params || {};
|
|
418
|
+
if (message?.method === "item/commandExecution/requestApproval") {
|
|
419
|
+
return {
|
|
420
|
+
type: "approval_request",
|
|
421
|
+
payload: {
|
|
422
|
+
runtime_request_id: message.id,
|
|
423
|
+
kind: params.networkApprovalContext ? "network_access" : "command_execution",
|
|
424
|
+
thread_id: params.threadId,
|
|
425
|
+
turn_id: params.turnId,
|
|
426
|
+
item_id: params.itemId,
|
|
427
|
+
approval_id: params.approvalId || null,
|
|
428
|
+
reason: params.reason || "",
|
|
429
|
+
command: params.command || "",
|
|
430
|
+
cwd: params.cwd || "",
|
|
431
|
+
available_decisions: ["approved", "rejected"],
|
|
432
|
+
runtime_available_decisions: ["accept", "acceptForSession", "decline", "cancel"],
|
|
433
|
+
raw: params
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (message?.method === "item/fileChange/requestApproval") {
|
|
438
|
+
return {
|
|
439
|
+
type: "approval_request",
|
|
440
|
+
payload: {
|
|
441
|
+
runtime_request_id: message.id,
|
|
442
|
+
kind: "file_change",
|
|
443
|
+
thread_id: params.threadId,
|
|
444
|
+
turn_id: params.turnId,
|
|
445
|
+
item_id: params.itemId,
|
|
446
|
+
reason: params.reason || "",
|
|
447
|
+
command: params.grantRoot ? `允许写入 ${params.grantRoot}` : "批准文件修改",
|
|
448
|
+
cwd: "",
|
|
449
|
+
available_decisions: ["approved", "rejected"],
|
|
450
|
+
runtime_available_decisions: ["accept", "acceptForSession", "decline", "cancel"],
|
|
451
|
+
raw: params
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (message?.method === "item/tool/requestUserInput") {
|
|
456
|
+
const questions = Array.isArray(params.questions) ? params.questions : [];
|
|
457
|
+
const command = questions
|
|
458
|
+
.map((question) => question.question || question.header || question.id)
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join("\n");
|
|
461
|
+
return {
|
|
462
|
+
type: "approval_request",
|
|
463
|
+
payload: {
|
|
464
|
+
runtime_request_id: message.id,
|
|
465
|
+
kind: "tool_user_input",
|
|
466
|
+
thread_id: params.threadId,
|
|
467
|
+
turn_id: params.turnId,
|
|
468
|
+
item_id: params.itemId,
|
|
469
|
+
reason: "工具需要用户确认或输入",
|
|
470
|
+
command: command || "工具需要用户确认或输入",
|
|
471
|
+
cwd: "",
|
|
472
|
+
available_decisions: ["approved", "rejected"],
|
|
473
|
+
runtime_available_decisions: questions.map((question) => ({
|
|
474
|
+
id: question.id,
|
|
475
|
+
options: Array.isArray(question.options) ? question.options.map((option) => option.label) : []
|
|
476
|
+
})),
|
|
477
|
+
questions,
|
|
478
|
+
raw: params
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function approvalDecisionForAppServer(decision) {
|
|
486
|
+
if (decision === "approved" || decision === "approve" || decision === "accept") {
|
|
487
|
+
return "accept";
|
|
488
|
+
}
|
|
489
|
+
if (decision === "cancel") {
|
|
490
|
+
return "cancel";
|
|
491
|
+
}
|
|
492
|
+
return "decline";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function selectToolUserInputAnswer(question, decision) {
|
|
496
|
+
const labels = (Array.isArray(question?.options) ? question.options : [])
|
|
497
|
+
.map((option) => String(option.label || "").trim())
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
const lowerDecision = String(decision || "").toLowerCase();
|
|
500
|
+
const preferred = lowerDecision === "approved" || lowerDecision === "approve" || lowerDecision === "accept"
|
|
501
|
+
? labels.find((label) => /^(accept|approve|allow|yes|ok)$/i.test(label)) || labels[0]
|
|
502
|
+
: lowerDecision === "cancel"
|
|
503
|
+
? labels.find((label) => /cancel/i.test(label))
|
|
504
|
+
: labels.find((label) => /^(decline|reject|deny|no)$/i.test(label)) || labels.find((label) => /decline|reject|deny/i.test(label));
|
|
505
|
+
return preferred || (lowerDecision === "cancel" ? "Cancel" : lowerDecision === "approved" ? "Accept" : "Decline");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function approvalResponseForAppServer(request, approvalResult) {
|
|
509
|
+
const decision = approvalResult?.decision || approvalResult?.status;
|
|
510
|
+
if (request?.method === "item/tool/requestUserInput") {
|
|
511
|
+
const answers = {};
|
|
512
|
+
for (const question of request.params?.questions || []) {
|
|
513
|
+
if (!question?.id) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
answers[question.id] = {
|
|
517
|
+
answers: [selectToolUserInputAnswer(question, decision)]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return { answers };
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
decision: approvalDecisionForAppServer(decision)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
class CodexAppServerRuntime extends EventEmitter {
|
|
528
|
+
static activeTurns = new Map();
|
|
529
|
+
|
|
530
|
+
constructor({ codexPathOverride, clientFactory, provider = "codex-app-server" } = {}) {
|
|
531
|
+
super();
|
|
532
|
+
this.provider = provider;
|
|
533
|
+
this.codexPathOverride = codexPathOverride || process.env.CODEX_CLI_PATH || undefined;
|
|
534
|
+
this.clientFactory = clientFactory || (() => new CodexAppServerClient({
|
|
535
|
+
codexPathOverride: this.codexPathOverride
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
createClient() {
|
|
540
|
+
return this.clientFactory();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async *run({ session, project, message, attachments = [], settings, requestApproval } = {}) {
|
|
544
|
+
const client = this.createClient();
|
|
545
|
+
const queue = new AsyncEventQueue();
|
|
546
|
+
const state = {
|
|
547
|
+
deltaItemIds: new Set(),
|
|
548
|
+
runtimeSessionId: null
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
client.on("notification", (notification) => {
|
|
552
|
+
if (
|
|
553
|
+
notification.method === "thread/started" &&
|
|
554
|
+
state.runtimeSessionId &&
|
|
555
|
+
extractThreadId(notification.params) === state.runtimeSessionId
|
|
556
|
+
) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
for (const event of convertAppServerNotification(notification, state)) {
|
|
560
|
+
queue.push(event);
|
|
561
|
+
}
|
|
562
|
+
if (notification.method === "turn/completed") {
|
|
563
|
+
const status = extractTurnStatus(notification.params);
|
|
564
|
+
if (status === "failed") {
|
|
565
|
+
queue.fail(new Error(extractTurnError(notification.params) || "Codex app-server 执行失败"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
queue.complete();
|
|
569
|
+
}
|
|
570
|
+
if (notification.method === "turn/failed" || notification.method === "error") {
|
|
571
|
+
queue.fail(new Error(extractTurnError(notification.params) || notification.params?.message || "Codex app-server 执行失败"));
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
client.on("request", async (request) => {
|
|
576
|
+
const approval = convertApprovalRequest(request);
|
|
577
|
+
if (!approval) {
|
|
578
|
+
client.respondError(request.id, new Error(`暂不支持 app-server request:${request.method}`));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
let approvalResult;
|
|
583
|
+
if (requestApproval) {
|
|
584
|
+
approvalResult = await requestApproval(approval.payload);
|
|
585
|
+
} else {
|
|
586
|
+
queue.push(approval);
|
|
587
|
+
approvalResult = { decision: "rejected" };
|
|
588
|
+
}
|
|
589
|
+
client.respond(request.id, approvalResponseForAppServer(request, approvalResult));
|
|
590
|
+
} catch (error) {
|
|
591
|
+
client.respondError(request.id, error);
|
|
592
|
+
queue.fail(error);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
client.on("error", (error) => queue.fail(error));
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
await client.initialize();
|
|
600
|
+
const threadResult = session?.runtime_session_id
|
|
601
|
+
? await client.request("thread/resume", buildThreadResumeParams({
|
|
602
|
+
runtimeSessionId: session.runtime_session_id,
|
|
603
|
+
projectPath: project.path,
|
|
604
|
+
settings
|
|
605
|
+
}))
|
|
606
|
+
: await client.request("thread/start", buildThreadStartParams({
|
|
607
|
+
projectPath: project.path,
|
|
608
|
+
settings
|
|
609
|
+
}));
|
|
610
|
+
const threadId = extractThreadId(threadResult);
|
|
611
|
+
if (!threadId) {
|
|
612
|
+
throw new Error("codex app-server 未返回 thread id");
|
|
613
|
+
}
|
|
614
|
+
state.runtimeSessionId = threadId;
|
|
615
|
+
yield {
|
|
616
|
+
type: "runtime_session",
|
|
617
|
+
payload: {
|
|
618
|
+
runtime_session_id: threadId,
|
|
619
|
+
working_directory: project.path
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const turnResult = await client.request("turn/start", buildTurnStartParams({
|
|
624
|
+
threadId,
|
|
625
|
+
projectPath: project.path,
|
|
626
|
+
message,
|
|
627
|
+
attachments,
|
|
628
|
+
settings
|
|
629
|
+
}));
|
|
630
|
+
const turnId = extractTurnId(turnResult);
|
|
631
|
+
if (!turnId) {
|
|
632
|
+
throw new Error("codex app-server 未返回 turn id");
|
|
633
|
+
}
|
|
634
|
+
if (session?.id) {
|
|
635
|
+
CodexAppServerRuntime.activeTurns.set(session.id, { client, threadId, turnId });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
for await (const event of queue) {
|
|
639
|
+
yield event;
|
|
640
|
+
}
|
|
641
|
+
await settleThreadHistory(client, threadId);
|
|
642
|
+
} finally {
|
|
643
|
+
if (session?.id) {
|
|
644
|
+
CodexAppServerRuntime.activeTurns.delete(session.id);
|
|
645
|
+
}
|
|
646
|
+
client.close();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async discoverCapabilities() {
|
|
651
|
+
const client = this.createClient();
|
|
652
|
+
try {
|
|
653
|
+
await client.initialize();
|
|
654
|
+
const result = await client.request("model/list", { limit: 100, includeHidden: false });
|
|
655
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
656
|
+
const models = [];
|
|
657
|
+
const reasoningEfforts = [];
|
|
658
|
+
const inputModalities = [];
|
|
659
|
+
let defaultModel = null;
|
|
660
|
+
for (const row of rows.filter((item) => item?.hidden !== true)) {
|
|
661
|
+
const model = String(row.model || row.id || "").trim();
|
|
662
|
+
if (model && !models.includes(model)) {
|
|
663
|
+
models.push(model);
|
|
664
|
+
}
|
|
665
|
+
if (model && row.isDefault === true) {
|
|
666
|
+
defaultModel = model;
|
|
667
|
+
}
|
|
668
|
+
for (const modality of row.inputModalities || []) {
|
|
669
|
+
const value = String(modality || "").trim();
|
|
670
|
+
if (value && !inputModalities.includes(value)) {
|
|
671
|
+
inputModalities.push(value);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const effortRow of row.supportedReasoningEfforts || []) {
|
|
675
|
+
const effort = String(effortRow.reasoningEffort || "").trim();
|
|
676
|
+
if (effort && !reasoningEfforts.includes(effort)) {
|
|
677
|
+
reasoningEfforts.push(effort);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return buildCapabilities(this.provider, {
|
|
682
|
+
models,
|
|
683
|
+
default_model: defaultModel,
|
|
684
|
+
input_modalities: inputModalities,
|
|
685
|
+
reasoning_efforts: reasoningEfforts
|
|
686
|
+
});
|
|
687
|
+
} finally {
|
|
688
|
+
client.close();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async cancelTurn({ session, threadId, turnId }) {
|
|
693
|
+
const active = session?.id ? CodexAppServerRuntime.activeTurns.get(session.id) : null;
|
|
694
|
+
if (active) {
|
|
695
|
+
await active.client.request("turn/interrupt", {
|
|
696
|
+
threadId: active.threadId,
|
|
697
|
+
turnId: active.turnId
|
|
698
|
+
});
|
|
699
|
+
return {};
|
|
700
|
+
}
|
|
701
|
+
if (threadId && turnId) {
|
|
702
|
+
const client = this.createClient();
|
|
703
|
+
try {
|
|
704
|
+
await client.initialize();
|
|
705
|
+
await client.request("turn/interrupt", { threadId, turnId });
|
|
706
|
+
return {};
|
|
707
|
+
} finally {
|
|
708
|
+
client.close();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const error = new Error("没有可取消的 Codex app-server turn。");
|
|
712
|
+
error.statusCode = 409;
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async steerTurn({ session, threadId, turnId, message, attachments = [] }) {
|
|
717
|
+
const active = session?.id ? CodexAppServerRuntime.activeTurns.get(session.id) : null;
|
|
718
|
+
if (active) {
|
|
719
|
+
return active.client.request("turn/steer", {
|
|
720
|
+
threadId: active.threadId,
|
|
721
|
+
expectedTurnId: active.turnId,
|
|
722
|
+
input: appServerInputItems(message, attachments)
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (threadId && turnId) {
|
|
726
|
+
const client = this.createClient();
|
|
727
|
+
try {
|
|
728
|
+
await client.initialize();
|
|
729
|
+
return await client.request("turn/steer", {
|
|
730
|
+
threadId,
|
|
731
|
+
expectedTurnId: turnId,
|
|
732
|
+
input: appServerInputItems(message, attachments)
|
|
733
|
+
});
|
|
734
|
+
} finally {
|
|
735
|
+
client.close();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const error = new Error("没有可追加输入的 Codex app-server turn。");
|
|
739
|
+
error.statusCode = 409;
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async listRuntimeSessions({ project, limit = 50 } = {}) {
|
|
744
|
+
const client = this.createClient();
|
|
745
|
+
try {
|
|
746
|
+
await client.initialize();
|
|
747
|
+
const result = await client.request("thread/list", {
|
|
748
|
+
limit,
|
|
749
|
+
cwd: project?.path || null
|
|
750
|
+
});
|
|
751
|
+
return Array.isArray(result?.data) ? result.data : [];
|
|
752
|
+
} finally {
|
|
753
|
+
client.close();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async readRuntimeSession({ runtimeSessionId, includeTurns = true } = {}) {
|
|
758
|
+
if (!runtimeSessionId) {
|
|
759
|
+
const error = new Error("runtimeSessionId 不能为空。");
|
|
760
|
+
error.statusCode = 400;
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
const client = this.createClient();
|
|
764
|
+
try {
|
|
765
|
+
await client.initialize();
|
|
766
|
+
return await client.request("thread/read", {
|
|
767
|
+
threadId: runtimeSessionId,
|
|
768
|
+
includeTurns
|
|
769
|
+
});
|
|
770
|
+
} finally {
|
|
771
|
+
client.close();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
module.exports = {
|
|
777
|
+
AsyncEventQueue,
|
|
778
|
+
CodexAppServerRuntime,
|
|
779
|
+
appServerApprovalPolicy,
|
|
780
|
+
appServerApprovalsReviewer,
|
|
781
|
+
appServerInputItems,
|
|
782
|
+
appServerSandboxPolicy,
|
|
783
|
+
buildThreadResumeParams,
|
|
784
|
+
buildThreadStartParams,
|
|
785
|
+
buildTurnStartParams,
|
|
786
|
+
convertAppServerNotification,
|
|
787
|
+
convertApprovalRequest,
|
|
788
|
+
approvalDecisionForAppServer,
|
|
789
|
+
approvalResponseForAppServer
|
|
790
|
+
};
|