@wukonglabs/wukong 0.1.2 → 0.1.4

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 (43) 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 +158 -50
  6. package/dist/index.js.map +1 -1
  7. package/dist/tui/app.d.ts +10 -0
  8. package/dist/tui/app.d.ts.map +1 -1
  9. package/dist/tui/app.js +41 -8
  10. package/dist/tui/app.js.map +1 -1
  11. package/dist/tui/history-entry.d.ts.map +1 -1
  12. package/dist/tui/history-entry.js +6 -4
  13. package/dist/tui/history-entry.js.map +1 -1
  14. package/dist/tui/live-block.js +1 -1
  15. package/dist/tui/live-block.js.map +1 -1
  16. package/dist/tui/permission-card.d.ts.map +1 -1
  17. package/dist/tui/permission-card.js +3 -1
  18. package/dist/tui/permission-card.js.map +1 -1
  19. package/dist/tui/plan-panel.d.ts.map +1 -1
  20. package/dist/tui/plan-panel.js +3 -1
  21. package/dist/tui/plan-panel.js.map +1 -1
  22. package/dist/tui/response-panel.d.ts.map +1 -1
  23. package/dist/tui/response-panel.js +3 -1
  24. package/dist/tui/response-panel.js.map +1 -1
  25. package/dist/tui/slash/menu.d.ts.map +1 -1
  26. package/dist/tui/slash/menu.js +4 -2
  27. package/dist/tui/slash/menu.js.map +1 -1
  28. package/dist/tui/slash/registry.js +1 -1
  29. package/dist/tui/slash/registry.js.map +1 -1
  30. package/dist/tui/text-input.d.ts.map +1 -1
  31. package/dist/tui/text-input.js +5 -3
  32. package/dist/tui/text-input.js.map +1 -1
  33. package/dist/tui/theme/theme.d.ts +4 -0
  34. package/dist/tui/theme/theme.d.ts.map +1 -1
  35. package/dist/tui/theme/theme.js +14 -10
  36. package/dist/tui/theme/theme.js.map +1 -1
  37. package/dist/tui/thinking-panel.d.ts.map +1 -1
  38. package/dist/tui/thinking-panel.js +7 -5
  39. package/dist/tui/thinking-panel.js.map +1 -1
  40. package/dist/tui/welcome.js +1 -1
  41. package/dist/tui/welcome.js.map +1 -1
  42. package/package.json +2 -2
  43. 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
  });
@@ -579,10 +604,12 @@ function extractModifiedFiles(trace) {
579
604
  }
580
605
  return [...files];
581
606
  }
582
- /** 检测配置中是否已存在至少一个带 apiKey 的 provider(env / 配置文件都会汇总到这里) */
607
+ /** 检测是否存在至少一个可用模型及其带 apiKey 的 Provider。 */
583
608
  function hasConfiguredProvider(cfg) {
584
- const providers = cfg.agent?.providers ?? {};
585
- 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
+ });
586
613
  }
