magic-agent 2.4.3 → 2.6.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 (207) hide show
  1. package/CLIENT-CONTRACT.md +467 -41
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +74 -0
  4. package/client-ui/pc/js/04-task-list.js +5 -1
  5. package/client-ui/pc/js/07-chat-stream-render.js +3 -3
  6. package/client-ui/pc/js/90-glue.js +40 -2
  7. package/client-ui/pc/js/92-composer.js +19 -4
  8. package/client-ui/pc/js/93-rail.js +16 -5
  9. package/client-ui/shared/conv/02-media-kinds.js +2 -2
  10. package/client-ui/shared/conv/03-chat-message.js +247 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +72 -7
  13. package/client-ui/shared/conv/19-attachments.js +157 -6
  14. package/client-ui/shared/conv/27-conv-ledger.js +5 -2
  15. package/client-ui/shared/conv/29-msg-variants.js +217 -0
  16. package/client-ui/shared/conv/conv.css +329 -174
  17. package/client-ui/shared/conv/dom-map.md +1 -0
  18. package/client-ui/shared/conversation.css +13 -2
  19. package/client-ui/shared/conversation.js +373 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/apps.css +16 -0
  22. package/client-ui-mobile/apps.js +406 -0
  23. package/client-ui-mobile/index.html +5554 -639
  24. package/client-ui-mobile/reqs.css +110 -1
  25. package/client-ui-mobile/reqs.js +533 -74
  26. package/client-ui-mobile/reqshot.js +505 -0
  27. package/client-ui-mobile/scripts.css +67 -0
  28. package/client-ui-mobile/scripts.js +599 -0
  29. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  30. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  31. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  32. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  33. package/cmd/magic-client/browsectl.go +199 -0
  34. package/cmd/magic-client/clientctl.go +69 -10
  35. package/cmd/magic-client/main.go +210 -32
  36. package/internal/agent/ask.go +45 -0
  37. package/internal/agent/ask_test.go +39 -0
  38. package/internal/agent/attachments_test.go +6 -2
  39. package/internal/agent/capability.go +7 -1
  40. package/internal/agent/claude.go +20 -15
  41. package/internal/agent/codebuddy.go +125 -34
  42. package/internal/agent/codebuddy_account_test.go +54 -0
  43. package/internal/agent/codebuddy_stream_test.go +103 -0
  44. package/internal/agent/credits.go +713 -0
  45. package/internal/agent/credits_minimax_test.go +97 -0
  46. package/internal/agent/credits_test.go +211 -0
  47. package/internal/agent/custom_models.go +155 -0
  48. package/internal/agent/custom_models_test.go +125 -0
  49. package/internal/agent/dsh.go +123 -49
  50. package/internal/agent/dsh_caps_cache_test.go +184 -0
  51. package/internal/agent/dsh_resume.go +157 -7
  52. package/internal/agent/dsh_test.go +108 -6
  53. package/internal/agent/engine.go +37 -1
  54. package/internal/agent/engine_base.go +21 -17
  55. package/internal/agent/engine_base_test.go +2 -2
  56. package/internal/agent/engine_test.go +47 -86
  57. package/internal/agent/env.go +173 -1
  58. package/internal/agent/env_test.go +114 -0
  59. package/internal/agent/fallback.go +313 -0
  60. package/internal/agent/fallback_test.go +340 -0
  61. package/internal/agent/launch.go +43 -1
  62. package/internal/agent/limitnotice.go +207 -1
  63. package/internal/agent/limitnotice_test.go +140 -0
  64. package/internal/agent/llmhttp.go +46 -4
  65. package/internal/agent/llmhttp_test.go +157 -0
  66. package/internal/agent/models.go +53 -1
  67. package/internal/agent/models_test.go +20 -5
  68. package/internal/agent/pidalive_windows_test.go +40 -0
  69. package/internal/agent/runcmd.go +26 -0
  70. package/internal/agent/runcmd_windows.go +36 -5
  71. package/internal/agent/stream.go +166 -6
  72. package/internal/agent/stream_test.go +25 -38
  73. package/internal/agent/stream_timeline_test.go +2 -37
  74. package/internal/agent/streamjson.go +119 -7
  75. package/internal/agent/tools_suffix_test.go +6 -3
  76. package/internal/agent/trae.go +50 -243
  77. package/internal/agent/trae_exec.go +433 -0
  78. package/internal/agent/trae_exec_test.go +181 -0
  79. package/internal/agent/wraplaunch_test.go +113 -0
  80. package/internal/cli/ask.go +168 -37
  81. package/internal/cli/ask_test.go +1 -1
  82. package/internal/cli/checkin.go +77 -0
  83. package/internal/cli/clientcmd.go +3 -2
  84. package/internal/cli/control.go +46 -5
  85. package/internal/cli/control_test.go +65 -0
  86. package/internal/cli/engines_test.go +103 -0
  87. package/internal/cli/events.go +15 -0
  88. package/internal/cli/events_test.go +50 -0
  89. package/internal/cli/judge.go +104 -0
  90. package/internal/cli/quotas.go +125 -0
  91. package/internal/cli/requirements.go +51 -0
  92. package/internal/cli/root.go +157 -11
  93. package/internal/cli/session.go +75 -3
  94. package/internal/cli/session_test.go +82 -3
  95. package/internal/cli/sessionlog.go +6 -0
  96. package/internal/cli/sessionlog_test.go +22 -2
  97. package/internal/cli/suggest.go +7 -5
  98. package/internal/cli/team.go +37 -9
  99. package/internal/cli/team_test.go +39 -0
  100. package/internal/client/ab.go +372 -0
  101. package/internal/client/ab_test.go +287 -0
  102. package/internal/client/apk.go +8 -0
  103. package/internal/client/apk_test.go +59 -0
  104. package/internal/client/apps.go +1078 -0
  105. package/internal/client/apps_test.go +516 -0
  106. package/internal/client/browse.go +84 -36
  107. package/internal/client/browse_test.go +107 -9
  108. package/internal/client/browseinput.go +481 -0
  109. package/internal/client/browseinput_test.go +216 -0
  110. package/internal/client/corecmd.go +23 -0
  111. package/internal/client/corecmd_other.go +8 -0
  112. package/internal/client/corecmd_windows.go +24 -0
  113. package/internal/client/coreexe.go +196 -0
  114. package/internal/client/coreexe_test.go +161 -0
  115. package/internal/client/coreupgrade.go +866 -0
  116. package/internal/client/coreupgrade_test.go +561 -0
  117. package/internal/client/daemon.go +297 -33
  118. package/internal/client/daemon_test.go +33 -0
  119. package/internal/client/engines_checkin_test.go +57 -0
  120. package/internal/client/fallback_flags_test.go +64 -0
  121. package/internal/client/feishu.go +1259 -0
  122. package/internal/client/feishu_test.go +649 -0
  123. package/internal/client/health_identity_test.go +84 -0
  124. package/internal/client/hostenv.go +68 -0
  125. package/internal/client/hostenv_test.go +37 -0
  126. package/internal/client/jobs.go +518 -24
  127. package/internal/client/jobs_auto.go +571 -0
  128. package/internal/client/jobs_auto_test.go +431 -0
  129. package/internal/client/jobs_test.go +299 -2
  130. package/internal/client/knowledge.go +854 -0
  131. package/internal/client/knowledge_test.go +510 -0
  132. package/internal/client/lan.go +3 -3
  133. package/internal/client/plugins.go +5 -1
  134. package/internal/client/prefs.go +67 -0
  135. package/internal/client/procstart_linux.go +59 -0
  136. package/internal/client/publicurl.go +82 -0
  137. package/internal/client/quotas_merge_test.go +107 -0
  138. package/internal/client/requirements.go +207 -2
  139. package/internal/client/requirements_shots_test.go +200 -0
  140. package/internal/client/requirements_test.go +103 -0
  141. package/internal/client/restart.go +288 -0
  142. package/internal/client/restart_handoff_test.go +230 -0
  143. package/internal/client/restart_test.go +290 -0
  144. package/internal/client/scripts.go +1047 -0
  145. package/internal/client/scripts_decode_implausible.go +38 -0
  146. package/internal/client/scripts_decode_other.go +31 -0
  147. package/internal/client/scripts_decode_test.go +130 -0
  148. package/internal/client/scripts_decode_windows.go +182 -0
  149. package/internal/client/scripts_test.go +739 -0
  150. package/internal/client/service.go +717 -97
  151. package/internal/client/service_test.go +217 -1
  152. package/internal/client/sessions_cache.go +148 -0
  153. package/internal/client/sessions_cache_test.go +163 -0
  154. package/internal/client/sessions_fold.go +165 -0
  155. package/internal/client/sessions_fold_test.go +204 -0
  156. package/internal/client/singleinstance.go +470 -0
  157. package/internal/client/singleinstance_test.go +607 -0
  158. package/internal/client/suggest.go +105 -2
  159. package/internal/client/suggest_test.go +86 -0
  160. package/internal/client/team_flags.go +25 -11
  161. package/internal/client/team_flags_test.go +39 -0
  162. package/internal/client/testmain_test.go +88 -0
  163. package/internal/client/workspace_default_test.go +158 -0
  164. package/internal/client/workspacelockenv_test.go +54 -0
  165. package/internal/config/config.go +48 -0
  166. package/internal/judge/judge.go +315 -0
  167. package/internal/requirements/bootstrap.go +16 -0
  168. package/internal/requirements/consolidate.go +100 -9
  169. package/internal/requirements/doc.go +154 -3
  170. package/internal/requirements/graph.go +138 -0
  171. package/internal/requirements/memory.go +7 -0
  172. package/internal/requirements/modules.go +312 -0
  173. package/internal/requirements/modules_test.go +373 -0
  174. package/internal/requirements/relations.go +155 -0
  175. package/internal/requirements/relations_test.go +231 -0
  176. package/internal/requirements/requirements.go +10 -0
  177. package/internal/requirements/shots.go +418 -0
  178. package/internal/requirements/shots_test.go +387 -0
  179. package/internal/session/multiclient_test.go +368 -0
  180. package/internal/session/session.go +175 -11
  181. package/internal/session/session_test.go +7 -0
  182. package/internal/session/status.go +21 -0
  183. package/internal/session/status_test.go +28 -0
  184. package/internal/session/title.go +16 -0
  185. package/internal/session/title_test.go +26 -0
  186. package/internal/session/workspace_lock.go +265 -28
  187. package/internal/suggest/suggest.go +23 -1
  188. package/internal/team/event.go +3 -1
  189. package/internal/team/execute.go +21 -6
  190. package/internal/team/fallback_route_test.go +80 -0
  191. package/internal/team/planner.go +15 -3
  192. package/internal/team/route.go +89 -17
  193. package/internal/team/team.go +18 -4
  194. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  195. package/npm/apk/version.json +6 -3
  196. package/npm/dist/darwin-arm64/magic-agent +0 -0
  197. package/npm/dist/darwin-arm64/magic-client +0 -0
  198. package/npm/dist/darwin-x64/magic-agent +0 -0
  199. package/npm/dist/darwin-x64/magic-client +0 -0
  200. package/npm/dist/linux-arm64/magic-agent +0 -0
  201. package/npm/dist/linux-arm64/magic-client +0 -0
  202. package/npm/dist/linux-x64/magic-agent +0 -0
  203. package/npm/dist/linux-x64/magic-client +0 -0
  204. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  205. package/npm/dist/win32-x64/magic-client.exe +0 -0
  206. package/npm/install.js +11 -0
  207. package/package.json +14 -3
