@wukonglabs/wukong 0.1.1 → 0.1.3

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 (54) hide show
  1. package/README.md +17 -8
  2. package/dist/cli/help.d.ts.map +1 -1
  3. package/dist/cli/help.js +4 -1
  4. package/dist/cli/help.js.map +1 -1
  5. package/dist/index.js +171 -54
  6. package/dist/index.js.map +1 -1
  7. package/dist/tui/app.d.ts +15 -1
  8. package/dist/tui/app.d.ts.map +1 -1
  9. package/dist/tui/app.js +51 -11
  10. package/dist/tui/app.js.map +1 -1
  11. package/dist/tui/hint-bar.js +1 -1
  12. package/dist/tui/hint-bar.js.map +1 -1
  13. package/dist/tui/history-entry.d.ts +3 -1
  14. package/dist/tui/history-entry.d.ts.map +1 -1
  15. package/dist/tui/history-entry.js +23 -4
  16. package/dist/tui/history-entry.js.map +1 -1
  17. package/dist/tui/history-item.d.ts +14 -8
  18. package/dist/tui/history-item.d.ts.map +1 -1
  19. package/dist/tui/live-block.js +1 -1
  20. package/dist/tui/live-block.js.map +1 -1
  21. package/dist/tui/permission-card.d.ts.map +1 -1
  22. package/dist/tui/permission-card.js +3 -1
  23. package/dist/tui/permission-card.js.map +1 -1
  24. package/dist/tui/plan-panel.d.ts.map +1 -1
  25. package/dist/tui/plan-panel.js +3 -1
  26. package/dist/tui/plan-panel.js.map +1 -1
  27. package/dist/tui/response-panel.d.ts +6 -1
  28. package/dist/tui/response-panel.d.ts.map +1 -1
  29. package/dist/tui/response-panel.js +10 -1
  30. package/dist/tui/response-panel.js.map +1 -1
  31. package/dist/tui/run-ink.d.ts +18 -0
  32. package/dist/tui/run-ink.d.ts.map +1 -1
  33. package/dist/tui/run-ink.js +74 -2
  34. package/dist/tui/run-ink.js.map +1 -1
  35. package/dist/tui/slash/menu.d.ts.map +1 -1
  36. package/dist/tui/slash/menu.js +4 -2
  37. package/dist/tui/slash/menu.js.map +1 -1
  38. package/dist/tui/slash/registry.js +1 -1
  39. package/dist/tui/slash/registry.js.map +1 -1
  40. package/dist/tui/text-input.d.ts +16 -1
  41. package/dist/tui/text-input.d.ts.map +1 -1
  42. package/dist/tui/text-input.js +96 -42
  43. package/dist/tui/text-input.js.map +1 -1
  44. package/dist/tui/theme/theme.d.ts +4 -0
  45. package/dist/tui/theme/theme.d.ts.map +1 -1
  46. package/dist/tui/theme/theme.js +14 -10
  47. package/dist/tui/theme/theme.js.map +1 -1
  48. package/dist/tui/thinking-panel.d.ts.map +1 -1
  49. package/dist/tui/thinking-panel.js +7 -5
  50. package/dist/tui/thinking-panel.js.map +1 -1
  51. package/dist/tui/welcome.js +1 -1
  52. package/dist/tui/welcome.js.map +1 -1
  53. package/package.json +2 -2
  54. package/wukong.json +12 -3
package/README.md CHANGED
@@ -80,26 +80,35 @@ Place at `~/.wukong/wukong.json` (global) or `./wukong.json` (project):
80
80
  ```json
81
81
  {
82
82
  "agent": {
83
- "defaultModel": "deepseek-chat",
83
+ "defaultModel": "deepseek-chat"
84
+ },
85
+ "llm": {
84
86
  "providers": {
85
- "openai": {
86
- "apiKey": "sk-xxx",
87
+ "deepseek": {
88
+ "type": "openai-compatible",
89
+ "apiKey": "${DEEPSEEK_API_KEY}",
87
90
  "baseUrl": "https://api.deepseek.com/v1"
88
91
  }
92
+ },
93
+ "models": {
94
+ "deepseek-chat": {
95
+ "provider": "deepseek",
96
+ "id": "deepseek-chat"
97
+ }
89
98
  }
90
99
  }
91
100
  }
92
101
  ```
