@keyou007/agent-bot 0.1.10 → 0.1.12

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.
Files changed (121) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +55 -43
  4. package/README.zh.md +55 -43
  5. package/assets/agent-bot-logo.png +0 -0
  6. package/config.example.yaml +11 -1
  7. package/dist/acp/AcpSessionManager.js +78 -46
  8. package/dist/acp/AcpSessionManager.js.map +1 -1
  9. package/dist/cli/FeishuAppConfiguration.js +1 -1
  10. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  11. package/dist/cli/FeishuAppRegistration.js +2 -2
  12. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  13. package/dist/cli/Initializer.js +1 -1
  14. package/dist/cli/Initializer.js.map +1 -1
  15. package/dist/cli/ServerStarter.js +7 -2
  16. package/dist/cli/ServerStarter.js.map +1 -1
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +20 -16
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +95 -0
  21. package/dist/cli/taskGroupOptions.js.map +1 -0
  22. package/dist/cli.js +74 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/codex/AppServerConnection.js +15 -1
  25. package/dist/codex/AppServerConnection.js.map +1 -1
  26. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  27. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  28. package/dist/codex/CodexProcessManager.js +1 -0
  29. package/dist/codex/CodexProcessManager.js.map +1 -1
  30. package/dist/codex/CodexRuntime.js +112 -11
  31. package/dist/codex/CodexRuntime.js.map +1 -1
  32. package/dist/commands/CommandRouter.js +64 -49
  33. package/dist/commands/CommandRouter.js.map +1 -1
  34. package/dist/config/loadConfig.js +11 -1
  35. package/dist/config/loadConfig.js.map +1 -1
  36. package/dist/config/schema.js +6 -1
  37. package/dist/config/schema.js.map +1 -1
  38. package/dist/console/ConsoleConnector.js +1 -1
  39. package/dist/console/ConsoleConnector.js.map +1 -1
  40. package/dist/console/ConsoleTurnPresenter.js +15 -8
  41. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  42. package/dist/deprecated-cli.js +5 -0
  43. package/dist/deprecated-cli.js.map +1 -0
  44. package/dist/feishu/CardRenderer.js +350 -15
  45. package/dist/feishu/CardRenderer.js.map +1 -1
  46. package/dist/feishu/FeishuMessageClient.js +51 -5
  47. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  48. package/dist/feishu/FeishuTurnPresenter.js +17 -8
  49. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  50. package/dist/feishu/transport.js +1 -1
  51. package/dist/feishu/transport.js.map +1 -1
  52. package/dist/index.js +38 -19
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/OutboundRouter.js +2 -2
  55. package/dist/presentation/OutboundRouter.js.map +1 -1
  56. package/dist/presentation/TurnStateReducer.js +2 -1
  57. package/dist/presentation/TurnStateReducer.js.map +1 -1
  58. package/dist/proxy/ProxySessionController.js +665 -225
  59. package/dist/proxy/ProxySessionController.js.map +1 -1
  60. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  61. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  62. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  63. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  64. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  65. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  66. package/dist/startup/SessionMetadataHydrator.js +2 -2
  67. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  68. package/dist/startup/StartupNotifier.js +1 -0
  69. package/dist/startup/StartupNotifier.js.map +1 -1
  70. package/dist/state/StateStore.js +21 -15
  71. package/dist/state/StateStore.js.map +1 -1
  72. package/dist/supervision/SafeRestartScheduler.js +5 -0
  73. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  74. package/dist/supervision/systemEnvironment.js +129 -0
  75. package/dist/supervision/systemEnvironment.js.map +1 -0
  76. package/dist/supervisor.js +13 -1
  77. package/dist/supervisor.js.map +1 -1
  78. package/docs/technical-reference.md +35 -21
  79. package/docs/technical-reference.zh.md +35 -21
  80. package/npm-shrinkwrap.json +4 -3
  81. package/package.json +4 -2
  82. package/skills/agent-bot/SKILL.md +73 -30
  83. package/src/acp/AcpSessionManager.ts +73 -54
  84. package/src/cli/FeishuAppConfiguration.ts +2 -2
  85. package/src/cli/FeishuAppRegistration.ts +4 -4
  86. package/src/cli/Initializer.ts +2 -2
  87. package/src/cli/ServerStarter.ts +8 -3
  88. package/src/cli/controlProtocol.ts +21 -1
  89. package/src/cli/help.ts +20 -16
  90. package/src/cli/taskGroupOptions.ts +149 -0
  91. package/src/cli.ts +98 -4
  92. package/src/codex/AppServerConnection.ts +21 -1
  93. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  94. package/src/codex/CodexProcessManager.ts +1 -0
  95. package/src/codex/CodexRuntime.ts +137 -15
  96. package/src/commands/CommandRouter.ts +71 -49
  97. package/src/commands/commandTypes.ts +6 -11
  98. package/src/config/loadConfig.ts +11 -1
  99. package/src/config/schema.ts +7 -1
  100. package/src/console/ConsoleConnector.ts +1 -1
  101. package/src/console/ConsoleTurnPresenter.ts +21 -8
  102. package/src/deprecated-cli.ts +9 -0
  103. package/src/feishu/CardRenderer.ts +423 -15
  104. package/src/feishu/FeishuMessageClient.ts +66 -3
  105. package/src/feishu/FeishuTurnPresenter.ts +22 -3
  106. package/src/feishu/transport.ts +1 -1
  107. package/src/index.ts +54 -24
  108. package/src/presentation/OutboundRouter.ts +15 -3
  109. package/src/presentation/TurnStateReducer.ts +2 -0
  110. package/src/presentation/turnViewTypes.ts +1 -0
  111. package/src/proxy/ProxySessionController.ts +870 -228
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
  120. package/src/supervision/systemEnvironment.ts +174 -0
  121. package/src/supervisor.ts +12 -1
