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,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
+ };