93
102
 
94
- Works with any OpenAI-compatible endpoint and Anthropic Claude. Provider type is auto-detected from model name / baseUrl.
103
+ Use `--model <alias>` at startup. In TUI, `/model` lists models and `/model <alias>` switches between turns.
95
104
 
96
105
  ### Environment Variables (Recommended)
97
106
 
98
107
  ```bash
99
- OPENAI_API_KEY=sk-xxx # → providers.openai.apiKey
100
- LLM_BASE_URL=https://api.xxx/v1 # → providers.openai.baseUrl
101
- LLM_MODEL=gpt-4o # → agent.defaultModel
102
- ANTHROPIC_API_KEY=sk-ant-xxx # → providers.anthropic.apiKey
108
+ OPENAI_API_KEY=sk-xxx # → llm.providers.openai.apiKey (if declared)
109
+ LLM_BASE_URL=https://api.xxx/v1 # → llm.providers.openai.baseUrl
110
+ LLM_MODEL=openai-default # → agent.defaultModel (declared alias)
111
+ ANTHROPIC_API_KEY=sk-ant-xxx # → llm.providers.anthropic.apiKey (if declared)
103
112
  ```
104
113
 
105
114
  Config file string values support `${VAR_NAME}` templates referencing env vars.
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,eAAO,MAAM,SAAS,QA+Ed,CAAC;AAET;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,eAAO,MAAM,SAAS,QAkFd,CAAC;AAET;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
package/dist/cli/help.js CHANGED
@@ -22,6 +22,7 @@ Wukong (悟空) — CI-First AI Agent CLI
22
22
  核心选项:
23
23
  -m, --message <text> 任务描述(agent 模式必填)
24
24
  -sp, --system-prompt <text> 自定义 system prompt
25
+ --model <alias> 选择 llm.models 中声明的模型别名
25
26
  -c, --continue 续接当前 workspace 最近一次会话(交互模式默认)
26
27
  -n, --new 强制新开一条会话(CI 模式默认)
27
28
  -s, --session-id <uuid> 精确指定要恢复的 session uuid
@@ -43,7 +44,9 @@ Wukong (悟空) — CI-First AI Agent CLI
43
44
 
44
45
  思考模式:
45
46
  --think 启用扩展思考模式
46
- --think-budget <tokens> 思考 token 预算(最小 1024)
47
+ --no-think 显式关闭思考模式
48
+ --think-budget <tokens> Anthropic 思考 token 预算(最小 1024)
49
+ --reasoning-effort <level> DeepSeek 思考强度: low | high | xhigh | max
47
50
 
48
51
  编辑确认:
49
52
  --confirm <mode> 确认模式: always | smart(默认) | never
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+ExB,CAAC,IAAI,EAAE,CAAC;AAET;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,OAAO,EAAE,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFxB,CAAC,IAAI,EAAE,CAAC;AAET;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,OAAO,EAAE,CAAC;AAC9B,CAAC"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as path from "node:path";
5
5
  import * as os from "node:os";
6
6
  // ── 核心能力全部来自独立发布的 SDK 包 ──
7
7
  // CLI 只做参数解析、会话编排与输出渲染,组装逻辑(createAgent)在 SDK 内。
