minimal-agent 0.12.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 (472) hide show
  1. package/README.md +6 -5
  2. package/package.json +62 -5
  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 +204 -185
  16. package/plugins/workflow-runner/src/index.ts +249 -224
  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 +404 -467
  28. package/plugins/workflow-runner/src/runner.ts +491 -535
  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 +172 -152
  54. package/plugins/workflow-runner/src/taskIdentity.ts +233 -200
  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 -105
  58. package/plugins/workflow-runner/src/types.ts +155 -520
  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 +542 -469
  68. package/plugins/workflow-runner/src/workflowState.ts +639 -589
  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/skill-creator/scripts/run_eval.py +3 -2
  92. package/skills/ui-design/SKILL.md +5 -2
  93. package/skills/ui-design/references/layout-composition.md +1 -1
  94. package/skills/workflow-creator/SKILL.md +344 -0
  95. package/skills/workflow-creator/assets/template/workflow.mjs +48 -0
  96. package/skills/workflow-creator/references/api.md +216 -0
  97. package/skills/workflow-creator/references/migration.md +103 -0
  98. package/skills/workflow-creator/references/patterns.md +779 -0
  99. package/skills/workflow-creator/scripts/list-resources.mjs +205 -0
  100. package/skills/workflow-creator/scripts/scaffold.mjs +148 -0
  101. package/skills/workflow-creator/scripts/validate.mjs +275 -0
  102. package/skills/workflow-creator/scripts/workflow-creator.test.mjs +82 -0
  103. package/src/advisor/advisorCoordinator.js +503 -0
  104. package/src/advisor/advisorCoordinator.ts +657 -0
  105. package/src/cli/configMenu.tsx +0 -1
  106. package/src/cli/print.js +44 -6
  107. package/src/cli/print.ts +73 -4
  108. package/src/cli/streamJson.js +5 -6
  109. package/src/cli/streamJson.ts +8 -4
  110. package/src/cli/wizards/advisorWizard.js +4 -2
  111. package/src/cli/wizards/advisorWizard.tsx +8 -2
  112. package/src/cli/wizards/contextAdvisorWizard.js +5 -4
  113. package/src/cli/wizards/contextAdvisorWizard.tsx +7 -6
  114. package/src/collaboration/taskFlow.js +115 -0
  115. package/src/collaboration/taskFlow.ts +169 -0
  116. package/src/config/configDefaults.js +41 -1
  117. package/src/config/configDefaults.ts +43 -1
  118. package/src/config/configDiagnostics.ts +2 -2
  119. package/src/config/configFile.js +40 -9
  120. package/src/config/configFile.ts +60 -11
  121. package/src/config/configService.js +443 -74
  122. package/src/config/configService.ts +723 -142
  123. package/src/config/numericLimits.js +22 -0
  124. package/src/config/numericLimits.ts +24 -0
  125. package/src/config.js +1 -0
  126. package/src/config.ts +3 -2
  127. package/src/context/budgetPlan.js +1 -1
  128. package/src/context/budgetPlan.ts +1 -1
  129. package/src/context/compact.js +7 -2
  130. package/src/context/compact.ts +9 -2
  131. package/src/context/microCompactLite.js +212 -13
  132. package/src/context/microCompactLite.ts +261 -12
  133. package/src/context/sessionRegistry.js +8 -1
  134. package/src/context/sessionRegistry.ts +18 -5
  135. package/src/context/toolResultArchive.js +72 -0
  136. package/src/context/toolResultArchive.ts +95 -0
  137. package/src/goal/errors.js +8 -0
  138. package/src/goal/errors.ts +18 -0
  139. package/src/goal/goalRuntime.js +344 -0
  140. package/src/goal/goalRuntime.ts +466 -0
  141. package/src/goal/goalStore.js +200 -0
  142. package/src/goal/goalStore.ts +219 -0
  143. package/src/goal/index.js +4 -0
  144. package/src/goal/index.ts +4 -0
  145. package/src/goal/statusMapping.js +71 -0
  146. package/src/goal/statusMapping.ts +95 -0
  147. package/src/goal/types.js +7 -0
  148. package/src/goal/types.ts +179 -0
  149. package/src/goal/workflowController.js +306 -0
  150. package/src/goal/workflowController.ts +360 -0
  151. package/src/harness/evidenceLedger.js +2 -0
  152. package/src/harness/evidenceLedger.ts +1 -0
  153. package/src/harness/stallGuard.js +9 -3
  154. package/src/harness/stallGuard.ts +7 -3
  155. package/src/images/imageResolver.js +161 -0
  156. package/src/images/imageResolver.ts +189 -0
  157. package/src/infra/verification.js +56 -20
  158. package/src/infra/verification.ts +87 -20
  159. package/src/llm/client.js +79 -21
  160. package/src/llm/client.ts +182 -35
  161. package/src/llm/gatewayClient.js +201 -0
  162. package/src/llm/gatewayClient.ts +299 -0
  163. package/src/llm/messageProvenance.js +46 -0
  164. package/src/llm/messageProvenance.ts +58 -0
  165. package/src/llm/modelProvider.js +1 -0
  166. package/src/llm/modelProvider.ts +22 -0
  167. package/src/llm/providers/anthropicFormat.js +108 -1
  168. package/src/llm/providers/anthropicFormat.ts +115 -4
  169. package/src/llm/providers/openaiFormat.js +9 -0
  170. package/src/llm/providers/openaiFormat.ts +12 -3
  171. package/src/loop.js +319 -54
  172. package/src/loop.ts +373 -57
  173. package/src/main.js +4 -1
  174. package/src/main.tsx +4 -1
  175. package/src/managedExecution/index.js +2 -0
  176. package/src/managedExecution/index.ts +2 -0
  177. package/src/managedExecution/registry.js +15 -0
  178. package/src/managedExecution/registry.ts +20 -0
  179. package/src/managedExecution/types.js +1 -0
  180. package/src/managedExecution/types.ts +86 -0
  181. package/src/modelRef.js +9 -0
  182. package/src/modelRef.ts +31 -0
  183. package/src/multimodal.js +14 -28
  184. package/src/multimodal.ts +17 -33
  185. package/src/net/lanDiscovery.js +65 -0
  186. package/src/net/lanDiscovery.ts +82 -0
  187. package/src/plugin-sdk.js +28 -3
  188. package/src/plugin-sdk.ts +39 -6
  189. package/src/plugins/pluginApi.ts +16 -1
  190. package/src/plugins/pluginLoader.js +4 -0
  191. package/src/plugins/pluginLoader.ts +6 -0
  192. package/src/plugins/pluginRunner.js +152 -9
  193. package/src/plugins/pluginRunner.ts +186 -8
  194. package/src/plugins/slashFallback.js +113 -0
  195. package/src/plugins/slashFallback.ts +137 -0
  196. package/src/prompts/policyRegistry.js +15 -8
  197. package/src/prompts/policyRegistry.ts +18 -8
  198. package/src/prompts/promptAssembly.js +103 -12
  199. package/src/prompts/promptAssembly.ts +114 -12
  200. package/src/prompts/skillList.js +21 -3
  201. package/src/prompts/skillList.ts +27 -2
  202. package/src/prompts/stableSystemPrompt.js +18 -0
  203. package/src/prompts/stableSystemPrompt.ts +33 -0
  204. package/src/runtime/agentRuntimeContext.js +8 -2
  205. package/src/runtime/agentRuntimeContext.ts +17 -4
  206. package/src/session/runAgentTurn.js +78 -12
  207. package/src/session/runAgentTurn.ts +105 -23
  208. package/src/session/runTurn.js +10 -9
  209. package/src/session/runTurn.ts +17 -8
  210. package/src/session/sessionState.ts +2 -0
  211. package/src/team/auth.js +186 -0
  212. package/src/team/auth.ts +298 -0
  213. package/src/team/cli.js +105 -0
  214. package/src/team/cli.ts +122 -0
  215. package/src/team/onlineToolContract.js +64 -0
  216. package/src/team/onlineToolContract.ts +109 -0
  217. package/src/team/toolPolicy.js +36 -0
  218. package/src/team/toolPolicy.ts +75 -0
  219. package/src/tools/advisor/advisor.js +148 -37
  220. package/src/tools/advisor/advisor.ts +161 -38
  221. package/src/tools/advisor/advisorDecision.js +71 -0
  222. package/src/tools/advisor/advisorDecision.ts +85 -0
  223. package/src/tools/agent/agent.js +18 -5
  224. package/src/tools/agent/agent.ts +21 -7
  225. package/src/tools/agent/agentControl.js +246 -0
  226. package/src/tools/agent/agentControl.ts +284 -0
  227. package/src/tools/agent/agentTaskService.js +40 -1
  228. package/src/tools/agent/agentTaskService.ts +44 -1
  229. package/src/tools/agent/agentTypes.js +14 -2
  230. package/src/tools/agent/agentTypes.ts +15 -2
  231. package/src/tools/applypatch/applyPatch.js +362 -0
  232. package/src/tools/applypatch/applyPatch.ts +428 -0
  233. package/src/tools/bash/bash.js +70 -18
  234. package/src/tools/bash/bash.ts +75 -20
  235. package/src/tools/capabilities.js +10 -5
  236. package/src/tools/capabilities.ts +10 -5
  237. package/src/tools/computeruse/computerIntent.js +32 -0
  238. package/src/tools/computeruse/computerIntent.ts +46 -0
  239. package/src/tools/computeruse/telemetryLog.js +12 -0
  240. package/src/tools/computeruse/telemetryLog.ts +16 -0
  241. package/src/tools/computeruse/vision/telemetry.js +1 -0
  242. package/src/tools/computeruse/vision/telemetry.ts +1 -0
  243. package/src/tools/effectiveToolSet.js +4 -0
  244. package/src/tools/effectiveToolSet.ts +4 -0
  245. package/src/tools/index.js +21 -2
  246. package/src/tools/index.ts +33 -2
  247. package/src/tools/powershell/powershell.js +18 -1
  248. package/src/tools/powershell/powershell.ts +18 -2
  249. package/src/tools/presentresult/presentresult.js +6 -15
  250. package/src/tools/presentresult/presentresult.ts +6 -15
  251. package/src/tools/read/read.js +69 -11
  252. package/src/tools/read/read.ts +70 -11
  253. package/src/tools/shared/fileUtils.js +1 -2
  254. package/src/tools/shared/fileUtils.ts +1 -2
  255. package/src/tools/shared/permanentDeleteGuard.js +34 -3
  256. package/src/tools/shared/permanentDeleteGuard.ts +43 -6
  257. package/src/tools/shared/toolOutputStore.js +23 -1
  258. package/src/tools/shared/toolOutputStore.ts +22 -1
  259. package/src/tools/skill/skill.js +9 -3
  260. package/src/tools/skill/skill.ts +8 -4
  261. package/src/tools/toolPromptGuides.js +117 -0
  262. package/src/tools/toolPromptGuides.ts +137 -0
  263. package/src/tools/webbrowser/agentBrowserPath.js +19 -9
  264. package/src/tools/webbrowser/agentBrowserPath.ts +70 -28
  265. package/src/tools/webbrowser/browser.js +27 -11
  266. package/src/tools/webbrowser/browser.ts +68 -24
  267. package/src/tools/webbrowser/browserAutomation.js +329 -0
  268. package/src/tools/webbrowser/browserAutomation.ts +467 -0
  269. package/src/tools/webbrowser/browserAxState.js +97 -0
  270. package/src/tools/webbrowser/browserAxState.ts +142 -0
  271. package/src/tools/webbrowser/browserBatch.js +1578 -0
  272. package/src/tools/webbrowser/browserBatch.ts +2382 -0
  273. package/src/tools/webbrowser/browserCdp.js +208 -0
  274. package/src/tools/webbrowser/browserCdp.ts +255 -0
  275. package/src/tools/webbrowser/browserConditions.js +24 -0
  276. package/src/tools/webbrowser/browserConditions.ts +45 -0
  277. package/src/tools/webbrowser/browserContent.js +281 -0
  278. package/src/tools/webbrowser/browserContent.ts +438 -0
  279. package/src/tools/webbrowser/browserControl.js +131 -30
  280. package/src/tools/webbrowser/browserControl.ts +210 -50
  281. package/src/tools/webbrowser/browserDaemon.js +22 -14
  282. package/src/tools/webbrowser/browserDaemon.ts +54 -21
  283. package/src/tools/webbrowser/browserFrames.js +47 -0
  284. package/src/tools/webbrowser/browserFrames.ts +54 -0
  285. package/src/tools/webbrowser/browserInteractionResolver.js +17 -0
  286. package/src/tools/webbrowser/browserInteractionResolver.ts +40 -0
  287. package/src/tools/webbrowser/browserLocator.js +140 -0
  288. package/src/tools/webbrowser/browserLocator.ts +194 -0
  289. package/src/tools/webbrowser/browserOutcome.js +10 -17
  290. package/src/tools/webbrowser/browserOutcome.ts +48 -31
  291. package/src/tools/webbrowser/browserRecovery.js +58 -0
  292. package/src/tools/webbrowser/browserRecovery.ts +83 -0
  293. package/src/tools/webbrowser/browserRunRegistry.js +35 -3
  294. package/src/tools/webbrowser/browserRunRegistry.ts +91 -15
  295. package/src/tools/webbrowser/browserService.js +28 -2
  296. package/src/tools/webbrowser/browserService.ts +49 -9
  297. package/src/tools/webbrowser/browserStream.js +132 -33
  298. package/src/tools/webbrowser/browserStream.ts +224 -50
  299. package/src/tools/webbrowser/browserTabs.js +48 -15
  300. package/src/tools/webbrowser/browserTabs.ts +178 -96
  301. package/src/tools/webbrowser/webbrowser.js +683 -144
  302. package/src/tools/webbrowser/webbrowser.ts +979 -236
  303. package/src/tools/webfetch/preapproved.js +0 -2
  304. package/src/tools/webfetch/preapproved.ts +0 -2
  305. package/src/tools/websearch/websearch.js +129 -8
  306. package/src/tools/websearch/websearch.ts +138 -6
  307. package/src/types.ts +450 -42
  308. package/src/ui/App.js +377 -58
  309. package/src/ui/App.tsx +576 -111
  310. package/src/ui/LiveArea.js +14 -8
  311. package/src/ui/LiveArea.tsx +14 -10
  312. package/src/ui/MessageList.js +63 -20
  313. package/src/ui/MessageList.tsx +101 -16
  314. package/src/ui/ModelPicker.js +7 -4
  315. package/src/ui/ModelPicker.tsx +55 -8
  316. package/src/ui/hooks/useChat.js +152 -163
  317. package/src/ui/hooks/useChat.ts +396 -334
  318. package/src/ui/loginCommand.js +56 -0
  319. package/src/ui/loginCommand.ts +63 -0
  320. package/src/ui/toolProgress.js +41 -0
  321. package/src/ui/toolProgress.ts +44 -0
  322. package/src/web/browserBridge.js +20 -2
  323. package/src/web/browserBridge.ts +20 -2
  324. package/src/web/protocol.ts +1897 -194
  325. package/workflows/adaptive-research/workflow.mjs +86 -0
  326. package/workflows/e2e-capabilities/workflow.mjs +102 -0
  327. package/workflows/e2e-llm/workflow.mjs +52 -0
  328. package/workflows/e2e-smoke/workflow.mjs +59 -0
  329. package/workflows/e2e-vision-schema/README.md +48 -0
  330. package/workflows/e2e-vision-schema/workflow.mjs +149 -0
  331. package/workflows/e2e-write-greet/workflow.mjs +37 -0
  332. package/workflows/file-census/README.md +14 -0
  333. package/workflows/file-census/scripts/.gitkeep +2 -0
  334. package/workflows/file-census/workflow.mjs +110 -0
  335. package/workflows/goal/README.md +166 -0
  336. package/workflows/goal/prompts/audit.md +57 -0
  337. package/workflows/goal/prompts/contract.md +56 -0
  338. package/workflows/goal/prompts/iterate.md +37 -0
  339. package/workflows/goal/workflow.mjs +501 -0
  340. package/workflows/illustrate/README.md +90 -0
  341. package/workflows/illustrate/assets/engines.json +20 -0
  342. package/workflows/illustrate/assets/extract-baidu.js +21 -0
  343. package/workflows/illustrate/assets/extract-bing.js +33 -0
  344. package/workflows/illustrate/assets/extract-google.js +31 -0
  345. package/workflows/illustrate/workflow.mjs +301 -0
  346. package/workflows/plan/workflow.mjs +389 -0
  347. package/workflows/question-crops/README.md +35 -0
  348. package/workflows/question-crops/prompts/inventory.md +23 -0
  349. package/workflows/question-crops/scripts/image-ops.mjs +305 -0
  350. package/workflows/question-crops/scripts/image-ops.test.mjs +95 -0
  351. package/workflows/question-crops/workflow.mjs +172 -0
  352. package/workflows/ralph-loop/workflow.mjs +162 -0
  353. package/workflows/research/DESIGN.md +449 -0
  354. package/workflows/research/README.md +74 -0
  355. package/workflows/research/prompts/s0-goal.md +43 -0
  356. package/workflows/research/prompts/s1-early-exit.md +22 -0
  357. package/workflows/research/prompts/s1-map.md +58 -0
  358. package/workflows/research/prompts/s2-options.md +53 -0
  359. package/workflows/research/prompts/s3-deep-dive.md +56 -0
  360. package/workflows/research/prompts/s4-verify.md +74 -0
  361. package/workflows/research/prompts/s5-synthesis.md +81 -0
  362. package/workflows/research/prompts/s5b-fill-gaps.md +32 -0
  363. package/workflows/research/prompts/s6-package.md +32 -0
  364. package/workflows/research/scripts/build-report.mjs +98 -0
  365. package/workflows/research/workflow.mjs +288 -0
  366. package/workflows/wechat-reading-shots/prompts/collect-params.md +16 -0
  367. package/workflows/wechat-reading-shots/prompts/evaluate.md +20 -0
  368. package/workflows/wechat-reading-shots/prompts/open-book.md +28 -0
  369. package/workflows/wechat-reading-shots/prompts/read-page.md +13 -0
  370. package/workflows/wechat-reading-shots/prompts/turn-batch.md +24 -0
  371. package/workflows/wechat-reading-shots/test/control-flow.test.ts +329 -0
  372. package/workflows/wechat-reading-shots/workflow.mjs +266 -0
  373. package/workflows/wrong-question-docx/README.md +55 -0
  374. package/workflows/wrong-question-docx/prompts/figure-qa.md +8 -0
  375. package/workflows/wrong-question-docx/prompts/transcribe.md +25 -0
  376. package/workflows/wrong-question-docx/scripts/workbook.mjs +857 -0
  377. package/workflows/wrong-question-docx/test/control-flow.test.ts +165 -0
  378. package/workflows/wrong-question-docx/test/workbook.test.mjs +303 -0
  379. package/workflows/wrong-question-docx/workflow.mjs +485 -0
  380. package/plugins/workflow-runner/src/agentCommon.js +0 -354
  381. package/plugins/workflow-runner/src/agentCommon.ts +0 -435
  382. package/plugins/workflow-runner/src/controlFlow/branch.js +0 -40
  383. package/plugins/workflow-runner/src/controlFlow/branch.ts +0 -46
  384. package/plugins/workflow-runner/src/controlFlow/loop.js +0 -211
  385. package/plugins/workflow-runner/src/controlFlow/loop.ts +0 -248
  386. package/plugins/workflow-runner/src/controlFlow/pause.js +0 -129
  387. package/plugins/workflow-runner/src/controlFlow/pause.ts +0 -141
  388. package/plugins/workflow-runner/src/controlFlow/signal.js +0 -21
  389. package/plugins/workflow-runner/src/controlFlow/signal.ts +0 -29
  390. package/plugins/workflow-runner/src/controlFlow/types.js +0 -18
  391. package/plugins/workflow-runner/src/controlFlow/types.ts +0 -63
  392. package/plugins/workflow-runner/src/dslManifest.js +0 -72
  393. package/plugins/workflow-runner/src/dslManifest.ts +0 -127
  394. package/plugins/workflow-runner/src/expressions.js +0 -618
  395. package/plugins/workflow-runner/src/expressions.ts +0 -622
  396. package/plugins/workflow-runner/src/loader.js +0 -573
  397. package/plugins/workflow-runner/src/loader.ts +0 -721
  398. package/plugins/workflow-runner/src/multimodal.js +0 -38
  399. package/plugins/workflow-runner/src/multimodal.ts +0 -64
  400. package/plugins/workflow-runner/src/recovery.js +0 -119
  401. package/plugins/workflow-runner/src/recovery.ts +0 -149
  402. package/plugins/workflow-runner/src/stepExecutors/agent.js +0 -173
  403. package/plugins/workflow-runner/src/stepExecutors/agent.ts +0 -234
  404. package/plugins/workflow-runner/src/stepExecutors/assert.js +0 -41
  405. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -50
  406. package/plugins/workflow-runner/src/stepExecutors/echo.js +0 -31
  407. package/plugins/workflow-runner/src/stepExecutors/echo.ts +0 -38
  408. package/plugins/workflow-runner/src/stepExecutors/llm.js +0 -99
  409. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -133
  410. package/plugins/workflow-runner/src/stepExecutors/set.js +0 -49
  411. package/plugins/workflow-runner/src/stepExecutors/set.ts +0 -56
  412. package/plugins/workflow-runner/src/stepExecutors/skill.js +0 -111
  413. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -140
  414. package/plugins/workflow-runner/src/stepExecutors/tool.js +0 -208
  415. package/plugins/workflow-runner/src/stepExecutors/tool.ts +0 -256
  416. package/plugins/workflow-runner/src/stepExecutors/verify.js +0 -58
  417. package/plugins/workflow-runner/src/stepExecutors/verify.ts +0 -68
  418. package/plugins/workflow-runner/src/stepPipeline.js +0 -277
  419. package/plugins/workflow-runner/src/stepPipeline.ts +0 -366
  420. package/plugins/workflow-runner/test/agent.test.ts +0 -587
  421. package/plugins/workflow-runner/test/agentLlmError.test.ts +0 -373
  422. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +0 -65
  423. package/plugins/workflow-runner/test/captureMisbind.test.ts +0 -99
  424. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -147
  425. package/plugins/workflow-runner/test/context-files.test.ts +0 -320
  426. package/plugins/workflow-runner/test/e2e.test.ts +0 -370
  427. package/plugins/workflow-runner/test/engineRobustness.test.ts +0 -226
  428. package/plugins/workflow-runner/test/expressions.test.ts +0 -397
  429. package/plugins/workflow-runner/test/foundation.test.ts +0 -126
  430. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  431. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +0 -47
  432. package/plugins/workflow-runner/test/hostCapability.test.ts +0 -108
  433. package/plugins/workflow-runner/test/illustrate.load.test.ts +0 -183
  434. package/plugins/workflow-runner/test/jsonPayload.test.ts +0 -83
  435. package/plugins/workflow-runner/test/loader.test.ts +0 -617
  436. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +0 -33
  437. package/plugins/workflow-runner/test/multimodal.test.ts +0 -342
  438. package/plugins/workflow-runner/test/output-schema.test.ts +0 -213
  439. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +0 -87
  440. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -269
  441. package/plugins/workflow-runner/test/ppt.load.test.ts +0 -1220
  442. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +0 -220
  443. package/plugins/workflow-runner/test/recovery.test.ts +0 -265
  444. package/plugins/workflow-runner/test/runner.test.ts +0 -911
  445. package/plugins/workflow-runner/test/runnerPreflight.test.ts +0 -379
  446. package/plugins/workflow-runner/test/skillTurnDone.test.ts +0 -71
  447. package/plugins/workflow-runner/test/stateLifecycle.test.ts +0 -150
  448. package/plugins/workflow-runner/test/stepPipeline.test.ts +0 -114
  449. package/plugins/workflow-runner/test/taskArbitration.test.ts +0 -338
  450. package/plugins/workflow-runner/test/taskLifecycle.test.ts +0 -270
  451. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +0 -221
  452. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +0 -29
  453. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +0 -168
  454. package/plugins/workflow-runner/test/webppt.load.test.ts +0 -250
  455. package/plugins/workflow-runner/test/word.load.test.ts +0 -208
  456. package/plugins/workflow-runner/test/workflows.smoke.test.ts +0 -76
  457. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +0 -207
  458. package/src/advisorState.js +0 -22
  459. package/src/cli/configWizard.js +0 -293
  460. package/src/tools/toolProgressBridge.js +0 -48
  461. package/workflows/adaptive-research.yaml +0 -50
  462. package/workflows/book-review-short.yaml +0 -125
  463. package/workflows/e2e-write-greet.yaml +0 -31
  464. package/workflows/illustrate.yaml +0 -227
  465. package/workflows/plan.yaml +0 -470
  466. package/workflows/ppt.yaml +0 -1670
  467. package/workflows/ralph-loop.yaml +0 -199
  468. package/workflows/schema.json +0 -176
  469. package/workflows/webppt.yaml +0 -496
  470. package/workflows/wechat-reading-shots.yaml +0 -315
  471. package/workflows/word.yaml +0 -720
  472. package/workflows/youtube-shorts.yaml +0 -202