@@ -0,0 +1,149 @@
1
+ import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
2
+
3
+ export interface TaskNewGroupOptions {
4
+ reference: string;
5
+ title?: string;
6
+ cwd?: string;
7
+ agentName?: string;
8
+ projectless: boolean;
9
+ json: boolean;
10
+ }
11
+
12
+ export interface TaskForkGroupOptions {
13
+ reference: string;
14
+ title?: string;
15
+ json: boolean;
16
+ }
17
+
18
+ export function parseTaskNewGroupOptions(
19
+ input: string[],
20
+ language: CliLanguage = cliLanguage,
21
+ ): TaskNewGroupOptions {
22
+ const positionals: string[] = [];
23
+ let cwd: string | undefined;
24
+ let agentName: string | undefined;
25
+ let projectless = false;
26
+ let json = false;
27
+ for (let index = 0; index < input.length; index += 1) {
28
+ const argument = input[index]!;
29
+ if (argument === "--json") {
30
+ json = true;
31
+ continue;
32
+ }
33
+ if (argument === "--nodir") {
34
+ if (projectless) throw optionError("newgroup", "--nodir", language);
35
+ if (cwd !== undefined) throw conflictingProjectOptions(language);
36
+ projectless = true;
37
+ continue;
38
+ }
39
+ if (argument === "--dir") {
40
+ if (projectless) throw conflictingProjectOptions(language);
41
+ if (cwd !== undefined) throw optionError("newgroup", "--dir", language);
42
+ const directory = input[index + 1];
43
+ if (!directory || directory.startsWith("--")) {
44
+ throw new Error(cliText(
45
+ "task newgroup requires a project directory after --dir.",
46
+ "task newgroup 需要在 --dir 后指定项目目录。",
47
+ language,
48
+ ));
49
+ }
50
+ cwd = directory;
51
+ index += 1;
52
+ continue;
53
+ }
54
+ if (argument === "--agent") {
55
+ if (agentName !== undefined) throw optionError("newgroup", "--agent", language);
56
+ const name = input[index + 1];
57
+ if (!name?.trim() || name.startsWith("--")) {
58
+ throw new Error(cliText(
59
+ "task newgroup requires an Agent standard name after --agent.",
60
+ "task newgroup 需要在 --agent 后指定 Agent 标准名。",
61
+ language,
62
+ ));
63
+ }
64
+ agentName = name.trim();
65
+ index += 1;
66
+ continue;
67
+ }
68
+ if (argument.startsWith("--")) throw unsupportedOption("newgroup", argument, language);
69
+ positionals.push(argument);
70
+ }
71
+ const [reference, ...titleParts] = positionals;
72
+ requireTaskReference("newgroup", reference, language);
73
+ return {
74
+ reference,
75
+ title: titleParts.join(" ").trim() || undefined,
76
+ cwd,
77
+ ...(agentName ? { agentName } : {}),
78
+ projectless,
79
+ json,
80
+ };
81
+ }
82
+
83
+ export function parseTaskForkGroupOptions(
84
+ input: string[],
85
+ language: CliLanguage = cliLanguage,
86
+ ): TaskForkGroupOptions {
87
+ const positionals: string[] = [];
88
+ let json = false;
89
+ for (const argument of input) {
90
+ if (argument === "--json") {
91
+ json = true;
92
+ continue;
93
+ }
94
+ if (argument.startsWith("--")) throw unsupportedOption("forkgroup", argument, language);
95
+ positionals.push(argument);
96
+ }
97
+ const [reference, ...titleParts] = positionals;
98
+ requireTaskReference("forkgroup", reference, language);
99
+ return {
100
+ reference,
101
+ title: titleParts.join(" ").trim() || undefined,
102
+ json,
103
+ };
104
+ }
105
+
106
+ function requireTaskReference(
107
+ action: "newgroup" | "forkgroup",
108
+ reference: string | undefined,
109
+ language: CliLanguage,
110
+ ): asserts reference is string {
111
+ if (reference && !reference.startsWith("--")) return;
112
+ throw new Error(cliText(
113
+ `task ${action} requires a source task number or task ID.`,
114
+ `task ${action} 需要源任务序号或任务 ID。`,
115
+ language,
116
+ ));
117
+ }
118
+
119
+ function optionError(
120
+ action: "newgroup",
121
+ option: "--agent" | "--dir" | "--nodir",
122
+ language: CliLanguage,
123
+ ): Error {
124
+ return new Error(cliText(
125
+ `task ${action} accepts ${option} only once.`,
126
+ `task ${action} 只能指定一次 ${option}。`,
127
+ language,
128
+ ));
129
+ }
130
+
131
+ function conflictingProjectOptions(language: CliLanguage): Error {
132
+ return new Error(cliText(
133
+ "task newgroup cannot combine --dir and --nodir.",
134
+ "task newgroup 的 --dir 和 --nodir 不能同时使用。",
135
+ language,
136
+ ));
137
+ }
138
+
139
+ function unsupportedOption(
140
+ action: "newgroup" | "forkgroup",
141
+ option: string,
142
+ language: CliLanguage,
143
+ ): Error {
144
+ return new Error(cliText(
145
+ `task ${action} does not support option: ${option}.`,
146
+ `task ${action} 不支持参数:${option}。`,
147
+ language,
148
+ ));
149
+ }
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.
7
7
  import {
8
8
  controlEndpoint,
9
9
  type ControlResponse,
10
+ type TaskGroupControlData,
10
11
  type TaskStatusControlData,
11
12
  } from "./cli/controlProtocol.js";
