minimal-agent 0.13.0 → 0.14.0

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 (470) hide show
  1. package/README.md +6 -5
  2. package/package.json +60 -4
  3. package/plugins/computer_use/.claude-plugin/plugin.json +1 -1
  4. package/plugins/cron-runner/src/commands.js +4 -4
  5. package/plugins/cron-runner/src/commands.ts +4 -4
  6. package/plugins/workflow-runner/.claude-plugin/plugin.json +2 -2
  7. package/plugins/workflow-runner/commands/workflow.md +57 -90
  8. package/plugins/workflow-runner/commands/workflows.md +4 -4
  9. package/plugins/workflow-runner/plugin.js +24 -13
  10. package/plugins/workflow-runner/plugin.ts +28 -13
  11. package/plugins/workflow-runner/src/discovery.js +160 -0
  12. package/plugins/workflow-runner/src/discovery.ts +194 -0
  13. package/plugins/workflow-runner/src/goalAnchor.js +53 -0
  14. package/plugins/workflow-runner/src/goalAnchor.ts +59 -0
  15. package/plugins/workflow-runner/src/index.js +197 -206
  16. package/plugins/workflow-runner/src/index.ts +248 -249
  17. package/plugins/workflow-runner/src/inputCoercion.js +40 -0
  18. package/plugins/workflow-runner/src/inputCoercion.ts +43 -0
  19. package/plugins/workflow-runner/src/journal.js +224 -0
  20. package/plugins/workflow-runner/src/journal.ts +274 -0
  21. package/plugins/workflow-runner/src/jsonSchema.js +132 -0
  22. package/plugins/workflow-runner/src/jsonSchema.ts +162 -0
  23. package/plugins/workflow-runner/src/moduleResolution.js +64 -0
  24. package/plugins/workflow-runner/src/moduleResolution.ts +71 -0
  25. package/plugins/workflow-runner/src/resourceInputs.js +220 -0
  26. package/plugins/workflow-runner/src/resourceInputs.ts +300 -0
  27. package/plugins/workflow-runner/src/runner.js +400 -489
  28. package/plugins/workflow-runner/src/runner.ts +488 -560
  29. package/plugins/workflow-runner/src/runtime/config.js +46 -0
  30. package/plugins/workflow-runner/src/runtime/config.ts +69 -0
  31. package/plugins/workflow-runner/src/runtime/context.js +1304 -0
  32. package/plugins/workflow-runner/src/runtime/context.ts +1786 -0
  33. package/plugins/workflow-runner/src/runtime/exec.js +34 -0
  34. package/plugins/workflow-runner/src/runtime/exec.ts +53 -0
  35. package/plugins/workflow-runner/src/runtime/flow.js +8 -0
  36. package/plugins/workflow-runner/src/runtime/flow.ts +12 -0
  37. package/plugins/workflow-runner/src/runtime/fs.js +27 -0
  38. package/plugins/workflow-runner/src/runtime/fs.ts +41 -0
  39. package/plugins/workflow-runner/src/runtime/model.js +6 -0
  40. package/plugins/workflow-runner/src/runtime/model.ts +8 -0
  41. package/plugins/workflow-runner/src/runtime/output.js +7 -0
  42. package/plugins/workflow-runner/src/runtime/output.ts +21 -0
  43. package/plugins/workflow-runner/src/runtime/scheduler.js +141 -0
  44. package/plugins/workflow-runner/src/runtime/scheduler.ts +166 -0
  45. package/plugins/workflow-runner/src/runtime/state.js +16 -0
  46. package/plugins/workflow-runner/src/runtime/state.ts +19 -0
  47. package/plugins/workflow-runner/src/runtime/tools.js +3 -0
  48. package/plugins/workflow-runner/src/runtime/tools.ts +6 -0
  49. package/plugins/workflow-runner/src/sandbox.js +141 -0
  50. package/plugins/workflow-runner/src/sandbox.ts +182 -0
  51. package/plugins/workflow-runner/src/scriptGuard.d.mts +38 -0
  52. package/plugins/workflow-runner/src/scriptGuard.mjs +449 -0
  53. package/plugins/workflow-runner/src/taskIdentity.js +151 -307
  54. package/plugins/workflow-runner/src/taskIdentity.ts +207 -355
  55. package/plugins/workflow-runner/src/toolRuntime.js +29 -8
  56. package/plugins/workflow-runner/src/toolRuntime.ts +36 -11
  57. package/plugins/workflow-runner/src/types.js +7 -211
  58. package/plugins/workflow-runner/src/types.ts +154 -623
  59. package/plugins/workflow-runner/src/util.js +47 -0
  60. package/plugins/workflow-runner/src/util.ts +51 -0
  61. package/plugins/workflow-runner/src/workflowContext.js +20 -0
  62. package/plugins/workflow-runner/src/workflowContext.ts +58 -0
  63. package/plugins/workflow-runner/src/workflowDependencies.js +36 -0
  64. package/plugins/workflow-runner/src/workflowDependencies.ts +59 -0
  65. package/plugins/workflow-runner/src/workflowFingerprint.js +118 -0
  66. package/plugins/workflow-runner/src/workflowFingerprint.ts +122 -0
  67. package/plugins/workflow-runner/src/workflowState.js +474 -802
  68. package/plugins/workflow-runner/src/workflowState.ts +575 -970
  69. package/plugins/workflow-runner/test/api.test.ts +223 -0
  70. package/plugins/workflow-runner/test/apiCapabilities.test.ts +614 -0
  71. package/plugins/workflow-runner/test/apiConcurrencyGate.test.ts +104 -0
  72. package/plugins/workflow-runner/test/apiEcho.test.ts +210 -0
  73. package/plugins/workflow-runner/test/apiReadText.test.ts +178 -0
  74. package/plugins/workflow-runner/test/apiResourceLedger.test.ts +184 -0
  75. package/plugins/workflow-runner/test/childWorkflow.test.ts +304 -0
  76. package/plugins/workflow-runner/test/discovery.test.ts +234 -0
  77. package/plugins/workflow-runner/test/goalAnchor.test.ts +106 -0
  78. package/plugins/workflow-runner/test/goalController.test.ts +317 -0
  79. package/plugins/workflow-runner/test/journalLanes.test.ts +198 -0
  80. package/plugins/workflow-runner/test/listResources.test.ts +106 -0
  81. package/plugins/workflow-runner/test/resourceInputs.test.ts +294 -0
  82. package/plugins/workflow-runner/test/sandbox.test.ts +126 -0
  83. package/plugins/workflow-runner/test/scheduler.test.ts +70 -0
  84. package/plugins/workflow-runner/test/scriptGuard.test.ts +276 -0
  85. package/plugins/workflow-runner/test/skillDocSync.test.ts +78 -0
  86. package/plugins/workflow-runner/test/taskIdentity.test.ts +288 -179
  87. package/plugins/workflow-runner/test/workflowFingerprint.test.ts +106 -0
  88. package/plugins/workflow-runner/test/workflowState.test.ts +429 -0
  89. package/skills/pptx/examples/assets/cover-bg.svg +23 -0
  90. package/skills/pptx/examples/assets/icon-spark.svg +5 -0
  91. package/skills/ui-design/SKILL.md +5 -2
  92. package/skills/ui-design/references/layout-composition.md +1 -1
  93. package/skills/workflow-creator/SKILL.md +344 -0
  94. package/skills/workflow-creator/assets/template/workflow.mjs +48 -0
  95. package/skills/workflow-creator/references/api.md +216 -0
  96. package/skills/workflow-creator/references/migration.md +103 -0
  97. package/skills/workflow-creator/references/patterns.md +779 -0
  98. package/skills/workflow-creator/scripts/list-resources.mjs +205 -0
  99. package/skills/workflow-creator/scripts/scaffold.mjs +148 -0
  100. package/skills/workflow-creator/scripts/validate.mjs +275 -0
  101. package/skills/workflow-creator/scripts/workflow-creator.test.mjs +82 -0
  102. package/src/advisor/advisorCoordinator.js +503 -0
  103. package/src/advisor/advisorCoordinator.ts +657 -0
  104. package/src/cli/configMenu.tsx +0 -1
  105. package/src/cli/print.js +27 -6
  106. package/src/cli/print.ts +48 -4
  107. package/src/cli/streamJson.js +5 -6
  108. package/src/cli/streamJson.ts +8 -4
  109. package/src/cli/wizards/advisorWizard.js +4 -2
  110. package/src/cli/wizards/advisorWizard.tsx +8 -2
  111. package/src/cli/wizards/contextAdvisorWizard.js +5 -4
  112. package/src/cli/wizards/contextAdvisorWizard.tsx +7 -6
  113. package/src/collaboration/taskFlow.js +115 -0
  114. package/src/collaboration/taskFlow.ts +169 -0
  115. package/src/config/configDefaults.js +41 -1
  116. package/src/config/configDefaults.ts +43 -1
  117. package/src/config/configDiagnostics.ts +2 -2
  118. package/src/config/configFile.js +40 -9
  119. package/src/config/configFile.ts +60 -11
  120. package/src/config/configService.js +419 -74
  121. package/src/config/configService.ts +703 -145
  122. package/src/config/numericLimits.js +22 -0
  123. package/src/config/numericLimits.ts +24 -0
  124. package/src/config.js +1 -0
  125. package/src/config.ts +3 -2
  126. package/src/context/budgetPlan.js +1 -1
  127. package/src/context/budgetPlan.ts +1 -1
  128. package/src/context/compact.js +7 -2
  129. package/src/context/compact.ts +9 -2
  130. package/src/context/microCompactLite.js +212 -13
  131. package/src/context/microCompactLite.ts +261 -12
  132. package/src/context/sessionRegistry.js +8 -1
  133. package/src/context/sessionRegistry.ts +18 -5
  134. package/src/context/toolResultArchive.js +72 -0
  135. package/src/context/toolResultArchive.ts +95 -0
  136. package/src/goal/errors.js +8 -0
  137. package/src/goal/errors.ts +18 -0
  138. package/src/goal/goalRuntime.js +344 -0
  139. package/src/goal/goalRuntime.ts +466 -0
  140. package/src/goal/goalStore.js +200 -0
  141. package/src/goal/goalStore.ts +219 -0
  142. package/src/goal/index.js +4 -0
  143. package/src/goal/index.ts +4 -0
  144. package/src/goal/statusMapping.js +71 -0
  145. package/src/goal/statusMapping.ts +95 -0
  146. package/src/goal/types.js +7 -0
  147. package/src/goal/types.ts +179 -0
  148. package/src/goal/workflowController.js +306 -0
  149. package/src/goal/workflowController.ts +360 -0
  150. package/src/harness/evidenceLedger.js +2 -0
  151. package/src/harness/evidenceLedger.ts +1 -0
  152. package/src/harness/stallGuard.js +9 -3
  153. package/src/harness/stallGuard.ts +7 -3
  154. package/src/images/imageResolver.js +161 -0
  155. package/src/images/imageResolver.ts +189 -0
  156. package/src/infra/verification.js +34 -5
  157. package/src/infra/verification.ts +39 -4
  158. package/src/llm/client.js +79 -21
  159. package/src/llm/client.ts +182 -35
  160. package/src/llm/gatewayClient.js +201 -0
  161. package/src/llm/gatewayClient.ts +299 -0
  162. package/src/llm/messageProvenance.js +46 -0
  163. package/src/llm/messageProvenance.ts +58 -0
  164. package/src/llm/modelProvider.js +1 -0
  165. package/src/llm/modelProvider.ts +22 -0
  166. package/src/llm/providers/anthropicFormat.js +108 -1
  167. package/src/llm/providers/anthropicFormat.ts +115 -4
  168. package/src/llm/providers/openaiFormat.js +9 -0
  169. package/src/llm/providers/openaiFormat.ts +12 -3
  170. package/src/loop.js +319 -54
  171. package/src/loop.ts +373 -57
  172. package/src/main.js +4 -1
  173. package/src/main.tsx +4 -1
  174. package/src/managedExecution/index.js +2 -0
  175. package/src/managedExecution/index.ts +2 -0
  176. package/src/managedExecution/registry.js +15 -0
  177. package/src/managedExecution/registry.ts +20 -0
  178. package/src/managedExecution/types.js +1 -0
  179. package/src/managedExecution/types.ts +86 -0
  180. package/src/modelRef.js +9 -0
  181. package/src/modelRef.ts +31 -0
  182. package/src/multimodal.js +14 -28
  183. package/src/multimodal.ts +17 -33
  184. package/src/net/lanDiscovery.js +65 -0
  185. package/src/net/lanDiscovery.ts +82 -0
  186. package/src/plugin-sdk.js +28 -3
  187. package/src/plugin-sdk.ts +39 -6
  188. package/src/plugins/pluginApi.ts +16 -1
  189. package/src/plugins/pluginLoader.js +4 -0
  190. package/src/plugins/pluginLoader.ts +6 -0
  191. package/src/plugins/pluginRunner.js +139 -7
  192. package/src/plugins/pluginRunner.ts +173 -9
  193. package/src/plugins/slashFallback.js +113 -0
  194. package/src/plugins/slashFallback.ts +137 -0
  195. package/src/prompts/policyRegistry.js +12 -32
  196. package/src/prompts/policyRegistry.ts +15 -32
  197. package/src/prompts/promptAssembly.js +103 -12
  198. package/src/prompts/promptAssembly.ts +114 -12
  199. package/src/prompts/skillList.js +21 -3
  200. package/src/prompts/skillList.ts +27 -2
  201. package/src/prompts/stableSystemPrompt.js +18 -0
  202. package/src/prompts/stableSystemPrompt.ts +33 -0
  203. package/src/runtime/agentRuntimeContext.js +8 -2
  204. package/src/runtime/agentRuntimeContext.ts +17 -4
  205. package/src/session/runAgentTurn.js +78 -12
  206. package/src/session/runAgentTurn.ts +105 -23
  207. package/src/session/runTurn.js +10 -9
  208. package/src/session/runTurn.ts +17 -8
  209. package/src/session/sessionState.ts +2 -0
  210. package/src/team/auth.js +186 -0
  211. package/src/team/auth.ts +298 -0
  212. package/src/team/cli.js +105 -0
  213. package/src/team/cli.ts +122 -0
  214. package/src/team/onlineToolContract.js +64 -0
  215. package/src/team/onlineToolContract.ts +109 -0
  216. package/src/team/toolPolicy.js +36 -0
  217. package/src/team/toolPolicy.ts +75 -0
  218. package/src/tools/advisor/advisor.js +90 -46
  219. package/src/tools/advisor/advisor.ts +100 -47
  220. package/src/tools/advisor/advisorDecision.js +71 -0
  221. package/src/tools/advisor/advisorDecision.ts +85 -0
  222. package/src/tools/agent/agent.js +18 -5
  223. package/src/tools/agent/agent.ts +21 -7
  224. package/src/tools/agent/agentControl.js +246 -0
  225. package/src/tools/agent/agentControl.ts +284 -0
  226. package/src/tools/agent/agentTaskService.js +40 -1
  227. package/src/tools/agent/agentTaskService.ts +44 -1
  228. package/src/tools/agent/agentTypes.js +14 -2
  229. package/src/tools/agent/agentTypes.ts +15 -2
  230. package/src/tools/applypatch/applyPatch.js +362 -0
  231. package/src/tools/applypatch/applyPatch.ts +428 -0
  232. package/src/tools/bash/bash.js +22 -15
  233. package/src/tools/bash/bash.ts +23 -17
  234. package/src/tools/capabilities.js +10 -5
  235. package/src/tools/capabilities.ts +10 -5
  236. package/src/tools/computeruse/computerIntent.js +32 -0
  237. package/src/tools/computeruse/computerIntent.ts +46 -0
  238. package/src/tools/computeruse/telemetryLog.js +12 -0
  239. package/src/tools/computeruse/telemetryLog.ts +16 -0
  240. package/src/tools/computeruse/vision/telemetry.js +1 -0
  241. package/src/tools/computeruse/vision/telemetry.ts +1 -0
  242. package/src/tools/effectiveToolSet.js +4 -0
  243. package/src/tools/effectiveToolSet.ts +4 -0
  244. package/src/tools/index.js +21 -2
  245. package/src/tools/index.ts +33 -2
  246. package/src/tools/presentresult/presentresult.js +6 -15
  247. package/src/tools/presentresult/presentresult.ts +6 -15
  248. package/src/tools/read/read.js +62 -9
  249. package/src/tools/read/read.ts +63 -9
  250. package/src/tools/shared/fileUtils.js +1 -2
  251. package/src/tools/shared/fileUtils.ts +1 -2
  252. package/src/tools/shared/toolOutputStore.js +23 -1
  253. package/src/tools/shared/toolOutputStore.ts +22 -1
  254. package/src/tools/skill/skill.js +9 -3
  255. package/src/tools/skill/skill.ts +8 -4
  256. package/src/tools/toolPromptGuides.js +117 -0
  257. package/src/tools/toolPromptGuides.ts +137 -0
  258. package/src/tools/webbrowser/agentBrowserPath.js +19 -9
  259. package/src/tools/webbrowser/agentBrowserPath.ts +70 -28
  260. package/src/tools/webbrowser/browser.js +27 -11
  261. package/src/tools/webbrowser/browser.ts +68 -24
  262. package/src/tools/webbrowser/browserAutomation.js +329 -0
  263. package/src/tools/webbrowser/browserAutomation.ts +467 -0
  264. package/src/tools/webbrowser/browserAxState.js +97 -0
  265. package/src/tools/webbrowser/browserAxState.ts +142 -0
  266. package/src/tools/webbrowser/browserBatch.js +1578 -0
  267. package/src/tools/webbrowser/browserBatch.ts +2382 -0
  268. package/src/tools/webbrowser/browserCdp.js +208 -0
  269. package/src/tools/webbrowser/browserCdp.ts +255 -0
  270. package/src/tools/webbrowser/browserConditions.js +24 -0
  271. package/src/tools/webbrowser/browserConditions.ts +45 -0
  272. package/src/tools/webbrowser/browserContent.js +281 -0
  273. package/src/tools/webbrowser/browserContent.ts +438 -0
  274. package/src/tools/webbrowser/browserControl.js +131 -34
  275. package/src/tools/webbrowser/browserControl.ts +210 -56
  276. package/src/tools/webbrowser/browserDaemon.js +22 -14
  277. package/src/tools/webbrowser/browserDaemon.ts +54 -21
  278. package/src/tools/webbrowser/browserFrames.js +47 -0
  279. package/src/tools/webbrowser/browserFrames.ts +54 -0
  280. package/src/tools/webbrowser/browserInteractionResolver.js +17 -0
  281. package/src/tools/webbrowser/browserInteractionResolver.ts +40 -0
  282. package/src/tools/webbrowser/browserLocator.js +140 -0
  283. package/src/tools/webbrowser/browserLocator.ts +194 -0
  284. package/src/tools/webbrowser/browserOutcome.js +10 -17
  285. package/src/tools/webbrowser/browserOutcome.ts +48 -31
  286. package/src/tools/webbrowser/browserRecovery.js +58 -0
  287. package/src/tools/webbrowser/browserRecovery.ts +83 -0
  288. package/src/tools/webbrowser/browserRunRegistry.js +35 -3
  289. package/src/tools/webbrowser/browserRunRegistry.ts +91 -15
  290. package/src/tools/webbrowser/browserService.js +28 -2
  291. package/src/tools/webbrowser/browserService.ts +49 -9
  292. package/src/tools/webbrowser/browserStream.js +132 -33
  293. package/src/tools/webbrowser/browserStream.ts +224 -50
  294. package/src/tools/webbrowser/browserTabs.js +48 -15
  295. package/src/tools/webbrowser/browserTabs.ts +178 -96
  296. package/src/tools/webbrowser/webbrowser.js +679 -144
  297. package/src/tools/webbrowser/webbrowser.ts +975 -236
  298. package/src/tools/webfetch/preapproved.js +0 -2
  299. package/src/tools/webfetch/preapproved.ts +0 -2
  300. package/src/tools/websearch/websearch.js +129 -8
  301. package/src/tools/websearch/websearch.ts +138 -6
  302. package/src/types.ts +440 -38
  303. package/src/ui/App.js +377 -58
  304. package/src/ui/App.tsx +576 -111
  305. package/src/ui/LiveArea.js +14 -8
  306. package/src/ui/LiveArea.tsx +14 -10
  307. package/src/ui/MessageList.js +63 -20
  308. package/src/ui/MessageList.tsx +101 -16
  309. package/src/ui/ModelPicker.js +7 -4
  310. package/src/ui/ModelPicker.tsx +55 -8
  311. package/src/ui/hooks/useChat.js +152 -163
  312. package/src/ui/hooks/useChat.ts +396 -334
  313. package/src/ui/loginCommand.js +56 -0
  314. package/src/ui/loginCommand.ts +63 -0
  315. package/src/ui/toolProgress.js +41 -0
  316. package/src/ui/toolProgress.ts +44 -0
  317. package/src/web/browserBridge.js +20 -2
  318. package/src/web/browserBridge.ts +20 -2
  319. package/src/web/protocol.ts +1897 -194
  320. package/workflows/adaptive-research/workflow.mjs +86 -0
  321. package/workflows/e2e-capabilities/workflow.mjs +102 -0
  322. package/workflows/e2e-llm/workflow.mjs +52 -0
  323. package/workflows/e2e-smoke/workflow.mjs +59 -0
  324. package/workflows/e2e-vision-schema/README.md +48 -0
  325. package/workflows/e2e-vision-schema/workflow.mjs +149 -0
  326. package/workflows/e2e-write-greet/workflow.mjs +37 -0
  327. package/workflows/file-census/README.md +14 -0
  328. package/workflows/file-census/scripts/.gitkeep +2 -0
  329. package/workflows/file-census/workflow.mjs +110 -0
  330. package/workflows/goal/README.md +166 -0
  331. package/workflows/goal/prompts/audit.md +57 -0
  332. package/workflows/goal/prompts/contract.md +56 -0
  333. package/workflows/goal/prompts/iterate.md +37 -0
  334. package/workflows/goal/workflow.mjs +501 -0
  335. package/workflows/illustrate/README.md +90 -0
  336. package/workflows/illustrate/assets/engines.json +20 -0
  337. package/workflows/illustrate/assets/extract-baidu.js +21 -0
  338. package/workflows/illustrate/assets/extract-bing.js +33 -0
  339. package/workflows/illustrate/assets/extract-google.js +31 -0
  340. package/workflows/illustrate/workflow.mjs +301 -0
  341. package/workflows/plan/workflow.mjs +389 -0
  342. package/workflows/question-crops/README.md +35 -0
  343. package/workflows/question-crops/prompts/inventory.md +23 -0
  344. package/workflows/question-crops/scripts/image-ops.mjs +305 -0
  345. package/workflows/question-crops/scripts/image-ops.test.mjs +95 -0
  346. package/workflows/question-crops/workflow.mjs +172 -0
  347. package/workflows/ralph-loop/workflow.mjs +162 -0
  348. package/workflows/research/README.md +74 -0
  349. package/workflows/research/prompts/s0-goal.md +43 -0
  350. package/workflows/research/prompts/s1-early-exit.md +22 -0
  351. package/workflows/research/prompts/s1-map.md +58 -0
  352. package/workflows/research/prompts/s2-options.md +53 -0
  353. package/workflows/research/prompts/s3-deep-dive.md +56 -0
  354. package/workflows/research/prompts/s4-verify.md +74 -0
  355. package/workflows/research/prompts/s5-synthesis.md +81 -0
  356. package/workflows/research/prompts/s5b-fill-gaps.md +32 -0
  357. package/workflows/research/prompts/s6-package.md +32 -0
  358. package/workflows/research/scripts/build-report.mjs +98 -0
  359. package/workflows/research/workflow.mjs +288 -0
  360. package/workflows/wechat-reading-shots/prompts/collect-params.md +16 -0
  361. package/workflows/wechat-reading-shots/prompts/evaluate.md +20 -0
  362. package/workflows/wechat-reading-shots/prompts/open-book.md +28 -0
  363. package/workflows/wechat-reading-shots/prompts/read-page.md +13 -0
  364. package/workflows/wechat-reading-shots/prompts/turn-batch.md +24 -0
  365. package/workflows/wechat-reading-shots/test/control-flow.test.ts +329 -0
  366. package/workflows/wechat-reading-shots/workflow.mjs +266 -0
  367. package/workflows/wrong-question-docx/README.md +55 -0
  368. package/workflows/wrong-question-docx/prompts/figure-qa.md +8 -0
  369. package/workflows/wrong-question-docx/prompts/transcribe.md +25 -0
  370. package/workflows/wrong-question-docx/scripts/workbook.mjs +857 -0
  371. package/workflows/wrong-question-docx/test/control-flow.test.ts +165 -0
  372. package/workflows/wrong-question-docx/test/workbook.test.mjs +303 -0
  373. package/workflows/wrong-question-docx/workflow.mjs +485 -0
  374. package/plugins/workflow-runner/src/agentCommon.js +0 -354
  375. package/plugins/workflow-runner/src/agentCommon.ts +0 -435
  376. package/plugins/workflow-runner/src/controlFlow/branch.js +0 -40
  377. package/plugins/workflow-runner/src/controlFlow/branch.ts +0 -46
  378. package/plugins/workflow-runner/src/controlFlow/loop.js +0 -211
  379. package/plugins/workflow-runner/src/controlFlow/loop.ts +0 -248
  380. package/plugins/workflow-runner/src/controlFlow/pause.js +0 -129
  381. package/plugins/workflow-runner/src/controlFlow/pause.ts +0 -141
  382. package/plugins/workflow-runner/src/controlFlow/signal.js +0 -21
  383. package/plugins/workflow-runner/src/controlFlow/signal.ts +0 -29
  384. package/plugins/workflow-runner/src/controlFlow/types.js +0 -18
  385. package/plugins/workflow-runner/src/controlFlow/types.ts +0 -63
  386. package/plugins/workflow-runner/src/dslManifest.js +0 -72
  387. package/plugins/workflow-runner/src/dslManifest.ts +0 -127
  388. package/plugins/workflow-runner/src/expressions.js +0 -639
  389. package/plugins/workflow-runner/src/expressions.ts +0 -640
  390. package/plugins/workflow-runner/src/loader.js +0 -573
  391. package/plugins/workflow-runner/src/loader.ts +0 -721
  392. package/plugins/workflow-runner/src/multimodal.js +0 -38
  393. package/plugins/workflow-runner/src/multimodal.ts +0 -64
  394. package/plugins/workflow-runner/src/recovery.js +0 -126
  395. package/plugins/workflow-runner/src/recovery.ts +0 -156
  396. package/plugins/workflow-runner/src/stepExecutors/agent.js +0 -173
  397. package/plugins/workflow-runner/src/stepExecutors/agent.ts +0 -234
  398. package/plugins/workflow-runner/src/stepExecutors/assert.js +0 -42
  399. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -52
  400. package/plugins/workflow-runner/src/stepExecutors/echo.js +0 -31
  401. package/plugins/workflow-runner/src/stepExecutors/echo.ts +0 -38
  402. package/plugins/workflow-runner/src/stepExecutors/llm.js +0 -99
  403. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -133
  404. package/plugins/workflow-runner/src/stepExecutors/set.js +0 -49
  405. package/plugins/workflow-runner/src/stepExecutors/set.ts +0 -56
  406. package/plugins/workflow-runner/src/stepExecutors/skill.js +0 -111
  407. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -140
  408. package/plugins/workflow-runner/src/stepExecutors/tool.js +0 -227
  409. package/plugins/workflow-runner/src/stepExecutors/tool.ts +0 -274
  410. package/plugins/workflow-runner/src/stepExecutors/verify.js +0 -61
  411. package/plugins/workflow-runner/src/stepExecutors/verify.ts +0 -72
  412. package/plugins/workflow-runner/src/stepPipeline.js +0 -277
  413. package/plugins/workflow-runner/src/stepPipeline.ts +0 -366
  414. package/plugins/workflow-runner/test/agent.test.ts +0 -587
  415. package/plugins/workflow-runner/test/agentLlmError.test.ts +0 -373
  416. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +0 -65
  417. package/plugins/workflow-runner/test/captureMisbind.test.ts +0 -99
  418. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -167
  419. package/plugins/workflow-runner/test/context-files.test.ts +0 -320
  420. package/plugins/workflow-runner/test/e2e.test.ts +0 -370
  421. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +0 -314
  422. package/plugins/workflow-runner/test/engineRobustness.test.ts +0 -234
  423. package/plugins/workflow-runner/test/expressions.test.ts +0 -439
  424. package/plugins/workflow-runner/test/foundation.test.ts +0 -126
  425. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  426. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +0 -47
  427. package/plugins/workflow-runner/test/hostCapability.test.ts +0 -108
  428. package/plugins/workflow-runner/test/illustrate.load.test.ts +0 -183
  429. package/plugins/workflow-runner/test/jsonPayload.test.ts +0 -83
  430. package/plugins/workflow-runner/test/loader.test.ts +0 -617
  431. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +0 -33
  432. package/plugins/workflow-runner/test/multimodal.test.ts +0 -342
  433. package/plugins/workflow-runner/test/output-schema.test.ts +0 -213
  434. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +0 -106
  435. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -269
  436. package/plugins/workflow-runner/test/ppt.load.test.ts +0 -1220
  437. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +0 -221
  438. package/plugins/workflow-runner/test/recovery.test.ts +0 -265
  439. package/plugins/workflow-runner/test/research.load.test.ts +0 -328
  440. package/plugins/workflow-runner/test/runner.test.ts +0 -911
  441. package/plugins/workflow-runner/test/runnerPreflight.test.ts +0 -436
  442. package/plugins/workflow-runner/test/skillTurnDone.test.ts +0 -71
  443. package/plugins/workflow-runner/test/stateLifecycle.test.ts +0 -198
  444. package/plugins/workflow-runner/test/stepPipeline.test.ts +0 -114
  445. package/plugins/workflow-runner/test/taskArbitration.test.ts +0 -354
  446. package/plugins/workflow-runner/test/taskLifecycle.test.ts +0 -324
  447. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +0 -221
  448. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +0 -29
  449. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +0 -168
  450. package/plugins/workflow-runner/test/webppt.load.test.ts +0 -250
  451. package/plugins/workflow-runner/test/word.load.test.ts +0 -208
  452. package/plugins/workflow-runner/test/workflows.smoke.test.ts +0 -76
  453. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +0 -207
  454. package/src/advisorState.js +0 -22
  455. package/src/cli/configWizard.js +0 -293
  456. package/src/tools/toolProgressBridge.js +0 -48
  457. package/workflows/adaptive-research.yaml +0 -50
  458. package/workflows/book-review-short.yaml +0 -125
  459. package/workflows/e2e-write-greet.yaml +0 -31
  460. package/workflows/illustrate.yaml +0 -227
  461. package/workflows/plan.yaml +0 -470
  462. package/workflows/ppt.yaml +0 -1670
  463. package/workflows/ralph-loop.yaml +0 -201
  464. package/workflows/research.yaml +0 -873
  465. package/workflows/schema.json +0 -176
  466. package/workflows/webppt.yaml +0 -496
  467. package/workflows/wechat-reading-shots.yaml +0 -315
  468. package/workflows/word.yaml +0 -720
  469. package/workflows/youtube-shorts.yaml +0 -202
  470. /package/workflows/{research.md → research/DESIGN.md} +0 -0