@@ -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,71 @@ 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
+ };
909
+ }
910
+
911
+ /**
912
+ * 同步版 advisor 可用性探测——仅用于 resolveAvailability 这类工具列表探测场景。
913
+ * 读当前内存快照(可能略滞后于磁盘最新值),但调用频率高、对实时性要求不高,
914
+ * 下一轮 LLM 请求前 configService.reload() 会刷新。
915
+ *
916
+ * 为什么不用 advisor() 的 fresh reload:
917
+ * resolveAvailability 是在主循环构造工具集时同步调用的,不能 await;
918
+ * 而且每次工具集构造都读盘的话开销太大(主循环每轮都可能构造一次)。
919
+ */
920
+ advisorAvailabilitySync(): {
921
+ enabled: boolean;
922
+ mode: 'auto' | 'manual';
923
+ modelId: string | null;
924
+ configured: boolean;
925
+ } {
926
+ const config = this.snapshot.config;
927
+ const advisor = config?.advisor;
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
+ }
938
+ const entry = config?.models?.find((m) => m.id === advisor.modelId);
939
+ if (!entry?.apiKey)
940
+ return { enabled: false, mode: 'auto', modelId: null, configured: false };
941
+ return {
942
+ enabled: advisor.enabled !== false,
943
+ mode: advisor.mode === 'manual' ? 'manual' : 'auto',
944
+ modelId: advisor.modelId,
945
+ configured: true,
946
+ };
432
947
  }
