@keyou007/agent-bot 0.1.12 → 0.1.13-alpha.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +23 -10
  3. package/README.zh.md +23 -10
  4. package/config.example.yaml +0 -24
  5. package/dist/cli/AgentPrerequisites.js +262 -0
  6. package/dist/cli/AgentPrerequisites.js.map +1 -0
  7. package/dist/cli/DefaultAgentSelection.js +47 -0
  8. package/dist/cli/DefaultAgentSelection.js.map +1 -0
  9. package/dist/cli/Initializer.js +165 -2
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/help.js +4 -4
  12. package/dist/cli.js +194 -4
  13. package/dist/cli.js.map +1 -1
  14. package/dist/codex/AppServerConnection.js +3 -3
  15. package/dist/codex/AppServerConnection.js.map +1 -1
  16. package/dist/codex/CodexProcessManager.js +4 -4
  17. package/dist/codex/CodexProcessManager.js.map +1 -1
  18. package/dist/codex/CodexRuntime.js +28 -16
  19. package/dist/codex/CodexRuntime.js.map +1 -1
  20. package/dist/config/loadConfig.js +0 -24
  21. package/dist/config/loadConfig.js.map +1 -1
  22. package/dist/console/ConsoleConnector.js +2 -2
  23. package/dist/console/ConsoleConnector.js.map +1 -1
  24. package/dist/feishu/CardRenderer.js +8 -8
  25. package/dist/feishu/CardRenderer.js.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/proxy/ProxySessionController.js +51 -51
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/dist/runtime/AcpRuntimeAdapter.js +3 -2
  30. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  31. package/docs/technical-reference.md +32 -17
  32. package/docs/technical-reference.zh.md +32 -17
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +7 -3
  35. package/skills/agent-bot/SKILL.md +12 -6
  36. package/src/cli/AgentPrerequisites.ts +377 -0
  37. package/src/cli/DefaultAgentSelection.ts +56 -0
  38. package/src/cli/Initializer.ts +204 -3
  39. package/src/cli/help.ts +4 -4
  40. package/src/cli.ts +293 -5
  41. package/src/codex/AppServerConnection.ts +3 -3
  42. package/src/codex/CodexProcessManager.ts +4 -4
  43. package/src/codex/CodexRuntime.ts +38 -18
  44. package/src/config/loadConfig.ts +0 -24
  45. package/src/console/ConsoleConnector.ts +2 -2
  46. package/src/feishu/CardRenderer.ts +8 -8
  47. package/src/index.ts +1 -1
  48. package/src/proxy/ProxySessionController.ts +52 -52
  49. package/src/runtime/AcpRuntimeAdapter.ts +3 -1