@@ -1,21 +1,35 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
- import type { Provider } from '../types.js';
4
3
  import {
5
- readMinimalAgentConfig,
6
- updateMinimalAgentConfig,
7
- defaultMaxTokens,
8
- type MinimalAgentConfig,
4
+ type ModelFallbackReason,
5
+ type ModelRef,
6
+ modelRefFromLegacy,
7
+ } from '../modelRef.js';
8
+ import {
9
+ type OnlineModelCatalogItem,
10
+ type TeamCredentials,
11
+ fetchOnlineModels,
12
+ loadTeamCredentials,
13
+ resolveOnlineModel,
14
+ } from '../team/auth.js';
15
+ import type { GatewayProvider, LocalProvider, Provider } from '../types.js';
16
+ import {
9
17
  type AdvisorConfig,
10
- type ModelConfig,
11
18
  type ApiFormat,
12
- type ToolConfigs,
13
- type SubagentsConfig,
14
19
  type BrowserConfig,
15
20
  type ComputerUseConfig,
16
- DEFAULT_SUBAGENTS_CONFIG,
21
+ DEFAULT_ADVISOR_CONFIG,
17
22
  DEFAULT_BROWSER_CONFIG,
18
23
  DEFAULT_COMPUTER_USE_CONFIG,
24
+ DEFAULT_SUBAGENTS_CONFIG,
25
+ type MinimalAgentConfig,
26
+ type ModelConfig,
27
+ type SubagentsConfig,
28
+ type ToolConfigs,
29
+ type WorkflowModelRoles,
30
+ defaultMaxTokens,
31
+ readMinimalAgentConfig,
32
+ updateMinimalAgentConfig,
19
33
  } from './configFile.js';
