@keyou007/agent-bot 0.1.24-alpha.0 → 0.1.24-alpha.2

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 (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +24 -4
  3. package/README.zh.md +24 -4
  4. package/dist/cli/SelfUpdateRunner.js +2 -0
  5. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  6. package/dist/codex/CodexEventMapper.js +90 -2
  7. package/dist/codex/CodexEventMapper.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +160 -12
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/commands/CommandRouter.js +14 -1
  11. package/dist/commands/CommandRouter.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +42 -11
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/feishu/FeishuMarkdown.js +70 -4
  15. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  16. package/dist/feishu/FeishuTurnPresenter.js +2 -1
  17. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/local-files/TurnPreviewPage.js +16 -1
  21. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  22. package/dist/presentation/TurnStateReducer.js +37 -7
  23. package/dist/presentation/TurnStateReducer.js.map +1 -1
  24. package/dist/proxy/ProxySessionController.js +58 -38
  25. package/dist/proxy/ProxySessionController.js.map +1 -1
  26. package/dist/supervision/SafeRestartNotifier.js +2 -0
  27. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  28. package/dist/supervision/SafeRestartScheduler.js +7 -0
  29. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  30. package/npm-shrinkwrap.json +2 -2
  31. package/package.json +1 -1
  32. package/skills/agent-bot/SKILL.md +12 -0
  33. package/src/cli/SelfUpdateRunner.ts +2 -0
  34. package/src/codex/CodexEventMapper.ts +97 -3
  35. package/src/codex/CodexRuntime.ts +163 -13
  36. package/src/commands/CommandRouter.ts +13 -2
  37. package/src/commands/commandTypes.ts +2 -2
  38. package/src/feishu/CardRenderer.ts +48 -12
  39. package/src/feishu/FeishuMarkdown.ts +69 -3
  40. package/src/feishu/FeishuTurnPresenter.ts +1 -0
  41. package/src/index.ts +4 -0
  42. package/src/local-files/TurnPreviewPage.ts +16 -1
  43. package/src/presentation/TurnStateReducer.ts +49 -9
  44. package/src/presentation/turnViewTypes.ts +4 -0
  45. package/src/proxy/ProxySessionController.ts +61 -40
  46. package/src/runtime/types.ts +13 -1
  47. package/src/supervision/SafeRestartNotifier.ts +2 -0
  48. package/src/supervision/SafeRestartScheduler.ts +7 -0
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
2
2
  import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
3
3
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
4
4
  import { AppServerRequestError } from "./AppServerConnection.js";
5
- import { mapCodexNotification } from "./CodexEventMapper.js";
5
+ import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
6
6
  import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
7
7
  import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
8
8
  import { threadWriterLockPath } from "./ThreadWriterProcess.js";
@@ -23,6 +23,7 @@ const FORK_SOURCE_TURN_PAGE_SIZE = 20;
23
23
  const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
24
24
  const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"];
25
25
  const BUILT_IN_CODEX_PROVIDER_ID = "openai";
26
+ const BUILT_IN_TRAEX_PROVIDER_ID = "trae";
26
27
  // A timed-out fork keeps running in App Server and can create an orphan thread.
27
28
  // Wait for its response; connection closure still rejects the request.
28
29
  const FORK_REQUEST_TIMEOUT_MS = 0;
@@ -33,6 +34,7 @@ export class CodexRuntime {
33
34
  sessions = new Map();
34
35
  listeners = new Set();
35
36
  approvals = new Map();
37
+ modeChanges = new Map();
36
38
  attachedClient;
37
39
  unsubscribe;
38
40
  unsubscribeDisconnect;
@@ -648,6 +650,37 @@ export class CodexRuntime {
648
650
  }
649
651
  }
650
652
  async respondToApproval(sessionId, requestId, decision) {
653
+ const modeChange = this.modeChanges.get(requestId);
654
+ if (modeChange) {
655
+ if (modeChange.sessionId !== sessionId
656
+ || this.sessions.get(sessionId)?.activeTurnId !== modeChange.turnId) {
657
+ throw new Error("Approval request is no longer pending.");
658
+ }
659
+ if (decision !== "accept" && decision !== "decline" && decision !== "cancel") {
660
+ throw new Error("Invalid mode change decision.");
661
+ }
662
+ if (modeChange.responding)
663
+ throw new Error("Approval response is already in progress.");
664
+ modeChange.responding = true;
665
+ try {
666
+ const client = await this.client();
667
+ if (this.modeChanges.get(requestId) !== modeChange)
668
+ throw new Error("Approval request is no longer pending.");
669
+ await client.request("mode/change/respond", {
670
+ threadId: this.requireSession(sessionId).remoteSessionId,
671
+ requestId: modeChange.requestId,
672
+ decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
673
+ }, CONTROL_REQUEST_TIMEOUT_MS);
674
+ if (this.modeChanges.get(requestId) === modeChange) {
675
+ this.modeChanges.delete(requestId);
676
+ this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision });
677
+ }
678
+ }
679
+ finally {
680
+ modeChange.responding = false;
681
+ }
682
+ return;
683
+ }
651
684
  const pending = this.approvals.get(requestId);