433
948
 
434
949
  async subagents(): Promise<SubagentsConfig> {
@@ -440,7 +955,9 @@ export class ConfigService {
440
955
  }
441
956
 
442
957
  async computerUse(): Promise<ComputerUseConfig> {
443
- return (await this.reload()).config?.computerUse ?? DEFAULT_COMPUTER_USE_CONFIG;
958
+ return (
959
+ (await this.reload()).config?.computerUse ?? DEFAULT_COMPUTER_USE_CONFIG
960
+ );
444
961
  }
445
962
 
446
963
  async tool<K extends ToolConfigKey>(key: K): Promise<ToolConfigs[K]> {
@@ -450,53 +967,104 @@ export class ConfigService {
450
967
  async toolStatus(): Promise<Record<ToolConfigKey, ToolStatus>> {
451
968
  const tools = (await this.reload()).config?.tools ?? {};
452
969
  return {
453
- webSearch: { state: tools.webSearch ? 'configured' : 'unconfigured', configured: !!tools.webSearch },
454
- imageGeneration: { state: tools.imageGeneration ? 'configured' : 'unconfigured', configured: !!tools.imageGeneration },
455
- openRouter: { state: tools.openRouter ? 'configured' : 'unconfigured', configured: !!tools.openRouter },
456
- 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
+ },
457
986
  };
458
987
  }
459
988
 
460
989
  async savePatch(patch: ConfigPatch): Promise<ConfigSnapshot> {
461
990
  const current = await this.getSnapshot();
462
- if (current.readOnly) throw new Error('当前使用环境变量只读配置,不能从设置页修改');
463
- 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
+ );
464
996
  return this.reload();
465
997
  }
466
998
 
467
999
  /** 现在只服务 tools 分区的实时录入表单(imageGeneration/textToSpeech 多字段、可能中途非法)。 */
468
- 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
+ }> {
469
1008
  void section;
470
1009
  const current = await this.getSnapshot();
471
- if (current.readOnly) throw new Error('当前使用环境变量只读配置,不能从设置页修改');
1010
+ if (current.readOnly)
1011
+ throw new Error('当前使用环境变量只读配置,不能从设置页修改');
472
1012
  if (!current.config) throw new Error('请先添加至少一个模型');
473
1013
  let state: AutosaveState = 'applied';
474
1014
  const draftFields: string[] = [];
475
1015
  await updateMinimalAgentConfig((latest) => {
476
1016
  let active = latest ?? current.config!;
477
- if (patch.subagents) active = { version: 4, ...applyConfigPatch(active, { subagents: patch.subagents }), savedAt: active.savedAt };
478
- 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
+ };
479
1029
  const drafts = structuredClone(active.drafts ?? {});
480
1030
  for (const key of Object.keys(patch.tools ?? {})) {
481
- const rawPiece = patch.tools![key as keyof NonNullable<ConfigPatch['tools']>];
1031
+ const rawPiece =
1032
+ patch.tools![key as keyof NonNullable<ConfigPatch['tools']>];
482
1033
  if (!rawPiece) continue;
483
- const previous = (drafts.tools ?? {})[key];
484
- 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
+ );
485
1039
  try {
486
- active = { version: 4, ...applyConfigPatch(active, { tools: { [key]: merged } as ConfigPatch['tools'] }), savedAt: active.savedAt };
487
- 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);
488
1048
  } catch {
489
1049
  state = 'draft';
490
1050
  draftFields.push(key);
491
- (drafts.tools ??= {})[key] = merged;
1051
+ const toolDrafts = drafts.tools ?? {};
1052
+ toolDrafts[key] = merged;
1053
+ drafts.tools = toolDrafts;
492
1054
  }
493
1055
  }