8
- import { createAgent, SessionManager, collectSmartContext, attachSmartContext, AutoVerifier, DEFAULT_VERIFY_CONFIG, compactIfNeeded, loadConfig, Logger, setGlobalLogger, resolveWorkspace, getDefaultEventBus, ToolAuthorizer, TOOL_NAMES, isToolName, } from "@wukonglabs/wukong-agent-sdk";
8
+ import { createAgent, SessionManager, collectSmartContext, attachSmartContext, AutoVerifier, DEFAULT_VERIFY_CONFIG, compactIfNeeded, loadConfig, resolveModelConfig, Logger, setGlobalLogger, resolveWorkspace, getDefaultEventBus, ToolAuthorizer, TOOL_NAMES, isToolName, } from "@wukonglabs/wukong-agent-sdk";
9
9
  // ── CLI 本地模块(不属于 SDK 对外能力) ──
10
10
  import { EXIT_CODES, resolveExitCode } from "./cli/exit-codes.js";
11
11
  import { HELP_TEXT, formatVersion, VERSION } from "./cli/help.js";
@@ -22,6 +22,7 @@ function parseArgs(argv) {
22
22
  positional: [],
23
23
  message: null,
24
24
  systemPrompt: null,
25
+ model: null,
25
26
  sessionId: null,
26
27
  continueSession: false,
27
28
  newSession: false,
@@ -31,7 +32,9 @@ function parseArgs(argv) {
31
32
  yes: false,
32
33
  confirmMode: null,
33
34
  think: false,
35
+ noThink: false,
34
36
  thinkBudget: null,
37
+ reasoningEffort: null,
35
38
  planMode: 'auto',
36
39
  verify: false,
37
40
  verifyCommand: null,
@@ -78,6 +81,9 @@ function parseArgs(argv) {
78
81
  case "-sp":
79
82
  result.systemPrompt = args[++i] ?? null;
80
83
  break;
84
+ case "--model":
85
+ result.model = args[++i] ?? null;
86
+ break;
81
87
  case "--session-id":
82
88
  case "-s":
83
89
  result.sessionId = args[++i] ?? null;
@@ -115,6 +121,11 @@ function parseArgs(argv) {
115
121
  break;
116
122
  case "--think":
117
123
  result.think = true;
124
+ result.noThink = false;
125
+ break;
126
+ case "--no-think":
127
+ result.noThink = true;
128
+ result.think = false;
118
129
  break;
119
130
  case "--think-budget":
120
131
  {
@@ -122,6 +133,17 @@ function parseArgs(argv) {
122
133
  if (!isNaN(budget) && budget >= 1024) {
123
134
  result.thinkBudget = budget;
124
135
  result.think = true; // --think-budget 隐含 --think
136
+ result.noThink = false;
137
+ }
138
+ }
139
+ break;
140
+ case "--reasoning-effort":
141
+ {
142
+ const effort = args[++i];
143
+ if (effort === 'low' || effort === 'high' || effort === 'xhigh' || effort === 'max') {
144
+ result.reasoningEffort = effort;
145
+ result.think = true; // reasoning_effort 隐含开启思考
146
+ result.noThink = false;
125
147
  }
126
148
  }
127
149
  break;
@@ -236,6 +258,7 @@ async function initAgent(opts) {
236
258
  return createAgent({
237
259
  config: opts.cfg,
238
260
  logger: opts.logger,
261
+ model: opts.model,
239
262
  confirmMode: opts.confirmMode,
240
263
  thinking: opts.thinking,
241
264
  ciMode: opts.ciMode ?? false,
@@ -252,8 +275,10 @@ async function runAgentCommand(cliArgs) {
252
275
  setGlobalLogger(logger);
253
276
  const cfg = await loadConfig();
254
277
  applyPermissionOverrides(cfg, cliArgs, logger);
278
+ const selectedModel = cliArgs.model ?? cfg.agent.defaultModel;
279
+ resolveModelConfig(cfg, selectedModel);
255
280
  const resolvedConfirmMode = cliArgs.yes ? 'never' : (cliArgs.confirmMode ?? cfg.tools.confirmMode ?? 'smart');
256
- const resolvedThinking = resolveThinking(cliArgs, cfg);
281
+ const resolvedThinking = resolveThinking(cliArgs, cfg, selectedModel);
257
282
  // CI 模式:--yes 或 --json 时自动启用
258
283
  const isCiMode = cliArgs.yes || cliArgs.json;
259
284
  // ── Session 策略解析 ──
@@ -299,7 +324,7 @@ async function runAgentCommand(cliArgs) {
299
324
  }
300
325
  };
301
326
  const { llm, contextBuilder, agent, effectiveMaxSteps, resolvedModel } = await initAgent({
302
- cfg, logger, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, ciMode: isCiMode, planMode: cliArgs.planMode,
327
+ cfg, logger, model: selectedModel, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, ciMode: isCiMode, planMode: cliArgs.planMode,
303
328
  overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
304
329
  onStream, tracerOpts: { enabled: !cliArgs.noTrace },
305
330
  });
@@ -463,16 +488,18 @@ async function runAgentCommand(cliArgs) {
463
488
  ...(metrics ? {
464
489
  metrics: {
465
490
  steps: metrics.steps,
491
+ llmRequests: metrics.llmRequests,
466
492
  totalTokens: metrics.totalTokens,
467
493
  toolCalls: metrics.toolCalls,
468
494
  toolFailures: metrics.toolFailures,
469
495
  llmRetries: metrics.llmRetries,
470
496
  durationMs,
471
497
  duration: `${(durationMs / 1000).toFixed(1)}s`,
472
- ...(metrics.cachedPromptTokens ? {
498
+ ...(metrics.cachedPromptTokens !== undefined && metrics.cacheInputTokens !== undefined ? {
473
499
  cachedPromptTokens: metrics.cachedPromptTokens,
474
- cacheHitRate: metrics.totalTokens.prompt > 0
475
- ? Number((metrics.cachedPromptTokens / metrics.totalTokens.prompt).toFixed(4))
500
+ cacheInputTokens: metrics.cacheInputTokens,
501
+ cacheHitRate: metrics.cacheInputTokens > 0
502
+ ? Number((metrics.cachedPromptTokens / metrics.cacheInputTokens).toFixed(4))
476
503
  : 0,
477
504
  } : {}),
478
505
  ...(metrics.cacheCreationTokens ? { cacheCreationTokens: metrics.cacheCreationTokens } : {}),
@@ -577,10 +604,12 @@ function extractModifiedFiles(trace) {
577
604
  }
578
605
  return [...files];
579
606
  }
580
- /** 检测配置中是否已存在至少一个带 apiKey 的 provider(env / 配置文件都会汇总到这里) */
607
+ /** 检测是否存在至少一个可用模型及其带 apiKey 的 Provider。 */
581
608
  function hasConfiguredProvider(cfg) {
582
- const providers = cfg.agent?.providers ?? {};
583
- return Object.values(providers).some((p) => !!p && typeof p.apiKey === "string" && p.apiKey.trim().length > 0);
609
+ return Object.values(cfg.llm.models).some((model) => {
610
+ const provider = cfg.llm.providers[model.provider];
611
+ return !!provider?.apiKey?.trim();
612
+ });
584
613
  }
585
614
  /**
586
615
  * 交互式配置向导:在用户未配置任何 Provider 时收集最小可用配置。
@@ -600,23 +629,27 @@ async function runSetupWizard(logger) {
600
629
  logger.info(" 3) OpenAI 兼容(自定义 baseUrl,如 DeepSeek / 通义 / 本地 vLLM 等)");
601
630
  const typeChoice = (await question("输入序号 [1]: ")).trim() || "1";
602
631
  let providerKey;
632
+ let modelAlias;
603
633
  let type;
604
634
  let defaultModel;
605
635
  let baseUrlRequired = false;
606
636
  switch (typeChoice) {
607
637
  case "2":
608
638
  providerKey = "anthropic";
639
+ modelAlias = "anthropic-default";
609
640
  type = "anthropic";
610
641
  defaultModel = "claude-sonnet-4-20250514";
611
642
  break;
612
643
  case "3":
613
- providerKey = "openai-compatible";
644
+ providerKey = "custom";
645
+ modelAlias = "custom-default";
614
646
  type = "openai-compatible";
615
647
  defaultModel = "";
616
648
  baseUrlRequired = true;
617
649
  break;
618
650
  default:
619
651
  providerKey = "openai";
652
+ modelAlias = "openai-default";
620
653
  type = "openai";
621
654
  defaultModel = "gpt-4o";
622
655
  break;
@@ -648,8 +681,8 @@ async function runSetupWizard(logger) {
648
681
  logger.error("❌ 未填写模型名,配置未完成");
649
682
  return null;
650
683
  }
651
- const providerConf = { apiKey, baseUrl, type, model };
652
- return { providerKey, model, providerConf };
684
+ const providerConf = { apiKey, baseUrl, type };
685
+ return { providerKey, modelAlias, modelId: model, providerConf };
653
686
  }
654
687
  finally {
655
688
  rl.close();
@@ -669,10 +702,18 @@ async function persistWukongConfig(setup, logger) {
669
702
  // 文件不存在或无法解析 → 从空对象开始
670
703
  }
671
704
  existing.agent = existing.agent ?? {};
672
- existing.agent.provider = setup.providerKey;
673
- existing.agent.defaultModel = setup.model;
674
- existing.agent.providers = existing.agent.providers ?? {};
675
- existing.agent.providers[setup.providerKey] = setup.providerConf;
705
+ delete existing.agent.provider;
706
+ delete existing.agent.providers;
707
+ delete existing.agent.thinking;
708
+ existing.agent.defaultModel = setup.modelAlias;
709
+ existing.llm = existing.llm ?? {};
710
+ existing.llm.providers = existing.llm.providers ?? {};
711
+ existing.llm.models = existing.llm.models ?? {};
712
+ existing.llm.providers[setup.providerKey] = setup.providerConf;
713
+ existing.llm.models[setup.modelAlias] = {
714
+ provider: setup.providerKey,
715
+ id: setup.modelId,
716
+ };
676
717
  try {
677
718
  await fs.mkdir(dir, { recursive: true });
678
719
  await fs.writeFile(file, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
@@ -799,16 +840,21 @@ async function runInteractive(cliArgs) {
799
840
  }
800
841
  await persistWukongConfig(setup, logger);
801
842
  // 直接注入内存配置,无需重新加载(避免本地 wukong.json 遮蔽全局配置的边界情况)
802
- cfg.agent.provider = setup.providerKey;
803
- cfg.agent.defaultModel = setup.model;
804
- cfg.agent.providers[setup.providerKey] = setup.providerConf;
843
+ cfg.agent.defaultModel = setup.modelAlias;
844
+ cfg.llm.providers[setup.providerKey] = setup.providerConf;
845
+ cfg.llm.models[setup.modelAlias] = {
846
+ provider: setup.providerKey,
847
+ id: setup.modelId,
848
+ };
805
849
  logger.info("");
806
850
  if (suppressTuiBootInfo) {
807
851
  logger.setLevel('warn');
808
852
  }
809
853
  }
854
+ const selectedModel = cliArgs.model ?? cfg.agent.defaultModel;
855
+ resolveModelConfig(cfg, selectedModel);
810
856
  const resolvedConfirmMode = cliArgs.yes ? 'never' : (cliArgs.confirmMode ?? cfg.tools.confirmMode ?? 'smart');
811
- const resolvedThinking = resolveThinking(cliArgs, cfg);
857
+ const resolvedThinking = resolveThinking(cliArgs, cfg, selectedModel);
812
858
  const sessionManager = new SessionManager(cfg.sessions.dir);
813
859
  const workspace = resolveWorkspace();
814
860
  // 交互模式不视为 CI(即使被 pipe,交互模式一定不会被 --yes/--json 触发)
@@ -921,7 +967,7 @@ async function runInteractiveReadline(cliArgs, cfg, logger, resolvedConfirmMode,
921
967
  }
922
968
  };
923
969
  const { llm, tools, contextBuilder, agent, resolvedModel } = await initAgent({
924
- cfg, logger, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, planMode: cliArgs.planMode,
970
+ cfg, logger, model: cliArgs.model ?? cfg.agent.defaultModel, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, planMode: cliArgs.planMode,
925
971
  overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
926
972
  onStream, tracerOpts: { enabled: !cliArgs.noTrace },
927
973
  shouldAbort, authorizeTool,
@@ -1046,11 +1092,11 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1046
1092
  };
1047
1093
  // 读取持久化输入历史(可选,失败不阻塞)
1048
1094
  const initialHistory = await loadInputHistory().catch(() => []);
1049
- // agent 引用:sendMessage 通过它跑 agent;init 完成前为 null
1050
- const agentRef = {
1051
- llm: null,
1052
- contextBuilder: null,
1053
- agent: null,
1095
+ // 当前运行时只在完整候选 Agent 构造成功后原子替换。
1096
+ const agentRef = { current: null };
1097
+ const streamSinkRef = { current: null };
1098
+ const onStreamProxy = (event) => {
1099
+ streamSinkRef.current?.(event);
1054
1100
  };
1055
1101
  const handlers = {
1056
1102
  eventBus: getDefaultEventBus(),
@@ -1066,12 +1112,46 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1066
1112
  authorizer,
1067
1113
  // M1:/compact 手动压缩(复用 index.ts 作用域内的 compactIfNeeded)
1068
1114
  compactIfNeeded: async () => {
1069
- if (!agentRef.agent || !agentRef.contextBuilder || !agentRef.llm)
1115
+ const runtime = agentRef.current;
1116
+ if (!runtime)
1070
1117
  return;
1071
- await compactIfNeeded(session, agentRef.contextBuilder.build(session, ''), agentRef.llm, cfg.sessions.compaction);
1118
+ await compactIfNeeded(session, runtime.contextBuilder.build(session, ''), runtime.llm, cfg.sessions.compaction);
1119
+ },
1120
+ listModels: () => Object.entries(cfg.llm.models)
1121
+ .filter(([, profile]) => {
1122
+ const apiKey = cfg.llm.providers[profile.provider]?.apiKey;
1123
+ return !!apiKey && !/^\$\{[^}]+\}$/.test(apiKey);
1124
+ })
1125
+ .map(([alias, profile]) => ({
1126
+ alias,
1127
+ id: profile.id,
1128
+ provider: profile.provider,
1129
+ })),
1130
+ switchModel: async (alias) => {
1131
+ resolveModelConfig(cfg, alias);
1132
+ const candidate = await initAgent({
1133
+ cfg,
1134
+ logger,
1135
+ model: alias,
1136
+ confirmMode: resolvedConfirmMode,
1137
+ thinking: resolveThinking(cliArgs, cfg, alias),
1138
+ planMode: cliArgs.planMode,
1139
+ overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
1140
+ onStream: onStreamProxy,
1141
+ tracerOpts: { enabled: !cliArgs.noTrace },
1142
+ shouldAbort,
1143
+ authorizeTool,
1144
+ });
1145
+ agentRef.current = candidate;
1146
+ return {
1147
+ alias: candidate.resolvedModelProfile.alias,
1148
+ id: candidate.resolvedModelProfile.id,
1149
+ provider: candidate.resolvedModelProfile.providerName,
1150
+ };
1072
1151
  },
1073
1152
  sendMessage: async (input) => {
1074
- if (!agentRef.agent || !agentRef.contextBuilder || !agentRef.llm) {
1153
+ const runtime = agentRef.current;
1154
+ if (!runtime) {
1075
1155
  throw new Error('Agent 未初始化');
1076
1156
  }
1077
1157
  // 持久化输入历史(异步,不阻塞)
@@ -1088,19 +1168,26 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1088
1168
  // Smart Context 失败不影响主流程
1089
1169
  }
1090
1170
  }
1091
- const messages = agentRef.contextBuilder.build(session, attachSmartContext(input, smartContextText));
1092
- const result = await agentRef.agent.runWithMessages(messages);
1171
+ const messages = runtime.contextBuilder.build(session, attachSmartContext(input, smartContextText));
1172
+ const result = await runtime.agent.runWithMessages(messages);
1093
1173
  await session.appendMessages([
1094
1174
  { role: "user", content: input },
1095
1175
  ...result.newMessages,
1096
1176
  ]);
1097
- const allMessages = agentRef.contextBuilder.build(session, "");
1098
- await compactIfNeeded(session, allMessages, agentRef.llm, cfg.sessions.compaction);
1177
+ const allMessages = runtime.contextBuilder.build(session, "");
1178
+ await compactIfNeeded(session, allMessages, runtime.llm, cfg.sessions.compaction);
1099
1179
  const durationMs = result.metrics?.durationMs ?? 0;
1100
1180
  return {
1101
1181
  response: result.response,
1102
- steps: result.steps,
1182
+ requestCount: result.metrics?.llmRequests ?? result.steps,
1103
1183
  totalTokens: result.totalTokens,
1184
+ ...(result.metrics?.cachedPromptTokens !== undefined &&
1185
+ result.metrics.cacheInputTokens !== undefined
1186
+ ? {
1187
+ cachedPromptTokens: result.metrics.cachedPromptTokens,
1188
+ cacheInputTokens: result.metrics.cacheInputTokens,
1189
+ }
1190
+ : {}),
1104
1191
  durationMs,
1105
1192
  };
1106
1193
  },
@@ -1122,20 +1209,15 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1122
1209
  // - 首帧被日志盖在可视区上方(用户看不到 Header/输入框)
1123
1210
  // - Static 追加历史项时打在错的位置(Header 被复制多次)
1124
1211
  // - 大量空线框(Ink 尝试重绘时高度计算失效)
1125
- // onStream 通过 sinkRef 桥接:先创建占位,Ink 启动时挂上真正的 sink
1126
- const streamSinkRef = { current: null };
1127
- const onStreamProxy = (event) => {
1128
- streamSinkRef.current?.(event);
1129
- };
1130
- const { llm, contextBuilder, agent, tools, resolvedModel, skillsLoaded } = await initAgent({
1131
- cfg, logger, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, planMode: cliArgs.planMode,
1212
+ // onStream 通过前面创建的稳定 sinkRef 桥接到 Ink。
1213
+ const initialRuntime = await initAgent({
1214
+ cfg, logger, model: cliArgs.model ?? cfg.agent.defaultModel, confirmMode: resolvedConfirmMode, thinking: resolvedThinking, planMode: cliArgs.planMode,
1132
1215
  overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
1133
1216
  onStream: onStreamProxy, tracerOpts: { enabled: !cliArgs.noTrace },
1134
1217
  shouldAbort, authorizeTool,
1135
1218
  });
1136
- agentRef.llm = llm;
1137
- agentRef.contextBuilder = contextBuilder;
1138
- agentRef.agent = agent;
1219
+ agentRef.current = initialRuntime;
1220
+ const { tools, resolvedModel, skillsLoaded } = initialRuntime;
1139
1221
  // ── init 完成后再启动 Ink ──
1140
1222
  // 此时终端上可能已经有几十行 skills 加载日志,Ink 启动后会:
1141
1223
  // - 立即切换到 alt-screen(取决于终端;不切换时也会把 UI 渲染在当前光标处)
@@ -1236,18 +1318,53 @@ function applyPermissionOverrides(cfg, cliArgs, logger) {
1236
1318
  logger.info(`🔒 强制拒绝工具: ${cliArgs.denyTools.join(', ')}`);
1237
1319
  }
1238
1320
  }
1239
- /** CLI 参数和配置文件解析思考模式配置 */
1240
- function resolveThinking(cliArgs, cfg) {
1241
- // CLI 参数优先
1242
- if (cliArgs.think) {
1243
- const budget = cliArgs.thinkBudget ?? cfg.agent.thinking?.budgetTokens ?? 10000;
1244
- return { type: 'enabled', budgetTokens: Math.max(budget, 1024) };
1321
+ /** 从所选模型与 CLI 参数解析本次运行的思考配置。 */
1322
+ function resolveThinking(cliArgs, cfg, modelAlias) {
1323
+ const configured = resolveModelConfig(cfg, modelAlias).thinking;
1324
+ const hasCliOverride = cliArgs.noThink || cliArgs.think || cliArgs.thinkBudget !== null || cliArgs.reasoningEffort !== null;
1325
+ if (!configured) {
1326
+ if (hasCliOverride) {
1327
+ throw new Error(`模型 "${modelAlias}" 未声明 thinking 协议,不能使用思考参数`);
1328
+ }
1329
+ return undefined;
1245
1330
  }
1246
- // 配置文件
1247
- if (cfg.agent.thinking?.enabled) {
1248
- return { type: 'enabled', budgetTokens: Math.max(cfg.agent.thinking.budgetTokens ?? 10000, 1024) };
1331
+ const configuredType = configured.enabled ? 'enabled' : 'disabled';
1332
+ if (!hasCliOverride) {
1333
+ return configured.protocol === 'deepseek'
1334
+ ? {
1335
+ protocol: 'deepseek',
1336
+ type: configuredType,
1337
+ ...(configured.reasoningEffort ? { reasoningEffort: configured.reasoningEffort } : {}),
1338
+ }
1339
+ : {
1340
+ protocol: 'anthropic',
1341
+ type: configuredType,
1342
+ ...(configured.budgetTokens !== undefined ? { budgetTokens: configured.budgetTokens } : {}),
1343
+ };
1249
1344
  }
1250
- return undefined;
1345
+ const type = cliArgs.noThink ? 'disabled' : 'enabled';
1346
+ if (configured.protocol === 'deepseek') {
1347
+ if (cliArgs.thinkBudget !== null) {
1348
+ throw new Error(`模型 "${modelAlias}" 使用 deepseek 思考协议,不支持 --think-budget`);
1349
+ }
1350
+ return {
1351
+ protocol: 'deepseek',
1352
+ type,
1353
+ ...(type === 'enabled' && (cliArgs.reasoningEffort ?? configured.reasoningEffort)
1354
+ ? { reasoningEffort: cliArgs.reasoningEffort ?? configured.reasoningEffort }
1355
+ : {}),
1356
+ };
1357
+ }
1358
+ if (cliArgs.reasoningEffort !== null) {
1359
+ throw new Error(`模型 "${modelAlias}" 使用 anthropic 思考协议,不支持 --reasoning-effort`);
1360
+ }
1361
+ return {
1362
+ protocol: 'anthropic',
1363
+ type,
1364
+ ...(type === 'enabled'
1365
+ ? { budgetTokens: Math.max(cliArgs.thinkBudget ?? configured.budgetTokens ?? 10000, 1024) }
1366
+ : {}),
1367
+ };
1251
1368
  }
1252
1369
  // 🛡️ 全局兜底:任何遗漏监听的异步 'error' 事件(如子进程 spawn 失败)
1253
1370
  // 都不应直接秒杀进程。记录日志并优雅退出,保留错误现场便于排查。