652
685
  if (!pending || pending.sessionId !== sessionId)
653
686
  throw new Error("Approval request is no longer pending.");
@@ -656,10 +689,15 @@ export class CodexRuntime {
656
689
  this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
657
690
  }
658
691
  async listModels(modelProvider, fallbackModel) {
692
+ return (await this.listModelsWithStatus(modelProvider, fallbackModel)).models;
693
+ }
694
+ async listModelsWithStatus(modelProvider, fallbackModel) {
659
695
  const requestedProvider = modelProvider?.trim();
696
+ const agentFamily = this.provider.getAgentFamily?.();
660
697
  if (!requestedProvider
661
- || (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && this.provider.getAgentFamily?.() !== "traex")) {
662
- return this.listCatalogModels();
698
+ || (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && agentFamily !== "traex")
699
+ || (requestedProvider === BUILT_IN_TRAEX_PROVIDER_ID && agentFamily === "traex")) {
700
+ return { models: await this.listCatalogModels() };
663
701
  }
664
702
  const config = await this.readConfig();
665
703
  const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
@@ -675,20 +713,24 @@ export class CodexRuntime {
675
713
  ? stringValue(config.model)?.trim()
676
714
  : undefined;
677
715
  try {
678
- return await fetchProviderModels({
716
+ const models = await fetchProviderModels({
679
717
  providerId: requestedProvider,
680
718
  config: (isRecord(providerConfig) ? providerConfig : {}),
681
719
  catalog,
682
720
  configuredDefaultModel,
683
721
  environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
684
722
  });
723
+ return { models };
685
724
  }
686
725
  catch (error) {
687
726
  const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
688
727
  if (!fallback)
689
728
  throw error;
690
729
  this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id }, "Provider model discovery failed; using the current or configured model as a fallback.");
691
- return [fallback];
730
+ return {
731
+ models: [fallback],
732
+ warning: `${error instanceof Error ? error.message : "Provider 模型列表读取失败。"} 暂时仅显示当前或已配置模型,服务可用性未确认。`,
733
+ };
692
734
  }
693
735
  }
694
736
  async listCatalogModels() {
@@ -771,6 +813,7 @@ export class CodexRuntime {
771
813
  for (const pending of this.approvals.values())
772
814
  pending.resolve({ decision: "cancel" });
773
815
  this.approvals.clear();
816
+ this.modeChanges.clear();
774
817
  this.sessionSyncs.clear();
775
818
  this.sessions.clear();
776
819
  this.provider.close();
@@ -858,13 +901,54 @@ export class CodexRuntime {
858
901
  return;
859
902
  }
860
903
  const sessionId = session.localSessionId;
861
- if (mapped.kind === "token_usage") {
904
+ if (mapped.kind === "mode_change_requested") {
905
+ const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
906
+ if (session.modeChangeRequestIds.has(requestId))
907
+ return;
908
+ session.modeChangeRequestIds.add(requestId);
909
+ this.modeChanges.set(requestId, {
910
+ sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
911
+ targetMode: mapped.targetMode, responding: false,
912
+ });
913
+ this.emit({
914
+ type: "approval_requested", sessionId, turnId: mapped.turnId,
915
+ request: {
916
+ id: requestId,
917
+ kind: "mode_change",
918
+ title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
919
+ reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
920
+ .filter(Boolean).join("\n"),
921
+ options: [
922
+ { id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
923
+ { id: "decline", label: "Reject" },
924
+ { id: "cancel", label: "Cancel Request" },
925
+ ],
926
+ },
927
+ });
928
+ }
929
+ else if (mapped.kind === "mode_change_resolved") {
930
+ const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
931
+ const pending = this.modeChanges.get(requestId);
932
+ if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
933
+ || pending.targetMode !== mapped.targetMode)
934
+ return;
935
+ this.modeChanges.delete(requestId);
936
+ this.emit({
937
+ type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
938
+ decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
939
+ });
940
+ }
941
+ else if (mapped.kind === "token_usage") {
862
942
  this.emit({
863
943
  type: "token_usage_updated",
864
944
  sessionId,
865
945
  turnId: mapped.turnId,
866
946
  lastTokens: mapped.lastTokens,
867
947
  cumulativeTokens: mapped.cumulativeTokens,
948
+ lastTotalTokens: mapped.lastTotalTokens,
949
+ cumulativeTotalTokens: mapped.cumulativeTotalTokens,
950
+ lastCachedTokens: mapped.lastCachedTokens,
951
+ cumulativeCachedTokens: mapped.cumulativeCachedTokens,
868
952
  contextTokens: mapped.contextTokens,
869
953
  });
870
954
  }
@@ -882,9 +966,19 @@ export class CodexRuntime {
882
966
  }
883
967
  else if (mapped.kind === "agent_message_phase") {
884
968
  session.messagePhases.set(mapped.itemId, mapped.phase);
969
+ if (mapped.phase === "final_answer")
970
+ this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
971
+ else if (session.unphasedFinalMessageId === mapped.itemId)
972
+ session.unphasedFinalMessageId = undefined;
885
973
  }
886
974
  else if (mapped.kind === "agent_delta") {
887
- if (session.messagePhases.get(mapped.itemId) === "commentary") {
975
+ const phase = session.messagePhases.get(mapped.itemId);
976
+ if (phase !== "final_answer") {
977
+ // Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
978
+ if (phase === undefined) {
979
+ session.unphasedMessages.set(mapped.itemId, (session.unphasedMessages.get(mapped.itemId) ?? "") + mapped.text);
980
+ session.unphasedFinalMessageId = mapped.itemId;
981
+ }
888
982
  this.emit({
889
983
  type: "progress",
890
984
  sessionId,
@@ -899,6 +993,19 @@ export class CodexRuntime {
899
993
  this.emit({ type: "agent_text_delta", sessionId, turnId: mapped.turnId, text: mapped.text });
900
994
  }
901
995
  }
996
+ else if (mapped.kind === "runtime_error") {
997
+ this.logger.warn({ sessionId, turnId: mapped.turnId, willRetry: mapped.willRetry, error: mapped.message }, "App Server reported a turn error.");
998
+ // Only turn/completed terminates the turn; native retries keep the same progress card.
999
+ this.emit({
1000
+ type: "progress",
1001
+ sessionId,
1002
+ turnId: mapped.turnId,
1003
+ activityId: `commentary:runtime-error:${mapped.turnId}`,
1004
+ text: `${mapped.willRetry ? "运行请求出错,Agent 正在重试" : "运行请求失败,等待 Agent 返回最终状态"}:${mapped.message}`,
1005
+ append: false,
1006
+ severity: "warning",
1007
+ });
1008
+ }
902
1009
  else if (mapped.kind === "progress") {
903
1010
  this.emit({
904
1011
  type: "progress",
@@ -913,6 +1020,8 @@ export class CodexRuntime {
913
1020
  this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
914
1021
  }
915
1022
  else if (mapped.kind === "tool") {
1023
+ if (mapped.phase === "started")
1024
+ session.unphasedFinalMessageId = undefined;
916
1025
  if (mapped.phase === "updated"
917
1026
  && mapped.tool.kind === "image_generation"
918
1027
  && mapped.tool.status === "completed"
@@ -936,6 +1045,11 @@ export class CodexRuntime {
936
1045
  });
937
1046
  }
938
1047
  else if (mapped.kind === "terminal") {
1048
+ if (mapped.status === "completed" && !session.finalText && session.unphasedFinalMessageId) {
1049
+ this.promoteUnphasedMessage(session, mapped.turnId, session.unphasedFinalMessageId);
1050
+ }
1051
+ session.unphasedMessages.clear();
1052
+ session.unphasedFinalMessageId = undefined;
939
1053
  session.activeTurnId = undefined;
940
1054
  session.activeTurnStartedAt = undefined;
941
1055
  session.terminalTurnIds.add(mapped.turnId);
@@ -992,6 +1106,19 @@ export class CodexRuntime {
992
1106
  });
993
1107
  return response;
994
1108
  }
1109
+ promoteUnphasedMessage(session, turnId, itemId) {
1110
+ const text = session.unphasedMessages.get(itemId);
1111
+ if (text === undefined)
1112
+ return;
1113
+ session.unphasedMessages.delete(itemId);
1114
+ if (session.unphasedFinalMessageId === itemId)
1115
+ session.unphasedFinalMessageId = undefined;
1116
+ session.finalText += text;
1117
+ this.emit({
1118
+ type: "agent_text_delta", sessionId: session.localSessionId, turnId, text,
1119
+ replacesActivityId: `commentary:${itemId}`,
1120
+ });
1121
+ }
995
1122
  async resolveReasoningEffort(input, response) {
996
1123
  if (input.reasoningEffort)
997
1124
  return input.reasoningEffort;
@@ -1081,6 +1208,9 @@ export class CodexRuntime {
1081
1208
  }
1082
1209
  session.activeTurnId = turnId;
1083
1210
  session.activeTurnStartedAt = startedAt;
1211
+ session.modeChangeRequestIds.clear();
1212
+ session.unphasedMessages.clear();
1213
+ session.unphasedFinalMessageId = undefined;
1084
1214
  session.finalText = "";
1085
1215
  session.generatedImagePaths = [];
1086
1216
  session.messagePhases.clear();
@@ -1098,6 +1228,14 @@ export class CodexRuntime {
1098
1228
  finishSnapshotTurn(session, turn) {
1099
1229
  if (session.activeTurnId !== turn.id)
1100
1230
  return;
1231
+ if (turn.status === "completed") {
1232
+ for (const item of finalResponseMessages(turn)) {
1233
+ if (item.id)
1234
+ this.promoteUnphasedMessage(session, turn.id, item.id);
1235
+ }
1236
+ }
1237
+ session.unphasedMessages.clear();
1238
+ session.unphasedFinalMessageId = undefined;
1101
1239
  session.activeTurnId = undefined;
1102
1240
  session.activeTurnStartedAt = undefined;
1103
1241
  session.terminalTurnIds.add(turn.id);
@@ -1111,7 +1249,7 @@ export class CodexRuntime {
1111
1249
  type: "turn_failed",
1112
1250
  sessionId: session.localSessionId,
1113
1251
  turnId: turn.id,
1114
- message: turn.error?.message ?? "App Server turn failed.",
1252
+ message: formatCodexError(turn.error) ?? "App Server turn failed.",
1115
1253
  });
1116
1254
  return;
1117
1255
  }
@@ -1152,9 +1290,11 @@ export class CodexRuntime {
1152
1290
  && input.lastTurnStatus !== "running"
1153
1291
  ? [input.lastTurnId]
1154
1292
  : []),
1293
+ modeChangeRequestIds: new Set(),
1155
1294
  finalText: "",
1156
1295
  generatedImagePaths: [],
1157
1296
  messagePhases: new Map(),
1297
+ unphasedMessages: new Map(),
1158
1298
  needsResume: false,
1159
1299
  canReplaceEmptyThread: !("remoteSessionId" in input),
1160
1300
  rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
@@ -1187,6 +1327,12 @@ export class CodexRuntime {
1187
1327
  return session;
1188
1328
  }
1189
1329
  emit(event) {
1330
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1331
+ for (const [requestId, pending] of this.modeChanges) {
1332
+ if (pending.sessionId === event.sessionId && pending.turnId === event.turnId)
1333
+ this.modeChanges.delete(requestId);
1334
+ }
1335
+ }
1190
1336
  for (const listener of this.listeners)
1191
1337
  listener(event);
1192
1338
  }
@@ -1194,6 +1340,7 @@ export class CodexRuntime {
1194
1340
  this.unsubscribe?.();
1195
1341
  this.unsubscribe = undefined;
1196
1342
  this.attachedClient = undefined;
1343
+ this.modeChanges.clear();
1197
1344
  for (const session of this.sessions.values()) {
1198
1345
  session.needsResume = true;
1199
1346
  const turnId = session.activeTurnId;
@@ -1285,11 +1432,12 @@ function turnStartedAt(turn) {
1285
1432
  return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
1286
1433
  }
1287
1434
  function extractFinalResponse(turn) {
1435
+ return finalResponseMessages(turn).map((item) => item.text.trim()).join("\n\n");
1436
+ }
1437
+ function finalResponseMessages(turn) {
1288
1438
  const messages = (turn.items ?? []).filter((item) => item.type === "agentMessage" && typeof item.text === "string" && item.text.trim());
1289
1439
  const finalMessages = messages.filter((item) => item.phase === "final_answer");
1290
- return (finalMessages.length ? finalMessages : messages.slice(-1))
1291
- .map((item) => item.text.trim())
1292
- .join("\n\n");
1440
+ return finalMessages.length ? finalMessages : messages.filter((item) => item.phase !== "commentary").slice(-1);
1293
1441
  }
1294
1442
  function extractGeneratedImagePaths(turn) {
1295
1443
  return uniqueStrings((turn.items ?? []).flatMap((item) => {
@@ -1353,7 +1501,7 @@ function remoteSessionSummary(thread) {
1353
1501
  finalResponse: lastTurn && lastTurn.status !== "inProgress"
1354
1502
  ? appendGeneratedImageMarkdown(extractFinalResponse(lastTurn), extractGeneratedImagePaths(lastTurn)) || undefined
1355
1503
  : undefined,
1356
- lastError: lastTurn?.error?.message,
1504
+ lastError: formatCodexError(lastTurn?.error),
1357
1505
  lastTurnToolCount: toolCounts?.total,
1358
1506
  lastTurnCompletedToolCount: toolCounts?.completed,
1359
1507
  lastTurnFailedToolCount: toolCounts?.failed,