587
614
  /**
588
615
  * 交互式配置向导:在用户未配置任何 Provider 时收集最小可用配置。
@@ -602,23 +629,27 @@ async function runSetupWizard(logger) {
602
629
  logger.info(" 3) OpenAI 兼容(自定义 baseUrl,如 DeepSeek / 通义 / 本地 vLLM 等)");
603
630
  const typeChoice = (await question("输入序号 [1]: ")).trim() || "1";
604
631
  let providerKey;
632
+ let modelAlias;
605
633
  let type;
606
634
  let defaultModel;
607
635
  let baseUrlRequired = false;
608
636
  switch (typeChoice) {
609
637
  case "2":
610
638
  providerKey = "anthropic";
639
+ modelAlias = "anthropic-default";
611
640
  type = "anthropic";
612
641
  defaultModel = "claude-sonnet-4-20250514";
613
642
  break;
614
643
  case "3":
615
- providerKey = "openai-compatible";
644
+ providerKey = "custom";
645
+ modelAlias = "custom-default";
616
646
  type = "openai-compatible";
617
647
  defaultModel = "";
618
648
  baseUrlRequired = true;
619
649
  break;
620
650
  default:
621
651
  providerKey = "openai";
652
+ modelAlias = "openai-default";
622
653
  type = "openai";
623
654
  defaultModel = "gpt-4o";
624
655
  break;
@@ -650,8 +681,8 @@ async function runSetupWizard(logger) {
650
681
  logger.error("❌ 未填写模型名,配置未完成");
651
682
  return null;
652
683
  }
653
- const providerConf = { apiKey, baseUrl, type, model };
654
- return { providerKey, model, providerConf };
684
+ const providerConf = { apiKey, baseUrl, type };
685
+ return { providerKey, modelAlias, modelId: model, providerConf };
655
686
  }
656
687
  finally {
657
688
  rl.close();
@@ -671,10 +702,18 @@ async function persistWukongConfig(setup, logger) {
671
702
  // 文件不存在或无法解析 → 从空对象开始
672
703
  }
673
704
  existing.agent = existing.agent ?? {};
674
- existing.agent.provider = setup.providerKey;
675
- existing.agent.defaultModel = setup.model;
676
- existing.agent.providers = existing.agent.providers ?? {};
677
- 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
+ };
678
717
  try {
679
718
  await fs.mkdir(dir, { recursive: true });
680
719
  await fs.writeFile(file, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
@@ -801,16 +840,21 @@ async function runInteractive(cliArgs) {
801
840
  }
802
841
  await persistWukongConfig(setup, logger);
803
842
  // 直接注入内存配置,无需重新加载(避免本地 wukong.json 遮蔽全局配置的边界情况)
804
- cfg.agent.provider = setup.providerKey;
805
- cfg.agent.defaultModel = setup.model;
806
- 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
+ };
807
849
  logger.info("");
808
850
  if (suppressTuiBootInfo) {
809
851
  logger.setLevel('warn');
810
852
  }
811
853
  }
854
+ const selectedModel = cliArgs.model ?? cfg.agent.defaultModel;
855
+ resolveModelConfig(cfg, selectedModel);
812
856
  const resolvedConfirmMode = cliArgs.yes ? 'never' : (cliArgs.confirmMode ?? cfg.tools.confirmMode ?? 'smart');
813
- const resolvedThinking = resolveThinking(cliArgs, cfg);
857
+ const resolvedThinking = resolveThinking(cliArgs, cfg, selectedModel);
814
858
  const sessionManager = new SessionManager(cfg.sessions.dir);
815
859
  const workspace = resolveWorkspace();
816
860
  // 交互模式不视为 CI(即使被 pipe,交互模式一定不会被 --yes/--json 触发)
@@ -923,7 +967,7 @@ async function runInteractiveReadline(cliArgs, cfg, logger, resolvedConfirmMode,
923
967
  }
924
968
  };
925
969
  const { llm, tools, contextBuilder, agent, resolvedModel } = await initAgent({
926
- 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,
927
971
  overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
928
972
  onStream, tracerOpts: { enabled: !cliArgs.noTrace },
929
973
  shouldAbort, authorizeTool,
@@ -1048,11 +1092,11 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1048
1092
  };
1049
1093
  // 读取持久化输入历史(可选,失败不阻塞)
1050
1094
  const initialHistory = await loadInputHistory().catch(() => []);
1051
- // agent 引用:sendMessage 通过它跑 agent;init 完成前为 null
1052
- const agentRef = {
1053
- llm: null,
1054
- contextBuilder: null,
1055
- agent: null,
1095
+ // 当前运行时只在完整候选 Agent 构造成功后原子替换。
1096
+ const agentRef = { current: null };
1097
+ const streamSinkRef = { current: null };
1098
+ const onStreamProxy = (event) => {
1099
+ streamSinkRef.current?.(event);
1056
1100
  };
1057
1101
  const handlers = {
1058
1102
  eventBus: getDefaultEventBus(),
@@ -1068,12 +1112,46 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1068
1112
  authorizer,
1069
1113
  // M1:/compact 手动压缩(复用 index.ts 作用域内的 compactIfNeeded)
1070
1114
  compactIfNeeded: async () => {
1071
- if (!agentRef.agent || !agentRef.contextBuilder || !agentRef.llm)
1115
+ const runtime = agentRef.current;
1116
+ if (!runtime)
1072
1117
  return;
1073
- 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
+ };
1074
1151
  },
1075
1152
  sendMessage: async (input) => {
1076
- if (!agentRef.agent || !agentRef.contextBuilder || !agentRef.llm) {
1153
+ const runtime = agentRef.current;
1154
+ if (!runtime) {
1077
1155
  throw new Error('Agent 未初始化');
1078
1156
  }
1079
1157
  // 持久化输入历史(异步,不阻塞)
@@ -1090,14 +1168,14 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1090
1168
  // Smart Context 失败不影响主流程
1091
1169
  }
1092
1170
  }
1093
- const messages = agentRef.contextBuilder.build(session, attachSmartContext(input, smartContextText));
1094
- 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);
1095
1173
  await session.appendMessages([
1096
1174
  { role: "user", content: input },
1097
1175
  ...result.newMessages,
1098
1176
  ]);
1099
- const allMessages = agentRef.contextBuilder.build(session, "");
1100
- 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);
1101
1179
  const durationMs = result.metrics?.durationMs ?? 0;
1102
1180
  return {
1103
1181
  response: result.response,
@@ -1131,20 +1209,15 @@ async function runInteractiveTui(cliArgs, cfg, logger, resolvedConfirmMode, reso
1131
1209
  // - 首帧被日志盖在可视区上方(用户看不到 Header/输入框)
1132
1210
  // - Static 追加历史项时打在错的位置(Header 被复制多次)
1133
1211
  // - 大量空线框(Ink 尝试重绘时高度计算失效)
1134
- // onStream 通过 sinkRef 桥接:先创建占位,Ink 启动时挂上真正的 sink
1135
- const streamSinkRef = { current: null };
1136
- const onStreamProxy = (event) => {
1137
- streamSinkRef.current?.(event);
1138
- };
1139
- const { llm, contextBuilder, agent, tools, resolvedModel, skillsLoaded } = await initAgent({
1140
- 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,
1141
1215
  overrides: { maxSteps: cliArgs.maxSteps, contextWindow: cliArgs.contextWindow },
1142
1216
  onStream: onStreamProxy, tracerOpts: { enabled: !cliArgs.noTrace },
1143
1217
  shouldAbort, authorizeTool,
1144
1218
  });
1145
- agentRef.llm = llm;
1146
- agentRef.contextBuilder = contextBuilder;
1147
- agentRef.agent = agent;
1219
+ agentRef.current = initialRuntime;
1220
+ const { tools, resolvedModel, skillsLoaded } = initialRuntime;
1148
1221
  // ── init 完成后再启动 Ink ──
1149
1222
  // 此时终端上可能已经有几十行 skills 加载日志,Ink 启动后会:
1150
1223
  // - 立即切换到 alt-screen(取决于终端;不切换时也会把 UI 渲染在当前光标处)
@@ -1245,18 +1318,53 @@ function applyPermissionOverrides(cfg, cliArgs, logger) {
1245
1318
  logger.info(`🔒 强制拒绝工具: ${cliArgs.denyTools.join(', ')}`);
1246
1319
  }
1247
1320
  }
1248
- /** CLI 参数和配置文件解析思考模式配置 */
1249
- function resolveThinking(cliArgs, cfg) {
1250
- // CLI 参数优先
1251
- if (cliArgs.think) {
1252
- const budget = cliArgs.thinkBudget ?? cfg.agent.thinking?.budgetTokens ?? 10000;
1253
- 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;
1330
+ }
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
+ };
1254
1344
  }
1255
- // 配置文件
1256
- if (cfg.agent.thinking?.enabled) {
1257
- return { type: 'enabled', budgetTokens: Math.max(cfg.agent.thinking.budgetTokens ?? 10000, 1024) };
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
+ };
1258
1357
  }
1259
- return undefined;
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
+ };
1260
1368
  }
1261
1369
  // 🛡️ 全局兜底:任何遗漏监听的异步 'error' 事件(如子进程 spawn 失败)
1262
1370
  // 都不应直接秒杀进程。记录日志并优雅退出,保留错误现场便于排查。