@@ -382,22 +382,102 @@ func TestDshHeadlessArgs(t *testing.T) {
382
382
  cases := []struct {
383
383
  name string
384
384
  sessionID string
385
+ patch string
385
386
  json bool
386
387
  want []string
387
388
  }{
388
- {"纯 headless", "", false, []string{"--profile", "headless", "任务"}},
389
- {"续接", "s1", false, []string{"--profile", "headless", "--session-id", "s1", "任务"}},
390
- {"事件流", "", true, []string{"--profile", "headless", "--json", "任务"}},
391
- {"续接 + 事件流", "s1", true, []string{"--profile", "headless", "--session-id", "s1", "--json", "任务"}},
389
+ {"纯 headless", "", "", false, []string{"--profile", "headless", "任务"}},
390
+ {"续接", "s1", "", false, []string{"--profile", "headless", "--session-id", "s1", "任务"}},
391
+ {"事件流", "", "", true, []string{"--profile", "headless", "--json", "任务"}},
392
+ {"续接 + 事件流", "s1", "", true, []string{"--profile", "headless", "--session-id", "s1", "--json", "任务"}},
393
+ // --patch 是**启动器** flag,必须在 --profile 之前(放到后面 headless 会报 unknown option)。
394
+ {"模型 patch", "s1", "/tmp/p.yml", false,
395
+ []string{"--patch", "/tmp/p.yml", "--profile", "headless", "--session-id", "s1", "任务"}},
392
396
  }
393
397
  for _, c := range cases {
394
- got := dshHeadlessArgs("headless", c.sessionID, "任务", c.json)
398
+ got := dshHeadlessArgs("headless", c.sessionID, c.patch, "任务", c.json)
395
399
  if fmt.Sprint(got) != fmt.Sprint(c.want) {
396
400
  t.Errorf("%s: %v want %v", c.name, got, c.want)
397
401
  }
398
402
  }
399
403
  }