494
- if (!hasEntries(drafts.tools)) delete drafts.tools;
1056
+ if (!hasEntries(drafts.tools)) drafts.tools = undefined;
495
1057
  return {
496
- models: active.models, baseModelId: active.baseModelId,
497
- ...(active.compactRatio !== undefined ? { compactRatio: active.compactRatio } : {}),
498
- advisor: active.advisor, subagents: active.subagents, browser: active.browser,
499
- 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,
500
1068
  ...(hasEntries(drafts) ? { drafts } : {}),
501
1069
  };
502
1070
  });
@@ -507,14 +1075,22 @@ export class ConfigService {
507
1075
  export const configService = new ConfigService();
508
1076
 
509
1077
  export const SENSITIVE_ENV_KEYS = new Set([
510
- 'MINIMAL_AGENT_API_KEY', 'TAVILY_API_KEY', 'OPENAI_IMAGE_API_KEY',
511
- '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',
512
1084
  ]);
513
1085
 
514
1086
  /** Child processes inherit normal runtime variables, never model or tool credentials. */
515
- export function sanitizeCredentialEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
516
- const env: NodeJS.ProcessEnv = { ...source };
517
- 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
+ }
518
1094
  return env;
519
1095
  }
520
1096
 
@@ -527,28 +1103,33 @@ export async function trustedToolEnv(
527
1103
  let cfg = await service.tool('imageGeneration');
528
1104
  if (!cfg) {
529
1105
  const openRouter = await service.tool('openRouter');
530
- if (openRouter) cfg = {
531
- provider: 'openrouter',
532
- baseURL: 'https://openrouter.ai/api/v1',
533
- apiKey: openRouter.apiKey,
534
- model: 'google/gemini-3-pro-image-preview',
535
- protocol: 'chat',
536
- };
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
+ };
537
1114
  }
538
1115
  if (cfg) {
539
1116
  env.OPENAI_IMAGE_PROVIDER = cfg.provider;
540
1117
  env.OPENAI_IMAGE_BASE_URL = cfg.baseURL;
541
1118
  env.OPENAI_IMAGE_API_KEY = cfg.apiKey;
542
1119
  env.OPENAI_IMAGE_MODEL = cfg.model;
543
- if (cfg.protocol && cfg.protocol !== 'auto') env.OPENAI_IMAGE_PROTOCOL = cfg.protocol;
544
- 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;
545
1123
  }
546
1124
  } else if (kind === 'openRouter') {
547
1125
  const cfg = await service.tool('openRouter');
548
1126
  if (cfg) env.OPENROUTER_API_KEY = cfg.apiKey;
549
1127
  } else {
550
1128
  const cfg = await service.tool('textToSpeech');
551
- 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
+ }
552
1133
  }
553
1134
  return env;
554
1135
  }