12
13
  import {
@@ -49,6 +50,10 @@ import {
49
50
  type ServerStartResult,
50
51
  } from "./cli/ServerStarter.js";
51
52
  import { taskChatRoute } from "./cli/taskChatRoute.js";
53
+ import {
54
+ parseTaskForkGroupOptions,
55
+ parseTaskNewGroupOptions,
56
+ } from "./cli/taskGroupOptions.js";
52
57
  import { StateStore, type SessionRecord } from "./state/StateStore.js";
53
58
  import {
54
59
  nodeDiagnosticReportArguments,
@@ -99,8 +104,8 @@ async function main(input: string[]): Promise<void> {
99
104
  return;
100
105
  }
101
106
  throw new Error(cliText(
102
- `Unknown command: ${command}. Run agent-bot --help for usage.`,
103
- `未知命令:${command}。运行 agent-bot --help 查看用法。`,
107
+ `Unknown command: ${command}. Run agentbot --help for usage.`,
108
+ `未知命令:${command}。运行 agentbot --help 查看用法。`,
104
109
  ));
105
110
  }
106
111
 
@@ -246,8 +251,8 @@ async function assertResetProfileServerStopped(configPath?: string): Promise<voi
246
251
  if (await isServerReachable(endpoint)) {
247
252
  throw new Error(
248
253
  cliText(
249
- "Cannot reset a running profile. Stop it with agent-bot --profile <directory> server stop, then try again.",
250
- "无法重置正在运行的 Profile。请先执行 agent-bot --profile <目录> server stop,然后重试。",
254
+ "Cannot reset a running profile. Stop it with agentbot --profile <directory> server stop, then try again.",
255
+ "无法重置正在运行的 Profile。请先执行 agentbot --profile <目录> server stop,然后重试。",
251
256
  ),
252
257
  );
253
258
  }
@@ -410,6 +415,51 @@ async function taskCommand(input: string[]): Promise<void> {
410
415
  process.stdout.write(cliText("Prompt submitted.\n", "提示词已提交。\n"));
411
416
  return;
412
417
  }
418
+ if (action === "newgroup") {
419
+ const options = parseTaskNewGroupOptions(rest);
420
+ const session = resolveTask(allSessions, options.reference);
421
+ const targetAgentName = options.agentName ?? session.agentName;
422
+ const targetAgent = config.agents[targetAgentName];
423
+ if (!targetAgent) {
424
+ throw new Error(cliText(
425
+ `Unknown Agent standard name: ${targetAgentName}.`,
426
+ `未知的 Agent 标准名:${targetAgentName}。`,
427
+ ));
428
+ }
429
+ if (options.projectless && targetAgent.kind !== "app-server") {
430
+ throw new Error(cliText(
431
+ "task newgroup --nodir is only available for App Server agents.",
432
+ "task newgroup --nodir 仅适用于 App Server Agent。",
433
+ ));
434
+ }
435
+ requireCliGroupUser(config.feishu.userOpenId);
436
+ const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
437
+ action: "task_new_group",
438
+ localSessionId: session.localSessionId,
439
+ ...(options.title ? { title: options.title } : {}),
440
+ ...(options.cwd ? { cwd: options.cwd } : {}),
441
+ ...(options.agentName ? { agentName: options.agentName } : {}),
442
+ ...(options.projectless ? { projectless: true } : {}),
443
+ }, 120_000);
444
+ const result = taskGroupControlData(response);
445
+ if (options.json) printJson(result);
446
+ else printTaskGroupResult(result, "newgroup");
447
+ return;
448
+ }
449
+ if (action === "forkgroup") {
450
+ const options = parseTaskForkGroupOptions(rest);
451
+ const session = resolveTask(allSessions, options.reference);
452
+ requireCliGroupUser(config.feishu.userOpenId);
453
+ const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
454
+ action: "task_fork_group",
455
+ localSessionId: session.localSessionId,
456
+ ...(options.title ? { title: options.title } : {}),
457
+ }, 120_000);
458
+ const result = taskGroupControlData(response);
459
+ if (options.json) printJson(result);
460
+ else printTaskGroupResult(result, "forkgroup");
461
+ return;
462
+ }
413
463
  const sessions = filterSessions(allSessions, rest);