20
34
  import { CONFIG_LIMITS, normalizeLimited } from './numericLimits.js';
21
35
 
@@ -42,28 +56,103 @@ export interface ConfigSnapshot {
42
56
  * 而不是「类型说必填、实现却防着 undefined」的分裂状态。
43
57
  */
44
58
  export type ModelOp =
45
- | { op: 'add'; model: Partial<Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>> }
46
- | { op: 'update'; id: string; patch: Partial<Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>> & { clearApiKey?: boolean } }
59
+ | {
60
+ op: 'add';
61
+ model: Partial<Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>>;
62
+ }
63
+ | {
64
+ op: 'update';
65
+ id: string;
66
+ patch: Partial<Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>> & {
67
+ clearApiKey?: boolean;
68
+ };
69
+ }
47
70
  | { op: 'remove'; id: string };
48
71
 
49
72
  export interface ConfigPatch {
50
73
  models?: ModelOp[];
51
74
  baseModelId?: string | null;
75
+ /** 私有部署/开发覆盖用的团队服务器地址;普通用户使用内置 KakadeAI 服务。 */
76
+ teamServerUrl?: string | null;
52
77
  compactRatio?: number;
53
- advisor?: (Partial<Omit<AdvisorConfig, 'contextPolicy'>> & { clear?: boolean });
78
+ advisor?: Partial<Omit<AdvisorConfig, 'contextPolicy'>> & { clear?: boolean };
79
+ /** Set a role to null/empty to remove its mapping. */
80
+ workflowModels?: Partial<Record<string, string | null>>;
54
81
  subagents?: Partial<SubagentsConfig>;
55
82
  browser?: Partial<BrowserConfig>;
56
83
  computerUse?: Partial<ComputerUseConfig>;
57
84
  tools?: {
58
- webSearch?: Partial<NonNullable<ToolConfigs['webSearch']>> & { clear?: boolean };
59
- imageGeneration?: Partial<NonNullable<ToolConfigs['imageGeneration']>> & { clear?: boolean; clearApiKey?: boolean };
60
- openRouter?: Partial<NonNullable<ToolConfigs['openRouter']>> & { clear?: boolean };
61
- textToSpeech?: Partial<NonNullable<ToolConfigs['textToSpeech']>> & { clear?: boolean; clearApiKey?: boolean };
85
+ webSearch?: Partial<NonNullable<ToolConfigs['webSearch']>> & {
86
+ clear?: boolean;
87
+ };
88
+ imageGeneration?: Partial<NonNullable<ToolConfigs['imageGeneration']>> & {
89
+ clear?: boolean;
90
+ clearApiKey?: boolean;
91
+ };
92
+ openRouter?: Partial<NonNullable<ToolConfigs['openRouter']>> & {
93
+ clear?: boolean;
94
+ };
95
+ textToSpeech?: Partial<NonNullable<ToolConfigs['textToSpeech']>> & {
96
+ clear?: boolean;
97
+ clearApiKey?: boolean;
98
+ };
62
99
  };
63
100
  }
64
101
 
65
102
  const DEFAULT_CONTEXT_WINDOW = 128_000;
66
103
 
104
+ export interface ModelResolution {
105
+ provider: Provider | null;
106
+ fallbackReason: ModelFallbackReason;
107
+ }
108
+
109
+ function gatewayFallbackReason(
110
+ model?: OnlineModelCatalogItem,
111
+ error?: unknown,
112
+ ): ModelFallbackReason {
113
+ const state = model?.availability?.state;
114
+ if (state === 'quota_exhausted' || state === 'concurrency_exceeded')
115
+ return 'quota_exhausted';
116
+ if (state === 'model_disabled' || state === 'disabled')
117
+ return 'model_disabled';
118
+ if (state === 'device_expired') return 'credential_expired';
119
+ if (state === 'not_authorized') return 'grant_revoked';
120
+ if (state === 'server_offline' || state === 'offline') return 'offline';
121
+ const message = error instanceof Error ? error.message : '';
122
+ if (message.includes('DEVICE_REVOKED') || message.includes('401'))
123
+ return 'credential_expired';
124
+ if (message.includes('MODEL_UNAVAILABLE') || message.includes('403'))
125
+ return 'grant_revoked';
126
+ return 'offline';
127
+ }
128
+
129
+ function gatewayProviderFromModel(
130
+ credentials: TeamCredentials,
131
+ model: OnlineModelCatalogItem,
132
+ ): GatewayProvider {
133
+ return {
134
+ name: model.displayName || model.model,
135
+ source: 'gateway',
136
+ remoteModelId: model.id,
137
+ gatewayUrl: credentials.serverUrl,
138
+ gatewayToken: credentials.deviceToken,
139
+ gatewayMaxToolCategories: model.maxToolCategories,
140
+ gatewayDefaultToolCategories: model.defaultToolCategories,
141
+ // Deprecated compatibility field: before the explicit max/default/session
142
+ // split this field was used as the provider boundary. Keep its safe
143
+ // default as the server maximum; the current Session selection is applied
144
+ // per turn by runAgentTurn/useChat.
145
+ gatewayToolCategories: model.maxToolCategories,
146
+ model: model.model,
147
+ contextWindow: model.contextWindow,
148
+ maxTokens: model.maxTokens,
149
+ apiFormat: model.apiFormat,
150
+ supportsVision: model.supportsVision,
151
+ gatewayCapabilities: model.capabilities,
152
+ promptCache: { mode: 'disabled' },
153
+ };
154
+ }
155
+
67
156
  function positiveInt(value: string | undefined, fallback: number): number {
68
157
  const parsed = Number.parseInt(value ?? '', 10);
69
158
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
@@ -87,25 +176,43 @@ function configFromEnv(): MinimalAgentConfig | null {
87
176
  const model = process.env.MINIMAL_AGENT_MODEL;
88
177
  if (!baseURL || !apiKey || !model) return null;
89
178
  const tools: ToolConfigs = {};
90
- if (process.env.TAVILY_API_KEY) tools.webSearch = { provider: 'tavily', apiKey: process.env.TAVILY_API_KEY };
91
- if (process.env.OPENAI_IMAGE_BASE_URL && process.env.OPENAI_IMAGE_API_KEY && process.env.OPENAI_IMAGE_MODEL) {
179
+ if (process.env.TAVILY_API_KEY)
180
+ tools.webSearch = {
181
+ provider: 'tavily',
182
+ apiKey: process.env.TAVILY_API_KEY,
183
+ };
184
+ if (
185
+ process.env.OPENAI_IMAGE_BASE_URL &&
186
+ process.env.OPENAI_IMAGE_API_KEY &&
187
+ process.env.OPENAI_IMAGE_MODEL
188
+ ) {
92
189
  tools.imageGeneration = {
93
190
  provider: process.env.OPENAI_IMAGE_PROVIDER ?? 'openai',
94
191
  baseURL: process.env.OPENAI_IMAGE_BASE_URL,
95
192
  apiKey: process.env.OPENAI_IMAGE_API_KEY,
96
193
  model: process.env.OPENAI_IMAGE_MODEL,
97
- protocol: process.env.OPENAI_IMAGE_PROTOCOL === 'chat' || process.env.OPENAI_IMAGE_PROTOCOL === 'images'
98
- ? process.env.OPENAI_IMAGE_PROTOCOL
99
- : 'auto',
194
+ protocol:
195
+ process.env.OPENAI_IMAGE_PROTOCOL === 'chat' ||
196
+ process.env.OPENAI_IMAGE_PROTOCOL === 'images'
197
+ ? process.env.OPENAI_IMAGE_PROTOCOL
198
+ : 'auto',
100
199
  };
101
200
  }
102
- if (process.env.OPENROUTER_API_KEY) tools.openRouter = { apiKey: process.env.OPENROUTER_API_KEY };
201
+ if (process.env.OPENROUTER_API_KEY)
202
+ tools.openRouter = { apiKey: process.env.OPENROUTER_API_KEY };
103
203
  if (process.env.MINIMAX_API_KEY && process.env.MINIMAX_GROUP_ID) {
104
- tools.textToSpeech = { provider: 'minimax', apiKey: process.env.MINIMAX_API_KEY, groupId: process.env.MINIMAX_GROUP_ID };
204
+ tools.textToSpeech = {
205
+ provider: 'minimax',
206
+ apiKey: process.env.MINIMAX_API_KEY,
207
+ groupId: process.env.MINIMAX_GROUP_ID,
208
+ };
105
209
  }
106
210
  const compactRatio = Number(process.env.MINIMAL_AGENT_COMPACT_RATIO);
107
211
  const name = process.env.MINIMAL_AGENT_PROVIDER ?? 'env';
108
- const contextWindow = positiveInt(process.env.MINIMAL_AGENT_CONTEXT_WINDOW, DEFAULT_CONTEXT_WINDOW);
212
+ const contextWindow = positiveInt(
213
+ process.env.MINIMAL_AGENT_CONTEXT_WINDOW,
214
+ DEFAULT_CONTEXT_WINDOW,
215
+ );
109
216
  const envModel: ModelConfig = {
110
217
  id: 'env-base',
111
218
  apiFormat: 'openai',
@@ -114,13 +221,17 @@ function configFromEnv(): MinimalAgentConfig | null {
114
221
  apiKey,
115
222
  model,
116
223
  contextWindow,
117
- maxTokens: positiveInt(process.env.MINIMAL_AGENT_MAX_TOKENS, defaultMaxTokens(contextWindow)),
224
+ maxTokens: positiveInt(
225
+ process.env.MINIMAL_AGENT_MAX_TOKENS,
226
+ defaultMaxTokens(contextWindow),
227
+ ),
118
228
  promptCache: {
119
- mode: process.env.MINIMAL_AGENT_PROMPT_CACHE_MODE === 'session-key'
120
- ? 'session-key'
121
- : process.env.MINIMAL_AGENT_PROMPT_CACHE_MODE === 'disabled'
122
- ? 'disabled'
123
- : 'auto',
229
+ mode:
230
+ process.env.MINIMAL_AGENT_PROMPT_CACHE_MODE === 'session-key'
231
+ ? 'session-key'
232
+ : process.env.MINIMAL_AGENT_PROMPT_CACHE_MODE === 'disabled'
233
+ ? 'disabled'
234
+ : 'auto',
124
235
  },
125
236
  enabled: true,
126
237
  createdAt: 0,
@@ -130,23 +241,39 @@ function configFromEnv(): MinimalAgentConfig | null {
130
241
  version: 4,
131
242
  models: [envModel],
132
243
  baseModelId: envModel.id,
244
+ teamServerUrl: process.env.KAKADEAI_SOCIAL_SERVER_URL?.trim() || null,
133
245
  ...(compactRatio > 0 && compactRatio <= 1 ? { compactRatio } : {}),
134
246
  subagents: {
135
247
  ...DEFAULT_SUBAGENTS_CONFIG,
136
- maxCallsPerTurn: positiveInt(process.env.AGENT_TOOL_MAX_CALLS_PER_TURN, 6),
248
+ maxCallsPerTurn: positiveInt(
249
+ process.env.AGENT_TOOL_MAX_CALLS_PER_TURN,
250
+ 6,
251
+ ),
137
252
  maxConcurrent: positiveInt(process.env.AGENT_TOOL_MAX_CONCURRENT, 3),
138
253
  timeoutMs: positiveInt(process.env.AGENT_TOOL_TIMEOUT_MS, 1_800_000),
139
- exploreMaxTurns: positiveInt(process.env.AGENT_TOOL_EXPLORE_MAX_TURNS, 50),
140
- generalMaxTurns: positiveInt(process.env.AGENT_TOOL_GENERAL_MAX_TURNS, 100),
254
+ exploreMaxTurns: positiveInt(
255
+ process.env.AGENT_TOOL_EXPLORE_MAX_TURNS,
256
+ 200,
257
+ ),
258
+ generalMaxTurns: positiveInt(
259
+ process.env.AGENT_TOOL_GENERAL_MAX_TURNS,
260
+ 100,
261
+ ),
141
262
  },
142
263
  browser: DEFAULT_BROWSER_CONFIG,
143
264
  computerUse: {
144
265
  enabled: process.env.MINIMAL_AGENT_COMPUTER_USE_ENABLED !== 'false',
145
266
  imagePolicy: 'single-adaptive',
146
- maxImagePixels: Math.min(4_000_000, Math.max(250_000, positiveInt(
147
- process.env.MINIMAL_AGENT_COMPUTER_USE_MAX_IMAGE_PIXELS,
148
- DEFAULT_COMPUTER_USE_CONFIG.maxImagePixels,
149
- ))),
267
+ maxImagePixels: Math.min(
268
+ 4_000_000,
269
+ Math.max(
270
+ 250_000,
271
+ positiveInt(
272
+ process.env.MINIMAL_AGENT_COMPUTER_USE_MAX_IMAGE_PIXELS,
273
+ DEFAULT_COMPUTER_USE_CONFIG.maxImagePixels,
274
+ ),
275
+ ),
276
+ ),
150
277
  observationFrames: 1,
151
278
  },
152
279
  tools,
@@ -154,8 +281,12 @@ function configFromEnv(): MinimalAgentConfig | null {
154
281
  };
155
282
  }
156
283
 
157
- function resolveProvider(entry: ModelConfig, compactRatio?: number): Provider {
284
+ function resolveProvider(
285
+ entry: ModelConfig,
286
+ compactRatio?: number,
287
+ ): LocalProvider {
158
288
  return {
289
+ source: 'local',
159
290
  name: entry.displayName ?? entry.model,
160
291
  baseURL: entry.baseURL,
161
292
  apiKey: entry.apiKey,
@@ -164,17 +295,31 @@ function resolveProvider(entry: ModelConfig, compactRatio?: number): Provider {
164
295
  maxTokens: entry.maxTokens ?? defaultMaxTokens(entry.contextWindow),
165
296
  apiFormat: entry.apiFormat,
166
297
  supportsVision: entry.supportsVision !== false,
298
+ ...(entry.supportsJsonSchema !== undefined
299
+ ? { supportsJsonSchema: entry.supportsJsonSchema }
300
+ : {}),
167
301
  promptCache: entry.promptCache ?? { mode: 'auto' },
168
302
  ...(compactRatio !== undefined ? { compactRatio } : {}),
303
+ // cache_edits 式回收:仅 Anthropic 格式 + 显式 env 开启时生效(非公开协议,
304
+ // 依赖支持该扩展的网关端点;语义与默认值说明见 types.ts Provider.cacheEdits)
305
+ ...(entry.apiFormat === 'anthropic' &&
306
+ process.env.MINIMAL_AGENT_CACHE_EDITS === '1'
307
+ ? { cacheEdits: true }
308
+ : {}),
169
309
  };
170
310
  }
171
311
 
172
- function mergeRecord(base: Record<string, unknown> | undefined, patch: Record<string, unknown>): Record<string, unknown> {
312
+ function mergeRecord(
313
+ base: Record<string, unknown> | undefined,
314
+ patch: Record<string, unknown>,
315
+ ): Record<string, unknown> {
173
316
  const out: Record<string, unknown> = { ...(base ?? {}) };
174
317
  for (const [key, value] of Object.entries(patch)) {
175
318
  if (value && typeof value === 'object' && !Array.isArray(value)) {
176
319
  out[key] = mergeRecord(
177
- out[key] && typeof out[key] === 'object' && !Array.isArray(out[key]) ? out[key] as Record<string, unknown> : undefined,
320
+ out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])
321
+ ? (out[key] as Record<string, unknown>)
322
+ : undefined,
178
323
  value as Record<string, unknown>,
179
324
  );
180
325
  } else if (value !== undefined) out[key] = value;
@@ -186,17 +331,33 @@ function hasEntries(value: object | undefined): boolean {
186
331
  return !!value && Object.keys(value).length > 0;
187
332
  }
188
333
 
189
- function applyModelsPatch(models: ModelConfig[], ops: ModelOp[]): ModelConfig[] {
334
+ /** Remove an optional config key while keeping the persisted object sparse. */
335
+ function omitKey<T extends object>(
336
+ value: T | undefined,
337
+ key: PropertyKey,
338
+ ): T | undefined {
339
+ if (!value) return value;
340
+ const { [key]: _removed, ...rest } = value as T &
341
+ Record<PropertyKey, unknown>;
342
+ return rest as T;
343
+ }
344
+
345
+ function applyModelsPatch(
346
+ models: ModelConfig[],
347
+ ops: ModelOp[],
348
+ ): ModelConfig[] {
190
349
  let out = [...models];
191
350
  const now = Date.now();
192
351
  for (const item of ops) {
193
352
  if (item.op === 'add') {
194
353
  const m = item.model;
195
- const apiFormat: ApiFormat = m.apiFormat === 'anthropic' ? 'anthropic' : 'openai';
354
+ const apiFormat: ApiFormat =
355
+ m.apiFormat === 'anthropic' ? 'anthropic' : 'openai';
196
356
  const baseURL = m.baseURL?.trim();
197
357
  const modelId = m.model?.trim();
198
358
  const apiKey = m.apiKey?.trim();
199
- if (!baseURL || !modelId || !apiKey) throw new Error('模型缺少 Base URL、模型 ID 或 API Key');
359
+ if (!baseURL || !modelId || !apiKey)
360
+ throw new Error('模型缺少 Base URL、模型 ID 或 API Key');
200
361
  out.push({
201
362
  id: randomUUID(),
202
363
  apiFormat,
@@ -204,9 +365,15 @@ function applyModelsPatch(models: ModelConfig[], ops: ModelOp[]): ModelConfig[]
204
365
  baseURL,
205
366
  apiKey,
206
367
  model: modelId,
207
- contextWindow: m.contextWindow && m.contextWindow > 0 ? m.contextWindow : DEFAULT_CONTEXT_WINDOW,
368
+ contextWindow:
369
+ m.contextWindow && m.contextWindow > 0
370
+ ? m.contextWindow
371
+ : DEFAULT_CONTEXT_WINDOW,
208
372
  ...(m.maxTokens ? { maxTokens: m.maxTokens } : {}),
209
373
  supportsVision: m.supportsVision !== false,
374
+ ...(m.supportsJsonSchema !== undefined
375
+ ? { supportsJsonSchema: m.supportsJsonSchema }
376
+ : {}),
210
377
  promptCache: m.promptCache ?? { mode: 'auto' },
211
378
  enabled: m.enabled ?? true,
212
379
  createdAt: now,
@@ -219,13 +386,27 @@ function applyModelsPatch(models: ModelConfig[], ops: ModelOp[]): ModelConfig[]
219
386
  const patch = item.patch;
220
387
  const next: ModelConfig = {
221
388
  ...existing,
222
- ...(patch.apiFormat ? { apiFormat: patch.apiFormat === 'anthropic' ? 'anthropic' : 'openai' } : {}),
223
- ...(patch.displayName !== undefined ? { displayName: patch.displayName.trim() || undefined } : {}),
389
+ ...(patch.apiFormat
390
+ ? {
391
+ apiFormat:
392
+ patch.apiFormat === 'anthropic' ? 'anthropic' : 'openai',
393
+ }
394
+ : {}),
395
+ ...(patch.displayName !== undefined
396
+ ? { displayName: patch.displayName.trim() || undefined }
397
+ : {}),
224
398
  ...(patch.baseURL?.trim() ? { baseURL: patch.baseURL.trim() } : {}),
225
399
  ...(patch.model?.trim() ? { model: patch.model.trim() } : {}),
226
400
  ...(patch.contextWindow ? { contextWindow: patch.contextWindow } : {}),
227
- ...(patch.maxTokens !== undefined ? { maxTokens: patch.maxTokens || undefined } : {}),
228
- ...(patch.supportsVision !== undefined ? { supportsVision: patch.supportsVision !== false } : {}),
401
+ ...(patch.maxTokens !== undefined
402
+ ? { maxTokens: patch.maxTokens || undefined }
403
+ : {}),
404
+ ...(patch.supportsVision !== undefined
405
+ ? { supportsVision: patch.supportsVision !== false }
406
+ : {}),
407
+ ...(patch.supportsJsonSchema !== undefined
408
+ ? { supportsJsonSchema: patch.supportsJsonSchema }
409
+ : {}),
229
410
  ...(patch.promptCache ? { promptCache: patch.promptCache } : {}),
230
411
  ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
231
412
  updatedAt: now,
@@ -240,18 +421,26 @@ function applyModelsPatch(models: ModelConfig[], ops: ModelOp[]): ModelConfig[]
240
421
  return out;
241
422
  }
242
423
 
243
- function applyConfigPatch(base: MinimalAgentConfig | null, patch: ConfigPatch): Omit<MinimalAgentConfig, 'version' | 'savedAt'> {
244
- const wasEmpty = (base?.models.length ?? 0) === 0 && (base?.baseModelId ?? null) === null;
424
+ function applyConfigPatch(
425
+ base: MinimalAgentConfig | null,
426
+ patch: ConfigPatch,
427
+ ): Omit<MinimalAgentConfig, 'version' | 'savedAt'> {
428
+ const wasEmpty =
429
+ (base?.models.length ?? 0) === 0 && (base?.baseModelId ?? null) === null;
245
430
  let models = base?.models ?? [];
246
431
  let baseModelId = base?.baseModelId ?? null;
247
432
 
248
433
  if (patch.models?.length) models = applyModelsPatch(models, patch.models);
249
434
 
250
435
  // 全新安装加第一个模型时自动设为基座——否则用户加完模型还是没法聊天。
251
- if (wasEmpty && baseModelId === null && models.length > 0) baseModelId = models[0]!.id;
436
+ if (wasEmpty && baseModelId === null && models.length > 0)
437
+ baseModelId = models[0]!.id;
252
438
 
253
439
  if (patch.baseModelId !== undefined) {
254
- if (patch.baseModelId !== null && !models.some((entry) => entry.id === patch.baseModelId)) {
440
+ if (
441
+ patch.baseModelId !== null &&
442
+ !models.some((entry) => entry.id === patch.baseModelId)
443
+ ) {
255
444
  throw new Error('要设为基座的模型不存在');
256
445
  }
257
446
  baseModelId = patch.baseModelId;
@@ -260,8 +449,16 @@ function applyConfigPatch(base: MinimalAgentConfig | null, patch: ConfigPatch):
260
449
  // 不变量:禁止删除/禁用当前基座模型指向的条目(先设别的为基座)。
261
450
  if (baseModelId !== null) {
262
451
  const baseEntry = models.find((entry) => entry.id === baseModelId);
263
- if (!baseEntry) throw new Error('当前基座模型已被删除,请先设置其他模型为基座');
264
- if (baseEntry.enabled === false) throw new Error('不能禁用当前基座模型,请先设置其他模型为基座');
452
+ if (!baseEntry)
453
+ throw new Error('当前基座模型已被删除,请先设置其他模型为基座');
454
+ if (baseEntry.enabled === false)
455
+ throw new Error('不能禁用当前基座模型,请先设置其他模型为基座');
456
+ }
457
+
458
+ // 团队服务器地址:不像 baseModelId 那样要交叉校验(不引用 models[]),一个裸字符串。
459
+ let teamServerUrl = base?.teamServerUrl ?? null;
460
+ if (patch.teamServerUrl !== undefined) {
461
+ teamServerUrl = patch.teamServerUrl?.trim() || null;
265
462
  }
266
463
 
267
464
  let advisor = base?.advisor;
@@ -270,90 +467,197 @@ function applyConfigPatch(base: MinimalAgentConfig | null, patch: ConfigPatch):
270
467
  advisor = undefined;
271
468
  } else {
272
469
  advisor = {
273
- enabled: patch.advisor.enabled ?? advisor?.enabled ?? true,
470
+ enabled: patch.advisor.enabled ?? advisor?.enabled ?? DEFAULT_ADVISOR_CONFIG.enabled,
274
471
  mode: patch.advisor.mode ?? advisor?.mode ?? 'auto',
275
- modelId: patch.advisor.modelId !== undefined ? patch.advisor.modelId : (advisor?.modelId ?? null),
472
+ modelId:
473
+ patch.advisor.modelId !== undefined
474
+ ? patch.advisor.modelId
475
+ : (advisor?.modelId ?? null),
276
476
  contextPolicy: 'full-relevant',
277
- shareSensitive: patch.advisor.shareSensitive ?? advisor?.shareSensitive ?? false,
477
+ shareSensitive:
478
+ patch.advisor.shareSensitive ?? advisor?.shareSensitive ?? false,
278
479
  inputTokenBudget: normalizeLimited(
279
480
  patch.advisor.inputTokenBudget ?? advisor?.inputTokenBudget,
280
- CONFIG_LIMITS.advisorInputTokenBudget, 128_000),
481
+ CONFIG_LIMITS.advisorInputTokenBudget,
482
+ DEFAULT_ADVISOR_CONFIG.inputTokenBudget,
483
+ ),
281
484
  timeoutMs: normalizeLimited(
282
485
  patch.advisor.timeoutMs ?? advisor?.timeoutMs,
283
- CONFIG_LIMITS.advisorTimeoutMs, 180_000),
486
+ CONFIG_LIMITS.advisorTimeoutMs,
487
+ DEFAULT_ADVISOR_CONFIG.timeoutMs,
488
+ ),
284
489
  };
285
490
  }
286
491
  }
287
492
  // 顾问引用的模型若已被删除或禁用,级联置空(软失联——解析层已把 null/悬空 id 当
288
493
  // 「未配置」优雅处理,不留死链接;这与基座模型的硬阻止是刻意的不对称:顾问本来就是
289
494
  // 可降级的第二意见,不像基座那样「没有就整个应用跑不起来」)。
290
- if (advisor?.modelId) {
495
+ if (advisor?.modelId && !advisor.modelId.startsWith('online:')) {
291
496
  const referenced = models.find((entry) => entry.id === advisor!.modelId);
292
- if (!referenced || referenced.enabled === false) advisor = { ...advisor, modelId: null };
497
+ if (!referenced || referenced.enabled === false)
498
+ advisor = { ...advisor, modelId: null };
499
+ }
500
+
501
+ const workflowModels: WorkflowModelRoles = {
502
+ ...(base?.workflowModels ?? {}),
503
+ };
504
+ for (const [role, modelId] of Object.entries(patch.workflowModels ?? {})) {
505
+ if (modelId) workflowModels[role] = modelId;
506
+ else delete workflowModels[role];
507
+ }
508
+ for (const role of Object.keys(workflowModels)) {
509
+ const modelId = workflowModels[role];
510
+ const configured = models.find((entry) => entry.id === modelId);
511
+ if (!configured || configured.enabled === false)
512
+ delete workflowModels[role];
293
513
  }
294
514
 
295
515
  // 比例类:保留 4 位小数(= 百分比 2 位),否则 UI 上下箭头按 0.01 步进会累加出
296
516
  // `0.8500000000000001` 这种浮点脏值直接写进 config.json。
297
- const compactRatio = patch.compactRatio !== undefined
298
- ? (patch.compactRatio > 0 && patch.compactRatio <= 1
299
- ? normalizeLimited(patch.compactRatio, CONFIG_LIMITS.compactRatio, CONFIG_LIMITS.compactRatio.max)
300
- : undefined)
301
- : base?.compactRatio;
517
+ const compactRatio =
518
+ patch.compactRatio !== undefined
519
+ ? patch.compactRatio > 0 && patch.compactRatio <= 1
520
+ ? normalizeLimited(
521
+ patch.compactRatio,
522
+ CONFIG_LIMITS.compactRatio,
523
+ CONFIG_LIMITS.compactRatio.max,
524
+ )
525
+ : undefined
526
+ : base?.compactRatio;
302
527
 
303
- const tools: ToolConfigs = { ...(base?.tools ?? {}) };
528
+ let tools: ToolConfigs = { ...(base?.tools ?? {}) };
304
529
  const drafts = structuredClone(base?.drafts ?? {});
305
530
  const toolPatch = patch.tools;
306
531
  if (toolPatch?.webSearch) {
307
- if (toolPatch.webSearch.clear) { delete tools.webSearch; delete drafts.tools?.webSearch; }
308
- else tools.webSearch = mergeRecord(tools.webSearch, { provider: 'tavily', ...toolPatch.webSearch }) as ToolConfigs['webSearch'];
532
+ if (toolPatch.webSearch.clear) {
533
+ tools = omitKey(tools, 'webSearch') ?? {};
534
+ if (drafts.tools) drafts.tools = omitKey(drafts.tools, 'webSearch');
535
+ } else
536
+ tools.webSearch = mergeRecord(tools.webSearch, {
537
+ provider: 'tavily',
538
+ ...toolPatch.webSearch,
539
+ }) as ToolConfigs['webSearch'];
309
540
  }
310
541
  if (toolPatch?.imageGeneration) {
311
- if (toolPatch.imageGeneration.clear) { delete tools.imageGeneration; delete drafts.tools?.imageGeneration; }
312
- else {
313
- const next = mergeRecord(tools.imageGeneration, toolPatch.imageGeneration) as NonNullable<ToolConfigs['imageGeneration']>;
542
+ if (toolPatch.imageGeneration.clear) {
543
+ tools = omitKey(tools, 'imageGeneration') ?? {};
544
+ if (drafts.tools) drafts.tools = omitKey(drafts.tools, 'imageGeneration');
545
+ } else {
546
+ const next = mergeRecord(
547
+ tools.imageGeneration,
548
+ toolPatch.imageGeneration,
549
+ ) as NonNullable<ToolConfigs['imageGeneration']>;
314
550
  if (toolPatch.imageGeneration.clearApiKey) next.apiKey = '';
315
- delete (next as typeof next & { clear?: boolean; clearApiKey?: boolean }).clear;
316
- delete (next as typeof next & { clear?: boolean; clearApiKey?: boolean }).clearApiKey;
317
- if (!next.baseURL || !next.apiKey || !next.model) throw new Error('图片生成缺少 Base URL、API Key 或模型名称');
318
- tools.imageGeneration = next;
551
+ const cleanNext = omitKey(
552
+ omitKey(
553
+ next as typeof next & { clear?: boolean; clearApiKey?: boolean },
554
+ 'clear',
555
+ ),
556
+ 'clearApiKey',
557
+ ) as NonNullable<ToolConfigs['imageGeneration']>;
558
+ if (!cleanNext.baseURL || !cleanNext.apiKey || !cleanNext.model)
559
+ throw new Error('图片生成缺少 Base URL、API Key 或模型名称');
560
+ tools.imageGeneration = cleanNext;
319
561
  }
320
562
  }
321
563
  if (toolPatch?.openRouter) {
322
- if (toolPatch.openRouter.clear) { delete tools.openRouter; delete drafts.tools?.openRouter; }
323
- else tools.openRouter = mergeRecord(tools.openRouter, toolPatch.openRouter) as ToolConfigs['openRouter'];
564
+ if (toolPatch.openRouter.clear) {
565
+ tools = omitKey(tools, 'openRouter') ?? {};
566
+ if (drafts.tools) drafts.tools = omitKey(drafts.tools, 'openRouter');
567
+ } else
568
+ tools.openRouter = mergeRecord(
569
+ tools.openRouter,
570
+ toolPatch.openRouter,
571
+ ) as ToolConfigs['openRouter'];
324
572
  }
325
573
  if (toolPatch?.textToSpeech) {
326
- if (toolPatch.textToSpeech.clear) { delete tools.textToSpeech; delete drafts.tools?.textToSpeech; }
327
- else {
328
- const next = mergeRecord(tools.textToSpeech, { provider: 'minimax', ...toolPatch.textToSpeech }) as NonNullable<ToolConfigs['textToSpeech']>;
574
+ if (toolPatch.textToSpeech.clear) {
575
+ tools = omitKey(tools, 'textToSpeech') ?? {};
576
+ if (drafts.tools) drafts.tools = omitKey(drafts.tools, 'textToSpeech');
577
+ } else {
578
+ const next = mergeRecord(tools.textToSpeech, {
579
+ provider: 'minimax',
580
+ ...toolPatch.textToSpeech,
581
+ }) as NonNullable<ToolConfigs['textToSpeech']>;
329
582
  if (toolPatch.textToSpeech.clearApiKey) next.apiKey = '';
330
- delete (next as typeof next & { clear?: boolean; clearApiKey?: boolean }).clear;
331
- delete (next as typeof next & { clear?: boolean; clearApiKey?: boolean }).clearApiKey;
332
- if (!next.apiKey || !next.groupId) throw new Error('MiniMax 语音缺少 API Key 或 Group ID');
333
- tools.textToSpeech = next;
583
+ const cleanNext = omitKey(
584
+ omitKey(
585
+ next as typeof next & { clear?: boolean; clearApiKey?: boolean },
586
+ 'clear',
587
+ ),
588
+ 'clearApiKey',
589
+ ) as NonNullable<ToolConfigs['textToSpeech']>;
590
+ if (!cleanNext.apiKey || !cleanNext.groupId)
591
+ throw new Error('MiniMax 语音缺少 API Key 或 Group ID');
592
+ tools.textToSpeech = cleanNext;
334
593
  }
335
594
  }
336
595
  // 数字字段必须在**写入时**就按同一份边界规整——此前这里是裸 spread,读回时才被
337
596
  // parseSubagents 的边界收窄,于是「存进去 300、读出来 100」,UI 表现成静默变回原值。
338
- const mergedSubagents = { ...(base?.subagents ?? DEFAULT_SUBAGENTS_CONFIG), ...(patch.subagents ?? {}) };
597
+ const mergedSubagents = {
598
+ ...(base?.subagents ?? DEFAULT_SUBAGENTS_CONFIG),
599
+ ...(patch.subagents ?? {}),
600
+ };
339
601
  const subagents: SubagentsConfig = {
340
602
  ...mergedSubagents,
341
- maxCallsPerTurn: normalizeLimited(mergedSubagents.maxCallsPerTurn, CONFIG_LIMITS.subagentMaxCallsPerTurn, DEFAULT_SUBAGENTS_CONFIG.maxCallsPerTurn),
342
- maxConcurrent: normalizeLimited(mergedSubagents.maxConcurrent, CONFIG_LIMITS.subagentMaxConcurrent, DEFAULT_SUBAGENTS_CONFIG.maxConcurrent),
343
- timeoutMs: normalizeLimited(mergedSubagents.timeoutMs, CONFIG_LIMITS.subagentTimeoutMs, DEFAULT_SUBAGENTS_CONFIG.timeoutMs),
344
- exploreMaxTurns: normalizeLimited(mergedSubagents.exploreMaxTurns, CONFIG_LIMITS.subagentExploreMaxTurns, DEFAULT_SUBAGENTS_CONFIG.exploreMaxTurns),
345
- generalMaxTurns: normalizeLimited(mergedSubagents.generalMaxTurns, CONFIG_LIMITS.subagentGeneralMaxTurns, DEFAULT_SUBAGENTS_CONFIG.generalMaxTurns),
603
+ maxCallsPerTurn: normalizeLimited(
604
+ mergedSubagents.maxCallsPerTurn,
605
+ CONFIG_LIMITS.subagentMaxCallsPerTurn,
606
+ DEFAULT_SUBAGENTS_CONFIG.maxCallsPerTurn,
607
+ ),
608
+ maxConcurrent: normalizeLimited(
609
+ mergedSubagents.maxConcurrent,
610
+ CONFIG_LIMITS.subagentMaxConcurrent,
611
+ DEFAULT_SUBAGENTS_CONFIG.maxConcurrent,
612
+ ),
613
+ timeoutMs: normalizeLimited(
614
+ mergedSubagents.timeoutMs,
615
+ CONFIG_LIMITS.subagentTimeoutMs,
616
+ DEFAULT_SUBAGENTS_CONFIG.timeoutMs,
617
+ ),
618
+ exploreMaxTurns: normalizeLimited(
619
+ mergedSubagents.exploreMaxTurns,
620
+ CONFIG_LIMITS.subagentExploreMaxTurns,
621
+ DEFAULT_SUBAGENTS_CONFIG.exploreMaxTurns,
622
+ ),
623
+ generalMaxTurns: normalizeLimited(
624
+ mergedSubagents.generalMaxTurns,
625
+ CONFIG_LIMITS.subagentGeneralMaxTurns,
626
+ DEFAULT_SUBAGENTS_CONFIG.generalMaxTurns,
627
+ ),
628
+ };
629
+ const browser: BrowserConfig = {
630
+ ...(base?.browser ?? DEFAULT_BROWSER_CONFIG),
631
+ ...(patch.browser ?? {}),
632
+ profileId: 'default',
346
633
  };
347
- const browser: BrowserConfig = { ...(base?.browser ?? DEFAULT_BROWSER_CONFIG), ...(patch.browser ?? {}), profileId: 'default' };
348
634
  const computerUse: ComputerUseConfig = {
349
635
  ...(base?.computerUse ?? DEFAULT_COMPUTER_USE_CONFIG),
350
636
  ...(patch.computerUse ?? {}),
351
637
  };
352
638
  computerUse.observationFrames = 1;
353
639
  computerUse.maxImagePixels = normalizeLimited(
354
- computerUse.maxImagePixels, CONFIG_LIMITS.computerUseMaxImagePixels, DEFAULT_COMPUTER_USE_CONFIG.maxImagePixels);
355
- if (!hasEntries(drafts.tools)) delete drafts.tools;
356
- return { models, baseModelId, ...(compactRatio !== undefined ? { compactRatio } : {}), advisor, subagents, browser, computerUse, tools, ...(hasEntries(drafts) ? { drafts } : {}) };
640
+ computerUse.maxImagePixels,
641
+ CONFIG_LIMITS.computerUseMaxImagePixels,
642
+ DEFAULT_COMPUTER_USE_CONFIG.maxImagePixels,
643
+ );
644
+ if (!hasEntries(drafts.tools)) drafts.tools = undefined;
645
+ return {
646
+ models,
647
+ baseModelId,
648
+ teamServerUrl,
649
+ ...(compactRatio !== undefined ? { compactRatio } : {}),
650
+ advisor,
651
+ // Preserve an explicit empty object through updateMinimalAgentConfig so
652
+ // removing the final role is distinguishable from an unrelated mutator
653
+ // that omitted this optional field.
654
+ workflowModels,
655
+ subagents,
656
+ browser,
657
+ computerUse,
658
+ tools,
659
+ ...(hasEntries(drafts) ? { drafts } : {}),
660
+ };
357
661
  }
358
662
 
359
663
  /** 仅不完整工具凭据使用草稿桶;其余设置都通过 patch 立即生效。 */
@@ -361,9 +665,16 @@ export type AutosaveSection = 'tools';
361
665
  export type AutosaveState = 'applied' | 'draft';
362
666
 
363
667
  export class ConfigService {
364
- private snapshot: ConfigSnapshot = { source: 'none', readOnly: false, config: null, revision: 0 };
668
+ private snapshot: ConfigSnapshot = {
669
+ source: 'none',
670
+ readOnly: false,
671
+ config: null,
672
+ revision: 0,
673
+ };
365
674
 
366
- get revision(): number { return this.snapshot.revision; }
675
+ get revision(): number {
676
+ return this.snapshot.revision;
677
+ }
367
678
 
368
679
  async reload(): Promise<ConfigSnapshot> {
369
680
  const envOnly = process.env.MINIMAL_AGENT_CONFIG_MODE === 'env';
@@ -374,11 +685,15 @@ export class ConfigService {
374
685
  readOnly: envOnly,
375
686
  config: json ?? env,
376
687
  };
377
- const changed = this.snapshot.revision === 0
378
- || this.snapshot.source !== next.source
379
- || this.snapshot.readOnly !== next.readOnly
380
- || JSON.stringify(this.snapshot.config) !== JSON.stringify(next.config);
381
- this.snapshot = { ...next, revision: changed ? this.snapshot.revision + 1 : this.snapshot.revision };
688
+ const changed =
689
+ this.snapshot.revision === 0 ||
690
+ this.snapshot.source !== next.source ||
691
+ this.snapshot.readOnly !== next.readOnly ||
692
+ JSON.stringify(this.snapshot.config) !== JSON.stringify(next.config);
693
+ this.snapshot = {
694
+ ...next,
695
+ revision: changed ? this.snapshot.revision + 1 : this.snapshot.revision,
696
+ };
382
697
  return this.getSnapshot();
383
698
  }
384
699
 
@@ -394,21 +709,166 @@ export class ConfigService {
394
709
  return resolveProvider(entry, config?.compactRatio);
395
710
  }
396
711
 
712
+ /**
713
+ * 团队登录服务器的默认地址:env 优先于 config.json,跟随 minimal-desktop 的
714
+ * `resolve_social_server()`(`src-tauri/src/lib.rs`)已验证的同一优先级——这两个
715
+ * 项目共享 `KAKADEAI_SOCIAL_SERVER_URL` 这一个环境变量名,语义必须一致,不能
716
+ * minimal-agent 这边搞成 config 优先。
717
+ *
718
+ * ⚠️ 不能简单读 `getSnapshot().config?.teamServerUrl`:那个 snapshot 在存在
719
+ * config.json 时完全不看 env(`reload()` 里 `config: json ?? env`,env 只在
720
+ * **没有** config.json 时才整体生效),会让"config.json 存在但没配地址、同时设了
721
+ * env"这种常见场景里 env 被错误地忽略。这里必须独立读 `process.env`。
722
+ *
723
+ * 返回值带 `source`,供诊断和私有部署使用;正式用户不需要接触服务器地址。
724
+ */
725
+ async resolveTeamServerUrl(): Promise<{
726
+ value: string | null;
727
+ source: 'env' | 'config' | 'default' | 'none';
728
+ }> {
729
+ const rawEnvValue = process.env.KAKADEAI_SOCIAL_SERVER_URL;
730
+ if (rawEnvValue !== undefined)
731
+ return {
732
+ value: rawEnvValue.trim() || null,
733
+ source: 'env',
734
+ };
735
+ const { config } = await this.getSnapshot();
736
+ const configValue = config?.teamServerUrl ?? null;
737
+ return configValue
738
+ ? { value: configValue, source: 'config' }
739
+ : { value: 'https://kakadeai.com', source: 'default' };
740
+ }
741
+
397
742
  async providerForModel(modelId?: string | null): Promise<Provider | null> {
398
743
  if (!modelId) return this.provider();
744
+ const ref = modelRefFromLegacy(modelId);
745
+ if (ref.source === 'gateway') {
746
+ const resolved = await this.providerForRef(ref);
747
+ return resolved;
748
+ }
399
749
  const { config } = await this.getSnapshot();
400
750
  const entry = config?.models.find((m) => m.id === modelId);
401
751
  if (!entry?.apiKey) return null;
402
752
  return resolveProvider(entry, config?.compactRatio);
403
753
  }
404
754
 
755
+ /** Resolve a portable workflow role from fresh cross-process config. */
756
+ async providerForWorkflowRole(role: string): Promise<Provider | null> {
757
+ const { config } = await this.reload();
758
+ const modelId = config?.workflowModels?.[role];
759
+ return modelId ? this.providerForModel(modelId) : null;
760
+ }
761
+
762
+ private async providerForGatewayModel(
763
+ modelId: string,
764
+ ): Promise<Provider | null> {
765
+ // A remembered gateway model must not make standalone mode inspect the
766
+ // team credential file or attempt a network request. The configured
767
+ // server is the explicit opt-in boundary; credentials are read only after
768
+ // it is present.
769
+ const configured = await this.resolveTeamServerUrl();
770
+ if (!configured.value) return null;
771
+ const resolved = await resolveOnlineModel(
772
+ modelId,
773
+ configured.value ?? undefined,
774
+ ).catch(() => null);
775
+ if (!resolved) return null;
776
+ const { credentials, model } = resolved;
777
+ if (model.availability && model.availability.state !== 'available')
778
+ return null;
779
+ return gatewayProviderFromModel(credentials, model);
780
+ }
781
+
782
+ /** Resolve the persisted source-aware identity without making callers encode
783
+ * gateway selections as the legacy `online:<id>` string. */
784
+ async providerForRef(ref?: ModelRef | null): Promise<Provider | null> {
785
+ if (!ref) return this.provider();
786
+ return ref.source === 'gateway'
787
+ ? this.providerForGatewayModel(ref.modelId)
788
+ : this.providerForModel(ref.modelId);
789
+ }
790
+
791
+ /**
792
+ * Resolve a persisted model reference and retain the reason when a gateway
793
+ * model cannot be used. The reason is intentionally kept at the local
794
+ * session boundary: it is UI/selection state, not a server-side session or
795
+ * prompt record.
796
+ */
797
+ async resolveModelRef(ref?: ModelRef | null): Promise<ModelResolution> {
798
+ if (!ref)
799
+ return {
800
+ provider: await this.providerForModel(null),
801
+ fallbackReason: null,
802
+ };
803
+ if (ref.source === 'local')
804
+ return {
805
+ provider: await this.providerForRef(ref),
806
+ fallbackReason: null,
807
+ };
808
+
809
+ const configured = await this.resolveTeamServerUrl();
810
+ if (!configured.value)
811
+ return { provider: null, fallbackReason: 'logged_out' };
812
+ const credentials = await loadTeamCredentials(configured.value);
813
+ if (!credentials) return { provider: null, fallbackReason: 'logged_out' };
814
+ try {
815
+ const models = await fetchOnlineModels(credentials);
816
+ const model = models.find((item) => item.id === ref.modelId);
817
+ if (!model) return { provider: null, fallbackReason: 'grant_revoked' };
818
+ if (
819
+ model.availability?.state !== undefined &&
820
+ model.availability.state !== 'available'
821
+ )
822
+ return {
823
+ provider: null,
824
+ fallbackReason: gatewayFallbackReason(model),
825
+ };
826
+ return {
827
+ provider: gatewayProviderFromModel(credentials, model),
828
+ fallbackReason: null,
829
+ };
830
+ } catch (error) {
831
+ return {
832
+ provider: null,
833
+ fallbackReason: gatewayFallbackReason(undefined, error),
834
+ };
835
+ }
836
+ }
837
+
838
+ /** Online defaults are server-owned and fetched only when team credentials
839
+ * exist. No standalone launch calls this path. */
840
+ async onlineDefaultProvider(): Promise<Provider | null> {
841
+ const configured = await this.resolveTeamServerUrl();
842
+ if (!configured.value) return null;
843
+ const credentials = await import('../team/auth.js')
844
+ .then((module) =>
845
+ module.loadTeamCredentials(configured.value ?? undefined),
846
+ )
847
+ .catch(() => null);
848
+ if (!credentials) return null;
849
+ const models = await import('../team/auth.js')
850
+ .then((module) => module.fetchOnlineModels(credentials))
851
+ .catch(() => []);
852
+ const model = models.find(
853
+ (item) =>
854
+ item.isDefault &&
855
+ (!item.availability || item.availability.state === 'available'),
856
+ );
857
+ if (!model) return null;
858
+ return gatewayProviderFromModel(credentials, model);
859
+ }
860
+
405
861
  /** 带 apiKey 的原始列表——仅服务端内部消费(桌面端 store.ts 的 readConfig() 会脱敏成 ModelView)。 */
406
862
  async models(): Promise<ModelConfig[]> {
407
863
  return (await this.getSnapshot()).config?.models ?? [];
408
864
  }
409
865
 
410
866
  async model(id: string): Promise<ModelConfig | null> {
411
- return (await this.getSnapshot()).config?.models.find((entry) => entry.id === id) ?? null;
867
+ return (
868
+ (await this.getSnapshot()).config?.models.find(
869
+ (entry) => entry.id === id,
870
+ ) ?? null
871
+ );
412
872
  }
413
873
 
414
874
  /**
@@ -419,16 +879,33 @@ export class ConfigService {
419
879
  * ModelConfig 镜像到返回对象顶层,让 advisor.ts 的 `advisor.supportsVision` 读取方式
420
880
  * 保持不变——它是解析后的便利视图,不是存储形状本身。
421
881
  */
422
- async advisor(): Promise<(AdvisorConfig & { provider: Provider; supportsVision: boolean }) | null> {
882
+ async advisor(): Promise<
883
+ (AdvisorConfig & { provider: Provider; supportsVision: boolean }) | null
884
+ > {
423
885
  // fresh reload(不用 getSnapshot 的缓存)——CLI/TUI/Desktop/Cron 共享同一份 config.json,
424
886
  // 跨进程时缓存会读到过期状态(如 A 进程 /advisor off 后 B 进程仍判定已启用)。这条不变量
425
887
  // 由 test/configService.test.ts 的 "advisor() always does a fresh reload" 用例锁定。
426
888
  const { config } = await this.reload();
427
889
  const advisor = config?.advisor;
428
890
  if (!advisor?.modelId) return null;
891
+ const advisorRef = modelRefFromLegacy(advisor.modelId);
892
+ if (advisorRef.source === 'gateway') {
893
+ const provider = await this.providerForRef(advisorRef);
894
+ return provider
895
+ ? {
896
+ ...advisor,
897
+ provider,
898
+ supportsVision: provider.supportsVision !== false,
899
+ }
900
+ : null;
901
+ }
429
902
  const entry = config!.models.find((m) => m.id === advisor.modelId);
430
903
  if (!entry?.apiKey) return null;
431
- return { ...advisor, provider: resolveProvider(entry), supportsVision: entry.supportsVision !== false };
904
+ return {
905
+ ...advisor,
906
+ provider: resolveProvider(entry),
907
+ supportsVision: entry.supportsVision !== false,
908
+ };
432
909
  }
433
910
 
434
911
  /**
@@ -440,12 +917,27 @@ export class ConfigService {
440
917
  * resolveAvailability 是在主循环构造工具集时同步调用的,不能 await;
441
918
  * 而且每次工具集构造都读盘的话开销太大(主循环每轮都可能构造一次)。
442
919
  */
443
- advisorAvailabilitySync(): { enabled: boolean; mode: 'auto' | 'manual'; modelId: string | null; configured: boolean } {
920
+ advisorAvailabilitySync(): {
921
+ enabled: boolean;
922
+ mode: 'auto' | 'manual';
923
+ modelId: string | null;
924
+ configured: boolean;
925
+ } {
444
926
  const config = this.snapshot.config;
445
927
  const advisor = config?.advisor;
446
- if (!advisor?.modelId) return { enabled: false, mode: 'auto', modelId: null, configured: false };
928
+ if (!advisor?.modelId)
929
+ return { enabled: false, mode: 'auto', modelId: null, configured: false };
930
+ if (modelRefFromLegacy(advisor.modelId).source === 'gateway') {
931
+ return {
932
+ enabled: advisor.enabled !== false,
933
+ mode: advisor.mode === 'manual' ? 'manual' : 'auto',
934
+ modelId: advisor.modelId,
935
+ configured: true,
936
+ };
937
+ }
447
938
  const entry = config?.models?.find((m) => m.id === advisor.modelId);
448
- if (!entry?.apiKey) return { enabled: false, mode: 'auto', modelId: null, configured: false };
939
+ if (!entry?.apiKey)
940
+ return { enabled: false, mode: 'auto', modelId: null, configured: false };
449
941
  return {
450
942
  enabled: advisor.enabled !== false,
451
943
  mode: advisor.mode === 'manual' ? 'manual' : 'auto',
@@ -463,7 +955,9 @@ export class ConfigService {
463
955
  }
464
956
 
465
957
  async computerUse(): Promise<ComputerUseConfig> {
466
- return (await this.reload()).config?.computerUse ?? DEFAULT_COMPUTER_USE_CONFIG;
958
+ return (
959
+ (await this.reload()).config?.computerUse ?? DEFAULT_COMPUTER_USE_CONFIG
960
+ );
467
961
  }
468
962
 
469
963
  async tool<K extends ToolConfigKey>(key: K): Promise<ToolConfigs[K]> {
@@ -473,53 +967,104 @@ export class ConfigService {
473
967
  async toolStatus(): Promise<Record<ToolConfigKey, ToolStatus>> {
474
968
  const tools = (await this.reload()).config?.tools ?? {};
475
969
  return {
476
- webSearch: { state: tools.webSearch ? 'configured' : 'unconfigured', configured: !!tools.webSearch },
477
- imageGeneration: { state: tools.imageGeneration ? 'configured' : 'unconfigured', configured: !!tools.imageGeneration },
478
- openRouter: { state: tools.openRouter ? 'configured' : 'unconfigured', configured: !!tools.openRouter },
479
- textToSpeech: { state: tools.textToSpeech ? 'configured' : 'unconfigured', configured: !!tools.textToSpeech },
970
+ webSearch: {
971
+ state: tools.webSearch ? 'configured' : 'unconfigured',
972
+ configured: !!tools.webSearch,
973
+ },
974
+ imageGeneration: {
975
+ state: tools.imageGeneration ? 'configured' : 'unconfigured',
976
+ configured: !!tools.imageGeneration,
977
+ },
978
+ openRouter: {
979
+ state: tools.openRouter ? 'configured' : 'unconfigured',
980
+ configured: !!tools.openRouter,
981
+ },
982
+ textToSpeech: {
983
+ state: tools.textToSpeech ? 'configured' : 'unconfigured',
984
+ configured: !!tools.textToSpeech,
985
+ },
480
986
  };
481
987
  }
482
988
 
483
989
  async savePatch(patch: ConfigPatch): Promise<ConfigSnapshot> {
484
990
  const current = await this.getSnapshot();
485
- if (current.readOnly) throw new Error('当前使用环境变量只读配置,不能从设置页修改');
486
- await updateMinimalAgentConfig((latest) => applyConfigPatch(latest ?? current.config, patch));
991
+ if (current.readOnly)
992
+ throw new Error('当前使用环境变量只读配置,不能从设置页修改');
993
+ await updateMinimalAgentConfig((latest) =>
994
+ applyConfigPatch(latest ?? current.config, patch),
995
+ );
487
996
  return this.reload();
488
997
  }
489
998
 
490
999
  /** 现在只服务 tools 分区的实时录入表单(imageGeneration/textToSpeech 多字段、可能中途非法)。 */
491
- async autosave(section: AutosaveSection, patch: ConfigPatch): Promise<{ snapshot: ConfigSnapshot; state: AutosaveState; draftFields: string[] }> {
1000
+ async autosave(
1001
+ section: AutosaveSection,
1002
+ patch: ConfigPatch,
1003
+ ): Promise<{
1004
+ snapshot: ConfigSnapshot;
1005
+ state: AutosaveState;
1006
+ draftFields: string[];
1007
+ }> {
492
1008
  void section;
493
1009
  const current = await this.getSnapshot();
494
- if (current.readOnly) throw new Error('当前使用环境变量只读配置,不能从设置页修改');
1010
+ if (current.readOnly)
1011
+ throw new Error('当前使用环境变量只读配置,不能从设置页修改');
495
1012
  if (!current.config) throw new Error('请先添加至少一个模型');
496
1013
  let state: AutosaveState = 'applied';
497
1014
  const draftFields: string[] = [];
498
1015
  await updateMinimalAgentConfig((latest) => {
499
1016
  let active = latest ?? current.config!;
500
- if (patch.subagents) active = { version: 4, ...applyConfigPatch(active, { subagents: patch.subagents }), savedAt: active.savedAt };
501
- if (patch.computerUse) active = { version: 4, ...applyConfigPatch(active, { computerUse: patch.computerUse }), savedAt: active.savedAt };
1017
+ if (patch.subagents)
1018
+ active = {
1019
+ version: 4,
1020
+ ...applyConfigPatch(active, { subagents: patch.subagents }),
1021
+ savedAt: active.savedAt,
1022
+ };
1023
+ if (patch.computerUse)
1024
+ active = {
1025
+ version: 4,
1026
+ ...applyConfigPatch(active, { computerUse: patch.computerUse }),
1027
+ savedAt: active.savedAt,
1028
+ };
502
1029
  const drafts = structuredClone(active.drafts ?? {});
503
1030
  for (const key of Object.keys(patch.tools ?? {})) {
504
- const rawPiece = patch.tools![key as keyof NonNullable<ConfigPatch['tools']>];
1031
+ const rawPiece =
1032
+ patch.tools![key as keyof NonNullable<ConfigPatch['tools']>];
505
1033
  if (!rawPiece) continue;
506
- const previous = (drafts.tools ?? {})[key];
507
- const merged = mergeRecord(previous, rawPiece as Record<string, unknown>);
1034
+ const previous = drafts.tools?.[key];
1035
+ const merged = mergeRecord(
1036
+ previous,
1037
+ rawPiece as Record<string, unknown>,
1038
+ );
508
1039
  try {
509
- active = { version: 4, ...applyConfigPatch(active, { tools: { [key]: merged } as ConfigPatch['tools'] }), savedAt: active.savedAt };
510
- delete drafts.tools?.[key];
1040
+ active = {
1041
+ version: 4,
1042
+ ...applyConfigPatch(active, {
1043
+ tools: { [key]: merged } as ConfigPatch['tools'],
1044
+ }),
1045
+ savedAt: active.savedAt,
1046
+ };
1047
+ drafts.tools = omitKey(drafts.tools, key);
511
1048
  } catch {
512
1049
  state = 'draft';
513
1050
  draftFields.push(key);
514
- (drafts.tools ??= {})[key] = merged;
1051
+ const toolDrafts = drafts.tools ?? {};
1052
+ toolDrafts[key] = merged;
1053
+ drafts.tools = toolDrafts;
515
1054
  }
516
1055
  }
517
- if (!hasEntries(drafts.tools)) delete drafts.tools;
1056
+ if (!hasEntries(drafts.tools)) drafts.tools = undefined;
518
1057
  return {
519
- models: active.models, baseModelId: active.baseModelId,
520
- ...(active.compactRatio !== undefined ? { compactRatio: active.compactRatio } : {}),
521
- advisor: active.advisor, subagents: active.subagents, browser: active.browser,
522
- computerUse: active.computerUse, tools: active.tools,
1058
+ models: active.models,
1059
+ baseModelId: active.baseModelId,
1060
+ ...(active.compactRatio !== undefined
1061
+ ? { compactRatio: active.compactRatio }
1062
+ : {}),
1063
+ advisor: active.advisor,
1064
+ subagents: active.subagents,
1065
+ browser: active.browser,
1066
+ computerUse: active.computerUse,
1067
+ tools: active.tools,
523
1068
  ...(hasEntries(drafts) ? { drafts } : {}),
524
1069
  };
525
1070
  });
@@ -530,14 +1075,22 @@ export class ConfigService {
530
1075
  export const configService = new ConfigService();
531
1076
 
532
1077
  export const SENSITIVE_ENV_KEYS = new Set([
533
- 'MINIMAL_AGENT_API_KEY', 'TAVILY_API_KEY', 'OPENAI_IMAGE_API_KEY',
534
- 'OPENROUTER_API_KEY', 'MINIMAX_API_KEY', 'MINIMAX_GROUP_ID',
1078
+ 'MINIMAL_AGENT_API_KEY',
1079
+ 'TAVILY_API_KEY',
1080
+ 'OPENAI_IMAGE_API_KEY',
1081
+ 'OPENROUTER_API_KEY',
1082
+ 'MINIMAX_API_KEY',
1083
+ 'MINIMAX_GROUP_ID',
535
1084
  ]);
536
1085
 
537
1086
  /** Child processes inherit normal runtime variables, never model or tool credentials. */
538
- export function sanitizeCredentialEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
539
- const env: NodeJS.ProcessEnv = { ...source };
540
- for (const key of SENSITIVE_ENV_KEYS) delete env[key];
1087
+ export function sanitizeCredentialEnv(
1088
+ source: NodeJS.ProcessEnv = process.env,
1089
+ ): NodeJS.ProcessEnv {
1090
+ let env: NodeJS.ProcessEnv = { ...source };
1091
+ for (const key of SENSITIVE_ENV_KEYS) {
1092
+ env = omitKey(env, key) ?? {};
1093
+ }
541
1094
  return env;
542
1095
  }
543
1096
 
@@ -550,28 +1103,33 @@ export async function trustedToolEnv(
550
1103
  let cfg = await service.tool('imageGeneration');
551
1104
  if (!cfg) {
552
1105
  const openRouter = await service.tool('openRouter');
553
- if (openRouter) cfg = {
554
- provider: 'openrouter',
555
- baseURL: 'https://openrouter.ai/api/v1',
556
- apiKey: openRouter.apiKey,
557
- model: 'google/gemini-3-pro-image-preview',
558
- protocol: 'chat',
559
- };
1106
+ if (openRouter)
1107
+ cfg = {
1108
+ provider: 'openrouter',
1109
+ baseURL: 'https://openrouter.ai/api/v1',
1110
+ apiKey: openRouter.apiKey,
1111
+ model: 'google/gemini-3-pro-image-preview',
1112
+ protocol: 'chat',
1113
+ };
560
1114
  }
561
1115
  if (cfg) {
562
1116
  env.OPENAI_IMAGE_PROVIDER = cfg.provider;
563
1117
  env.OPENAI_IMAGE_BASE_URL = cfg.baseURL;
564
1118
  env.OPENAI_IMAGE_API_KEY = cfg.apiKey;
565
1119
  env.OPENAI_IMAGE_MODEL = cfg.model;
566
- if (cfg.protocol && cfg.protocol !== 'auto') env.OPENAI_IMAGE_PROTOCOL = cfg.protocol;
567
- else delete env.OPENAI_IMAGE_PROTOCOL;
1120
+ if (cfg.protocol && cfg.protocol !== 'auto')
1121
+ env.OPENAI_IMAGE_PROTOCOL = cfg.protocol;
1122
+ else env.OPENAI_IMAGE_PROTOCOL = undefined;
568
1123
  }
569
1124
  } else if (kind === 'openRouter') {
570
1125
  const cfg = await service.tool('openRouter');
571
1126
  if (cfg) env.OPENROUTER_API_KEY = cfg.apiKey;
572
1127
  } else {
573
1128
  const cfg = await service.tool('textToSpeech');
574
- if (cfg) { env.MINIMAX_API_KEY = cfg.apiKey; env.MINIMAX_GROUP_ID = cfg.groupId; }
1129
+ if (cfg) {
1130
+ env.MINIMAX_API_KEY = cfg.apiKey;
1131
+ env.MINIMAX_GROUP_ID = cfg.groupId;
1132
+ }
575
1133
  }
576
1134
  return env;
577
1135
  }