package/src/cli/help.ts CHANGED
@@ -3,13 +3,13 @@ import { cliLanguage, type CliLanguage } from "./i18n.js";
3
3
  export function renderCliHelp(version: string, language: CliLanguage = cliLanguage): string {
4
4
  if (language === "zh") {
5
5
  return `Agent Bot ${version}
6
- 通过飞书使用本地 Codex 和 ACP Agent。
6
+ 通过飞书使用本地 Codex、TraeX 和兼容 ACP 的 Agent。
7
7
 
8
8
  用法:
9
9
  agentbot [选项] <命令>
10
10
 
11
11
  常用命令:
12
- init 初始化飞书应用并启动服务
12
+ init 检查并选择 Agent,初始化飞书应用和服务
13
13
  init --reset 重置并重新配置显式指定的 Profile
14
14
  console 打开本地控制台
15
15
  server start 在后台启动 Agent Bot
@@ -51,13 +51,13 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
51
51
  }
52
52
 
53
53
  return `Agent Bot ${version}
54
- Use local Codex and ACP agents from Lark/Feishu.
54
+ Use Codex, TraeX, and compatible ACP agents from Lark/Feishu.
55
55
 
56
56
  Usage:
57
57
  agentbot [options] <command>
58
58
 
59
59
  Common commands:
60
- init Initialize the Lark app and start the service
60
+ init Check and select an Agent; initialize the Lark app and service
61
61
  init --reset Reset and reconfigure an explicit profile
62
62
  console Open the local console
63
63
  server start Start Agent Bot in the background
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { createInterface } from "node:readline";
5
6
  import { loadConfig, loadConfigWithoutEnvironmentMutation } from "./config/loadConfig.js";
6
7
  import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
7
8
  import {
@@ -14,8 +15,10 @@ import {
14
15
  acquireInitializationLock,
15
16
  cleanupFeishuCredentialTemporaryFiles,
16
17
  initializeAgentBot,
18
+ readConfiguredAgentSelection,
17
19
  readFeishuCredentials,
18
20
  shouldCreateFeishuApp,
21
+ writeDefaultAgent,
19
22
  writeFeishuCredentials,
20
23
  type InitializationResult,
21
24
  type InitializationStatus,
@@ -38,6 +41,18 @@ import {
38
41
  } from "./cli/FeishuAppConfiguration.js";
39
42
  import { renderCliHelp } from "./cli/help.js";
40
43
  import { cliLanguage, cliText, localizeCliErrorMessage } from "./cli/i18n.js";
44
+ import {
45
+ inspectSupportedAgent,
46
+ inspectSupportedAgents,
47
+ runSupportedAgentMaintenance,
48
+ type SupportedAgentInspection,
49
+ } from "./cli/AgentPrerequisites.js";
50
+ import {
51
+ parseMaintenanceSelection,
52
+ resolveDefaultAgentChoice,
53
+ selectableDefaultAgents,
54
+ type SelectableDefaultAgent,
55
+ } from "./cli/DefaultAgentSelection.js";
41
56
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
42
57
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
43
58
  import { readPackageVersion } from "./cli/packageVersion.js";
@@ -50,6 +65,7 @@ import {
50
65
  type ServerStartResult,
51
66
  } from "./cli/ServerStarter.js";
52
67
  import { taskChatRoute } from "./cli/taskChatRoute.js";
68
+ import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
53
69
  import {
54
70
  parseTaskForkGroupOptions,
55
71
  parseTaskNewGroupOptions,
@@ -112,6 +128,11 @@ async function main(input: string[]): Promise<void> {
112
128
  type FeishuInitializationStatus = "created" | "existing" | "skipped";
113
129
 
114
130
  interface InitCommandResult extends InitializationResult {
131
+ agents: InitializedAgent[];
132
+ defaultAgent: {
133
+ name: string;
134
+ status: "selected" | "existing";
135
+ };
115
136
  feishu: {
116
137
  status: FeishuInitializationStatus;
117
138
  appId?: string;
@@ -120,6 +141,13 @@ interface InitCommandResult extends InitializationResult {
120
141
  server: InitializationServerResult;
121
142
  }
122
143
 
144
+ interface InitializedAgent extends SupportedAgentInspection {
145
+ assistance?: {
146
+ status: "completed" | "skipped" | "unavailable" | "failed";
147
+ error?: string;
148
+ };
149
+ }
150
+
123
151
  async function initCommand(
124
152
  input: string[],
125
153
  configPath: string | undefined,
@@ -135,13 +163,15 @@ async function initCommand(
135
163
  if (options.reset) await assertResetProfileServerStopped(configPath);
136
164
  paths = initializeAgentBot({ configPath, reset: options.reset });
137
165
  if (!options.json) printInitializationPaths(paths);
166
+ const agents = await initializeSupportedAgents(options.json);
167
+ const defaultAgent = await selectDefaultAgent(paths.config.path, agents, options.json);
138
168
  initializationLock ??= acquireInitializationLock(paths.home.path);
139
169
  cleanupFeishuCredentialTemporaryFiles(paths.env.path);
140
170
  const feishu = await initializeFeishu(
141
171
  paths,
142
172
  options,
143
173
  );
144
- initialized = { ...paths, feishu };
174
+ initialized = { ...paths, agents, defaultAgent, feishu };
145
175
  } finally {
146
176
  initializationLock?.release();
147
177
  }
@@ -156,6 +186,259 @@ async function initCommand(
156
186
  else printInitializationServerResult(server);
157
187
  }
158
188
 
189
+ async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
190
+ const output = json ? process.stderr : process.stdout;
191
+ output.write(cliText(
192
+ "\nAgent setup\nChecking Codex and TraeX...\n",
193
+ "\nAgent 设置\n正在检查 Codex 和 TraeX...\n",
194
+ ));
195
+ const inspections = await inspectSupportedAgents();
196
+ for (const inspection of inspections) printAgentInspection(inspection, output);
197
+ const actionable = inspections.filter((inspection) => inspection.action);
198
+ if (actionable.length === 0) return inspections;
199
+
200
+ output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
201
+ for (const [index, inspection] of actionable.entries()) {
202
+ output.write(cliText(
203
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`,
204
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`,
205
+ ));
206
+ }
207
+
208
+ let selected = new Set<number>();
209
+ if (!process.stdin.isTTY) {
210
+ output.write(cliText(
211
+ "No interactive terminal is available. Run the commands above manually if needed.\n",
212
+ "当前没有交互式终端,如有需要请手动执行上面的命令。\n",
213
+ ));
214
+ } else {
215
+ selected = new Set(await promptForAgentMaintenance(actionable.length));
216
+ if (selected.size === 0) {
217
+ output.write(cliText("No Agent changes selected.\n", "已跳过 Agent 安装和升级。\n"));
218
+ }
219
+ }
220
+
221
+ const initialized: InitializedAgent[] = [];
222
+ for (const inspection of inspections) {
223
+ if (!inspection.action) {
224
+ initialized.push(inspection);
225
+ continue;
226
+ }
227
+ const actionIndex = actionable.indexOf(inspection);
228
+ if (!selected.has(actionIndex)) {
229
+ initialized.push({
230
+ ...inspection,
231
+ assistance: { status: process.stdin.isTTY ? "skipped" : "unavailable" },
232
+ });
233
+ continue;
234
+ }
235
+
236
+ output.write(cliText(
237
+ `\nRunning ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}\n`,
238
+ `\n正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}\n`,
239
+ ));
240
+ const maintenance = await runSupportedAgentMaintenance(inspection.id, inspection.action.kind);
241
+ if (maintenance.status !== 0 || maintenance.error) {
242
+ const error = maintenance.error ?? cliText(
243
+ `Command exited with code ${maintenance.status ?? "unknown"}.`,
244
+ `命令退出码为 ${maintenance.status ?? "未知"}。`,
245
+ );
246
+ output.write(cliText(
247
+ `${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.\n`,
248
+ `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。\n`,
249
+ ));
250
+ initialized.push({ ...inspection, assistance: { status: "failed", error } });
251
+ continue;
252
+ }
253
+
254
+ refreshCurrentProcessPath();
255
+ const refreshed = await inspectSupportedAgent(inspection.id);
256
+ if (refreshed.installedVersion) {
257
+ output.write(cliText(
258
+ `${inspection.name} is ready (${refreshed.installedVersion}).\n`,
259
+ `${inspection.name} 已就绪(${refreshed.installedVersion})。\n`,
260
+ ));
261
+ } else {
262
+ output.write(cliText(
263
+ `${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.\n`,
264
+ `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。\n`,
265
+ ));
266
+ }
267
+ initialized.push({ ...refreshed, assistance: { status: "completed" } });
268
+ }
269
+ return initialized;
270
+ }
271
+
272
+ async function selectDefaultAgent(
273
+ configPath: string,
274
+ inspections: InitializedAgent[],
275
+ json: boolean,
276
+ ): Promise<InitCommandResult["defaultAgent"]> {
277
+ const output = json ? process.stderr : process.stdout;
278
+ const configured = readConfiguredAgentSelection(configPath);
279
+ const choices = selectableDefaultAgents(configured.agents, inspections);
280
+ if (!process.stdin.isTTY) {
281
+ if (!configured.defaultAgent || !configured.agents.some((agent) => agent.name === configured.defaultAgent)) {
282
+ throw new Error(cliText(
283
+ "A default Agent is not configured. Run agentbot init in an interactive terminal and select one.",
284
+ "尚未配置默认 Agent。请在交互式终端中运行 agentbot init 并选择一个 Agent。",
285
+ ));
286
+ }
287
+ output.write(cliText(
288
+ `Default Agent: ${configured.defaultAgent} (kept because no interactive terminal is available)\n`,
289
+ `默认 Agent:${configured.defaultAgent}(当前没有交互式终端,已保留现有设置)\n`,
290
+ ));
291
+ return { name: configured.defaultAgent, status: "existing" };
292
+ }
293
+ if (choices.length === 0) {
294
+ throw new Error(cliText(
295
+ "No available configured Agent can be selected. Install Codex or TraeX, then run agentbot init again.",
296
+ "没有可选择的已配置 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
297
+ ));
298
+ }
299
+
300
+ output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
301
+ for (const [index, choice] of choices.entries()) {
302
+ const version = choice.installedVersion ? ` (${choice.installedVersion})` : "";
303
+ const current = choice.name === configured.defaultAgent
304
+ ? cliText(" [current]", " [当前]")
305
+ : "";
306
+ output.write(` ${index + 1}. ${choice.name} - ${choice.title}${version}${current}\n`);
307
+ }
308
+ const selected = choices[await promptForDefaultAgent(choices, configured.defaultAgent)];
309
+ if (!selected) throw new Error(cliText("Default Agent selection failed.", "默认 Agent 选择失败。"));
310
+ const updated = writeDefaultAgent(configPath, selected.name);
311
+ output.write(updated
312
+ ? cliText(`Saved default Agent: ${selected.name}\n`, `已保存默认 Agent:${selected.name}\n`)
313
+ : cliText(`Default Agent unchanged: ${selected.name}\n`, `默认 Agent 保持不变:${selected.name}\n`));
314
+ return { name: selected.name, status: updated ? "selected" : "existing" };
315
+ }
316
+
317
+ function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
318
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
319
+ return new Promise((resolve, reject) => {
320
+ let settled = false;
321
+ const finish = (selection: number[]): void => {
322
+ settled = true;
323
+ readline.close();
324
+ resolve(selection);
325
+ };
326
+ const ask = (): void => {
327
+ readline.question(cliText(
328
+ "Enter action numbers separated by commas, type all, or press Enter to skip: ",
329
+ "请输入要执行的操作编号(多个用逗号分隔),输入 all 选择全部,直接回车跳过:",
330
+ ), (answer) => {
331
+ const selection = parseMaintenanceSelection(answer, choiceCount);
332
+ if (selection) {
333
+ finish(selection);
334
+ return;
335
+ }
336
+ process.stderr.write(cliText(
337
+ `Enter numbers from 1 to ${choiceCount}, all, or press Enter to skip.\n`,
338
+ `请输入 1 到 ${choiceCount} 之间的编号、all,或直接回车跳过。\n`,
339
+ ));
340
+ ask();
341
+ });
342
+ };
343
+ listenForPromptCancellation(readline, () => {
344
+ settled = true;
345
+ reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
346
+ }, () => settled);
347
+ ask();
348
+ });
349
+ }
350
+
351
+ function promptForDefaultAgent(
352
+ choices: SelectableDefaultAgent[],
353
+ currentAgent?: string,
354
+ ): Promise<number> {
355
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
356
+ return new Promise((resolve, reject) => {
357
+ let settled = false;
358
+ const finish = (index: number): void => {
359
+ settled = true;
360
+ readline.close();
361
+ resolve(index);
362
+ };
363
+ const ask = (): void => {
364
+ const currentAvailable = choices.some((choice) => choice.name === currentAgent);
365
+ readline.question(cliText(
366
+ `Enter a number or Agent name${currentAvailable ? ` [${currentAgent}]` : ""}: `,
367
+ `请输入编号或 Agent 标准名${currentAvailable ? ` [${currentAgent}]` : ""}:`,
368
+ ), (answer) => {
369
+ const index = resolveDefaultAgentChoice(answer, choices, currentAgent);
370
+ if (index !== undefined) {
371
+ finish(index);
372
+ return;
373
+ }
374
+ process.stderr.write(cliText(
375
+ `Enter a number from 1 to ${choices.length} or an Agent standard name.\n`,
376
+ `请输入 1 到 ${choices.length} 之间的编号,或 Agent 标准名。\n`,
377
+ ));
378
+ ask();
379
+ });
380
+ };
381
+ listenForPromptCancellation(readline, () => {
382
+ settled = true;
383
+ reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
384
+ }, () => settled);
385
+ ask();
386
+ });
387
+ }
388
+
389
+ function listenForPromptCancellation(
390
+ readline: ReturnType<typeof createInterface>,
391
+ cancel: () => void,
392
+ isSettled: () => boolean,
393
+ ): void {
394
+ readline.once("SIGINT", () => {
395
+ if (!isSettled()) cancel();
396
+ readline.close();
397
+ });
398
+ readline.once("close", () => {
399
+ if (!isSettled()) cancel();
400
+ });
401
+ }
402
+
403
+ function printAgentInspection(
404
+ inspection: SupportedAgentInspection,
405
+ output: NodeJS.WriteStream,
406
+ ): void {
407
+ if (inspection.state === "missing") {
408
+ output.write(cliText(
409
+ ` ${inspection.name}: not installed\n`,
410
+ ` ${inspection.name}:未安装\n`,
411
+ ));
412
+ return;
413
+ }
414
+ if (inspection.state === "outdated") {
415
+ output.write(cliText(
416
+ ` ${inspection.name}: ${inspection.installedVersion}; update available (${inspection.latestVersion})\n`,
417
+ ` ${inspection.name}:${inspection.installedVersion};可升级到 ${inspection.latestVersion}\n`,
418
+ ));
419
+ return;
420
+ }
421
+ if (inspection.latestCheckFailed) {
422
+ output.write(cliText(
423
+ ` ${inspection.name}: ${inspection.installedVersion}; latest-version check unavailable\n`,
424
+ ` ${inspection.name}:${inspection.installedVersion};无法检查最新版本\n`,
425
+ ));
426
+ return;
427
+ }
428
+ output.write(cliText(
429
+ ` ${inspection.name}: ${inspection.installedVersion} (up to date)\n`,
430
+ ` ${inspection.name}:${inspection.installedVersion}(已是最新)\n`,
431
+ ));
432
+ }
433
+
434
+ function refreshCurrentProcessPath(): void {
435
+ const refreshed = refreshedSystemEnvironment();
436
+ if (!refreshed.refreshed) return;
437
+ const pathEntry = Object.entries(refreshed.environment)
438
+ .find(([name]) => name.toLowerCase() === "path");
439
+ if (pathEntry?.[1]) process.env.PATH = pathEntry[1];
440
+ }
441
+
159
442
  async function initializeFeishu(
160
443
  paths: InitializationResult,
161
444
  options: InitCommandOptions,
@@ -167,6 +450,9 @@ async function initializeFeishu(
167
450
  ...(existing.appId ? { appId: existing.appId } : {}),
168
451
  };
169
452
  }
453
+ if (!options.json) {
454
+ process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
455
+ }
170
456
 
171
457
  const controller = new AbortController();
172
458
  const onInterrupt = (): void => controller.abort(new Error(cliText(
@@ -202,8 +488,8 @@ async function initializeFeishu(
202
488
 
203
489
  if (!options.json) {
204
490
  process.stdout.write(cliText(
205
- "\nChecking Lark app permissions, events, and callbacks...\n",
206
- "\n正在检查飞书应用权限、事件和回调...\n",
491
+ "Checking app permissions, events, and callbacks...\n",
492
+ "正在检查应用权限、事件和回调...\n",
207
493
  ));
208
494
  }
209
495
  const manualPermissionSkip = new AbortController();
@@ -624,8 +910,8 @@ function printSkillStatus(status: SkillRegistrationStatus): void {
624
910
 
625
911
  function printInitializationPaths(result: InitializationResult): void {
626
912
  process.stdout.write(cliText(
627
- "Preparing the Agent Bot user environment...\n",
628
- "正在准备 Agent Bot 用户环境...\n",
913
+ "Profile setup\n",
914
+ "Profile 设置\n",
629
915
  ));
630
916
  if (result.reset) {
631
917
  process.stdout.write(`${cliText("Reset backup: ", "重置备份:")}${result.reset.backupPath}\n`);
@@ -642,6 +928,7 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
642
928
  "\nAgent Bot initialization completed.\n",
643
929
  "\nAgent Bot 初始化完成。\n",
644
930
  ));
931
+ process.stdout.write(`${cliText("Default Agent: ", "默认 Agent:")}${result.defaultAgent.name}\n`);
645
932
  if (result.feishu.status === "created") {
646
933
  process.stdout.write(cliText(
647
934
  `Lark app: created and credentials saved (${result.feishu.appId})\n`,
@@ -852,6 +1139,7 @@ function feishuFeatureWarnings(missing: FeishuConfigurationChallenge["missing"])
852
1139
 
853
1140
  function initializationStatusLabel(status: InitializationStatus): string {
854
1141
  if (status === "created") return cliText("created", "已创建");
1142
+ if (status === "updated") return cliText("updated with missing settings", "已补全缺失配置");
855
1143
  if (status === "reset") return cliText("reset from template", "已从模板重置");
856
1144
  return cliText("already exists; unchanged", "已存在,未修改");
857
1145
  }
@@ -108,7 +108,7 @@ export class AppServerConnection {
108
108
  try {
109
109
  message = JSON.parse(line) as AppServerMessage;
110
110
  } catch (error) {
111
- this.logger.warn({ line, error }, "Ignoring non-JSON Codex App Server stdout line.");
111
+ this.logger.warn({ line, error }, "Ignoring non-JSON App Server stdout line.");
112
112
  return;
113
113
  }
114
114
  this.handleMessage(message);
@@ -128,7 +128,7 @@ export class AppServerConnection {
128
128
  for (const listener of this.notificationListeners) listener(message.method, message.params);
129
129
  return;
130
130
  }
131
- this.logger.warn({ message }, "Ignoring unknown Codex App Server message shape.");
131
+ this.logger.warn({ message }, "Ignoring unknown App Server message shape.");
132
132
  }
133
133
 
134
134
  private handleResponse(message: AppServerResponse): void {
@@ -139,7 +139,7 @@ export class AppServerConnection {
139
139
  responseId: message.id,
140
140
  responseKind: "error" in message ? "error" : "result",
141
141
  },
142
- "Received Codex App Server response for unknown request.",
142
+ "Received App Server response for unknown request.",
143
143
  );
144
144
  return;
145
145
  }
@@ -27,15 +27,15 @@ export class CodexProcessManager implements AppServerClientProvider {
27
27
  const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
28
28
  this.client = client;
29
29
  readline.createInterface({ input: child.stderr, crlfDelay: Infinity }).on("line", (line) => {
30
- this.logger.debug({ line }, "Codex App Server stderr.");
30
+ this.logger.debug({ line }, "App Server stderr.");
31
31
  });
32
- child.once("error", (error) => this.logger.error({ error }, "Codex App Server process error."));
32
+ child.once("error", (error) => this.logger.error({ error }, "App Server process error."));
33
33
  child.once("exit", (code, signal) => {
34
- this.logger.warn({ code, signal }, "Codex App Server process exited.");
34
+ this.logger.warn({ code, signal }, "App Server process exited.");
35
35
  client.close();
36
36
  if (this.client === client) this.client = undefined;
37
37
  if (this.child === child) this.child = undefined;
38
- const error = new Error(`Codex App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
38
+ const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
39
39
  for (const listener of this.disconnectListeners) listener(error);
40
40
  });
41
41
  await client.request("initialize", {
@@ -163,7 +163,7 @@ export class CodexRuntime implements AgentRuntime {
163
163
  if (!isUnsupportedExcludeTurnsError(error)) throw error;
164
164
  this.logger.warn(
165
165
  { error },
166
- "Codex App Server does not support thread/fork excludeTurns; retrying without it.",
166
+ "App Server does not support thread/fork excludeTurns; retrying without it.",
167
167
  );
168
168
  response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
169
169
  }
@@ -343,7 +343,7 @@ export class CodexRuntime implements AgentRuntime {
343
343
  );
344
344
  this.logger.info(
345
345
  { ...(sessionId ? { sessionId } : {}), threadId: remoteSessionId, turnId },
346
- "Codex accepted the turn interrupt request.",
346
+ "App Server accepted the turn interrupt request.",
347
347
  );
348
348
  }
349
349
 
@@ -554,7 +554,7 @@ export class CodexRuntime implements AgentRuntime {
554
554
  const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === threadId);
555
555
  if (session?.activeTurnId && status && status !== "active") {
556
556
  void this.synchronizeSession(session.localSessionId).catch((error: unknown) => {
557
- this.logger.warn({ error, sessionId: session.localSessionId, status }, "Failed to reconcile Codex thread status.");
557
+ this.logger.warn({ error, sessionId: session.localSessionId, status }, "Failed to reconcile App Server thread status.");
558
558
  });
559
559
  }
560
560
  return;
@@ -566,7 +566,7 @@ export class CodexRuntime implements AgentRuntime {
566
566
  if (session.terminalTurnIds.has(mapped.turnId)) {
567
567
  this.logger.debug(
568
568
  { sessionId: session.localSessionId, turnId: mapped.turnId, method },
569
- "Ignoring a replayed terminal Codex turn event.",
569
+ "Ignoring a replayed terminal App Server turn event.",
570
570
  );
571
571
  return;
572
572
  }
@@ -586,7 +586,7 @@ export class CodexRuntime implements AgentRuntime {
586
586
  notificationTurnId: mapped.turnId,
587
587
  notificationStartedAt: mapped.startedAt,
588
588
  },
589
- "Ignoring a replayed historical Codex turn start.",
589
+ "Ignoring a replayed historical App Server turn start.",
590
590
  );
591
591
  return;
592
592
  }
@@ -597,10 +597,10 @@ export class CodexRuntime implements AgentRuntime {
597
597
  if (!session.activeTurnId || session.activeTurnId !== mapped.turnId) {
598
598
  this.logger.debug(
599
599
  { sessionId: session.localSessionId, activeTurnId: session.activeTurnId, notificationTurnId: mapped.turnId, method },
600
- "Ignoring out-of-order Codex notification and scheduling reconciliation.",
600
+ "Ignoring out-of-order App Server notification and scheduling reconciliation.",
601
601
  );
602
602
  void this.synchronizeSession(session.localSessionId).catch((error: unknown) => {
603
- this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to reconcile out-of-order Codex notification.");
603
+ this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to reconcile out-of-order App Server notification.");
604
604
  });
605
605
  return;
606
606
  }
@@ -671,9 +671,12 @@ export class CodexRuntime implements AgentRuntime {
671
671
  if (mapped.status === "cancelled") {
672
672
  this.emit({ type: "turn_cancelled", sessionId, turnId: mapped.turnId });
673
673
  } else if (mapped.status === "failed") {
674
- this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "Codex turn failed." });
674
+ this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "App Server turn failed." });
675
675
  } else {
676
- const finalResponse = appendGeneratedImageMarkdown(session.finalText, session.generatedImagePaths);
676
+ const finalResponse = appendModelSwitchGuidance(
677
+ appendGeneratedImageMarkdown(session.finalText, session.generatedImagePaths),
678
+ session.model,
679
+ );
677
680
  this.emit({
678
681
  type: "turn_completed",
679
682
  sessionId,
@@ -703,7 +706,7 @@ export class CodexRuntime implements AgentRuntime {
703
706
  turnId,
704
707
  request: {
705
708
  id: requestId,
706
- title: stringValue(params.command) ?? "Codex approval request",
709
+ title: stringValue(params.command) ?? "Agent approval request",
707
710
  command: stringValue(params.command),
708
711
  reason: stringValue(params.reason),
709
712
  options: [
@@ -781,7 +784,7 @@ export class CodexRuntime implements AgentRuntime {
781
784
  type: "turn_failed",
782
785
  sessionId: session.localSessionId,
783
786
  turnId: activeTurnId,
784
- message: "Codex no longer reports this execution in the thread history.",
787
+ message: "The App Server no longer reports this execution in the thread history.",
785
788
  });
786
789
  }
787
790
  return;
@@ -828,7 +831,7 @@ export class CodexRuntime implements AgentRuntime {
828
831
  type: "turn_failed",
829
832
  sessionId: session.localSessionId,
830
833
  turnId: turn.id,
831
- message: turn.error?.message ?? "Codex turn failed.",
834
+ message: turn.error?.message ?? "App Server turn failed.",
832
835
  });
833
836
  return;
834
837
  }
@@ -836,9 +839,12 @@ export class CodexRuntime implements AgentRuntime {
836
839
  ...session.generatedImagePaths,
837
840
  ...extractGeneratedImagePaths(turn),
838
841
  ]);
839
- const finalResponse = appendGeneratedImageMarkdown(
840
- extractFinalResponse(turn) || session.finalText,
841
- generatedImagePaths,
842
+ const finalResponse = appendModelSwitchGuidance(
843
+ appendGeneratedImageMarkdown(
844
+ extractFinalResponse(turn) || session.finalText,
845
+ generatedImagePaths,
846
+ ),
847
+ session.model,
842
848
  );
843
849
  session.finalText = finalResponse;
844
850
  session.generatedImagePaths = [];
@@ -888,13 +894,13 @@ export class CodexRuntime implements AgentRuntime {
888
894
 
889
895
  private requireSession(sessionId: string): CodexSession {
890
896
  const session = this.sessions.get(sessionId);
891
- if (!session) throw new Error(`Unknown Codex session: ${sessionId}`);
897
+ if (!session) throw new Error(`Unknown App Server session: ${sessionId}`);
892
898
  return session;
893
899
  }
894
900
 
895
901
  private requireActiveTurn(sessionId: string, turnId: string): CodexSession {
896
902
  const session = this.requireSession(sessionId);
897
- if (session.activeTurnId !== turnId) throw new Error(`Codex turn is no longer active: ${turnId}`);
903
+ if (session.activeTurnId !== turnId) throw new Error(`App Server turn is no longer active: ${turnId}`);
898
904
  return session;
899
905
  }
900
906
 
@@ -917,7 +923,7 @@ export class CodexRuntime implements AgentRuntime {
917
923
  type: "turn_failed",
918
924
  sessionId: session.localSessionId,
919
925
  turnId,
920
- message: `Codex App Server disconnected: ${error.message}`,
926
+ message: `App Server disconnected: ${error.message}`,
921
927
  });
922
928
  }
923
929
  for (const [requestId, pending] of this.approvals) {
@@ -978,6 +984,20 @@ function permissionParams(mode: PermissionMode): { approvalPolicy: "never" | "on
978
984
  : { approvalPolicy: "on-request", sandbox: "workspace-write" };
979
985
  }
980
986
 
987
+ const MODEL_SWITCH_GUIDANCE = "请发送 `/model` 切换到其他模型后重试。";
988
+
989
+ function appendModelSwitchGuidance(response: string, model?: string): string {
990
+ if (!hasKnownCodeModeFailure(response) || response.includes(MODEL_SWITCH_GUIDANCE)) return response;
991
+ const modelLabel = model?.trim() ? ` \`${model.trim()}\`` : "";
992
+ const guidance = `> 当前模型${modelLabel} 的本地工具执行通道异常。${MODEL_SWITCH_GUIDANCE}`;
993
+ return response.trimEnd() ? `${response.trimEnd()}\n\n${guidance}` : guidance;
994
+ }
995
+
996
+ function hasKnownCodeModeFailure(response: string): boolean {
997
+ return /code-mode host closed its stdout/i.test(response)
998
+ || /exec expects an object containing raw JavaScript in [`'"]?input[`'"]?/i.test(response);
999
+ }
1000
+
981
1001
  function threadLifecycleParams(cwd: string): {
982
1002
  developerInstructions: string;
983
1003
  } {
@@ -43,30 +43,6 @@ agents:
43
43
  - "stdio://"
44
44
  env: {}
45
45
 
46
- coco:
47
- title: "Coco"
48
- command: "coco"
49
- args:
50
- - "acp"
51
- - "serve"
52
- env: {}
53
-
54
- coco-yolo:
55
- title: "Coco YOLO"
56
- command: "coco"
57
- args:
58
- - "acp"
59
- - "serve"
60
- - "--yolo"
61
- env: {}
62
-
63
- example:
64
- title: "Example ACP Agent"
65
- command: "node"
66
- args:
67
- - "./examples/example-acp-agent.js"
68
- env: {}
69
-
70
46
  defaults:
71
47
  agent: "codex"
72
48
  cwd: "."
@@ -12,8 +12,8 @@ export class ConsoleConnector {
12
12
 
13
13
  start(): void {
14
14
  if (this.reader) return;
15
- this.logger.info("Starting local Codex test console.");
16
- console.log("Codex console ready. Type /help or enter a prompt.");
15
+ this.logger.info("Starting the local Agent Bot console.");
16
+ console.log("Agent Bot console ready. Type /help or enter a prompt.");
17
17
  const reader = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "agentbot> " });
18
18
  this.reader = reader;
19
19
  reader.prompt();