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.
@@ -0,0 +1,418 @@
1
+ const { execFile } = require("node:child_process");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const { buildCapabilities, codexPolicyForMode } = require("../shared/capabilities");
5
+
6
+ const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
7
+ const modelCatalogCache = new Map();
8
+
9
+ async function loadCodexSdk() {
10
+ try {
11
+ return await import("@openai/codex-sdk");
12
+ } catch (error) {
13
+ const wrapped = new Error(`无法加载 @openai/codex-sdk:${error.message}`);
14
+ wrapped.statusCode = 500;
15
+ wrapped.cause = error;
16
+ throw wrapped;
17
+ }
18
+ }
19
+
20
+ function resolveCodexExecutable(codexPathOverride = process.env.CODEX_CLI_PATH) {
21
+ if (codexPathOverride) {
22
+ const resolved = path.resolve(codexPathOverride);
23
+ if (fs.existsSync(resolved)) {
24
+ return resolved;
25
+ }
26
+ const error = new Error(`CODEX_CLI_PATH 不可用:${codexPathOverride}`);
27
+ error.statusCode = 500;
28
+ throw error;
29
+ }
30
+
31
+ const localBinary = path.join(__dirname, "..", "..", "node_modules", ".bin", process.platform === "win32" ? "codex.cmd" : "codex");
32
+ if (fs.existsSync(localBinary)) {
33
+ return localBinary;
34
+ }
35
+ return "codex";
36
+ }
37
+
38
+ function buildCodexThreadOptions({ projectPath, settings }) {
39
+ const policy = codexPolicyForMode(settings.mode);
40
+ const approvalPolicy = settings.approval_policy || "on-request";
41
+ return {
42
+ model: settings.model,
43
+ sandboxMode: policy.sandbox,
44
+ workingDirectory: projectPath,
45
+ skipGitRepoCheck: true,
46
+ modelReasoningEffort: settings.reasoning_effort,
47
+ networkAccessEnabled: policy.networkAccess,
48
+ approvalPolicy
49
+ };
50
+ }
51
+
52
+ function parseCodexModelCatalog(catalog) {
53
+ const rows = Array.isArray(catalog?.models)
54
+ ? catalog.models
55
+ : Array.isArray(catalog?.data)
56
+ ? catalog.data
57
+ : [];
58
+ const visibleRows = rows
59
+ .filter((row) => row && typeof row === "object")
60
+ .filter((row) => row.hidden !== true && row.visibility !== "hidden");
61
+ const sortedRows = visibleRows.slice().sort((a, b) => {
62
+ const left = Number.isFinite(a.priority) ? a.priority : Number.MAX_SAFE_INTEGER;
63
+ const right = Number.isFinite(b.priority) ? b.priority : Number.MAX_SAFE_INTEGER;
64
+ return left - right;
65
+ });
66
+ const models = [];
67
+ const reasoningEfforts = [];
68
+ for (const row of sortedRows) {
69
+ const model = String(row.slug || row.model || row.id || "").trim();
70
+ if (model && !models.includes(model)) {
71
+ models.push(model);
72
+ }
73
+ const efforts = row.supported_reasoning_levels || row.supportedReasoningEfforts || [];
74
+ for (const effortRow of efforts) {
75
+ const effort = String(effortRow.effort || effortRow.reasoningEffort || "").trim();
76
+ if (effort && !reasoningEfforts.includes(effort)) {
77
+ reasoningEfforts.push(effort);
78
+ }
79
+ }
80
+ }
81
+ return {
82
+ models,
83
+ reasoning_efforts: reasoningEfforts
84
+ };
85
+ }
86
+
87
+ function parseJsonObject(stdout) {
88
+ const text = String(stdout || "").trim();
89
+ try {
90
+ return JSON.parse(text);
91
+ } catch (error) {
92
+ const start = text.indexOf("{");
93
+ const end = text.lastIndexOf("}");
94
+ if (start >= 0 && end > start) {
95
+ return JSON.parse(text.slice(start, end + 1));
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ function runCodexDebugModels({ codexPathOverride, timeoutMs = 5000, execFileImpl = execFile } = {}) {
102
+ const codexPath = resolveCodexExecutable(codexPathOverride);
103
+ return new Promise((resolve, reject) => {
104
+ execFileImpl(codexPath, ["debug", "models"], {
105
+ env: process.env,
106
+ maxBuffer: 50 * 1024 * 1024,
107
+ timeout: timeoutMs
108
+ }, (error, stdout, stderr) => {
109
+ if (error) {
110
+ reject(new Error((stderr || error.message).slice(-4000)));
111
+ return;
112
+ }
113
+ try {
114
+ resolve(parseJsonObject(stdout));
115
+ } catch (parseError) {
116
+ reject(new Error(`Codex 模型目录不是合法 JSON:${parseError.message}`));
117
+ }
118
+ });
119
+ });
120
+ }
121
+
122
+ async function discoverCodexCapabilities({
123
+ codexPathOverride,
124
+ timeoutMs = 5000,
125
+ force = false,
126
+ runDebugModels = runCodexDebugModels,
127
+ provider = "codex"
128
+ } = {}) {
129
+ const cacheKey = codexPathOverride || process.env.CODEX_CLI_PATH || "default";
130
+ const nowMs = Date.now();
131
+ const cached = modelCatalogCache.get(cacheKey);
132
+ if (!force && cached && nowMs - cached.updatedAt < MODEL_CACHE_TTL_MS) {
133
+ return cached.capabilities;
134
+ }
135
+
136
+ const catalog = await runDebugModels({ codexPathOverride, timeoutMs });
137
+ const parsed = parseCodexModelCatalog(catalog);
138
+ const capabilities = buildCapabilities(provider, parsed);
139
+ modelCatalogCache.set(cacheKey, {
140
+ capabilities,
141
+ updatedAt: nowMs
142
+ });
143
+ return capabilities;
144
+ }
145
+
146
+ function renderValue(value) {
147
+ if (value === undefined || value === null) {
148
+ return "";
149
+ }
150
+ if (typeof value === "string") {
151
+ return value;
152
+ }
153
+ if (Array.isArray(value)) {
154
+ return value.map((item) => renderValue(item)).filter(Boolean).join("\n");
155
+ }
156
+ if (typeof value === "object") {
157
+ if (typeof value.text === "string") {
158
+ return value.text;
159
+ }
160
+ if (Array.isArray(value.content)) {
161
+ return renderValue(value.content);
162
+ }
163
+ try {
164
+ return JSON.stringify(value, null, 2);
165
+ } catch {
166
+ return String(value);
167
+ }
168
+ }
169
+ return String(value);
170
+ }
171
+
172
+ function fileChangeMessage(item) {
173
+ const changes = Array.isArray(item.changes) ? item.changes : [];
174
+ if (!changes.length) {
175
+ return item.status === "failed" ? "文件修改失败" : "文件修改完成";
176
+ }
177
+ const summary = changes
178
+ .map((change) => `${change.kind || "update"} ${change.path || ""}`.trim())
179
+ .join(", ");
180
+ return item.status === "failed" ? `文件修改失败:${summary}` : `文件修改完成:${summary}`;
181
+ }
182
+
183
+ function convertThreadItemEvent(event) {
184
+ const item = event.item || {};
185
+ const itemStatus = item.status || (event.type === "item.completed" ? "completed" : "in_progress");
186
+
187
+ if (item.type === "agent_message") {
188
+ if (event.type !== "item.completed") {
189
+ return [];
190
+ }
191
+ const text = String(item.text || "").trim();
192
+ return text ? [{ type: "delta", payload: { text } }] : [];
193
+ }
194
+
195
+ if (item.type === "reasoning") {
196
+ const text = String(item.text || "").trim();
197
+ return text ? [{ type: "activity", payload: { message: text, kind: "agent" } }] : [];
198
+ }
199
+
200
+ if (item.type === "command_execution") {
201
+ if (event.type === "item.started") {
202
+ return [
203
+ {
204
+ type: "tool_use",
205
+ payload: {
206
+ tool_name: "exec_command",
207
+ tool_input: { cmd: item.command || "" },
208
+ tool_use_id: item.id || null
209
+ }
210
+ }
211
+ ];
212
+ }
213
+ if (event.type === "item.completed") {
214
+ return [
215
+ {
216
+ type: "tool_result",
217
+ payload: {
218
+ tool_use_id: item.id || null,
219
+ content: String(item.aggregated_output || ""),
220
+ is_error: itemStatus === "failed" || (Number.isInteger(item.exit_code) && item.exit_code !== 0)
221
+ }
222
+ }
223
+ ];
224
+ }
225
+ return [{ type: "activity", payload: { message: "命令执行中", kind: "tool_progress" } }];
226
+ }
227
+
228
+ if (item.type === "mcp_tool_call") {
229
+ if (event.type === "item.started") {
230
+ return [
231
+ {
232
+ type: "tool_use",
233
+ payload: {
234
+ tool_name: item.tool || "mcp_tool",
235
+ tool_input: item.arguments || {},
236
+ tool_use_id: item.id || null
237
+ }
238
+ }
239
+ ];
240
+ }
241
+ if (event.type === "item.completed") {
242
+ const content = item.error?.message || renderValue(item.result?.content || item.result);
243
+ return [
244
+ {
245
+ type: "tool_result",
246
+ payload: {
247
+ tool_use_id: item.id || null,
248
+ content,
249
+ is_error: itemStatus === "failed" || Boolean(item.error)
250
+ }
251
+ }
252
+ ];
253
+ }
254
+ return [{ type: "activity", payload: { message: `调用工具:${item.tool || "mcp_tool"}`, kind: "tool_progress" } }];
255
+ }
256
+
257
+ if (item.type === "web_search") {
258
+ if (event.type === "item.started") {
259
+ return [
260
+ {
261
+ type: "tool_use",
262
+ payload: {
263
+ tool_name: "web_search",
264
+ tool_input: { query: item.query || "" },
265
+ tool_use_id: item.id || null
266
+ }
267
+ }
268
+ ];
269
+ }
270
+ if (event.type === "item.completed") {
271
+ return [
272
+ {
273
+ type: "tool_result",
274
+ payload: {
275
+ tool_use_id: item.id || null,
276
+ content: `搜索完成:${item.query || ""}`,
277
+ is_error: false
278
+ }
279
+ }
280
+ ];
281
+ }
282
+ }
283
+
284
+ if (item.type === "file_change") {
285
+ return [{ type: "activity", payload: { message: fileChangeMessage(item), kind: "tool_progress" } }];
286
+ }
287
+
288
+ if (item.type === "todo_list") {
289
+ const todos = Array.isArray(item.items) ? item.items : [];
290
+ const completed = todos.filter((todo) => todo.completed).length;
291
+ return [{ type: "activity", payload: { message: `计划进度:${completed}/${todos.length}`, kind: "status" } }];
292
+ }
293
+
294
+ if (item.type === "error") {
295
+ return [{ type: "error", payload: { message: item.message || "Codex 事件错误" } }];
296
+ }
297
+
298
+ return [];
299
+ }
300
+
301
+ function convertCodexEvent(event) {
302
+ if (!event || typeof event !== "object") {
303
+ return [];
304
+ }
305
+
306
+ if (event.type === "thread.started") {
307
+ return [
308
+ {
309
+ type: "runtime_session",
310
+ payload: {
311
+ runtime_session_id: event.thread_id || null,
312
+ working_directory: ""
313
+ }
314
+ }
315
+ ];
316
+ }
317
+
318
+ if (event.type === "turn.started") {
319
+ return [{ type: "activity", payload: { message: "Codex 开始处理任务", kind: "status" } }];
320
+ }
321
+
322
+ if (event.type === "turn.completed") {
323
+ return [{ type: "usage", payload: { usage: event.usage || null } }];
324
+ }
325
+
326
+ if (event.type === "turn.failed") {
327
+ return [{ type: "error", payload: { message: event.error?.message || "Codex 执行失败" } }];
328
+ }
329
+
330
+ if (event.type === "error") {
331
+ return [{ type: "error", payload: { message: event.message || "Codex 事件错误" } }];
332
+ }
333
+
334
+ if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
335
+ return convertThreadItemEvent(event);
336
+ }
337
+
338
+ return [];
339
+ }
340
+
341
+ class CodexRuntime {
342
+ constructor({ codexPathOverride, cliPath, sdk, provider = "codex" } = {}) {
343
+ this.provider = provider;
344
+ this.codexPathOverride = codexPathOverride || cliPath || process.env.CODEX_CLI_PATH || undefined;
345
+ this.sdk = sdk || null;
346
+ }
347
+
348
+ async createClient() {
349
+ const sdk = this.sdk || await loadCodexSdk();
350
+ return new sdk.Codex({
351
+ codexPathOverride: this.codexPathOverride,
352
+ config: {
353
+ show_raw_agent_reasoning: false
354
+ }
355
+ });
356
+ }
357
+
358
+ async *run({ session, project, message, attachments = [], settings }) {
359
+ const threadOptions = buildCodexThreadOptions({
360
+ projectPath: project.path,
361
+ settings
362
+ });
363
+ const client = await this.createClient();
364
+ const thread = session.runtime_session_id
365
+ ? client.resumeThread(session.runtime_session_id, threadOptions)
366
+ : client.startThread(threadOptions);
367
+
368
+ yield {
369
+ type: "activity",
370
+ payload: {
371
+ message: `启动 Codex SDK:${project.path}`,
372
+ kind: "status"
373
+ }
374
+ };
375
+
376
+ const images = attachments.filter((image) => image?.path);
377
+ const input = images.length
378
+ ? [{ type: "text", text: message }, ...images.map((image) => ({ type: "local_image", path: image.path }))]
379
+ : message;
380
+ const { events } = await thread.runStreamed(input);
381
+ for await (const event of events) {
382
+ if (event.type === "turn.failed") {
383
+ const error = new Error(event.error?.message || "Codex 执行失败");
384
+ error.statusCode = 500;
385
+ throw error;
386
+ }
387
+ if (event.type === "error") {
388
+ const error = new Error(event.message || "Codex 事件错误");
389
+ error.statusCode = 500;
390
+ throw error;
391
+ }
392
+ for (const normalized of convertCodexEvent(event)) {
393
+ yield normalized;
394
+ }
395
+ }
396
+
397
+ yield {
398
+ type: "complete",
399
+ payload: {
400
+ message: "Codex 执行完成。"
401
+ }
402
+ };
403
+ }
404
+
405
+ async discoverCapabilities() {
406
+ return discoverCodexCapabilities({ codexPathOverride: this.codexPathOverride, provider: this.provider });
407
+ }
408
+ }
409
+
410
+ module.exports = {
411
+ CodexRuntime,
412
+ buildCodexThreadOptions,
413
+ convertCodexEvent,
414
+ discoverCodexCapabilities,
415
+ parseCodexModelCatalog,
416
+ resolveCodexExecutable,
417
+ runCodexDebugModels
418
+ };
@@ -0,0 +1,140 @@
1
+ function wait(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+
5
+ class MockRuntime {
6
+ constructor({ delayMs = 10 } = {}) {
7
+ this.provider = "mock";
8
+ this.delayMs = delayMs;
9
+ }
10
+
11
+ async *run({ session, project, message, attachments = [], settings, requestApproval }) {
12
+ yield {
13
+ type: "activity",
14
+ payload: {
15
+ message: `Mock agent 已连接 ${project.path}`,
16
+ kind: "status"
17
+ }
18
+ };
19
+ await wait(this.delayMs);
20
+
21
+ yield {
22
+ type: "tool_use",
23
+ payload: {
24
+ tool_name: "inspect_project",
25
+ tool_input: {
26
+ path: project.path,
27
+ mode: settings.mode
28
+ }
29
+ }
30
+ };
31
+ await wait(this.delayMs);
32
+
33
+ yield {
34
+ type: "tool_result",
35
+ payload: {
36
+ tool_name: "inspect_project",
37
+ content: "项目结构已读取,准备执行用户任务。",
38
+ is_error: false
39
+ }
40
+ };
41
+ await wait(this.delayMs);
42
+
43
+ if (
44
+ /approval|权限确认|需要权限/i.test(message) &&
45
+ requestApproval &&
46
+ settings.mode !== "auto-review" &&
47
+ settings.approval_policy !== "never"
48
+ ) {
49
+ const decision = await requestApproval({
50
+ runtime_request_id: `mock-${Date.now()}`,
51
+ kind: "command_execution",
52
+ command: "mock dangerous command",
53
+ cwd: project.path,
54
+ reason: "Mock runtime 请求权限确认",
55
+ available_decisions: ["approved", "rejected"]
56
+ });
57
+ if (decision.status === "cancelled" || decision.decision === "cancel") {
58
+ yield {
59
+ type: "cancelled",
60
+ payload: {
61
+ message: "Mock runtime 权限请求已取消。"
62
+ }
63
+ };
64
+ return;
65
+ }
66
+ if (decision.status !== "approved" && decision.decision !== "approved") {
67
+ const error = new Error("Mock runtime 权限请求被拒绝。");
68
+ error.statusCode = 403;
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ if (/browser|浏览器|页面/i.test(message)) {
74
+ yield {
75
+ type: "browser_output",
76
+ payload: {
77
+ title: "Agent Anywhere",
78
+ url: "http://localhost:8787",
79
+ summary: "浏览器输出区域可展示 URL、标题、截图摘要或检查结果。"
80
+ }
81
+ };
82
+ await wait(this.delayMs);
83
+ }
84
+
85
+ const summary = [
86
+ `已在 ${session.provider} 会话中接收任务:${message}`,
87
+ attachments.length ? `已接收 ${attachments.length} 张图片。` : "",
88
+ `模型 ${settings.model},思考强度 ${settings.reasoning_effort},模式 ${settings.mode}。`
89
+ ].filter(Boolean).join("\n");
90
+
91
+ for (const chunk of summary.match(/.{1,18}/g) || [summary]) {
92
+ yield {
93
+ type: "delta",
94
+ payload: {
95
+ text: chunk
96
+ }
97
+ };
98
+ await wait(this.delayMs);
99
+ }
100
+
101
+ yield {
102
+ type: "complete",
103
+ payload: {
104
+ message: summary
105
+ }
106
+ };
107
+ }
108
+
109
+ async cancelTurn() {
110
+ return {};
111
+ }
112
+
113
+ async steerTurn({ message, attachments = [] }) {
114
+ return { accepted: true, message, image_count: attachments.length };
115
+ }
116
+
117
+ async listRuntimeSessions({ project, limit = 50 } = {}) {
118
+ return [{
119
+ id: "mock-thread",
120
+ runtime_session_id: "mock-thread",
121
+ title: "Mock runtime session",
122
+ cwd: project?.path || "",
123
+ provider: this.provider
124
+ }].slice(0, limit);
125
+ }
126
+
127
+ async readRuntimeSession({ runtimeSessionId, includeTurns = true } = {}) {
128
+ return {
129
+ thread: {
130
+ id: runtimeSessionId || "mock-thread",
131
+ title: "Mock runtime session"
132
+ },
133
+ turns: includeTurns ? [] : undefined
134
+ };
135
+ }
136
+ }
137
+
138
+ module.exports = {
139
+ MockRuntime
140
+ };