@keyou007/agent-bot 0.1.20 → 0.1.21

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 (59) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +15 -5
  3. package/README.zh.md +15 -5
  4. package/dist/codex/CodexEventMapper.js +21 -1
  5. package/dist/codex/CodexEventMapper.js.map +1 -1
  6. package/dist/codex/CodexProcessManager.js +3 -0
  7. package/dist/codex/CodexProcessManager.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +136 -4
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/codex/ProviderModels.js +159 -0
  11. package/dist/codex/ProviderModels.js.map +1 -0
  12. package/dist/commands/CommandRouter.js +8 -3
  13. package/dist/commands/CommandRouter.js.map +1 -1
  14. package/dist/console/ConsoleTurnPresenter.js +2 -1
  15. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  16. package/dist/feishu/CardRenderer.js +46 -16
  17. package/dist/feishu/CardRenderer.js.map +1 -1
  18. package/dist/feishu/FeishuTurnPresenter.js +34 -8
  19. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/local-files/LocalFileViewerServer.js +151 -16
  23. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  24. package/dist/local-files/TurnPreviewPage.js +725 -0
  25. package/dist/local-files/TurnPreviewPage.js.map +1 -0
  26. package/dist/presentation/OutboundRouter.js +6 -1
  27. package/dist/presentation/OutboundRouter.js.map +1 -1
  28. package/dist/presentation/TurnStateReducer.js +142 -3
  29. package/dist/presentation/TurnStateReducer.js.map +1 -1
  30. package/dist/proxy/ProxySessionController.js +186 -80
  31. package/dist/proxy/ProxySessionController.js.map +1 -1
  32. package/dist/state/StateStore.js +7 -0
  33. package/dist/state/StateStore.js.map +1 -1
  34. package/dist/utils/formatStorageSize.js +15 -0
  35. package/dist/utils/formatStorageSize.js.map +1 -0
  36. package/docs/technical-reference.md +7 -1
  37. package/docs/technical-reference.zh.md +7 -1
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/skills/agent-bot/SKILL.md +5 -1
  41. package/src/codex/CodexEventMapper.ts +31 -1
  42. package/src/codex/CodexProcessManager.ts +4 -0
  43. package/src/codex/CodexRuntime.ts +153 -9
  44. package/src/codex/ProviderModels.ts +185 -0
  45. package/src/commands/CommandRouter.ts +7 -2
  46. package/src/commands/commandTypes.ts +1 -1
  47. package/src/console/ConsoleTurnPresenter.ts +3 -0
  48. package/src/feishu/CardRenderer.ts +47 -16
  49. package/src/feishu/FeishuTurnPresenter.ts +46 -4
  50. package/src/index.ts +2 -0
  51. package/src/local-files/LocalFileViewerServer.ts +166 -17
  52. package/src/local-files/TurnPreviewPage.ts +792 -0
  53. package/src/presentation/OutboundRouter.ts +8 -0
  54. package/src/presentation/TurnStateReducer.ts +175 -2
  55. package/src/presentation/turnViewTypes.ts +13 -0
  56. package/src/proxy/ProxySessionController.ts +186 -67
  57. package/src/runtime/types.ts +18 -1
  58. package/src/state/StateStore.ts +9 -0
  59. package/src/utils/formatStorageSize.ts +13 -0
@@ -21,6 +21,7 @@ import { errorLogValue } from "../logging/errorLogValue.js";
21
21
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
22
22
  import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
23
23
  import { createId } from "../utils/id.js";
24
+ import { formatStorageSize } from "../utils/formatStorageSize.js";
24
25
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
25
26
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
26
27
  import { executeShellCommand, } from "../utils/executeShellCommand.js";
@@ -108,7 +109,7 @@ const HELP_COMMAND_SECTIONS = [
108
109
  usage: "[序号或任务 ID]",
109
110
  description: "从当前或指定任务最近完成轮次创建分支",
110
111
  },