400
404
 
405
+ // headless 提示词传输:Windows 走 stdin("-"),其余平台保持位置参数。
406
+ func TestDshHeadlessPromptArg(t *testing.T) {
407
+ arg, stdin := dshHeadlessPromptArg("【用户】\n只回复 OK")
408
+ if platformIsWindows() {
409
+ if arg != "-" || stdin != "【用户】\n只回复 OK" {
410
+ t.Errorf("Windows 应走 stdin:arg=%q stdin=%q", arg, stdin)
411
+ }
412
+ return
413
+ }
414
+ if arg != "【用户】\n只回复 OK" || stdin != "" {
415
+ t.Errorf("非 Windows 应走位置参数:arg=%q stdin=%q", arg, stdin)
416
+ }
417
+ }
418
+
419
+ // dshHeadlessPatchFor:把 -m 落成 agent-default-model 覆盖;没 -m 不落文件。
420
+ func TestDshHeadlessPatchFor(t *testing.T) {
421
+ dir := t.TempDir()
422
+ w, _ := argsCaptureCLI(t, dir, "dsh", `echo 'ok'`)
423
+
424
+ if path, cleanup := dshHeadlessPatchFor(context.Background(), w.bin, ""); path != "" {
425
+ t.Fatalf("没有 -m 不该写 patch,got %q", path)
426
+ } else {
427
+ cleanup()
428
+ }
429
+
430
+ path, cleanup := dshHeadlessPatchFor(context.Background(), w.bin, "volcengine/deepseek-v4.1-flash-1017")
431
+ if path == "" {
432
+ t.Fatal("有 -m 应写出临时 patch")
433
+ }
434
+ data, err := os.ReadFile(path)
435
+ if err != nil {
436
+ t.Fatalf("读 patch: %v", err)
437
+ }
438
+ body := string(data)
439
+ for _, want := range []string{"id: agent-default-model", `provider: "volcengine"`, `model: "deepseek-v4.1-flash-1017"`} {
440
+ if !strings.Contains(body, want) {
441
+ t.Errorf("patch 内容缺 %q:%q", want, body)
442
+ }
443
+ }
444
+ cleanup()
445
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
446
+ t.Errorf("cleanup 应删掉临时 patch,stat err = %v", err)
447
+ }
448
+ }
449
+
450
+ // 续接轮把 -m 透成 --patch:模型选择在 headless 通道同样生效(2026-10-25 报障)。
451
+ func TestDshResumePassesModelPatch(t *testing.T) {
452
+ dir := t.TempDir()
453
+ w, log := argsCaptureCLI(t, dir, "dsh", `echo 'ok'`)
454
+
455
+ resp, err := (&DshEngine{BinPath: w.bin}).Complete(context.Background(), Request{
456
+ Messages: []Message{{Role: "user", Content: "hi"}},
457
+ SessionID: "sess-1",
458
+ Model: "volcengine/deepseek-v4.1-flash-1017",
459
+ })
460
+ if err != nil {
461
+ t.Fatalf("Complete: %v", err)
462
+ }
463
+ args := readFileString(t, log)
464
+ var headlessLine string
465
+ for _, ln := range strings.Split(args, "\n") {
466
+ if strings.Contains(ln, "--patch") {
467
+ headlessLine = ln
468
+ }
469
+ }
470
+ if headlessLine == "" {
471
+ t.Fatalf("续接轮应带 --patch(headless 没有 --model):%q", args)
472
+ }
473
+ if strings.Index(headlessLine, "--patch") > strings.Index(headlessLine, "--profile") {
474
+ t.Errorf("--patch 必须排在 --profile 之前:%q", headlessLine)
475
+ }
476
+ if resp.Model != "volcengine/deepseek-v4.1-flash-1017" {
477
+ t.Errorf("Model = %q want 本轮 -m 指定的那条路由", resp.Model)
478
+ }
479
+ }
480
+
401
481
  // -c/--continue:按会话登记表找**同一 workspace** 下最近一条 dsh 会话(其它引擎不算)。
402
482
  func TestDshLatestSessionIDPrefersWorkspace(t *testing.T) {
403
483
  sess := t.TempDir()
@@ -870,6 +950,28 @@ func TestDshListModelsFromProfilePatch(t *testing.T) {
870
950
  }
871
951
  }
872
952
 
