@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
@@ -59,9 +59,11 @@ import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
59
59
  import type {
60
60
  AgentRuntime,
61
61
  ApprovalDecision,
62
+ ModelOption,
62
63
  PermissionMode,
63
64
  RemoteCompletedTurnSummary,
64
65
  RemoteSessionActivity,
66
+ RemoteSessionMetrics,
65
67
  RemoteSessionPage,
66
68
  RemoteSessionSummary,
67
69
  RuntimeGoal,
@@ -87,6 +89,7 @@ import {
87
89
  type TurnAnchorRecord,
88
90
  } from "../state/StateStore.js";
89
91
  import { createId } from "../utils/id.js";
92
+ import { formatStorageSize } from "../utils/formatStorageSize.js";
90
93
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
91
94
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
92
95
  import {
@@ -257,7 +260,7 @@ const HELP_COMMAND_SECTIONS: Array<{
257
260
  usage: "[序号或任务 ID]",
258
261
  description: "从当前或指定任务最近完成轮次创建分支",
259
262
  },
260
- { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
263
+ { command: "/turns", usage: "[Turn ID 或序号]", description: "浏览历史轮次,并可 Reset 对话上下文;带参数查看运行详情" },
261
264
  {
262
265
  command: "/title",
263
266
  usage: "&#60;新标题&#62;",
@@ -1433,6 +1436,7 @@ export class ProxySessionController {
1433
1436
  scopedRecord.title,
1434
1437
  scopedRecord.cwd,
1435
1438
  this.agentLabel(scopedRecord.agentName),
1439
+ scopedRecord.model,
1436
1440
  );
1437
1441
  const promptMessageId = await this.outbound.withReplyTarget(
1438
1442
  responseContextKey,
@@ -1680,16 +1684,10 @@ export class ProxySessionController {
1680
1684
  const loaded = await this.loadSession(record);
1681
1685
  if (setting === "provider" && nextValue) {
1682
1686
  await this.assertModelProvider(loaded, nextValue);
1683
- const models = await loaded.runtime.listModels();
1684
- const model = models.find((candidate) => candidate.id === loaded.session.model)
1685
- ?? models.find((candidate) => candidate.isDefault);
1687
+ const models = await loaded.runtime.listModels(nextValue, loaded.session.model);
1688
+ const model = selectProviderModel(models, loaded.session.model);
1686
1689
  if (!model) throw new Error("The current runtime has no model available for a Provider change.");
1687
- const effort = model.supportedReasoningEfforts.some(
1688
- (candidate) => candidate.value === loaded.session.reasoningEffort,
1689
- )
1690
- ? loaded.session.reasoningEffort
1691
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1692
- if (!effort) throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
1690
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
1693
1691
  await this.changeProviderSettings(loaded, {
1694
1692
  modelProvider: nextValue,
1695
1693
  model: model.id,
@@ -1697,13 +1695,12 @@ export class ProxySessionController {
1697
1695
  permissionMode: loaded.session.permissionMode,
1698
1696
  });
1699
1697
  } else if (setting === "model" && nextValue) {
1700
- const models = await loaded.runtime.listModels();
1698
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1701
1699
  const selected = models.find((candidate) => candidate.id === nextValue);
1702
1700
  if (!selected) throw new Error(`Unknown model: ${nextValue}`);
1703
1701
  const currentEffort = loaded.session.reasoningEffort;
1704
- const nextEffort = currentEffort && selected.supportedReasoningEfforts.some(
1705
- (candidate) => candidate.value === currentEffort,
1706
- )
1702
+ const nextEffort = currentEffort && (selected.supportedReasoningEfforts.length === 0
1703
+ || selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort))
1707
1704
  ? currentEffort
1708
1705
  : selected.defaultReasoningEffort;
1709
1706
  await loaded.runtime.setModel(record.localSessionId, nextValue);
@@ -1712,7 +1709,7 @@ export class ProxySessionController {
1712
1709
  }
1713
1710
  this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
1714
1711
  } else if (setting === "thinking" && nextValue) {
1715
- const models = await loaded.runtime.listModels();
1712
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1716
1713
  const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
1717
1714
  ?? models.find((candidate) => candidate.isDefault);
1718
1715
  if (!currentModel) throw new Error("The current runtime has no model with configurable reasoning effort.");
@@ -1731,7 +1728,7 @@ export class ProxySessionController {
1731
1728
  }
1732
1729
  const session = this.store.getSession(record.localSessionId) ?? record;
1733
1730
  if (setting && setting !== "provider") await this.persistAgentExecutionDefaults(session.localSessionId);
1734
- const models = await loaded.runtime.listModels();
1731
+ const models = await loaded.runtime.listModels(session.modelProvider, session.model);
1735
1732
  const currentModel = models.find((candidate) => candidate.id === session.model)
1736
1733
  ?? models.find((candidate) => candidate.isDefault);
1737
1734
  const providers = loaded.runtime.listModelProviders
@@ -2077,8 +2074,13 @@ export class ProxySessionController {
2077
2074
  await this.setGroupMute(contextKey, command.enabled);
2078
2075
  return;
2079
2076
  case "turns":
2080
- await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
2081
- await this.openResetHistory(contextKey);
2077
+ if (command.turnReference) {
2078
+ await this.outbound.sendText(contextKey, "正在读取轮次运行信息,请稍后。");
2079
+ await this.openTurnDetails(contextKey, command.turnReference);
2080
+ } else {
2081
+ await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
2082
+ await this.openResetHistory(contextKey);
2083
+ }
2082
2084
  return;
2083
2085
  case "model":
2084
2086
  await this.openExecutionSettings(contextKey, "model");
@@ -2116,6 +2118,7 @@ export class ProxySessionController {
2116
2118
  session.title,
2117
2119
  session.cwd,
2118
2120
  this.agentLabel(session.agentName),
2121
+ session.model,
2119
2122
  );
2120
2123
  if (!session.lastTurnId) continue;
2121
2124
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error: unknown) => {
@@ -2202,6 +2205,7 @@ export class ProxySessionController {
2202
2205
  session.title,
2203
2206
  session.cwd,
2204
2207
  this.agentLabel(session.agentName),
2208
+ session.model,
2205
2209
  );
2206
2210
  }
2207
2211
  try {
@@ -2330,6 +2334,7 @@ export class ProxySessionController {
2330
2334
  session.title,
2331
2335
  session.cwd,
2332
2336
  this.agentLabel(session.agentName),
2337
+ session.model,
2333
2338
  );
2334
2339
  if (announce) {
2335
2340
  await this.outbound.withReplyTarget(contextKey, replyTarget, () =>
@@ -2611,6 +2616,7 @@ export class ProxySessionController {
2611
2616
  record.title,
2612
2617
  record.cwd,
2613
2618
  this.agentLabel(record.agentName),
2619
+ record.model,
2614
2620
  );
2615
2621
  await this.outbound.startPendingTurn(
2616
2622
  record.localSessionId,
@@ -2969,6 +2975,7 @@ export class ProxySessionController {
2969
2975
  forkTitle,
2970
2976
  source.cwd,
2971
2977
  this.agentLabel(source.agentName),
2978
+ source.model,
2972
2979
  );
2973
2980
 
2974
2981
  try {
@@ -3329,6 +3336,7 @@ export class ProxySessionController {
3329
3336
  forked.title ?? plan.forkTitle,
3330
3337
  plan.cwd,
3331
3338
  this.agentLabel(plan.agentName),
3339
+ plan.model,
3332
3340
  );
3333
3341
  this.store.audit(contextKey, "session_forked", {
3334
3342
  sourceLocalSessionId: plan.source?.localSessionId,
@@ -3404,14 +3412,15 @@ export class ProxySessionController {
3404
3412
  private async hydrateTaskTurnHistory(
3405
3413
  current: SessionRecord,
3406
3414
  requiredCount = RESET_HISTORY_PAGE_SIZE,
3415
+ targetTurnId?: string,
3407
3416
  ): Promise<boolean> {
3408
3417
  const existing = this.turnHistoryHydrations.get(current.localSessionId);
3409
3418
  if (existing) await existing;
3410
3419
  const hydration = (async () => {
3411
3420
  if (this.store.getForkHistorySource(current.localSessionId)) {
3412
- return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount);
3421
+ return this.hydrateForkTurnHistory(current.contextKey, current, requiredCount, new Set(), targetTurnId);
3413
3422
  }
3414
- return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount);
3423
+ return this.hydrateRemoteTurnHistory(current.contextKey, current, requiredCount, targetTurnId);
3415
3424
  })();
3416
3425
  this.turnHistoryHydrations.set(current.localSessionId, hydration);
3417
3426
  try {
@@ -3428,6 +3437,7 @@ export class ProxySessionController {
3428
3437
  current: SessionRecord,
3429
3438
  requiredCount: number,
3430
3439
  visited = new Set<string>(),
3440
+ targetTurnId?: string,
3431
3441
  ): Promise<boolean> {
3432
3442
  if (visited.has(current.localSessionId)) return false;
3433
3443
  visited.add(current.localSessionId);
@@ -3456,7 +3466,7 @@ export class ProxySessionController {
3456
3466
  ? Math.max(0, localAnchorIndex)
3457
3467
  : localAnchorIndex >= 0 ? localAnchorIndex : availableCount;
3458
3468
  const neededInheritedCount = Math.max(0, requiredCount - ownTurnCount);
3459
- if (localAnchorIndex >= 0 && availableCount >= requiredCount
3469
+ if (!targetTurnId && localAnchorIndex >= 0 && availableCount >= requiredCount
3460
3470
  && (!state.anchorFound || (state.loadedCount ?? 0) >= neededInheritedCount)) return !state.complete;
3461
3471
  const sourceSession = source.sourceLocalSessionId
3462
3472
  ? this.store.getSession(source.sourceLocalSessionId)
@@ -3469,6 +3479,7 @@ export class ProxySessionController {
3469
3479
  sourceSession.contextKey, sourceSession,
3470
3480
  this.store.listTaskTurnGraphIndex(sourceSession.localSessionId).length + requiredCount - availableCount,
3471
3481
  visited,
3482
+ targetTurnId,
3472
3483
  );
3473
3484
  if (!hasMore && this.store.hasImportedForkTurnHistory(sourceSession.localSessionId)) {
3474
3485
  this.store.audit(contextKey, "fork_turn_history_imported", {
@@ -3485,8 +3496,10 @@ export class ProxySessionController {
3485
3496
  if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
3486
3497
  const seenCursors = new Set<string>();
3487
3498
  try {
3488
- while (!state.complete && (state.loadedCount ?? 0) < neededInheritedCount) {
3489
- const remaining = neededInheritedCount - (state.loadedCount ?? 0);
3499
+ while (!state.complete && (targetTurnId
3500
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
3501
+ : (state.loadedCount ?? 0) < neededInheritedCount)) {
3502
+ const remaining = targetTurnId ? RESET_HISTORY_PAGE_SIZE : neededInheritedCount - (state.loadedCount ?? 0);
3490
3503
  const page = await runtime.listRemoteTurnSummaries(source.sourceRemoteSessionId, {
3491
3504
  cursor: state.cursor, limit: Math.min(RESET_HISTORY_PAGE_SIZE, remaining),
3492
3505
  });
@@ -3550,6 +3563,7 @@ export class ProxySessionController {
3550
3563
  contextKey: string,
3551
3564
  current: SessionRecord,
3552
3565
  requiredCount: number,
3566
+ targetTurnId?: string,
3553
3567
  ): Promise<boolean> {
3554
3568
  if (!current.remoteSessionId) return false;
3555
3569
  const key = JSON.stringify([current.agentName, current.remoteSessionId, "turn-card"]);
@@ -3561,10 +3575,12 @@ export class ProxySessionController {
3561
3575
  if (runtime.kind !== "codex" || !runtime.listRemoteTurnSummaries) return false;
3562
3576
  const seen = new Set<string>();
3563
3577
  try {
3564
- while (!state.complete && (state.loadedCount ?? 0) < requiredCount) {
3578
+ while (!state.complete && (targetTurnId
3579
+ ? !this.store.hasTaskHistoryTurn(current.localSessionId, targetTurnId)
3580
+ : (state.loadedCount ?? 0) < requiredCount)) {
3565
3581
  const page = await runtime.listRemoteTurnSummaries(current.remoteSessionId, {
3566
3582
  cursor: state.cursor,
3567
- limit: Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
3583
+ limit: targetTurnId ? RESET_HISTORY_PAGE_SIZE : Math.min(RESET_HISTORY_PAGE_SIZE, requiredCount - (state.loadedCount ?? 0)),
3568
3584
  });
3569
3585
  if (page.nextCursor && (page.nextCursor === state.cursor || seen.has(page.nextCursor))) {
3570
3586
  throw new Error("App Server repeated a Turn history cursor.");
@@ -3838,6 +3854,7 @@ export class ProxySessionController {
3838
3854
  initialTitle,
3839
3855
  sessionCwd,
3840
3856
  this.agentLabel(agentName),
3857
+ resolvedExecutionSettings.model,
3841
3858
  );
3842
3859
  const runtime = this.runtimes.forAgent(agentName);
3843
3860
  try {
@@ -3855,6 +3872,7 @@ export class ProxySessionController {
3855
3872
  permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
3856
3873
  });
3857
3874
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3875
+ this.outbound.updateSessionModel(localSessionId, session.model);
3858
3876
  const saved = this.store.getSession(localSessionId) ?? record;
3859
3877
  if (announce) {
3860
3878
  const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
@@ -4127,6 +4145,7 @@ export class ProxySessionController {
4127
4145
  record.title,
4128
4146
  record.cwd,
4129
4147
  this.agentLabel(record.agentName),
4148
+ record.model,
4130
4149
  );
4131
4150
  const loading = (async (): Promise<LoadedSession> => {
4132
4151
  if (record.lastTurnId) {
@@ -4277,14 +4296,20 @@ export class ProxySessionController {
4277
4296
  });
4278
4297
  }
4279
4298
 
4299
+ const enrichedEvent: RuntimeEvent = event.type === "context_compaction"
4300
+ ? {
4301
+ ...event,
4302
+ turnCount: event.turnCount ?? this.store.countTaskTurns(event.sessionId),
4303
+ }
4304
+ : event;
4280
4305
  const presentationEvent = event.type === "turn_failed"
4281
4306
  && isRetryableLlmTurnFailure(event.message)
4282
4307
  && (failedAttempt?.retryCount ?? 0) >= MAX_LLM_TURN_RETRIES
4283
4308
  ? {
4284
- ...event,
4309
+ ...enrichedEvent,
4285
4310
  message: `${event.message}\n\n已自动重试 ${MAX_LLM_TURN_RETRIES} 次,仍未成功。`,
4286
4311
  }
4287
- : event;
4312
+ : enrichedEvent;
4288
4313
  let presentationError: unknown;
4289
4314
  try {
4290
4315
  await this.outbound.onEvent(presentationEvent);
@@ -4863,6 +4888,7 @@ export class ProxySessionController {
4863
4888
  forked.title ?? current.title,
4864
4889
  current.cwd,
4865
4890
  this.agentLabel(current.agentName),
4891
+ current.model,
4866
4892
  );
4867
4893
  this.store.audit(contextKey, "session_reset_to_turn", {
4868
4894
  localSessionId: current.localSessionId,
@@ -4926,12 +4952,13 @@ export class ProxySessionController {
4926
4952
  };
4927
4953
  if (record) {
4928
4954
  const loaded = await this.loadSession(record);
4929
- const models = await loaded.runtime.listModels();
4930
- const currentModel = models.find((model) => model.id === loaded.session.model)
4931
- ?? models.find((model) => model.isDefault);
4932
4955
  const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
4933
4956
  const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
4934
4957
  const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
4958
+ const models = await loaded.runtime.listModels(currentProvider, loaded.session.model);
4959
+ const currentModel = models.find((model) => model.id === loaded.session.model)
4960
+ ?? models.find((model) => model.isDefault)
4961
+ ?? models[0];
4935
4962
  const currentEffort = currentModel?.supportedReasoningEfforts.some(
4936
4963
  (option) => option.value === loaded.session.reasoningEffort,
4937
4964
  )
@@ -5000,16 +5027,10 @@ export class ProxySessionController {
5000
5027
  await this.outbound.sendText(contextKey, `正在切换到 Provider ${modelProvider},请稍后。`);
5001
5028
  const loaded = await this.loadSession(record);
5002
5029
  await this.assertModelProvider(loaded, modelProvider);
5003
- const models = await loaded.runtime.listModels();
5004
- const model = models.find((candidate) => candidate.id === loaded.session.model)
5005
- ?? models.find((candidate) => candidate.isDefault);
5030
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
5031
+ const model = selectProviderModel(models, loaded.session.model);
5006
5032
  if (!model) throw new Error("当前运行时没有可用于 Provider 切换的模型。");
5007
- const effort = model.supportedReasoningEfforts.some(
5008
- (candidate) => candidate.value === loaded.session.reasoningEffort,
5009
- )
5010
- ? loaded.session.reasoningEffort
5011
- : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
5012
- if (!effort) throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
5033
+ const effort = selectProviderReasoningEffort(model, loaded.session.reasoningEffort);
5013
5034
  await this.applyProviderSettings(contextKey, {
5014
5035
  provider: modelProvider,
5015
5036
  model: model.id,
@@ -5034,11 +5055,11 @@ export class ProxySessionController {
5034
5055
  ): Promise<void> {
5035
5056
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
5036
5057
  await this.assertModelProvider(loaded, modelProvider);
5037
- const models = await loaded.runtime.listModels();
5058
+ const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
5038
5059
  const card = this.cardRenderer.renderModelSelector({
5039
5060
  sessionId,
5040
5061
  contextKey,
5041
- currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
5062
+ currentModel: selectProviderModel(models, loaded.session.model)?.id,
5042
5063
  reasoningEffort: loaded.session.reasoningEffort,
5043
5064
  models,
5044
5065
  modelProvider,
@@ -5058,21 +5079,24 @@ export class ProxySessionController {
5058
5079
  ): Promise<void> {
5059
5080
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
5060
5081
  await this.assertModelProvider(loaded, modelProvider);
5061
- const models = await loaded.runtime.listModels();
5082
+ const models = await loaded.runtime.listModels(modelProvider, model);
5062
5083
  const selected = models.find((candidate) => candidate.id === model);
5063
5084
  if (!selected) throw new Error(`未知模型:${model}`);
5064
- const currentEffort = selected.supportedReasoningEfforts.some(
5085
+ const options = selected.supportedReasoningEfforts.length > 0
5086
+ ? selected.supportedReasoningEfforts
5087
+ : [{ value: selectProviderReasoningEffort(selected, loaded.session.reasoningEffort) }];
5088
+ const currentEffort = options.some(
5065
5089
  (option) => option.value === loaded.session.reasoningEffort,
5066
5090
  )
5067
5091
  ? loaded.session.reasoningEffort
5068
- : selected.defaultReasoningEffort;
5092
+ : selected.defaultReasoningEffort ?? options[0]?.value;
5069
5093
  const card = this.cardRenderer.renderReasoningSelector({
5070
5094
  sessionId,
5071
5095
  contextKey,
5072
5096
  modelProvider,
5073
5097
  model,
5074
5098
  currentEffort,
5075
- options: selected.supportedReasoningEfforts,
5099
+ options,
5076
5100
  permissionMode,
5077
5101
  unifiedSettings: true,
5078
5102
  });
@@ -5173,9 +5197,10 @@ export class ProxySessionController {
5173
5197
  effort: string,
5174
5198
  ): Promise<void> {
5175
5199
  await this.assertModelProvider(loaded, modelProvider);
5176
- const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
5200
+ const selected = (await loaded.runtime.listModels(modelProvider, model)).find((candidate) => candidate.id === model);
5177
5201
  if (!selected) throw new Error(`未知模型:${model}`);
5178
- if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
5202
+ if (selected.supportedReasoningEfforts.length > 0
5203
+ && !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
5179
5204
  const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
5180
5205
  throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
5181
5206
  }
@@ -5209,12 +5234,13 @@ export class ProxySessionController {
5209
5234
  ? this.requireSession(contextKey, options.sessionId)
5210
5235
  : this.requireCurrentSession(contextKey);
5211
5236
  const loaded = await this.loadSession(record);
5212
- const models = await loaded.runtime.listModels();
5237
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
5213
5238
  const selected = models.find((item) => item.id === model);
5214
5239
  if (!selected) throw new Error(`未知模型:${model}`);
5215
5240
  const currentEffort = loaded.session.reasoningEffort;
5216
5241
  const compatible = currentEffort
5217
- ? selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
5242
+ ? selected.supportedReasoningEfforts.length === 0
5243
+ || selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
5218
5244
  : false;
5219
5245
  const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
5220
5246
 
@@ -5645,7 +5671,7 @@ export class ProxySessionController {
5645
5671
  ? this.requireSession(contextKey, options.sessionId)
5646
5672
  : this.requireCurrentSession(contextKey);
5647
5673
  const loaded = await this.loadSession(record);
5648
- const models = await loaded.runtime.listModels();
5674
+ const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
5649
5675
  const currentModel = models.find((item) => item.id === loaded.session.model)
5650
5676
  ?? models.find((item) => item.isDefault);
5651
5677
  if (!currentModel) throw new Error("当前运行时没有可配置思考强度的模型。");
@@ -5710,6 +5736,71 @@ export class ProxySessionController {
5710
5736
  );
5711
5737
  }
5712
5738
 
5739
+ private taskTurnIds(current: SessionRecord): string[] {
5740
+ const ids = this.store.listTaskTurnGraphIndex(current.localSessionId).map((turn) => turn.turnId);
5741
+ if (current.lastTurnStatus === "running" && current.lastTurnId && !ids.includes(current.lastTurnId)) {
5742
+ ids.unshift(current.lastTurnId);
5743
+ }
5744
+ return ids;
5745
+ }
5746
+
5747
+ private runningTurnSnapshot(current: SessionRecord, turnId: string): TurnViewState {
5748
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
5749
+ if (snapshot) return snapshot;
5750
+ const attempt = this.store.findIncompleteTurnAttemptByTurnId(turnId);
5751
+ const startedAt = Date.parse(attempt?.createdAt ?? "");
5752
+ return {
5753
+ sessionId: current.localSessionId,
5754
+ turnId,
5755
+ taskTitle: current.title,
5756
+ prompt: attempt?.promptText ?? current.title,
5757
+ status: "running",
5758
+ startedAt: Number.isFinite(startedAt) ? startedAt : Date.now(),
5759
+ assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
5760
+ };
5761
+ }
5762
+
5763
+ private async openTurnDetails(contextKey: string, reference: string): Promise<void> {
5764
+ const current = this.requireCurrentSession(contextKey);
5765
+ let turnId: string | undefined;
5766
+ if (/^\d+$/.test(reference)) {
5767
+ const index = Number(reference);
5768
+ const runningCount = current.lastTurnStatus === "running" && current.lastTurnId ? 1 : 0;
5769
+ const hasMore = index > runningCount && this.isCodexSession(current)
5770
+ ? await this.hydrateTaskTurnHistory(current, index - runningCount)
5771
+ : false;
5772
+ turnId = this.taskTurnIds(current)[index - 1];
5773
+ if (!turnId) {
5774
+ throw new Error(hasMore
5775
+ ? "轮次历史暂未加载完成,请稍后重试。"
5776
+ : `未找到序号为 ${reference} 的轮次。请发送 /turn 查看当前轮次序号。`);
5777
+ }
5778
+ } else {
5779
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(reference));
5780
+ if (snapshot && (snapshot.sessionId === current.localSessionId
5781
+ || this.store.hasTaskHistoryTurn(current.localSessionId, reference))) {
5782
+ turnId = reference;
5783
+ } else if (current.lastTurnStatus === "running" && current.lastTurnId === reference) {
5784
+ turnId = reference;
5785
+ } else if (this.isCodexSession(current) && (!snapshot || this.store.getForkHistorySource(current.localSessionId))) {
5786
+ const hasMore = await this.hydrateTaskTurnHistory(current, 0, reference);
5787
+ if (this.store.hasTaskHistoryTurn(current.localSessionId, reference)) turnId = reference;
5788
+ else if (hasMore) throw new Error("轮次历史暂未加载完成,请稍后重试。");
5789
+ }
5790
+ if (!turnId && snapshot) throw new Error("这个 Turn 不属于当前任务的历史。请先切换到对应任务。");
5791
+ if (!turnId) throw new Error(`未找到当前任务的 Turn:${reference}`);
5792
+ }
5793
+
5794
+ if (current.lastTurnStatus === "running" && current.lastTurnId === turnId
5795
+ && !this.store.getTurnPromptSummary(turnId)) {
5796
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(
5797
+ this.runningTurnSnapshot(current, turnId),
5798
+ ));
5799
+ return;
5800
+ }
5801
+ await this.outbound.showDetails(contextKey, turnId);
5802
+ }
5803
+
5713
5804
  private async openResetHistory(
5714
5805
  contextKey: string,
5715
5806
  options: ResetHistoryCardOptions = {},
@@ -5729,27 +5820,11 @@ export class ProxySessionController {
5729
5820
  && !completedTurnIds.has(current.lastTurnId)
5730
5821
  ? current.lastTurnId
5731
5822
  : undefined;
5732
- const persistedRunningSnapshot = runningTurnId
5733
- ? turnViewSnapshot(this.store.getTurnSnapshot(runningTurnId))
5734
- : undefined;
5735
5823
  const runningAttempt = runningTurnId
5736
5824
  ? this.store.findIncompleteTurnAttemptByTurnId(runningTurnId)
5737
5825
  : undefined;
5738
- const attemptStartedAt = Date.parse(runningAttempt?.createdAt ?? "");
5739
5826
  const runningSnapshot: TurnViewState | undefined = runningTurnId
5740
- ? persistedRunningSnapshot ?? {
5741
- sessionId: current.localSessionId,
5742
- turnId: runningTurnId,
5743
- prompt: runningAttempt?.promptText ?? current.title,
5744
- status: "running",
5745
- startedAt: Number.isFinite(attemptStartedAt) ? attemptStartedAt : Date.now(),
5746
- assistantText: "",
5747
- plan: [],
5748
- activities: [],
5749
- completedTools: [],
5750
- failedTools: [],
5751
- fileSummary: [],
5752
- }
5827
+ ? this.runningTurnSnapshot(current, runningTurnId)
5753
5828
  : undefined;
5754
5829
  const runningTurn = runningTurnId && runningSnapshot
5755
5830
  ? {
@@ -6400,6 +6475,7 @@ export class ProxySessionController {
6400
6475
  existing.title,
6401
6476
  existing.cwd,
6402
6477
  this.agentLabel(existing.agentName),
6478
+ existing.model,
6403
6479
  );
6404
6480
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
6405
6481
  return;
@@ -6433,6 +6509,7 @@ export class ProxySessionController {
6433
6509
  remote.title ?? remote.preview,
6434
6510
  remote.cwd,
6435
6511
  this.agentLabel(agentName),
6512
+ remote.model,
6436
6513
  );
6437
6514
  await this.outbound.sendText(
6438
6515
  contextKey,
@@ -6732,6 +6809,7 @@ export class ProxySessionController {
6732
6809
  }
6733
6810
 
6734
6811
  let remote: RemoteSessionSummary | undefined;
6812
+ let metrics: RemoteSessionMetrics | undefined;
6735
6813
  if (current?.status === "running") {
6736
6814
  const runtime = this.runtimes.forAgent(current.agentName);
6737
6815
  if (runtime.getSession(current.localSessionId)) {
@@ -6764,6 +6842,13 @@ export class ProxySessionController {
6764
6842
  this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Agent goal status.");
6765
6843
  }
6766
6844
  }
6845
+ if (runtime.readRemoteSessionMetrics) {
6846
+ try {
6847
+ metrics = await runtime.readRemoteSessionMetrics(current.remoteSessionId);
6848
+ } catch (error) {
6849
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect App Server task metrics.");
6850
+ }
6851
+ }
6767
6852
  }
6768
6853
 
6769
6854
  if (current) current = mergeRemoteTaskStatus(current, remote);
@@ -6798,6 +6883,7 @@ export class ProxySessionController {
6798
6883
  `**Agent**:${cardCode(agent.title)}`,
6799
6884
  `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`,
6800
6885
  `**App Server 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`,
6886
+ statusTaskMetricsLine(metrics?.turnCount ?? this.store.countTaskTurns(current.localSessionId), metrics?.storageBytes),
6801
6887
  `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`,
6802
6888
  `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`,
6803
6889
  );
@@ -6864,6 +6950,15 @@ export class ProxySessionController {
6864
6950
  options: StatusCardOptions = {},
6865
6951
  ): Promise<void> {
6866
6952
  const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
6953
+ const runtime = this.runtimes.forAgent(agentName);
6954
+ let metrics: RemoteSessionMetrics | undefined;
6955
+ if (runtime.readRemoteSessionMetrics) {
6956
+ try {
6957
+ metrics = await runtime.readRemoteSessionMetrics(remote.id);
6958
+ } catch (error) {
6959
+ this.logger.warn({ error, remoteSessionId: remote.id }, "Failed to inspect App Server task metrics.");
6960
+ }
6961
+ }
6867
6962
  const actionReference = remoteSessionReference(agentName, remote.id);
6868
6963
  const sections: CardSection[] = [
6869
6964
  {
@@ -6874,6 +6969,7 @@ export class ProxySessionController {
6874
6969
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
6875
6970
  `**Agent**:${cardCode(agentName)}`,
6876
6971
  `**App Server 任务 ID**:${cardCode(remote.id)}`,
6972
+ statusTaskMetricsLine(metrics?.turnCount, metrics?.storageBytes),
6877
6973
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
6878
6974
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
6879
6975
  ],
@@ -8101,6 +8197,12 @@ function formatDuration(milliseconds: number): string {
8101
8197
  return milliseconds < 1_000 ? `${milliseconds}ms` : `${(milliseconds / 1_000).toFixed(1)}s`;
8102
8198
  }
8103
8199
 
8200
+ function statusTaskMetricsLine(turnCount: number | undefined, storageBytes: number | undefined): string {
8201
+ const turns = turnCount === undefined ? "未知" : `${Math.max(0, Math.round(turnCount)).toLocaleString("en-US")} 轮`;
8202
+ const storage = storageBytes === undefined ? "未知" : formatStorageSize(storageBytes);
8203
+ return `**已执行轮次 / 磁盘占用**:${turns} / ${storage}`;
8204
+ }
8205
+
8104
8206
  function formatStatusTime(value: string): string {
8105
8207
  const date = new Date(value);
8106
8208
  if (Number.isNaN(date.getTime())) return value;
@@ -8574,6 +8676,23 @@ function deduplicateFileReferences(
8574
8676
  });
8575
8677
  }
8576
8678
 
8679
+ function selectProviderModel(models: ModelOption[], previousModel?: string): ModelOption | undefined {
8680
+ return models.find((model) => model.id === previousModel)
8681
+ ?? models.find((model) => model.isDefault)
8682
+ ?? models[0];
8683
+ }
8684
+
8685
+ function selectProviderReasoningEffort(model: ModelOption, previousEffort?: string): string {
8686
+ if (previousEffort && (model.supportedReasoningEfforts.length === 0
8687
+ || model.supportedReasoningEfforts.some((option) => option.value === previousEffort))) {
8688
+ return previousEffort;
8689
+ }
8690
+ return model.defaultReasoningEffort
8691
+ ?? model.supportedReasoningEfforts[0]?.value
8692
+ ?? previousEffort
8693
+ ?? "medium";
8694
+ }
8695
+
8577
8696
  function recoveryCardPrompt(prompt: string): string {
8578
8697
  return `恢复重启前的任务:${prompt}`;
8579
8698
  }
@@ -73,6 +73,17 @@ export type AgentEvent =
73
73
  turnId: string;
74
74
  lastTokens: number;
75
75
  cumulativeTokens: number;
76
+ contextTokens?: number;
77
+ }
78
+ | {
79
+ type: "context_compaction";
80
+ sessionId: string;
81
+ turnId: string;
82
+ phase: "started" | "completed";
83
+ compactionId?: string;
84
+ timestampMs?: number;
85
+ turnCount?: number;
86
+ storageBytes?: number;
76
87
  }
77
88
  | {
78
89
  type: "progress";
@@ -156,6 +167,11 @@ export interface RemoteSessionActivity {
156
167
  activeTurnId?: string;
157
168
  }
158
169
 
170
+ export interface RemoteSessionMetrics {
171
+ turnCount: number;
172
+ storageBytes?: number;
173
+ }
174
+
159
175
  export interface RuntimeSession {
160
176
  localSessionId: string;
161
177
  remoteSessionId: string;
@@ -237,6 +253,7 @@ export interface AgentRuntime {
237
253
  readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
238
254
  listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
239
255
  readRemoteSession?(remoteSessionId: string, view?: "metadata" | "latest" | "latest-full"): Promise<RemoteSessionSummary>;
256
+ readRemoteSessionMetrics?(remoteSessionId: string): Promise<RemoteSessionMetrics>;
240
257
  readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
241
258
  listRemoteTurnSummaries?(remoteSessionId: string, input: { cursor?: string; limit: number }): Promise<RemoteTurnPage>;
242
259
  inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
@@ -261,7 +278,7 @@ export interface AgentRuntime {
261
278
  persist?: (session: RuntimeSession) => Promise<void>,
262
279
  ): Promise<RuntimeSession>;
263
280
  respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
264
- listModels(): Promise<ModelOption[]>;
281
+ listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
265
282
  listModelProviders?(): Promise<ModelProviderOption[]>;
266
283
  release?(options?: { force?: boolean }): Promise<RuntimeReleaseResult>;
267
284
  onEvent(listener: (event: RuntimeEvent) => void): () => void;
@@ -1332,6 +1332,15 @@ export class StateStore {
1332
1332
  }));
1333
1333
  }
1334
1334
 
1335
+ countTaskTurns(localSessionId: string): number {
1336
+ const turnIds = new Set(this.listTaskTurnGraphIndex(localSessionId).map((turn) => turn.turnId));
1337
+ const own = this.db.prepare(
1338
+ "SELECT turn_id AS turnId FROM turn_snapshots WHERE local_session_id = ?",
1339
+ ).all(localSessionId) as Array<{ turnId: string }>;
1340
+ for (const turn of own) turnIds.add(turn.turnId);
1341
+ return turnIds.size;
1342
+ }
1343
+
1335
1344
  listTaskTurnGraphIndex(localSessionId: string): CompletedTurnIndexRecord[] {
1336
1345
  const backfilled = new Set<string>();
1337
1346
  const backfill = (id: string): void => {