414
464
  if (action === "list") {
415
465
  if (rest.includes("--json")) printJson(sessions);
@@ -840,6 +890,50 @@ function ensureOk(response: ControlResponse): void {
840
890
  ));
841
891
  }
842
892
 
893
+ function requireCliGroupUser(userOpenId: string | undefined): asserts userOpenId is string {
894
+ if (userOpenId?.startsWith("ou_")) return;
895
+ throw new Error(cliText(
896
+ "The profile has no authorizing Lark user to invite. Run agentbot init again or configure FEISHU_USER_OPEN_ID.",
897
+ "当前 Profile 没有可邀请的飞书授权用户。请重新运行 agentbot init,或配置 FEISHU_USER_OPEN_ID。",
898
+ ));
899
+ }
900
+
901
+ function taskGroupControlData(response: ControlResponse): TaskGroupControlData {
902
+ ensureOk(response);
903
+ const data = response.data as Partial<TaskGroupControlData> | undefined;
904
+ if (
905
+ !data
906
+ || typeof data.sourceLocalSessionId !== "string"
907
+ || !data.group
908
+ || typeof data.group.chatId !== "string"
909
+ || typeof data.group.contextKey !== "string"
910
+ || typeof data.group.name !== "string"
911
+ || !data.task
912
+ || typeof data.task.localSessionId !== "string"
913
+ ) {
914
+ throw new Error(cliText(
915
+ "Agent Bot returned an invalid task-group result.",
916
+ "Agent Bot 返回了无效的任务群结果。",
917
+ ));
918
+ }
919
+ return data as TaskGroupControlData;
920
+ }
921
+
922
+ function printTaskGroupResult(
923
+ result: TaskGroupControlData,
924
+ action: "newgroup" | "forkgroup",
925
+ ): void {
926
+ process.stdout.write(cliText(
927
+ `${action === "forkgroup" ? "Fork group" : "Lark group"} created: ${result.group.name}\n`,
928
+ `${action === "forkgroup" ? "Fork 群" : "飞书群"}已创建:${result.group.name}\n`,
929
+ ));
930
+ process.stdout.write(`${cliText("Chat ID: ", "群 ID:")}${result.group.chatId}\n`);
931
+ process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${result.task.localSessionId}\n`);
932
+ if (result.task.remoteSessionId) {
933
+ process.stdout.write(`${cliText("Agent task ID: ", "Agent 任务 ID:")}${result.task.remoteSessionId}\n`);
934
+ }
935
+ }
936
+
843
937
  function optionValue(args: string[], name: string): string | undefined {
844
938
  const index = args.indexOf(name);
845
939
  return index >= 0 ? args[index + 1] : undefined;
@@ -26,6 +26,21 @@ interface PendingRequest {
26
26
 
27
27
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
28
28
 
29
+ export class AppServerRequestError extends Error {
30
+ readonly data: unknown;
31
+
32
+ constructor(
33
+ readonly method: string,
34
+ readonly code: number,
35
+ readonly serverMessage: string,
36
+ data?: unknown,
37
+ ) {
38
+ super(`${method} failed: ${serverMessage}`);
39
+ this.name = "AppServerRequestError";
40
+ this.data = data;
41
+ }
42
+ }
43
+
29
44
  export class AppServerConnection {
30
45
  private nextId = 1;
31
46
  private closed = false;
@@ -131,7 +146,12 @@ export class AppServerConnection {
131
146
  this.pending.delete(message.id);
132
147
  if (pending.timeout) clearTimeout(pending.timeout);
133
148
  if ("error" in message) {
134
- pending.reject(new Error(`${pending.method} failed: ${message.error.message}`));
149
+ pending.reject(new AppServerRequestError(
150
+ pending.method,
151
+ message.error.code,
152
+ message.error.message,
153
+ message.error.data,
154
+ ));
135
155
  } else {
136
156
  pending.resolve(message.result);
137
157
  }
@@ -16,12 +16,14 @@ interface ThreadPathRow {
16
16
 
17
17
  interface ThreadSettingsRow {
18
18
  id: string;
19
+ model_provider?: string | null;
19
20
  model: string | null;
20
21
  reasoning_effort: string | null;
21
22
  approval_mode: string | null;
22
23
  }
23
24
 
24
25
  export interface CodexLocalThreadSettings {
26
+ modelProvider?: string;
25
27
  model?: string;
26
28
  reasoningEffort?: string;
27
29
  permissionMode?: PermissionMode;
@@ -79,10 +81,15 @@ export class CodexLocalActivityDetector {
79
81
  fileMustExist: true,
80
82
  });
81
83
  const placeholders = ids.map(() => "?").join(", ");
84
+ const columns = new Set(
85
+ (database.pragma("table_info(threads)") as Array<{ name: string }>).map((column) => column.name),
86
+ );
87
+ const providerColumn = columns.has("model_provider") ? ", model_provider" : "";
82
88
  const rows = database.prepare(
83
- `SELECT id, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
89
+ `SELECT id${providerColumn}, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
84
90
  ).all(...ids) as ThreadSettingsRow[];
85
91
  return new Map(rows.map((row) => [row.id, {
92
+ ...(row.model_provider?.trim() ? { modelProvider: row.model_provider.trim() } : {}),
86
93
  ...(row.model?.trim() ? { model: row.model.trim() } : {}),
87
94
  ...(row.reasoning_effort?.trim() ? { reasoningEffort: row.reasoning_effort.trim() } : {}),
88
95
  ...(row.approval_mode?.trim()
@@ -40,6 +40,7 @@ export class CodexProcessManager implements AppServerClientProvider {
40
40
  });
41
41
  await client.request("initialize", {
42
42
  clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
43
+ capabilities: { experimentalApi: true },
43
44
  });
44
45
  client.notify("initialized", {});
45
46
  return client;
@@ -5,6 +5,7 @@ import type {
5
5
  CreateRuntimeSessionInput,
6
6
  ForkRuntimeSessionInput,
7
7
  ModelOption,
8
+ ModelProviderOption,
8
9
  PermissionMode,
9
10
  RemoteSessionActivity,
10
11
  RemoteSessionPage,
@@ -13,12 +14,14 @@ import type {
13
14
  RuntimeGoal,
14
15
  RuntimeGoalUpdate,
15
16
  RuntimeEvent,
17
+ RuntimeExecutionSettings,
16
18
  RuntimePrompt,
17
19
  RuntimeSession,
18
20
  RuntimeSessionMetadata,
19
21
  } from "../runtime/types.js";
20
22
  import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
21
23
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
24
+ import { AppServerRequestError } from "./AppServerConnection.js";
22
25
  import { mapCodexNotification } from "./CodexEventMapper.js";
23
26
  import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
24
27
  import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
@@ -32,12 +35,13 @@ const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
32
35
  "Call Graphics.CopyFromScreen with those physical coordinates and validate that the saved bitmap dimensions exactly equal the selected physical capture bounds.",
33
36
  ].join(" ");
34
37
 
35
- const SESSION_REQUEST_TIMEOUT_MS = 30_000;
38
+ const SESSION_REQUEST_TIMEOUT_MS = 60_000;
36
39
  const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
37
40
  const SYNC_REQUEST_TIMEOUT_MS = 5_000;
38
41
  // A timed-out fork keeps running in App Server and can create an orphan thread.
39
42
  // Wait for its response; connection closure still rejects the request.
40
43
  const FORK_REQUEST_TIMEOUT_MS = 0;
44
+ type ThreadListSortKey = "recency_at" | "updated_at";
41
45
 
42
46
  export interface AppServerClient {
43
47
  request<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
@@ -81,6 +85,7 @@ export class CodexRuntime implements AgentRuntime {
81
85
  private readonly unsubscribeDisconnect?: () => void;
82
86
  private readonly sessionSyncs = new Map<string, Promise<RuntimeSession>>();
83
87
  private readonly localActivityDetector?: CodexLocalActivityDetector;
88
+ private threadListSortKey: ThreadListSortKey = "recency_at";
84
89
 
85
90
  constructor(
86
91
  private readonly provider: AppServerClientProvider,
@@ -100,6 +105,7 @@ export class CodexRuntime implements AgentRuntime {
100
105
  const response = await client.request<ThreadResponse>("thread/start", {
101
106
  cwd: input.cwd,
102
107
  model: input.model,
108
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
103
109
  threadSource: "user",
104
110
  ...threadLifecycleParams(input.cwd),
105
111
  ...permissionParams(input.permissionMode),
@@ -122,8 +128,10 @@ export class CodexRuntime implements AgentRuntime {
122
128
  const client = await this.client();
123
129
  const response = await client.request<ThreadResponse>("thread/resume", {
124
130
  threadId: input.remoteSessionId,
131
+ excludeTurns: true,
125
132
  cwd: input.cwd,
126
133
  model: input.model,
134
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
127
135
  ...threadLifecycleParams(input.cwd),
128
136
  ...permissionParams(input.permissionMode),
129
137
  }, SESSION_REQUEST_TIMEOUT_MS);
@@ -135,15 +143,30 @@ export class CodexRuntime implements AgentRuntime {
135
143
 
136
144
  async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
137
145
  const client = await this.client();
138
- const response = await client.request<ThreadResponse>("thread/fork", {
146
+ const forkParams = {
139
147
  threadId: input.remoteSessionId,
140
148
  lastTurnId: input.lastTurnId,
141
149
  cwd: input.cwd,
142
150
  model: input.model,
151
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
143
152
  threadSource: "user",
144
153
  ...threadLifecycleParams(input.cwd),
145
154
  ...permissionParams(input.permissionMode),
146
- }, FORK_REQUEST_TIMEOUT_MS);
155
+ };
156
+ let response: ThreadResponse;
157
+ try {
158
+ response = await client.request<ThreadResponse>("thread/fork", {
159
+ ...forkParams,
160
+ excludeTurns: true,
161
+ }, FORK_REQUEST_TIMEOUT_MS);
162
+ } catch (error) {
163
+ if (!isUnsupportedExcludeTurnsError(error)) throw error;
164
+ this.logger.warn(
165
+ { error },
166
+ "Codex App Server does not support thread/fork excludeTurns; retrying without it.",
167
+ );
168
+ response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
169
+ }
147
170
  const requestedTitle = normalizeTaskTitle(input.title);
148
171
  if (requestedTitle) {
149
172
  await client.request("thread/name/set", {
@@ -194,18 +217,18 @@ export class CodexRuntime implements AgentRuntime {
194
217
  limit?: number;
195
218
  } = {}): Promise<RemoteSessionPage> {
196
219
  const client = await this.client();
197
- const response = await client.request<ThreadListResponse>(
198
- "thread/list",
199
- {
200
- cursor: input.cursor,
201
- limit: input.limit ?? 20,
202
- sortKey: "recency_at",
203
- sortDirection: "desc",
204
- archived: false,
205
- searchTerm: input.searchTerm,
206
- },
207
- CONTROL_REQUEST_TIMEOUT_MS,
208
- );
220
+ let response: ThreadListResponse;
221
+ try {
222
+ response = await this.requestThreadList(client, input, this.threadListSortKey);
223
+ } catch (error) {
224
+ if (this.threadListSortKey !== "recency_at" || !isUnsupportedRecencySortError(error)) throw error;
225
+ this.threadListSortKey = "updated_at";
226
+ this.logger.warn(
227
+ { error },
228
+ "App Server does not support thread/list recency_at sorting; retrying with updated_at.",
229
+ );
230
+ response = await this.requestThreadList(client, input, this.threadListSortKey);
231
+ }
209
232
  const sessions = await Promise.all(response.data.map(async (thread) => {
210
233
  const listed = remoteSessionSummary(thread);
211
234
  if (listed.status === "active" || listed.lastTurnStatus === "inProgress") return listed;
@@ -229,6 +252,25 @@ export class CodexRuntime implements AgentRuntime {
229
252
  };
230
253
  }
231
254
 
255
+ private requestThreadList(
256
+ client: AppServerClient,
257
+ input: { searchTerm?: string; cursor?: string; limit?: number },
258
+ sortKey: ThreadListSortKey,
259
+ ): Promise<ThreadListResponse> {
260
+ return client.request<ThreadListResponse>(
261
+ "thread/list",
262
+ {
263
+ cursor: input.cursor,
264
+ limit: input.limit ?? 20,
265
+ sortKey,
266
+ sortDirection: "desc",
267
+ archived: false,
268
+ searchTerm: input.searchTerm,
269
+ },
270
+ CONTROL_REQUEST_TIMEOUT_MS,
271
+ );
272
+ }
273
+
232
274
  async readRemoteSession(remoteSessionId: string): Promise<RemoteSessionSummary> {
233
275
  const response = await (await this.client()).request<ThreadReadResponse>(
234
276
  "thread/read",
@@ -375,6 +417,28 @@ export class CodexRuntime implements AgentRuntime {
375
417
  this.requireSession(sessionId).permissionMode = mode;
376
418
  }
377
419
 
420
+ async setExecutionSettings(
421
+ sessionId: string,
422
+ settings: RuntimeExecutionSettings,
423
+ ): Promise<RuntimeSession> {
424
+ const session = this.requireSession(sessionId);
425
+ const response = await (await this.client()).request<ThreadResponse>("thread/resume", {
426
+ threadId: session.remoteSessionId,
427
+ excludeTurns: true,
428
+ cwd: session.cwd,
429
+ modelProvider: settings.modelProvider,
430
+ model: settings.model,
431
+ ...threadLifecycleParams(session.cwd),
432
+ ...permissionParams(settings.permissionMode),
433
+ }, SESSION_REQUEST_TIMEOUT_MS);
434
+ session.modelProvider = response.modelProvider ?? settings.modelProvider;
435
+ session.model = response.model ?? settings.model;
436
+ session.reasoningEffort = settings.reasoningEffort;
437
+ session.permissionMode = settings.permissionMode;
438
+ session.needsResume = false;
439
+ return session;
440
+ }
441
+
378
442
  async respondToApproval(
379
443
  sessionId: string,
380
444
  requestId: string,
@@ -412,6 +476,33 @@ export class CodexRuntime implements AgentRuntime {
412
476
  }));
413
477
  }
414
478
 
479
+ async listModelProviders(): Promise<ModelProviderOption[]> {
480
+ const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
481
+ "config/read",
482
+ {},
483
+ CONTROL_REQUEST_TIMEOUT_MS,
484
+ );
485
+ const config = response.config ?? {};
486
+ const defaultProvider = stringValue(config.model_provider)?.trim();
487
+ const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
488
+ const providers = new Map<string, ModelProviderOption>();
489
+ for (const [id, value] of Object.entries(configuredProviders)) {
490
+ if (!id.trim()) continue;
491
+ const displayName = isRecord(value) ? stringValue(value.name)?.trim() : undefined;
492
+ providers.set(id, {
493
+ id,
494
+ ...(displayName ? { displayName } : {}),
495
+ ...(id === defaultProvider ? { isDefault: true } : {}),
496
+ });
497
+ }
498
+ if (defaultProvider && !providers.has(defaultProvider)) {
499
+ providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
500
+ }
501
+ return [...providers.values()].sort((left, right) =>
502
+ Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id),
503
+ );
504
+ }
505
+
415
506
  onEvent(listener: (event: RuntimeEvent) => void): () => void {
416
507
  this.listeners.add(listener);
417
508
  return () => this.listeners.delete(listener);
@@ -642,8 +733,10 @@ export class CodexRuntime implements AgentRuntime {
642
733
  if (!session.needsResume) return;
643
734
  await client.request("thread/resume", {
644
735
  threadId: session.remoteSessionId,
736
+ excludeTurns: true,
645
737
  cwd: session.cwd,
646
738
  model: session.model,
739
+ ...(session.modelProvider ? { modelProvider: session.modelProvider } : {}),
647
740
  ...threadLifecycleParams(session.cwd),
648
741
  ...permissionParams(session.permissionMode),
649
742
  }, SESSION_REQUEST_TIMEOUT_MS);
@@ -772,6 +865,7 @@ export class CodexRuntime implements AgentRuntime {
772
865
  title: normalizeTaskTitle(response.thread.name)
773
866
  ?? normalizeTaskTitle(response.thread.preview)
774
867
  ?? input.title,
868
+ modelProvider: input.modelProvider ?? response.modelProvider,
775
869
  model: input.model ?? response.model,
776
870
  reasoningEffort,
777
871
  permissionMode: input.permissionMode,
@@ -835,6 +929,7 @@ export class CodexRuntime implements AgentRuntime {
835
929
 
836
930
  interface ThreadResponse {
837
931
  thread: { id: string; name?: string | null; preview?: string };
932
+ modelProvider?: string;
838
933
  model?: string;
839
934
  reasoningEffort?: string | null;
840
935
  }
@@ -853,6 +948,7 @@ interface CodexThreadSnapshot {
853
948
  name?: string | null;
854
949
  preview?: string;
855
950
  cwd?: string;
951
+ modelProvider?: string;
856
952
  source?: unknown;
857
953
  createdAt?: number;
858
954
  updatedAt?: number;
@@ -933,6 +1029,31 @@ function uniqueStrings(values: string[]): string[] {
933
1029
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
934
1030
  }
935
1031
 
1032
+ function isUnsupportedExcludeTurnsError(error: unknown): boolean {
1033
+ if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
1034
+ const data = typeof error.data === "string"
1035
+ ? error.data
1036
+ : error.data === undefined
1037
+ ? ""
1038
+ : JSON.stringify(error.data);
1039
+ const details = `${error.serverMessage} ${data}`;
1040
+ if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
1041
+ return error.code === -32602
1042
+ || /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
1043
+ }
1044
+
1045
+ function isUnsupportedRecencySortError(error: unknown): boolean {
1046
+ if (!(error instanceof AppServerRequestError) || error.method !== "thread/list") return false;
1047
+ const data = typeof error.data === "string"
1048
+ ? error.data
1049
+ : error.data === undefined
1050
+ ? ""
1051
+ : JSON.stringify(error.data);
1052
+ const details = `${error.serverMessage} ${data}`;
1053
+ return /recency_at/i.test(details)
1054
+ && /unknown|unrecognized|unexpected|unsupported|not supported|invalid/i.test(details);
1055
+ }
1056
+
936
1057
  function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
937
1058
  const lastTurn = thread.turns?.at(-1);
938
1059
  const lastCompletedTurn = [...(thread.turns ?? [])]
@@ -956,6 +1077,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
956
1077
  cwd: thread.cwd ?? "",
957
1078
  source: codexSourceLabel(thread.source),
958
1079
  status,
1080
+ modelProvider: thread.modelProvider,
959
1081
  createdAt: thread.createdAt,
960
1082
  updatedAt: thread.updatedAt,
961
1083
  recencyAt: thread.recencyAt ?? undefined,