953
+ // home 级 $DSH_HOME/cordis.patch.yml(对所有 profile 生效)也要进探测链:
954
+ // 2026-10-23 实测 dsh 0.2.1-rc.2 重写配置后 profile 级 patch 清成空 [],
955
+ // provider 只剩 home 级这一份,不读它就整条链空手而归。
956
+ func TestDshListModelsFromHomePatch(t *testing.T) {
957
+ t.Setenv("MAGIC_AGENT_DSH_PROFILE", "")
958
+ home := writeDshPatch(t, "sdk", "[]\n")
959
+ if err := os.WriteFile(filepath.Join(home, "cordis.patch.yml"),
960
+ []byte("- id: llm-pi-ai\n config:\n providers:\n minimax:\n models:\n - id: MiniMax-M3.1-Flash\n"), 0o644); err != nil {
961
+ t.Fatal(err)
962
+ }
963
+ t.Setenv("DSH_HOME", home)
964
+
965
+ got, err := (&DshEngine{}).ListModels(context.Background())
966
+ if err != nil {
967
+ t.Fatalf("ListModels: %v", err)
968
+ }
969
+ want := []string{"minimax/MiniMax-M3.1-Flash"}
970
+ if !equalStrings(got, want) {
971
+ t.Errorf("ListModels() = %v\n want %v", got, want)
972
+ }
973
+ }
974
+
873
975
  // 两条通道(sdk + headless 回退)的 patch 取并集:magic-agent 两条都会用到。