111
- { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
112
+ { command: "/turns", usage: "[Turn ID 或序号]", description: "浏览历史轮次,并可 Reset 对话上下文;带参数查看运行详情" },
112
113
  {
113
114
  command: "/title",
114
115
  usage: "<新标题>",
@@ -1031,7 +1032,7 @@ export class ProxySessionController {
1031
1032
  ?? { ...record, contextKey: responseContextKey };
1032
1033
  const previous = this.messageQueues.get(responseContextKey) ?? Promise.resolve();
1033
1034
  const next = previous.catch(() => undefined).then(async () => {
1034
- this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName));
1035
+ this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName), scopedRecord.model);
1035
1036
  const promptMessageId = await this.outbound.withReplyTarget(responseContextKey, existingReplyTarget, () => this.outbound.sendText(responseContextKey, promptText));
1036
1037
  const turnReplyTarget = isThreadContextKey(responseContextKey)
1037
1038
  ? promptMessageId
@@ -1200,16 +1201,11 @@ export class ProxySessionController {
1200
1201
  const loaded = await this.loadSession(record);
1201
1202
  if (setting === "provider" && nextValue) {
1202
1203
  await this.assertModelProvider(loaded, nextValue);
1203
- const models = await loaded.runtime.listModels();
1204
- const model = models.find((candidate) => candidate.id === loaded.session.model)
1205
- ?? models.find((candidate) => candidate.isDefault);
1204
+ const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
1205
+ const model = selectProviderModel(models, loaded.session.model);
1206
1206
  if (!model)
1207
1207
  throw new Error("The current runtime has no model available for a Provider change.");
1208
- const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
1209
- ? loaded.session.reasoningEffort
1210
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1211
- if (!effort)
1212
- throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
1208
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
1213
1209
  await this.changeProviderSettings(loaded, {
1214
1210
  modelProvider: nextValue,
1215
1211
  model: model.id,
@@ -1218,12 +1214,13 @@ export class ProxySessionController {
1218
1214
  });
1219
1215
  }
1220
1216
  else if (setting === "model" && nextValue) {
1221
- const models = await loaded.runtime.listModels();
1217
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1222
1218
  const selected = models.find((candidate) => candidate.id === nextValue);
1223
1219
  if (!selected)
1224
1220
  throw new Error(`Unknown model: ${nextValue}`);
1225
1221
  const currentEffort = loaded.session.reasoningEffort;
1226
- const nextEffort = currentEffort && selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort)
1222
+ const nextEffort = currentEffort && (selected.supportedReasoningEfforts.length === 0
1223
+ || selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
1227
1224
  ? currentEffort
1228
1225
  : selected.defaultReasoningEffort;
1229
1226
  await loaded.runtime.setModel(record.localSessionId, nextValue);
@@ -1233,7 +1230,7 @@ export class ProxySessionController {
1233
1230
  this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
1234
1231
  }
1235
1232
  else if (setting === "thinking" && nextValue) {
1236
- const models = await loaded.runtime.listModels();
1233
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1237
1234
  const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
1238
1235
  ?? models.find((candidate) => candidate.isDefault);
1239
1236
  if (!currentModel)
@@ -1255,7 +1252,7 @@ export class ProxySessionController {
1255
1252
  const session = this.store.getSession(record.localSessionId) ?? record;
1256
1253
  if (setting && setting !== "provider")
1257
1254
  await this.persistAgentExecutionDefaults(session.localSessionId);
1258
- const models = await loaded.runtime.listModels();
1255
+ const models = await loaded.runtime.listModels(session.modelProvider, session.model);
1259
1256
  const currentModel = models.find((candidate) => candidate.id === session.model)
1260
1257
  ?? models.find((candidate) => candidate.isDefault);
1261
1258
  const providers = loaded.runtime.listModelProviders
@@ -1542,8 +1539,14 @@ export class ProxySessionController {
1542
1539
  await this.setGroupMute(contextKey, command.enabled);
1543
1540
  return;
1544
1541
  case "turns":
1545
- await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
1546
- await this.openResetHistory(contextKey);
1542
+ if (command.turnReference) {
1543
+ await this.outbound.sendText(contextKey, "正在读取轮次运行信息,请稍后。");
1544
+ await this.openTurnDetails(contextKey, command.turnReference);
1545
+ }
1546
+ else {
1547
+ await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
1548
+ await this.openResetHistory(contextKey);
1549
+ }
1547
1550
  return;
1548
1551
  case "model":
1549
1552
  await this.openExecutionSettings(contextKey, "model");
@@ -1577,7 +1580,7 @@ export class ProxySessionController {
1577
1580
  : context.contextKey;
1578
1581
  if (!this.outbound.canRoute(turnContextKey))
1579
1582
  continue;
1580
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1583
+ this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1581
1584
  if (!session.lastTurnId)
1582
1585
  continue;
1583
1586
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error) => {
@@ -1651,7 +1654,7 @@ export class ProxySessionController {
1651
1654
  continue;
1652
1655
  }
1653
1656
  if (session) {
1654
- this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1657
+ this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1655
1658
  }
1656
1659
  try {
1657
1660
  await this.handleRuntimeEvent({
@@ -1763,7 +1766,7 @@ export class ProxySessionController {
1763
1766
  const replyTarget = attempt.replyMessageId
1764
1767
  ? { messageId: attempt.replyMessageId, replyInThread: true }
1765
1768
  : undefined;
1766
- this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName));
1769
+ this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1767
1770
  if (announce) {
1768
1771
  await this.outbound.withReplyTarget(contextKey, replyTarget, () => this.outbound.sendText(contextKey, "检测到任务在 Agent Bot 重启前尚未完成,正在自动恢复。")).catch((error) => {
1769
1772
  this.logger.warn({ error, attemptId: attempt.attemptId, contextKey }, "Failed to send the task recovery notification.");
@@ -1965,7 +1968,7 @@ export class ProxySessionController {
1965
1968
  }
1966
1969
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
1967
1970
  if (!configuredRuntime.getSession(record.localSessionId)) {
1968
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
1971
+ this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
1969
1972
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text);
1970
1973
  }
1971
1974
  let loaded;
@@ -2258,7 +2261,7 @@ export class ProxySessionController {
2258
2261
  permissionMode: source.permissionMode ?? "auto",
2259
2262
  });
2260
2263
  this.store.setCurrentSession(message.contextKey, localSessionId);
2261
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName));
2264
+ this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName), source.model);
2262
2265
  try {
2263
2266
  const forked = await runtime.forkSession({
2264
2267
  localSessionId,
@@ -2555,7 +2558,7 @@ export class ProxySessionController {
2555
2558
  lastTurnId: plan.lastTurnId,
2556
2559
  lastTurnStatus: plan.lastTurnStatus,
2557
2560
  });
2558
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
2561
+ this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName), plan.model);
2559
2562
  this.store.audit(contextKey, "session_forked", {
2560
2563
  sourceLocalSessionId: plan.source?.localSessionId,
2561
2564
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -2616,15 +2619,15 @@ export class ProxySessionController {
2616
2619
  return 0;
2617
2620
  }
2618
2621
  }
2619
- async hydrateTaskTurnHistory(current, requiredCount = RESET_HISTORY_PAGE_SIZE) {
2622
+ async hydrateTaskTurnHistory(current, requiredCount = RESET_HISTORY_PAGE_SIZE, targetTurnId) {
2620
2623
  const existing = this.turnHistoryHydrations.get(current.localSessionId);
2621
2624
  if (existing)
2622
2625
  await existing;
2623
2626
  const hydration = (async () => {
2624
2627
  if (this.store.getForkHistorySource(current.localSessionId)) {
2625
- return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount);
2628
+ return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount, new Set(), targetTurnId);
2626
2629
  }
2627
- return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount);
2630
+ return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount, targetTurnId);
2628
2631
  })();
2629
2632
  this.turnHistoryHydrations.set(current.localSessionId, hydration);
2630
2633
  try {
@@ -2636,7 +2639,7 @@ export class ProxySessionController {
2636
2639
  }
2637
2640
  }
2638
2641
  }
2639
- async hydrateForkTurnHistory(contextKey, current, requiredCount, visited = new Set()) {
2642
+ async hydrateForkTurnHistory(contextKey, current, requiredCount, visited = new Set(), targetTurnId) {
2640
2643
  if (visited.has(current.localSessionId))
2641
2644
  return false;
2642
2645
  visited.add(current.localSessionId);
@@ -2667,7 +2670,7 @@ export class ProxySessionController {
2667
2670
  ? Math.max(0, localAnchorIndex)
2668
2671
  : localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
2669
2672
  const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
2670
- if (localAnchorIndex >= 0 && availableCount >= requiredCount
2673
+ if (!targetTurnId && localAnchorIndex >= 0 && availableCount >= requiredCount
2671
2674
  && (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount))
2672
2675
  return !state.complete;
2673
2676
  const sourceSession = source.sourceLocalSessionId
@@ -2677,7 +2680,7 @@ export class ProxySessionController {
2677
2680
  // the Agent materializes that intermediate branch.
2678
2681
  if (sourceSession && sourceSession.localSessionId !== current.localSessionId
2679
2682
  && this.store.getForkHistorySource(sourceSession.localSessionId)?.sourceTurnId === source.sourceTurnId) {
2680
- const hasMore = await this.hydrateForkTurnHistory(sourceSession.contextKey, sourceSession, this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount, visited);
2683
+ const hasMore = await this.hydrateForkTurnHistory(sourceSession.contextKey, sourceSession, this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount, visited, targetTurnId);
2681
2684
  if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
2682
2685
  this.store.audit(contextKey, "fork_turn_history_imported", {
2683
2686
  forkedLocalSessionId: current.localSessionId,
@@ -2695,8 +2698,10 @@ export class ProxySessionController {
2695
2698
  return false;
2696
2699
  const seenCursors = new Set();
2697
2700
  try {
2698
- while (!state.complete && (state.loadedCount ?? 0) < neededInheritedCount) {
2699
- const remaining = neededInheritedCount - (state.loadedCount ?? 0);
2701
+ while (!state.complete && (targetTurnId
2702
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
2703
+ : (state.loadedCount ?? 0) < neededInheritedCount)) {
2704
+ const remaining = targetTurnId ? RESET_HISTORY_PAGE_SIZE : neededInheritedCount - (state.loadedCount ?? 0);
2700
2705
  const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
2701
2706
  cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
2702
2707
  });
@@ -2756,7 +2761,7 @@ export class ProxySessionController {
2756
2761
  }
2757
2762
  return true;
2758
2763
  }
2759
- async hydrateRemoteTurnHistory(contextKey, current, requiredCount) {
2764
+ async hydrateRemoteTurnHistory(contextKey, current, requiredCount, targetTurnId) {
2760
2765
  if (!current.remoteSessionId)
2761
2766
  return false;
2762
2767
  const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
@@ -2770,10 +2775,12 @@ export class ProxySessionController {
2770
2775
  return false;
2771
2776
  const seen = new Set();
2772
2777
  try {
2773
- while (!state.complete && (state.loadedCount ?? 0) < requiredCount) {
2778
+ while (!state.complete && (targetTurnId
2779
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
2780
+ : (state.loadedCount ?? 0) < requiredCount)) {
2774
2781
  const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
2775
2782
  cursor: state.cursor,
2776
- limit: Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
2783
+ limit: targetTurnId ? RESET_HISTORY_PAGE_SIZE : Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
2777
2784
  });
2778
2785
  if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
2779
2786
  throw new Error("App Server repeated a Turn history cursor.");
@@ -2998,7 +3005,7 @@ export class ProxySessionController {
2998
3005
  });
2999
3006
  }
3000
3007
  this.store.setCurrentSession(contextKey, localSessionId);
3001
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName));
3008
+ this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName), resolvedExecutionSettings.model);
3002
3009
  const runtime = this.runtimes.forAgent(agentName);
3003
3010
  try {
3004
3011
  if (prepareTurn) {
@@ -3015,6 +3022,7 @@ export class ProxySessionController {
3015
3022
  permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
3016
3023
  });
3017
3024
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3025
+ this.outbound.updateSessionModel(localSessionId, session.model);
3018
3026
  const saved = this.store.getSession(localSessionId) ?? record;
3019
3027
  if (announce) {
3020
3028
  const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
@@ -3162,7 +3170,7 @@ export class ProxySessionController {
3162
3170
  record: { ...loaded.record, contextKey: record.contextKey },
3163
3171
  };
3164
3172
  }
3165
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
3173
+ this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
3166
3174
  const loading = (async () => {
3167
3175
  if (record.lastTurnId) {
3168
3176
  try {
@@ -3303,14 +3311,20 @@ export class ProxySessionController {
3303
3311
  lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
3304
3312
  });
3305
3313
  }
3314
+ const enrichedEvent = event.type === "context_compaction"
3315
+ ? {
3316
+ ...event,
3317
+ turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
3318
+ }
3319
+ : event;
3306
3320
  const presentationEvent = event.type === "turn_failed"
3307
3321
  && isRetryableLlmTurnFailure(event.message)
3308
3322
  && (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
3309
3323
  ? {
3310
- ...event,
3324
+ ...enrichedEvent,
3311
3325
  message: `${event.message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`,
3312
3326
  }
3313
- : event;
3327
+ : enrichedEvent;
3314
3328
  let presentationError;
3315
3329
  try {
3316
3330
  await this.outbound.onEvent(presentationEvent);
@@ -3807,7 +3821,7 @@ export class ProxySessionController {
3807
3821
  lastTurnId: turnId,
3808
3822
  lastTurnStatus: "completed",
3809
3823
  });
3810
- this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName));
3824
+ this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName), current.model);
3811
3825
  this.store.audit(contextKey, "session_reset_to_turn", {
3812
3826
  localSessionId: current.localSessionId,
3813
3827
  previousRemoteSessionId,
@@ -3857,12 +3871,13 @@ export class ProxySessionController {
3857
3871
  };
3858
3872
  if (record) {
3859
3873
  const loaded = await this.loadSession(record);
3860
- const models = await loaded.runtime.listModels();
3861
- const currentModel = models.find((model) => model.id === loaded.session.model)
3862
- ?? models.find((model) => model.isDefault);
3863
3874
  const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
3864
3875
  const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
3865
3876
  const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
3877
+ const models = await loaded.runtime.listModels(currentProvider, loaded.session.model);
3878
+ const currentModel = models.find((model) => model.id === loaded.session.model)
3879
+ ?? models.find((model) => model.isDefault)
3880
+ ?? models[0];
3866
3881
  const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
3867
3882
  ? loaded.session.reasoningEffort
3868
3883
  : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
@@ -3923,16 +3938,11 @@ export class ProxySessionController {
3923
3938
  await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
3924
3939
  const loaded = await this.loadSession(record);
3925
3940
  await this.assertModelProvider(loaded, modelProvider);
3926
- const models = await loaded.runtime.listModels();
3927
- const model = models.find((candidate) => candidate.id === loaded.session.model)
3928
- ?? models.find((candidate) => candidate.isDefault);
3941
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
3942
+ const model = selectProviderModel(models, loaded.session.model);
3929
3943
  if (!model)
3930
3944
  throw new Error("当前运行时没有可用于 Provider 切换的模型。");
3931
- const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
3932
- ? loaded.session.reasoningEffort
3933
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
3934
- if (!effort)
3935
- throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
3945
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
3936
3946
  await this.applyProviderSettings(contextKey, {
3937
3947
  provider: modelProvider,
3938
3948
  model: model.id,
@@ -3946,11 +3956,11 @@ export class ProxySessionController {
3946
3956
  async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
3947
3957
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
3948
3958
  await this.assertModelProvider(loaded, modelProvider);
3949
- const models = await loaded.runtime.listModels();
3959
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
3950
3960
  const card = this.cardRenderer.renderModelSelector({
3951
3961
  sessionId,
3952
3962
  contextKey,
3953
- currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
3963
+ currentModel: selectProviderModel(models, loaded.session.model)?.id,
3954
3964
  reasoningEffort: loaded.session.reasoningEffort,
3955
3965
  models,
3956
3966
  modelProvider,
@@ -3962,20 +3972,23 @@ export class ProxySessionController {
3962
3972
  async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
3963
3973
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
3964
3974
  await this.assertModelProvider(loaded, modelProvider);
3965
- const models = await loaded.runtime.listModels();
3975
+ const models = await loaded.runtime.listModels(modelProvider, model);
3966
3976
  const selected = models.find((candidate) => candidate.id === model);
3967
3977
  if (!selected)
3968
3978
  throw new Error(`未知模型:${model}`);
3969
- const currentEffort = selected.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
3979
+ const options = selected.supportedReasoningEfforts.length > 0
3980
+ ? selected.supportedReasoningEfforts
3981
+ : [{ value: selectProviderReasoningEffort(selected, loaded.session.reasoningEffort) }];
3982
+ const currentEffort = options.some((option) => option.value === loaded.session.reasoningEffort)
3970
3983
  ? loaded.session.reasoningEffort
3971
- : selected.defaultReasoningEffort;
3984
+ : selected.defaultReasoningEffort ?? options[0]?.value;
3972
3985
  const card = this.cardRenderer.renderReasoningSelector({
3973
3986
  sessionId,
3974
3987
  contextKey,
3975
3988
  modelProvider,
3976
3989
  model,
3977
3990
  currentEffort,
3978
- options: selected.supportedReasoningEfforts,
3991
+ options,
3979
3992
  permissionMode,
3980
3993
  unifiedSettings: true,
3981
3994
  });
@@ -4053,10 +4066,11 @@ export class ProxySessionController {
4053
4066
  }
4054
4067
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
4055
4068
  await this.assertModelProvider(loaded, modelProvider);
4056
- const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
4069
+ const selected = (await loaded.runtime.listModels(modelProvider, model)).find((candidate) => candidate.id === model);
4057
4070
  if (!selected)
4058
4071
  throw new Error(`未知模型:${model}`);
4059
- if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
4072
+ if (selected.supportedReasoningEfforts.length > 0
4073
+ && !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
4060
4074
  const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
4061
4075
  throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
4062
4076
  }
@@ -4087,13 +4101,14 @@ export class ProxySessionController {
4087
4101
  ? this.requireSession(contextKey, options.sessionId)
4088
4102
  : this.requireCurrentSession(contextKey);
4089
4103
  const loaded = await this.loadSession(record);
4090
- const models = await loaded.runtime.listModels();
4104
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
4091
4105
  const selected = models.find((item) => item.id === model);
4092
4106
  if (!selected)
4093
4107
  throw new Error(`未知模型:${model}`);
4094
4108
  const currentEffort = loaded.session.reasoningEffort;
4095
4109
  const compatible = currentEffort
4096
- ? selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
4110
+ ? selected.supportedReasoningEfforts.length === 0
4111
+ || selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
4097
4112
  : false;
4098
4113
  const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
4099
4114
  await loaded.runtime.setModel(loaded.record.localSessionId, model);
@@ -4474,7 +4489,7 @@ export class ProxySessionController {
4474
4489
  ? this.requireSession(contextKey, options.sessionId)
4475
4490
  : this.requireCurrentSession(contextKey);
4476
4491
  const loaded = await this.loadSession(record);
4477
- const models = await loaded.runtime.listModels();
4492
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
4478
4493
  const currentModel = models.find((item) => item.id === loaded.session.model)
4479
4494
  ?? models.find((item) => item.isDefault);
4480
4495
  if (!currentModel)
@@ -4527,6 +4542,73 @@ export class ProxySessionController {
4527
4542
  ? `已切换为自动执行模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`
4528
4543
  : `已切换为执行前确认模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`);
4529
4544
  }
4545
+ taskTurnIds(current) {
4546
+ const ids = this.store.listTaskTurnGraphIndex(current.localSessionId).map((turn) => turn.turnId);
4547
+ if (current.lastTurnStatus === "running" && current.lastTurnId && !ids.includes(current.lastTurnId)) {
4548
+ ids.unshift(current.lastTurnId);
4549
+ }
4550
+ return ids;
4551
+ }
4552
+ runningTurnSnapshot(current, turnId) {
4553
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
4554
+ if (snapshot)
4555
+ return snapshot;
4556
+ const attempt = this.store.findIncompleteTurnAttemptByTurnId(turnId);
4557
+ const startedAt = Date.parse(attempt?.createdAt ?? "");
4558
+ return {
4559
+ sessionId: current.localSessionId,
4560
+ turnId,
4561
+ taskTitle: current.title,
4562
+ prompt: attempt?.promptText ?? current.title,
4563
+ status: "running",
4564
+ startedAt: Number.isFinite(startedAt) ? startedAt : Date.now(),
4565
+ assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
4566
+ };
4567
+ }
4568
+ async openTurnDetails(contextKey, reference) {
4569
+ const current = this.requireCurrentSession(contextKey);
4570
+ let turnId;
4571
+ if (/^\d+$/.test(reference)) {
4572
+ const index = Number(reference);
4573
+ const runningCount = current.lastTurnStatus === "running" && current.lastTurnId ? 1 : 0;
4574
+ const hasMore = index > runningCount && this.isCodexSession(current)
4575
+ ? await this.hydrateTaskTurnHistory(current, index - runningCount)
4576
+ : false;
4577
+ turnId = this.taskTurnIds(current)[index - 1];
4578
+ if (!turnId) {
4579
+ throw new Error(hasMore
4580
+ ? "轮次历史暂未加载完成,请稍后重试。"
4581
+ : `未找到序号为 ${reference} 的轮次。请发送 /turn 查看当前轮次序号。`);
4582
+ }
4583
+ }
4584
+ else {
4585
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(reference));
4586
+ if (snapshot && (snapshot.sessionId === current.localSessionId
4587
+ || this.store.hasTaskHistoryTurn(current.localSessionId, reference))) {
4588
+ turnId = reference;
4589
+ }
4590
+ else if (current.lastTurnStatus === "running" && current.lastTurnId === reference) {
4591
+ turnId = reference;
4592
+ }
4593
+ else if (this.isCodexSession(current) && (!snapshot || this.store.getForkHistorySource(current.localSessionId))) {
4594
+ const hasMore = await this.hydrateTaskTurnHistory(current, 0, reference);
4595
+ if (this.store.hasTaskHistoryTurn(current.localSessionId, reference))
4596
+ turnId = reference;
4597
+ else if (hasMore)
4598
+ throw new Error("轮次历史暂未加载完成,请稍后重试。");
4599
+ }
4600
+ if (!turnId && snapshot)
4601
+ throw new Error("这个 Turn 不属于当前任务的历史。请先切换到对应任务。");
4602
+ if (!turnId)
4603
+ throw new Error(`未找到当前任务的 Turn:${reference}`);
4604
+ }
4605
+ if (current.lastTurnStatus === "running" && current.lastTurnId === turnId
4606
+ && !this.store.getTurnPromptSummary(turnId)) {
4607
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(this.runningTurnSnapshot(current, turnId)));
4608
+ return;
4609
+ }
4610
+ await this.outbound.showDetails(contextKey, turnId);
4611
+ }
4530
4612
  async openResetHistory(contextKey, options = {}) {
4531
4613
  const current = this.requireCurrentSession(contextKey);
4532
4614
  if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
@@ -4543,27 +4625,11 @@ export class ProxySessionController {
4543
4625
  && !completedTurnIds.has(current.lastTurnId)
4544
4626
  ? current.lastTurnId
4545
4627
  : undefined;
4546
- const persistedRunningSnapshot = runningTurnId
4547
- ? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
4548
- : undefined;
4549
4628
  const runningAttempt = runningTurnId
4550
4629
  ? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
4551
4630
  : undefined;
4552
- const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
4553
4631
  const runningSnapshot = runningTurnId
4554
- ? persistedRunningSnapshot ?? {
4555
- sessionId: current.localSessionId,
4556
- turnId: runningTurnId,
4557
- prompt: runningAttempt?.promptText ?? current.title,
4558
- status: "running",
4559
- startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
4560
- assistantText: "",
4561
- plan: [],
4562
- activities: [],
4563
- completedTools: [],
4564
- failedTools: [],
4565
- fileSummary: [],
4566
- }
4632
+ ? this.runningTurnSnapshot(current, runningTurnId)
4567
4633
  : undefined;
4568
4634
  const runningTurn = runningTurnId && runningSnapshot
4569
4635
  ? {
@@ -5165,7 +5231,7 @@ export class ProxySessionController {
5165
5231
  await this.assertSessionTurnOwnership(existing, runtime);
5166
5232
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
5167
5233
  this.store.setCurrentSession(contextKey, existing.localSessionId);
5168
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName));
5234
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model);
5169
5235
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
5170
5236
  return;
5171
5237
  }
@@ -5192,7 +5258,7 @@ export class ProxySessionController {
5192
5258
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
5193
5259
  });
5194
5260
  this.store.setCurrentSession(contextKey, localSessionId);
5195
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName));
5261
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model);
5196
5262
  await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
5197
5263
  }
5198
5264
  async stopSessionReference(contextKey, taskId) {
@@ -5454,6 +5520,7 @@ export class ProxySessionController {
5454
5520
  current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
5455
5521
  }
5456
5522
  let remote;
5523
+ let metrics;
5457
5524
  if (current?.status === "running") {
5458
5525
  const runtime = this.runtimes.forAgent(current.agentName);
5459
5526
  if (runtime.getSession(current.localSessionId)) {
@@ -5491,6 +5558,14 @@ export class ProxySessionController {
5491
5558
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Agent goal status.");
5492
5559
  }
5493
5560
  }
5561
+ if (runtime.readRemoteSessionMetrics) {
5562
+ try {
5563
+ metrics = await runtime.readRemoteSessionMetrics(current.remoteSessionId);
5564
+ }
5565
+ catch (error) {
5566
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task metrics.");
5567
+ }
5568
+ }
5494
5569
  }
5495
5570
  if (current)
5496
5571
  current = mergeRemoteTaskStatus(current, remote);
@@ -5517,7 +5592,7 @@ export class ProxySessionController {
5517
5592
  const resultLabel = remoteActive
5518
5593
  ? "执行中"
5519
5594
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
5520
- taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Agent 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
5595
+ taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Agent 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, statusTaskMetricsLine(metrics?.turnCount ?? this.store.countTaskTurns(current.localSessionId), metrics?.storageBytes), `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
5521
5596
  }
5522
5597
  const sections = [
5523
5598
  { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
@@ -5575,6 +5650,16 @@ export class ProxySessionController {
5575
5650
  }
5576
5651
  async statusForCodexTask(contextKey, reference, options = {}) {
5577
5652
  const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5653
+ const runtime = this.runtimes.forAgent(agentName);
5654
+ let metrics;
5655
+ if (runtime.readRemoteSessionMetrics) {
5656
+ try {
5657
+ metrics = await runtime.readRemoteSessionMetrics(remote.id);
5658
+ }
5659
+ catch (error) {
5660
+ this.logger.warn({ error, remoteSessionId: remote.id }, "Failed to inspect App Server task metrics.");
5661
+ }
5662
+ }
5578
5663
  const actionReference = remoteSessionReference(agentName, remote.id);
5579
5664
  const sections = [
5580
5665
  {
@@ -5585,6 +5670,7 @@ export class ProxySessionController {
5585
5670
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
5586
5671
  `**Agent**:${cardCode(agentName)}`,
5587
5672
  `**App Server 任务 ID**:${cardCode(remote.id)}`,
5673
+ statusTaskMetricsLine(metrics?.turnCount, metrics?.storageBytes),
5588
5674
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
5589
5675
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
5590
5676
  ],
@@ -6578,6 +6664,11 @@ function statusExcerpt(value, maxLength) {
6578
6664
  function formatDuration(milliseconds) {
6579
6665
  return milliseconds < 1_000 ? `${milliseconds}ms` : `${(milliseconds / 1_000).toFixed(1)}s`;
6580
6666
  }
6667
+ function statusTaskMetricsLine(turnCount, storageBytes) {
6668
+ const turns = turnCount === undefined ? "未知" : `${Math.max(0, Math.round(turnCount)).toLocaleString("en-US")} 轮`;
6669
+ const storage = storageBytes === undefined ? "未知" : formatStorageSize(storageBytes);
6670
+ return `**已执行轮次 / 磁盘占用**:${turns} / ${storage}`;
6671
+ }
6581
6672
  function formatStatusTime(value) {
6582
6673
  const date = new Date(value);
6583
6674
  if (Number.isNaN(date.getTime()))
@@ -7012,6 +7103,21 @@ function deduplicateFileReferences(files) {
7012
7103
  return true;
7013
7104
  });
7014
7105
  }
7106
+ function selectProviderModel(models, previousModel) {
7107
+ return models.find((model) => model.id === previousModel)
7108
+ ?? models.find((model) => model.isDefault)
7109
+ ?? models[0];
7110
+ }
7111
+ function selectProviderReasoningEffort(model, previousEffort) {
7112
+ if (previousEffort && (model.supportedReasoningEfforts.length === 0
7113
+ || model.supportedReasoningEfforts.some((option) => option.value === previousEffort))) {
7114
+ return previousEffort;
7115
+ }
7116
+ return model.defaultReasoningEffort
7117
+ ?? model.supportedReasoningEfforts[0]?.value
7118
+ ?? previousEffort
7119
+ ?? "medium";
7120
+ }
7015
7121
  function recoveryCardPrompt(prompt) {
7016
7122
  return `恢复重启前的任务:${prompt}`;
7017
7123
  }