874
976
  func TestDshListModelsUnionAcrossProfiles(t *testing.T) {
875
977
  t.Setenv("MAGIC_AGENT_DSH_PROFILE", "")
@@ -1175,7 +1277,7 @@ func TestDshIgnoredParamsProduceNoFlags(t *testing.T) {
1175
1277
  _ = req.Temperature
1176
1278
  _ = req.Tools
1177
1279
  _ = req.Timeout
1178
- args := dshHeadlessArgs(e.profile(), "", "【用户】\nhi", false)
1280
+ args := dshHeadlessArgs(e.profile(), "", "", "【用户】\nhi", false)
1179
1281
  want := []string{"--profile", "headless", "【用户】\nhi"}
1180
1282
  if fmt.Sprint(args) != fmt.Sprint(want) {
1181
1283
  t.Errorf("headless args = %v want %v", args, want)
@@ -12,7 +12,7 @@
12
12
  // prompt.go — 多轮消息扁平化为单条 prompt 的公共逻辑
13
13
  // claude.go — Claude Code CLI 引擎(--print --output-format json)
14
14
  // codebuddy.go — CodeBuddy Code CLI 引擎(一个独立 CLI、两个账号)
15
- // trae.go — Trae CLI 引擎(-p,无 --model flag,-c model.name= 覆盖)
15
+ // trae.go — TRAE CLI 2.0 引擎(exec --json,-m 指定模型)
16
16
  // llmengine.go — simonw/LLM CLI 引擎(-m / -s / -o 透传)
17
17
  // codex.go — Codex CLI 引擎(exec / exec resume + --output-schema)
18
18
  // openclaw.go — OpenClaw CLI 引擎(agent --local --json)
@@ -92,6 +92,24 @@ type Request struct {
92
92
  // 引擎实现负责剥掉可能的 "engine/model" 前缀。
93
93
  Model string
94
94
 
95
+ // FallbackEngine / FallbackModel 主引擎失败时的**备用引擎**(2026-10-24)。
96
+ //
97
+ // 只在「主引擎只有过程、没有正文」时降级(见 fallback.go):已经流出正文再换
98
+ // 模型重来,用户会看到两段互相矛盾的答案。降级时 SessionID / Continue 一律清空 ——
99
+ // 备用引擎没有主引擎那条会话,拿旧 id 去 resume 只会报「会话不存在」。
100
+ // 空引擎 = 没配备用,行为与以前完全一致。引擎实现不读这两个字段。
101
+ FallbackEngine string
102
+ FallbackModel string
103
+
104
+ // FallbackDown 降级标记:为 true 时本轮请求**本身就是备用引擎在跑**(见 fallback.go)。
105
+ //
106
+ // 为什么要有它(2026-10-26 修「主备没真的发挥作用」):模型清单只属于它自己的引擎。
107
+ // 用户配的备用模型(如 codebuddy 的 `hy3`)拿到别的引擎上只会吃一句
108
+ // `--model hy3 未知`,于是备用「跑了但没用」。fallbackTarget 因此清空 Model,
109
+ // 这个字段让引擎侧在 Model 为空时**不要回落到自己的默认模型** —— 配置里的备用模型
110
+ // 作废时「用备用引擎自己的默认模型重跑」比「带着一个必然失败的模型再失败一次」有用。
111
+ FallbackDown bool
112
+
95
113
  // SystemPrompt 可选系统提示词。各引擎自行决定注入方式
96
114
  //(claude/codebuddy用 --append-system-prompt,trae 拼进 prompt 头)。
97
115
  SystemPrompt string
@@ -211,6 +229,24 @@ type Request struct {
211
229
  // 默认值是否开启按引擎区分,见 AppendDefaultOn(dsh 默认关)。
212
230
  Append <-chan string
213
231
 
232
+ // Reply 原生控制应答通道(可写回引擎自己的控制协议行)。
233
+ //
234
+ // 为什么需要(2026-10-25 实测):codebuddy / claude 在 headless 下遇到需要宿主裁决的动作
235
+ // (计划审批 ExitPlanMode、manual 档下的工具授权)时会发一行
236
+ //
237
+ // {"type":"control_request","request_id":"perm_…","request":{"subtype":"can_use_tool",…}}
238
+ //
239
+ // 然后**原地等 stdin 上的 control_response**。不回就整轮挂住(用户侧看到的是
240
+ // 「转圈、没有审批卡」)。回的内容必须走子进程 stdin —— 不能当成下一条 user 消息,
241
+ // 那时 CLI 正阻塞在控制请求上,user 消息要等到这一轮结束才会被读。
242
+ //
243
+ // 语义:非 nil 时本次调用的 stdin 会**保持打开**(首条 user 消息之后不 EOF),
244
+ // 通道里每收到一行就原样写进子进程 stdin;这一轮收尾(turn_end / result / error)
245
+ // 或 Stream 返回时关掉写端,让 CLI 正常退出。
246
+ // 只有 stream-json 输入通道的引擎(claude / codebuddy / codebuddy-ai)支持;其余引擎
247
+ // 拿到非 nil 会在 CLI 层被挡下(见 AppendSupportOf)。
248
+ Reply <-chan string
249
+
214
250
  // OnSessionID 引擎发现 session_id 时回调(用于「看历史」持久化)。
215
251
  //
216
252
  // 谁需要它:CLI 在 runStreamAsk 里包一个 sessionWriter,每条事件落盘到
@@ -22,16 +22,16 @@ import (
22
22
  // ┌──────────────┬──────────────────────────┬───────────────────────────┬────────────────────────────┬──────────────────┬───────────────────────┬─────────────────────┬──────────────────────┐
23
23
  // │ Request 字段 │ claude │ codebuddy │ trae │ llm │ codex │ openclaw │ dsh │
24
24
  // ├──────────────┼──────────────────────────┼───────────────────────────┼────────────────────────────┼──────────────────┼───────────────────────┼─────────────────────┼──────────────────────┤
25
- // │ Model │ --model <m> │ --model <m> │ -c model.name=<m> │ -m <m> │ -m <m> │ --model <m> │ 不支持(配置层)⑨ │
26
- // │ SessionID │ --resume <id> │ --resume <id> │ --resume=<id> │ --cid <id> │ exec resume <id> │ --session-id <id> │ 不支持(显式报错)⑨ │
27
- // │ Continue │ --continue │ --continue │ --resume(自动选最近会话) │ -c │ resume --last │ sessions 查最新 id │ 不支持(显式报错)⑨ │
25
+ // │ Model │ --model <m> │ --model <m> │ -m <m> │ -m <m> │ -m <m> │ --model <m> │ 不支持(配置层)⑨ │
26
+ // │ SessionID │ --resume <id> │ --resume <id> │ exec resume <id> │ --cid <id> │ exec resume <id> │ --session-id <id> │ 不支持(显式报错)⑨ │
27
+ // │ Continue │ --continue │ --continue │ exec resume --last │ -c │ resume --last │ sessions 查最新 id │ 不支持(显式报错)⑨ │
28
28
  // │ MaxTokens │ --settings(env 注入)① │ --settings(env 注入)① │ 不支持(静默忽略)② │ -o max_tokens │ 不支持(静默忽略)② │ 静默忽略② │ 静默忽略② │
29
29
  // │ Temperature │ 不支持 │ 不支持 │ 不支持(静默忽略)② │ -o temperature │ 不支持(静默忽略)② │ 静默忽略② │ 静默忽略② │
30
30
  // │ Reasoning │ --effort <l> │ --effort <l> │ 不支持(忽略) │ 不支持 │ -c reasoning_effort │ 不支持 │ 不支持 │
31
31
  // │ Tools │ --tools / 档位③ │ --tools / 档位③ │ --disallowed-tool / 全放行③│ 不支持 │ -s 沙箱档位④ │ 内嵌 agent(忽略) │ 内嵌 agent(忽略)⑨ │
32
- // │ Permission │ --permission-mode⑧ │ --permission-mode⑧ │ 未接线(yaml 配置) │ 不支持 │ 未接线(沙箱×审批两轴)│ 未接线(exec.mode) │ 未接线(配置层预设)⑨│
32
+ // │ Permission │ --permission-mode⑧ │ --permission-mode⑧ │ 未接线(仅 -y) │ 不支持 │ 未接线(沙箱×审批两轴)│ 未接线(exec.mode) │ 未接线(配置层预设)⑨│
33
33
  // │ SystemPrompt │ --append-system-prompt │ --append-system-prompt │ 展平进 prompt 头部 │ -s │ -c developer_instr.⑤ │ 展平进 prompt 头部 │ 展平进 prompt 头部 │
34
- // │ Timeout │ 进程级超时 │ 进程级超时 │ + --query-timeout │ 进程级超时 │ 进程级超时 │ + --timeout <sec> │ 进程级超时 │
34
+ // │ Timeout │ 进程级超时 │ 进程级超时 │ 进程级超时 │ 进程级超时 │ 进程级超时 │ + --timeout <sec> │ 进程级超时 │
35
35
  // │ JSONSchema │ 不支持 │ 不支持 │ 不支持 │ 输出后处理抽 JSON│ --output-schema 原生 │ 输出后处理抽 JSON │ 输出后处理抽 JSON │
36
36
  // │ Workspace │ 子进程 cwd⑥ │ 子进程 cwd⑥ │ 子进程 cwd⑥ │ 不支持⑦ │ -C/--cd + cwd⑥ │ 不支持⑦ │ 子进程 cwd⑥ │
37
37
  // └──────────────┴──────────────────────────┴───────────────────────────┴────────────────────────────┴──────────────────┴───────────────────────┴─────────────────────┴──────────────────────┘
@@ -41,11 +41,10 @@ import (
41
41
  //
42
42
  // 注入输出上限(已用真实 CLI --help 验证);两者无 temperature 原生参数,也无对应 settings 键。
43
43
  //
44
- // ② trae 的 `-c <k>=<v>` 会整体覆盖模型配置块(实测设置 model.max_tokens 后模型名漂移并触发配额错误),
45
- //
46
- // 因此 MaxTokens / Temperature 在 trae 上明确不支持;codex 实测 `-c model_max_output_tokens=` /
47
- // `-c temperature=` 会被 CLI 静默忽略(无对应生效键);openclaw agent 无相关 flag ——
48
- // 三者构造参数时统一静默忽略。
44
+ // ② trae 2.0 的 exec 没有 max-tokens / temperature 原生参数(1.x 的 `-c` 覆盖
45
+ // 会整体替换模型配置块,曾实测导致模型名漂移),因此这两项在 trae 上明确不支持;
46
+ // codex 实测 `-c model_max_output_tokens=` / `-c temperature=` 会被 CLI 静默忽略
47
+ // (无对应生效键);openclaw agent 无相关 flag —— 三者构造参数时统一静默忽略。
49
48
  //
50
49
  // ③ Tools 语义:关闭 = claude/codebuddy `--tools ""`;trae `--disallowed-tool`
51
50
  //
@@ -53,7 +52,7 @@ import (
53
52
  // 开启 = claude/codebuddy `--permission-mode <档位>`(四档模型,见 ⑧ 与 permission.go;
54
53
  // 改造前恒传 `--dangerously-skip-permissions` / `-y` = 第 4 档);trae `-y`。
55
54
  // 白名单 = claude/codebuddy `--tools a,b` + `--permission-mode <档位>`;trae **无收窄能力**
56
- // (`--allowed-tool` 只做自动批准、不裁剪工具集,实测仍下发全部 18 个工具),
55
+ // (`--allowed-tool` 只做自动批准、不裁剪工具集,实测仍下发全部工具),
57
56
  // 故白名单降级为「默认开启所有工具」= 全放行 `-y`,与 codex 的白名单降级同构。
58
57
  // llm 无工具概念。
59
58
  //
@@ -170,10 +169,15 @@ var (
170
169
  binName: "trae-cli",
171
170
  envVar: "MAGIC_AGENT_TRAE_BIN",
172
171
  candidates: []string{
172
+ // TRAE CLI 2.0 的官方 v2 安装器落点:主程序 traex,
173
+ // 同目录还有 trae-cli / traecli 兼容入口(.cmd / 符号链接)。
174
+ "~/AppData/Local/Programs/TraeCLI/bin/traex.exe",
175
+ "~/.local/bin/traex",
176
+ "~/.trae/cli/current/traex",
177
+ // 兼容入口(PATH 不干净时的兜底)
173
178
  "~/.local/bin/trae-cli",
174
- "~/bin/trae-cli",
175
179
  },
176
- notFound: "trae-cli not found (install trae-cli or set MAGIC_AGENT_TRAE_BIN)",
180
+ notFound: "TRAE CLI not found (install TRAE CLI or set MAGIC_AGENT_TRAE_BIN)",
177
181
  }
178
182
  llmBase = cliBase{
179
183
  binName: "llm",
@@ -370,9 +374,9 @@ func WorkspaceSupportOf(engine string) string {
370
374
  // codex npm install -g @openai/codex
371
375
  // openclaw npm install -g openclaw@latest(官方文档;装完通常还要 openclaw onboard --install-daemon)
372
376
  // dsh npm i -g @deepseek-ai/dsh
373
- // trae sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"
374
- // (官方 TraeCode CLI 安装脚本,落在 ~/.local/bin —— 正是本包探测链的候选位,
375
- // 故不必在同一行里 export PATH)
377
+ // trae sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"
378
+ // (官方 TRAE CLI 2.0 安装脚本;Windows 用
379
+ // `irm https://trae.cn/trae-cli/install_v2.ps1 | iex`。1.x 已不再支持)
376
380
  // llm python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm
377
381
  // (与 npm postinstall 同一条隔离安装路径,避开系统 Python;另有 pip / pipx / brew)
378
382
  //
@@ -400,7 +404,7 @@ func InstallCommandOf(engine string) string {
400
404
  case "dsh":
401
405
  return "npm i -g @deepseek-ai/dsh"
402
406
  case "trae":
403
- return `sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"`
407
+ return `sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"`
404
408
  case "llm":
405
409
  return "python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm"
406
410
  }
@@ -199,7 +199,7 @@ func TestInstallCommandOf(t *testing.T) {
199
199
  "codex": "npm install -g @openai/codex",
200
200
  "openclaw": "npm install -g openclaw@latest",
201
201
  "dsh": "npm i -g @deepseek-ai/dsh",
202
- "trae": `sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"`,
202
+ "trae": `sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"`,
203
203
  "llm": "python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm",
204
204
  }
205
205
  for engine, want := range cases {
@@ -288,7 +288,7 @@ func TestFirstSemver(t *testing.T) {
288
288
  {"codex-cli 0.154.0", "0.154.0"},
289
289
  {"OpenClaw 2026.6.11 (e085fa1)", "2026.6.11"},
290
290
  {"0.1.5-rc.2", "0.1.5-rc.2"},
291
- {"trae-cli 0.120.52\n", "0.120.52"},
291
+ {"traecli 0.211.1\n", "0.211.1"},
292
292
  {"1.2.3+build.7", "1.2.3+build.7"},
293
293
  // 认不出来就空串(**不编**)
294
294
  {"no version here", ""},
@@ -4,10 +4,9 @@ package agent
4
4
  //
5
5
  // 用 fake CLI 脚本(写入临时目录的 shell 脚本)模拟三家 CLI 的行为,
6
6
  // 验证:
7
- // - 参数构造(--model、--tools ""、--output-format、-c model.name、
8
- // --query-timeout 映射)
7
+ // - 参数构造(--model、--tools ""、--output-format、-m model、exec resume)
9
8
  // - 输出解析(claude JSON envelope、codebuddy 三种 envelope、
10
- // user_query 回显剥离、trae 纯文本)
9
+ // user_query 回显剥离、trae 2.0 的 exec --json 事件流)
11
10
  // - FlattenPrompt 扁平化逻辑
12
11
  // - 引擎注册表 / Lookup
13
12
 
@@ -209,7 +208,7 @@ func TestReasoningEffortArgs(t *testing.T) {
209
208
  }
210
209
  // dsh 的 launcher/headless 没有 reasoning 开关:NoThinking 对它保持现状,
211
210
  // 不能塞 --effort(实测启动器报 "--profile <name> is required")。
212
- dshNo := dshHeadlessArgs("headless", "", "hi", false)
211
+ dshNo := dshHeadlessArgs("headless", "", "", "hi", false)
213
212
  if len(dshNo) < 3 || dshNo[0] != "--profile" || dshNo[1] != "headless" {
214
213
  t.Errorf("dsh 参数不应被 NoThinking 改坏: %v", dshNo)
215
214
  }
@@ -488,11 +487,11 @@ func TestCodeBuddyDefaultModel(t *testing.T) {
488
487
 
489
488
  // ── Trae 引擎 ─────────────────────────────────────────────────
490
489
 
491
- func TestTraeCompletePlainText(t *testing.T) {
490
+ func TestTraeComplete(t *testing.T) {
492
491
  dir := t.TempDir()
493
492
  log := filepath.Join(dir, "args.log")
494
- cli := filepath.Join(dir, "trae-cli")
495
- script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log + "\necho 'trae 的回答'\n"
493
+ cli := filepath.Join(dir, "traex")
494
+ script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log + "\n" + traeV2Stub("trae 的回答", "th-1")
496
495
  if err := os.WriteFile(cli, []byte(script), 0o755); err != nil {
497
496
  t.Fatal(err)
498
497
  }
@@ -510,16 +509,18 @@ func TestTraeCompletePlainText(t *testing.T) {
510
509
  if resp.Text != "trae 的回答" {
511
510
  t.Errorf("Text = %q", resp.Text)
512
511
  }
512
+ if resp.SessionID != "th-1" {
513
+ t.Errorf("SessionID = %q want th-1", resp.SessionID)
514
+ }
513
515
 
514
516
  data, _ := os.ReadFile(log)
515
517
  args := string(data)
516
- // 默认模型:不传 -c model.name
517
- if strings.Contains(args, "model.name") {
518
- t.Errorf("default model should not pass -c model.name: %q", args)
518
+ // 2.0:exec --json,不再有 -p / --query-timeout / model.name
519
+ if !strings.Contains(args, "exec") || !strings.Contains(args, "--json") {
520
+ t.Errorf("2.0 应走 exec --json: %q", args)
519
521
  }
520
- // 超时映射到 --query-timeout
521
- if !strings.Contains(args, "--query-timeout 1m30s") {
522
- t.Errorf("--query-timeout not mapped: %q", args)
522
+ if strings.Contains(args, "--query-timeout") || strings.Contains(args, "model.name") {
523
+ t.Errorf("不应出现 1.x 参数: %q", args)
523
524
  }
524
525
  // 工具禁用
525
526
  if !strings.Contains(args, "--disallowed-tool Bash") {
@@ -534,8 +535,8 @@ func TestTraeCompletePlainText(t *testing.T) {
534
535
  func TestTraeExplicitModel(t *testing.T) {
535
536
  dir := t.TempDir()
536
537
  log := filepath.Join(dir, "args.log")
537
- cli := filepath.Join(dir, "trae-cli")
538
- script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log + "\necho 'ok'\n"
538
+ cli := filepath.Join(dir, "traex")
539
+ script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log + "\n" + traeV2Stub("ok", "th-1")
539
540
  if err := os.WriteFile(cli, []byte(script), 0o755); err != nil {
540
541
  t.Fatal(err)
541
542
  }
@@ -552,59 +553,19 @@ func TestTraeExplicitModel(t *testing.T) {
552
553
  }
553
554
  data, _ := os.ReadFile(log)
554
555
  args := string(data)
555
- if !strings.Contains(args, "-c model.name=My-MiniMax-M3") {
556
- t.Errorf("-c model.name not passed: %q", args)
556
+ if !strings.Contains(args, "-m My-MiniMax-M3") {
557
+ t.Errorf("-m model not passed: %q", args)
557
558
  }
558
559
  }
559
560
 
560
- func TestTraeTimeoutCappedAt600s(t *testing.T) {
561
- dir := t.TempDir()
562
- log := filepath.Join(dir, "args.log")
563
- cli := filepath.Join(dir, "trae-cli")
564
- script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log + "\necho 'ok'\n"
565
- if err := os.WriteFile(cli, []byte(script), 0o755); err != nil {
566
- t.Fatal(err)
567
- }
568
- e := &TraeEngine{BinPath: cli}
569
- _, err := e.Complete(context.Background(), Request{
570
- Messages: []Message{{Role: "user", Content: "hi"}},
571
- Timeout: 30 * time.Minute,
572
- })
573
- if err != nil {
574
- t.Fatalf("Complete: %v", err)
575
- }
576
- data, _ := os.ReadFile(log)
577
- if !strings.Contains(string(data), "--query-timeout 10m0s") {
578
- t.Errorf("query-timeout should cap at 600s: %q", string(data))
579
- }
561
+ // traeV2Stub 造 trae 2.0 的 JSONL 事件流(thread.started + 正文 + turn.completed)。
562
+ func traeV2Stub(text, sessionID string) string {
563
+ return "printf '%s\\n' '{\"type\":\"thread.started\",\"thread_id\":\"" + sessionID + "\"}'\n" +
564
+ "printf '%s\\n' '{\"type\":\"item.completed\",\"item\":{\"id\":\"i\",\"type\":\"agent_message\",\"text\":\"" + text + "\"}}'\n" +
565
+ "printf '%s\\n' '{\"type\":\"turn.completed\"}'\n"
580
566
  }
581
567
 
582
- // ── TraeDefaultModel 配置探测 ─────────────────────────────────
583
-
584
- func TestTraeDefaultModel(t *testing.T) {
585
- home := t.TempDir()
586
- t.Setenv("HOME", home)
587
-
588
- // 无配置文件 → 空串
589
- if got := TraeDefaultModel(); got != "" {
590
- t.Errorf("no config: got %q want empty", got)
591
- }
592
-
593
- // 有配置 → 读 model.name
594
- cfgDir := filepath.Join(home, ".trae")
595
- if err := os.MkdirAll(cfgDir, 0o755); err != nil {
596
- t.Fatal(err)
597
- }
598
- yaml := "model:\n name: My-MiniMax-M3\nmodels:\n- name: \"My-MiniMax-M3\"\n"
599
- if err := os.WriteFile(filepath.Join(cfgDir, "trae_cli.yaml"), []byte(yaml), 0o644); err != nil {
600
- t.Fatal(err)
601
- }
602
- if got := TraeDefaultModel(); got != "My-MiniMax-M3" {
603
- t.Errorf("got %q want My-MiniMax-M3", got)
604
- }
605
- }
606
-
607
- // ── 会话续接(SessionID → --resume 映射)──────────────────────
568
+ // ── 会话续接(SessionID → exec resume 映射)────────────────────
608
569
 
609
570
  // argsCaptureCLI 写一个把 "$*" 逐行记入 args.log 的假 CLI,返回 (引擎, log 路径)。
610
571
  func argsCaptureCLI(t *testing.T, dir, name, cannedOutput string) (*engineWithLog, string) {
@@ -737,9 +698,9 @@ echo '{"type":"result","result":"好"}'
737
698
  }
738
699
  }
739
700
 
740
- func TestTraeResumeSessionEqualsForm(t *testing.T) {
701
+ func TestTraeResumeSession(t *testing.T) {
741
702
  dir := t.TempDir()
742
- w, log := argsCaptureCLI(t, dir, "trae-cli", `echo '续上了'`)
703
+ w, log := argsCaptureCLI(t, dir, "trae-cli", traeV2Stub("续上了", "tr-9"))
743
704
  e := w.trae()
744
705
  resp, err := e.Complete(context.Background(), Request{
745
706
  Messages: []Message{{Role: "user", Content: "hi"}},
@@ -748,21 +709,23 @@ func TestTraeResumeSessionEqualsForm(t *testing.T) {
748
709
  if err != nil {
749
710
  t.Fatalf("Complete: %v", err)
750
711
  }
751
- // trae 纯文本模式 CLI 不回 session_id → 回显请求里的 id,保证逐轮透传。
712
+ // 2.0 的 thread.started 会给出真实会话 id。
752
713
  if resp.SessionID != "tr-9" {
753
- t.Errorf("Response.SessionID = %q want tr-9(请求回显)", resp.SessionID)
714
+ t.Errorf("Response.SessionID = %q want tr-9", resp.SessionID)
754
715
  }
755
716
  data, _ := os.ReadFile(log)
756
717
  args := string(data)
757
- // pflag 可选值风格:必须等号形式,空格形式下裸 id 会被吞成 AUTO。
758
- if !strings.Contains(args, "--resume=tr-9") {
759
- t.Errorf("--resume=tr-9 (equals form) not passed: %q", args)
718
+ if !strings.Contains(args, "exec resume tr-9") {
719
+ t.Errorf("exec resume tr-9 not passed: %q", args)
720
+ }
721
+ if strings.Contains(args, "--resume") {
722
+ t.Errorf("2.0 不应再出现 --resume: %q", args)
760
723
  }
761
724
  }
762
725
 
763
726
  func TestTraeNewSessionDefault(t *testing.T) {
764
727
  dir := t.TempDir()
765
- w, log := argsCaptureCLI(t, dir, "trae-cli", `echo 'ok'`)
728
+ w, log := argsCaptureCLI(t, dir, "trae-cli", traeV2Stub("ok", "th-new"))
766
729
  e := w.trae()
767
730
  resp, err := e.Complete(context.Background(), Request{
768
731
  Messages: []Message{{Role: "user", Content: "hi"}},
@@ -770,13 +733,12 @@ func TestTraeNewSessionDefault(t *testing.T) {
770
733
  if err != nil {
771
734
  t.Fatalf("Complete: %v", err)
772
735
  }
773
- // 新会话:请求里没有 id,回显也是空。
774
- if resp.SessionID != "" {
775
- t.Errorf("Response.SessionID = %q want empty", resp.SessionID)
736
+ if resp.SessionID != "th-new" {
737
+ t.Errorf("Response.SessionID = %q want th-new", resp.SessionID)
776
738
  }
777
739
  data, _ := os.ReadFile(log)
778
- if strings.Contains(string(data), "--resume") {
779
- t.Errorf("默认新会话不应带 --resume: %q", string(data))
740
+ if strings.Contains(string(data), "resume") {
741
+ t.Errorf("默认新会话不应带 resume: %q", string(data))
780
742
  }
781
743
  }
782
744
 
@@ -822,11 +784,10 @@ func TestCodeBuddyContinueSession(t *testing.T) {
822
784
  }
823
785
  }
824
786
 
825
- // trae 无 --continue:裸 --resume 触发 AUTO(续接最近一次会话),
826
- // 且不能是等号形式(等号形式是显式 id 续接)。
827
- func TestTraeContinueBareResume(t *testing.T) {
787
+ // trae 2.0:Continue → `exec resume --last`(不再有裸 --resume)。
788
+ func TestTraeContinueResumeLast(t *testing.T) {
828
789
  dir := t.TempDir()
829
- w, log := argsCaptureCLI(t, dir, "trae-cli", `echo 'ok'`)
790
+ w, log := argsCaptureCLI(t, dir, "trae-cli", traeV2Stub("ok", "th-last"))
830
791
  e := w.trae()
831
792
  if _, err := e.Complete(context.Background(), Request{
832
793
  Messages: []Message{{Role: "user", Content: "hi"}},
@@ -836,11 +797,11 @@ func TestTraeContinueBareResume(t *testing.T) {
836
797
  }
837
798
  data, _ := os.ReadFile(log)
838
799
  args := string(data)
839
- if !strings.Contains(args, "--resume") {
840
- t.Errorf("bare --resume not passed: %q", args)
800
+ if !strings.Contains(args, "exec resume --last") {
801
+ t.Errorf("exec resume --last not passed: %q", args)
841
802
  }
842
- if strings.Contains(args, "--resume=") {
843
- t.Errorf("Continue 应为裸 --resume,不带 =id: %q", args)
803
+ if strings.Contains(args, "--resume") {
804
+ t.Errorf("2.0 不应再出现 --resume: %q", args)
844
805
  }
845
806
  }
846
807
 
@@ -857,7 +818,7 @@ func TestSessionPriorityOverContinue(t *testing.T) {
857
818
  }{
858
819
  {"claude", "s-1", "--resume s-1", "--continue", "claude", `echo '{"type":"result","result":"ok"}'`},
859
820
  {"codebuddy", "cb-1", "--resume cb-1", "--continue", "codebuddy", `echo '{"type":"result","result":"ok"}'`},
860
- {"trae", "tr-1", "--resume=tr-1", "--continue", "trae-cli", `echo 'ok'`},
821
+ {"trae", "tr-1", "exec resume tr-1", "--continue", "trae-cli", traeV2Stub("ok", "tr-1")},
861
822
  }
862
823
  for _, tc := range cases {
863
824
  t.Run(tc.name, func(t *testing.T) {
@@ -999,7 +960,7 @@ func TestTemperatureIgnoredByCLIs(t *testing.T) {
999
960
  _, err := w.codebuddy().Complete(context.Background(), req)
1000
961
  return err
1001
962
  }},
1002
- {"trae", "trae-cli", `echo 'ok'`,
963
+ {"trae", "trae-cli", traeV2Stub("ok", "th-1"),
1003
964
  func(w *engineWithLog, req Request) error {
1004
965
  _, err := w.trae().Complete(context.Background(), req)
1005
966
  return err