minimal-agent 0.10.0 → 0.12.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 (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
package/src/loop.js CHANGED
@@ -31,26 +31,84 @@ import { autoCompactIfNeeded, getCompactThreshold } from './context/compact.js';
31
31
  import { microCompact, incrementTurn, expireOldEntries } from './context/microCompactLite.js';
32
32
  import { isPromptTooLongError, reactiveCompactIfApplicable, } from './context/reactiveCompact.js';
33
33
  import { snipCompactIfNeeded } from './context/snipCompact.js';
34
- import { countMessagesTokens, estimateToolsTokens } from './context/tokenCounter.js';
34
+ import { countMessagesTokens, countTextTokens, estimateToolsTokens, } from './context/tokenCounter.js';
35
+ import { buildContextBudgetPlan, fitToolsForEmergency } from './context/budgetPlan.js';
36
+ import { resizeContextBundle } from './context/contextBundle.js';
35
37
  import { chat as defaultChat } from './llm/client.js';
36
38
  import { buildUserContent } from './multimodal.js';
37
- import { ALL_TOOLS, executeTool as defaultExecuteTool, getToolByName } from './tools/index.js';
39
+ import { ALL_TOOLS, executeTool as defaultExecuteTool, getToolByName, getToolGateDetails, } from './tools/index.js';
38
40
  import { configService } from './config/configService.js';
41
+ import { capabilityForToolName } from './tools/capabilities.js';
42
+ import { createEffectiveToolSet } from './tools/effectiveToolSet.js';
43
+ import { hydrateSessionWorktree } from './tools/worktree/sessionWorktree.js';
44
+ import { capabilitiesForTools, enabledPolicyIds, } from './prompts/policyRegistry.js';
45
+ import { EvidenceLedger } from './harness/evidenceLedger.js';
46
+ import { StallGuard } from './harness/stallGuard.js';
47
+ import { validateFinalClaims } from './harness/claimValidation.js';
48
+ import { createAgentRuntimeContext, } from './runtime/agentRuntimeContext.js';
49
+ const DELIVERY_ARTIFACT = '(?:文件|合同|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
50
+ const DELIVERY_ACTION = '(?:发|发送|交付|提交|给|传|制作|生成|创建|导出|输出|修改|更新|完成)';
51
+ const ENGLISH_DELIVERY_ARTIFACT = '(?:file|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
52
+ const ENGLISH_DELIVERY_ACTION = '(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce)';
53
+ /** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
54
+ export function expectsFormalDelivery(input) {
55
+ const compact = input.replace(/\s+/g, '');
56
+ const chinese = new RegExp(`${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?`, 'i');
57
+ const english = new RegExp(`\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`, 'i');
58
+ return chinese.test(compact) || english.test(input);
59
+ }
60
+ const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
61
+ Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
62
+ Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
39
63
  /**
40
- * 工具 schema 的估算 token 数(ALL_TOOLS 固定,首次使用时算一次并缓存)。
41
- * 每轮 LLM 请求都会带上整套工具 schema,但 countMessagesTokens 只数历史消息、
64
+ * 每轮 LLM 请求都会带上工具 schema,但 countMessagesTokens 只数历史消息、
42
65
  * 漏了这部分 → 系统性低估。autoCompact 判定阈值时加上它才准。
43
66
  *
44
67
  * ⚠️ 必须懒求值而非模块顶层求值:Agent 工具引入了 tools/index → agent → loop →
45
68
  * tools/index 的模块环,若从 tools/index 一侧先进入,顶层求值 ALL_TOOLS 时它还在
46
69
  * TDZ(临时死区),启动即 ReferenceError。函数体内访问时模块图早已初始化完毕。
47
70
  */
48
- let toolsSchemaTokensCache = null;
49
- function getToolsSchemaTokens() {
50
- if (toolsSchemaTokensCache === null) {
51
- toolsSchemaTokensCache = estimateToolsTokens(ALL_TOOLS);
71
+ /**
72
+ * 本轮**真正发出去的**工具 schema 估算 token 数。
73
+ *
74
+ * 曾经这里数的是 `ALL_TOOLS`,那是错的:每回合发给模型的是 `activeTools`,
75
+ * 会被 `allowedTools`(工作流步骤 / 子 agent 收窄)、`subagents.enabled`、
76
+ * `computerRun` 是否注入等条件过滤掉一部分。ComputerUse 单个工具的 schema
77
+ * 实测就有 ~5800 token(全量 ~18000 的三分之一),在没暴露它的会话里照样
78
+ * 被计入压缩阈值 → **压缩提前触发**;被 `allowed_tools` 收窄到两三个工具的
79
+ * 子 agent 偏差更大。
80
+ *
81
+ * 缓存必须按工具集**键控**:模块级单值缓存配上按回合变化的工具集,会让所有
82
+ * 后续回合都拿到第一回合那个数字 —— 比不缓存更糟,因为它错得很稳定。
83
+ */
84
+ const toolsSchemaTokensCache = new Map();
85
+ function getToolsSchemaTokens(tools, signature) {
86
+ const key = signature ?? tools.map((t) => t.name).join(',');
87
+ let cached = toolsSchemaTokensCache.get(key);
88
+ if (cached === undefined) {
89
+ cached = estimateToolsTokens(tools);
90
+ toolsSchemaTokensCache.set(key, cached);
52
91
  }
53
- return toolsSchemaTokensCache;
92
+ return cached;
93
+ }
94
+ /**
95
+ * 顾问咨询配额:基线 1 次,之后每累计 N 次工具调用解锁 1 次,封顶 3 次。
96
+ *
97
+ * 曾经是硬编码 1 次/回合。那个数字隐含「一个用户回合 = 一个决策点」的假设,
98
+ * 在短问答里成立,在「继续,完成后续所有工作」这类一句话跑几十步的长回合里
99
+ * 直接失效 —— 真正有价值的咨询点至少有三个(方案定型前、高风险批次做完后、
100
+ * 收尾汇报前),它们全落在同一个用户回合内,配额只够挑一个:
101
+ * 挑方案阶段就没法收尾复核,挑收尾就等于方案已经错着做完了。
102
+ *
103
+ * 用「已执行的工具调用数」当刻度,而不是轮数或时间:它是本回合真实工作量的
104
+ * 确定性度量,不引入任何模型判断。反向激励可忽略 —— 咨询对模型不是奖励,
105
+ * 刷工具调用换配额的成本远高于收益。
106
+ */
107
+ const ADVISOR_QUOTA_BASE = 1;
108
+ const ADVISOR_QUOTA_MAX = 3;
109
+ const ADVISOR_QUOTA_UNLOCK_EVERY_TOOL_CALLS = 15;
110
+ export function advisorQuotaFor(executedToolCalls) {
111
+ return Math.min(ADVISOR_QUOTA_MAX, ADVISOR_QUOTA_BASE + Math.floor(executedToolCalls / ADVISOR_QUOTA_UNLOCK_EVERY_TOOL_CALLS));
54
112
  }
55
113
  /**
56
114
  * 执行一次"用户输入 → 模型回答完成"的完整流程。
@@ -59,31 +117,133 @@ function getToolsSchemaTokens() {
59
117
  */
60
118
  export async function* runQuery(userInput, options) {
61
119
  const { provider, history, signal, sessionState } = options;
120
+ // Tool screenshots are transient evidence for the tool continuation that produced them.
121
+ // Never carry a prior turn's base64 image into a new user request.
122
+ evictPriorVisionShots(history);
62
123
  const maxTurns = options.maxTurns ?? 50;
124
+ const runtimeContext = options.runtimeContext ?? createAgentRuntimeContext({
125
+ surface: 'headless',
126
+ profile: 'main',
127
+ cwd: process.cwd(),
128
+ workspaceId: options.memoryContext?.workspaceId,
129
+ sessionId: options.memoryContext?.sessionId,
130
+ owner: options.memoryContext?.access !== 'denied' ? 'local-owner' : 'untrusted',
131
+ memoryAccess: options.memoryContext?.access ?? 'denied',
132
+ interactiveAsk: false,
133
+ browser: options.browserRun,
134
+ computer: options.computerRun,
135
+ computerIntervention: options.computerIntervention,
136
+ toolGate: options.toolGate,
137
+ });
63
138
  // DI fallback:测试可传 stub,生产走真实模块单例
64
139
  const chatFn = options.chat ?? defaultChat;
65
140
  const executeToolFn = options.executeTool ?? defaultExecuteTool;
141
+ const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
142
+ const transientFor = (tools) => {
143
+ const opaque = options.transientContext?.trim() ?? '';
144
+ const bundle = options.transientBundle;
145
+ if (!bundle)
146
+ return opaque;
147
+ const basePlan = buildContextBudgetPlan({
148
+ provider,
149
+ history,
150
+ tools,
151
+ transientContext: opaque,
152
+ });
153
+ const availableForBundle = Math.max(0, basePlan.inputLimit - basePlan.historyTokens - basePlan.toolTokens - basePlan.transientTokens);
154
+ const selected = availableForBundle >= bundle.diagnostics.usedTokens
155
+ ? bundle
156
+ : resizeContextBundle(bundle, availableForBundle);
157
+ return [selected.text, opaque].filter(Boolean).join('\n\n');
158
+ };
159
+ let resultPresented = false;
160
+ let presentedResultUnverified = false;
161
+ let deliveryCorrectionAttempted = false;
66
162
  const subagents = await configService.subagents();
163
+ await hydrateSessionWorktree({
164
+ sessionState,
165
+ workspaceId: runtimeContext.workspaceId,
166
+ sessionId: runtimeContext.sessionId,
167
+ cwd: runtimeContext.cwd,
168
+ });
67
169
  const toolCallContext = {
68
170
  history,
69
- advisorCalls: { used: 0, max: 1 },
171
+ sessionState,
172
+ // 配额随本回合工作量增长(见 advisorQuotaFor);下面每轮工具执行完后上调。
173
+ advisorCalls: { used: 0, max: advisorQuotaFor(0) },
70
174
  advisorInvocation: 'agent',
71
175
  // Agent 等派生工具起嵌套子查询所需的运行态:与主循环同一 provider、
72
176
  // 同一审批闸门(子 agent 不绕过宿主策略)。agentCalls 与 advisorCalls 同构,
73
177
  // 限同一回合内的子任务派生总数,防失控 fan-out。
74
178
  provider,
75
- toolGate: options.toolGate,
179
+ toolGate: runtimeContext.toolGate ?? options.toolGate,
76
180
  agentCalls: { used: 0, max: subagents.enabled ? subagents.maxCallsPerTurn : 0 },
77
181
  subagentConfig: subagents,
78
- browserRun: options.browserRun,
182
+ browserRun: runtimeContext.browser ?? options.browserRun,
183
+ computerRun: runtimeContext.computer ?? options.computerRun,
184
+ computerIntervention: runtimeContext.computerIntervention ?? options.computerIntervention,
185
+ currentUserInput: userInput,
186
+ userMessageId: options.userMessageId,
187
+ memoryAccess: runtimeContext.memoryAccess,
188
+ workspaceId: runtimeContext.workspaceId,
189
+ sessionId: runtimeContext.sessionId,
190
+ cwd: runtimeContext.cwd,
191
+ promptCache: options.promptCache,
79
192
  };
193
+ /** 本回合累计执行的工具调用数(顾问配额的刻度,见 advisorQuotaFor)。 */
194
+ let executedToolCalls = 0;
80
195
  // 实际下发给模型的工具集:allowedTools 未传时给全量;
81
196
  // 传入数组时严格按数组过滤,[] 表示本轮不下发任何工具。
82
197
  // 仅影响呈现给 LLM 的工具清单,不影响 executeTool 的内部调度能力。
83
- const activeTools = options.allowedTools !== undefined
84
- ? ALL_TOOLS.filter((t) => options.allowedTools.includes(t.name))
85
- : ALL_TOOLS.filter((tool) => tool.name !== 'Agent' || subagents.enabled);
198
+ const hostTools = options.availableTools ?? ALL_TOOLS;
199
+ const availableTools = hostTools.filter((tool) => (tool.name !== 'Agent' || subagents.enabled)
200
+ && (tool.name !== 'ComputerUse' || runtimeContext.computer !== undefined));
201
+ const effectiveToolSet = createEffectiveToolSet({
202
+ availableTools,
203
+ runtime: runtimeContext,
204
+ allowedNames: options.allowedTools,
205
+ requiredNames: options.requiredTools,
206
+ });
207
+ const activeTools = effectiveToolSet.tools;
208
+ const formalDeliveryRequired = explicitFileDeliveryRequested
209
+ && runtimeContext.profile === 'main'
210
+ && runtimeContext.surface !== 'workflow'
211
+ && effectiveToolSet.names.has('PresentResult');
212
+ let currentEffectiveToolSet = effectiveToolSet;
213
+ let lastRequestTransientContext = [options.transientBundle?.text, options.transientContext]
214
+ .filter(Boolean).join('\n\n');
215
+ const evidenceLedger = new EvidenceLedger();
216
+ const stallGuard = new StallGuard();
217
+ const harnessDiagnostics = (stopReason) => ({
218
+ promptVersion: 'minimal-agent:prompt:v2',
219
+ basePromptHash: crypto.createHash('sha256')
220
+ .update(history[0]?.role === 'system' ? history[0].content : '')
221
+ .digest('hex'),
222
+ enabledPolicyIds: enabledPolicyIds({
223
+ profile: runtimeContext.profile,
224
+ surface: runtimeContext.surface,
225
+ cwd: runtimeContext.cwd,
226
+ capabilities: capabilitiesForTools(currentEffectiveToolSet.tools),
227
+ toolNames: currentEffectiveToolSet.names,
228
+ }),
229
+ enabledCapabilities: [...new Set(currentEffectiveToolSet.tools
230
+ .map((tool) => capabilityForToolName(tool.name))
231
+ .filter((capability) => Boolean(capability)))],
232
+ effectiveToolSetSignature: currentEffectiveToolSet.signature,
233
+ tokenBudget: buildContextBudgetPlan({
234
+ provider,
235
+ history,
236
+ tools: currentEffectiveToolSet.tools,
237
+ transientContext: lastRequestTransientContext,
238
+ }),
239
+ stopReason,
240
+ evidence: evidenceLedger.snapshot(),
241
+ });
86
242
  let userContent;
243
+ // 行内图片是从用户文本里**猜**出来的(拖拽进终端的路径就走这条路),猜错很正常:
244
+ // 路径可能还没生成、已删除、或纯粹是在讨论一个文件名。这类失败只降级成文本 + 一条
245
+ // warning,绝不能像下面 catch 那样打死整个回合——否则「随口提一个 .png 路径」=回合当场死。
246
+ const skippedInlineImages = [];
87
247
  try {
88
248
  userContent = await buildUserContent({
89
249
  text: userInput,
@@ -91,9 +251,11 @@ export async function* runQuery(userInput, options) {
91
251
  parseInlineImages: options.parseInlineImages,
92
252
  detail: options.imageDetail,
93
253
  maxLongSidePixel: options.imageMaxLongSidePixel,
254
+ onInlineImageSkipped: (source, reason) => skippedInlineImages.push({ source, reason }),
94
255
  });
95
256
  }
96
257
  catch (e) {
258
+ // 走到这里只剩「显式传入的图」读失败(-p --image / 工具实况截图)——调用方明确要了这张图。
97
259
  yield {
98
260
  type: 'error',
99
261
  error: `图片输入处理失败:${e.message}`,
@@ -101,18 +263,26 @@ export async function* runQuery(userInput, options) {
101
263
  };
102
264
  return;
103
265
  }
266
+ for (const skipped of skippedInlineImages) {
267
+ yield {
268
+ type: 'warning',
269
+ message: `未作为图片附加:${skipped.source}(${skipped.reason}),已按普通文本处理。`,
270
+ };
271
+ }
104
272
  // 1. 用户消息入栈(v2:填 timestamp,让 MessageList 的 turn 边界横线
105
273
  // 能拿到首条 user 的真实时间,否则 fallback Date.now() 会丢失"提交时刻"语义)
106
- history.push({
274
+ const userMessage = {
107
275
  role: 'user',
108
276
  content: userContent,
109
277
  ...(options.userAttachments?.length ? { attachments: options.userAttachments } : {}),
278
+ ...(options.userBrowserReferences?.length ? { browserReferences: options.userBrowserReferences } : {}),
110
279
  timestamp: Date.now(),
111
- id: crypto.randomUUID(),
112
- });
280
+ id: options.userMessageId ?? crypto.randomUUID(),
281
+ };
282
+ history.push(userMessage);
113
283
  // 立即通知 UI:用户消息已入栈,触发 bump 让 <Static> 马上把它 commit 进 scrollback,
114
284
  // 不必等整轮 assistant 落定(保证用户输入第一时间置顶,符合 T-A-O-R 展示顺序)。
115
- yield { type: 'user_message_committed' };
285
+ yield { type: 'user_message_committed', message: userMessage };
116
286
  // 反应式压缩自救:本 runQuery 内最多触发 MAX_REACTIVE_PER_QUERY 次(防本轮死循环);
117
287
  // 跨 runQuery 的连续失败由 reactiveCompact.ts 的 circuit breaker(consecutiveFailures)兜底。
118
288
  let reactiveCount = 0;
@@ -120,6 +290,12 @@ export async function* runQuery(userInput, options) {
120
290
  // 上一轮 LLM 返回的真实 prompt token 数(done.promptTokens;provider 开 include_usage 才有)。
121
291
  // 作为 autoCompact 的权威判据,校正本地估算的系统性低估;压缩后清空(历史已变,旧值失真)。
122
292
  let lastPromptTokens;
293
+ // 提示词缓存观测位(纯展示,不参与任何控制流)。与 lastPromptTokens 同生命周期:
294
+ // 压缩后一并清空,避免把压缩前的命中数据挂到压缩后的新历史上误导排障。
295
+ let lastCachedPromptTokens;
296
+ let lastCacheMissTokens;
297
+ let lastCacheWriteTokens;
298
+ let lastPromptCacheUsage = { reported: false };
123
299
  let turn = 0;
124
300
  let workStarted = false;
125
301
  while (turn < maxTurns) {
@@ -148,46 +324,166 @@ export async function* runQuery(userInput, options) {
148
324
  }
149
325
  // 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
150
326
  // 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
151
- try {
152
- const compact = await autoCompactIfNeeded(history, provider, {
153
- actualPromptTokens: lastPromptTokens,
154
- toolsTokens: getToolsSchemaTokens(),
155
- signal,
156
- });
157
- if (compact.compacted) {
158
- yield { type: 'compact_start' };
159
- yield { type: 'stage_change', stage: 'compacting' };
160
- // in-place 替换 history(保持调用方持有的引用有效)
161
- history.length = 0;
162
- history.push(...compact.messages);
163
- // 历史已压缩变小,旧的真实 usage 不再代表当前请求 → 清空,下一轮先用估算判据
164
- lastPromptTokens = undefined;
165
- yield { type: 'compact_done', before: compact.before, after: compact.after };
166
- // 压缩完会回到 LLM 调用,stage 退回 thinking
167
- yield { type: 'stage_change', stage: 'thinking' };
168
- }
169
- }
170
- catch (e) {
171
- // 压缩失败不要让整个对话挂掉:先 snip 砍最老消息释放空间再继续,
172
- // 避免下一步 LLM 直接撞 prompt-too-long(code='compact_failed' 非致命,不 return)。
327
+ let requestTransientContext = [
328
+ transientFor(activeTools),
329
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
330
+ ].filter(Boolean).join('\n\n');
331
+ lastRequestTransientContext = requestTransientContext;
332
+ const preCompactBudget = buildContextBudgetPlan({
333
+ provider,
334
+ history,
335
+ tools: activeTools,
336
+ transientContext: requestTransientContext,
337
+ });
338
+ if (preCompactBudget.overBudget || lastPromptTokens !== undefined) {
173
339
  try {
174
- const before = countMessagesTokens(history);
175
- const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
176
- if (snipped.messagesRemoved > 0) {
340
+ const compact = await autoCompactIfNeeded(history, provider, {
341
+ actualPromptTokens: lastPromptTokens,
342
+ toolsTokens: getToolsSchemaTokens(activeTools, effectiveToolSet.signature),
343
+ transientTokens: countTextTokens(requestTransientContext),
344
+ signal,
345
+ promptCache: options.promptCache,
346
+ chat: chatFn,
347
+ });
348
+ if (compact.compacted) {
349
+ yield { type: 'compact_start' };
350
+ yield { type: 'stage_change', stage: 'compacting' };
177
351
  history.length = 0;
178
- history.push(...snipped.messages);
352
+ history.push(...compact.messages);
179
353
  lastPromptTokens = undefined;
180
- yield { type: 'compact_done', before, after: countMessagesTokens(history) };
354
+ lastCachedPromptTokens = undefined;
355
+ lastCacheMissTokens = undefined;
356
+ lastCacheWriteTokens = undefined;
357
+ lastPromptCacheUsage = { reported: false };
358
+ yield {
359
+ type: 'compact_done',
360
+ before: compact.before,
361
+ after: compact.after,
362
+ trigger: 'threshold',
363
+ mode: compact.mode,
364
+ degraded: compact.degraded,
365
+ ...(compact.fallbackReason ? { fallbackReason: compact.fallbackReason } : {}),
366
+ ...(compact.messagesRemoved !== undefined ? { messagesRemoved: compact.messagesRemoved } : {}),
367
+ ...(compact.tokensFreed !== undefined ? { tokensFreed: compact.tokensFreed } : {}),
368
+ };
369
+ yield { type: 'stage_change', stage: 'thinking' };
181
370
  }
182
371
  }
183
- catch {
184
- /* snip 也失败 → 真的只能硬上 */
372
+ catch (e) {
373
+ if (signal?.aborted || e.name === 'AbortError')
374
+ throw e;
375
+ // 压缩失败不要让整个对话挂掉:先 snip 砍最老消息释放空间再继续。
376
+ let recovered = false;
377
+ try {
378
+ const before = countMessagesTokens(history);
379
+ const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
380
+ if (snipped.messagesRemoved > 0) {
381
+ history.length = 0;
382
+ history.push(...snipped.messages);
383
+ lastPromptTokens = undefined;
384
+ lastCachedPromptTokens = undefined;
385
+ lastCacheMissTokens = undefined;
386
+ lastCacheWriteTokens = undefined;
387
+ lastPromptCacheUsage = { reported: false };
388
+ recovered = true;
389
+ yield {
390
+ type: 'compact_done',
391
+ before,
392
+ after: countMessagesTokens(history),
393
+ trigger: 'threshold',
394
+ mode: 'snip-fallback',
395
+ degraded: true,
396
+ fallbackReason: 'llm-error',
397
+ messagesRemoved: snipped.messagesRemoved,
398
+ tokensFreed: snipped.tokensFreed,
399
+ };
400
+ }
401
+ }
402
+ catch {
403
+ /* snip 也失败 → 真的只能硬上 */
404
+ }
405
+ if (!recovered) {
406
+ // 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
407
+ // 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
408
+ yield {
409
+ type: 'error',
410
+ error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${e.message}`,
411
+ code: 'compact_failed',
412
+ };
413
+ }
185
414
  }
415
+ }
416
+ // Re-open transient capacity after compaction. If the compacted tail is still
417
+ // too large, trim old history before sacrificing any model-visible ability.
418
+ requestTransientContext = [
419
+ transientFor(activeTools),
420
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
421
+ ].filter(Boolean).join('\n\n');
422
+ lastRequestTransientContext = requestTransientContext;
423
+ let postCompactBudget = buildContextBudgetPlan({
424
+ provider,
425
+ history,
426
+ tools: activeTools,
427
+ transientContext: requestTransientContext,
428
+ });
429
+ if (postCompactBudget.overBudget) {
430
+ const before = countMessagesTokens(history);
431
+ const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
432
+ if (snipped.messagesRemoved > 0) {
433
+ history.length = 0;
434
+ history.push(...snipped.messages);
435
+ yield {
436
+ type: 'compact_done',
437
+ before,
438
+ after: countMessagesTokens(history),
439
+ trigger: 'post-budget',
440
+ mode: 'snip-fallback',
441
+ degraded: true,
442
+ fallbackReason: 'still-over-budget',
443
+ messagesRemoved: snipped.messagesRemoved,
444
+ tokensFreed: snipped.tokensFreed,
445
+ };
446
+ requestTransientContext = [
447
+ transientFor(activeTools),
448
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
449
+ ].filter(Boolean).join('\n\n');
450
+ lastRequestTransientContext = requestTransientContext;
451
+ postCompactBudget = buildContextBudgetPlan({
452
+ provider,
453
+ history,
454
+ tools: activeTools,
455
+ transientContext: requestTransientContext,
456
+ });
457
+ }
458
+ }
459
+ const emergencyFit = fitToolsForEmergency({
460
+ provider,
461
+ history,
462
+ tools: activeTools,
463
+ transientContext: requestTransientContext,
464
+ requiredNames: effectiveToolSet.requiredNames,
465
+ });
466
+ currentEffectiveToolSet = emergencyFit.removedNames.length === 0
467
+ ? effectiveToolSet
468
+ : createEffectiveToolSet({
469
+ availableTools: emergencyFit.tools,
470
+ runtime: runtimeContext,
471
+ requiredNames: [...effectiveToolSet.requiredNames],
472
+ });
473
+ if (emergencyFit.removedNames.length > 0) {
474
+ yield {
475
+ type: 'warning',
476
+ message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
477
+ };
478
+ }
479
+ if (emergencyFit.plan.overBudget) {
186
480
  yield {
187
481
  type: 'error',
188
- error: `自动压缩失败(已尝试 snip 兜底后继续):${e.message}`,
189
- code: 'compact_failed',
482
+ error: `当前请求即使经过上下文降级仍需要约 ${emergencyFit.plan.totalInputTokens} tokens,超过模型输入上限 ${emergencyFit.plan.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
483
+ code: 'context_overflow',
190
484
  };
485
+ yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('error') };
486
+ return;
191
487
  }
192
488
  // 3. 调 LLM 并组装 assistant 消息
193
489
  let assistantText = '';
@@ -212,17 +508,13 @@ export async function* runQuery(userInput, options) {
212
508
  yield { type: 'stage_change', stage: 'thinking' };
213
509
  workStarted = true;
214
510
  try {
215
- const modelMessages = options.transientContext?.trim()
216
- ? [
217
- { role: 'system', content: options.transientContext.trim() },
218
- ...history,
219
- ]
220
- : history;
511
+ const modelMessages = buildModelMessages(history, requestTransientContext, userMessage.id);
221
512
  for await (const ev of chatFn({
222
513
  provider,
223
514
  messages: modelMessages,
224
- tools: activeTools,
515
+ tools: currentEffectiveToolSet.tools,
225
516
  signal,
517
+ promptCache: options.promptCache,
226
518
  })) {
227
519
  if (ev.type === 'text_delta') {
228
520
  if (!stageStreamingYielded) {
@@ -255,6 +547,15 @@ export async function* runQuery(userInput, options) {
255
547
  // 真实 prompt token(provider 开 include_usage 才有)→ 喂给下一轮 autoCompact 做权威判据
256
548
  if (typeof ev.promptTokens === 'number')
257
549
  lastPromptTokens = ev.promptTokens;
550
+ // 缓存命中观测:只在 provider 真实报告时才记录(undefined ≠ 0,见 types.ts 注释)
551
+ if (typeof ev.cachedPromptTokens === 'number')
552
+ lastCachedPromptTokens = ev.cachedPromptTokens;
553
+ if (typeof ev.cacheMissTokens === 'number')
554
+ lastCacheMissTokens = ev.cacheMissTokens;
555
+ if (typeof ev.cacheWriteTokens === 'number')
556
+ lastCacheWriteTokens = ev.cacheWriteTokens;
557
+ if (ev.promptCacheUsage)
558
+ lastPromptCacheUsage = ev.promptCacheUsage;
258
559
  }
259
560
  }
260
561
  }
@@ -277,17 +578,27 @@ export async function* runQuery(userInput, options) {
277
578
  reactiveCount++;
278
579
  yield { type: 'compact_start' };
279
580
  yield { type: 'stage_change', stage: 'compacting' };
280
- const result = await reactiveCompactIfApplicable(history, provider, e, sessionState?.reactive, signal);
581
+ const result = await reactiveCompactIfApplicable(history, provider, e, sessionState?.reactive, signal, {}, options.promptCache);
281
582
  // 仅当确实压下去了(after < before)才重试;否则(没压动 / 未恢复)退回错误路径,杜绝死循环。
282
583
  const shrank = (result.after ?? 0) < (result.before ?? 0);
283
584
  if (result.recovered && shrank) {
284
585
  history.length = 0;
285
586
  history.push(...result.messages);
286
587
  lastPromptTokens = undefined; // 历史已变小,旧真实 usage 失真
588
+ lastCachedPromptTokens = undefined;
589
+ lastCacheMissTokens = undefined;
590
+ lastCacheWriteTokens = undefined;
591
+ lastPromptCacheUsage = { reported: false };
287
592
  yield {
288
593
  type: 'compact_done',
289
594
  before: result.before ?? 0,
290
595
  after: result.after ?? 0,
596
+ trigger: 'provider-overflow',
597
+ mode: result.mode,
598
+ degraded: result.degraded,
599
+ ...(result.fallbackReason ? { fallbackReason: result.fallbackReason } : {}),
600
+ ...(result.messagesRemoved !== undefined ? { messagesRemoved: result.messagesRemoved } : {}),
601
+ ...(result.tokensFreed !== undefined ? { tokensFreed: result.tokensFreed } : {}),
291
602
  };
292
603
  // 压缩完回到 LLM 调用
293
604
  yield { type: 'stage_change', stage: 'thinking' };
@@ -338,6 +649,16 @@ export async function* runQuery(userInput, options) {
338
649
  yield { type: 'assistant_message', message: assistantMsg };
339
650
  // 4. 没有工具调用 → 整轮交互结束
340
651
  if (toolCallsByIndex.length === 0) {
652
+ // A model occasionally writes a path/link in prose and stops without formally
653
+ // presenting it. Give it one model-only corrective pass; the reminder is
654
+ // transient and therefore never pollutes persisted conversation history.
655
+ if (formalDeliveryRequired
656
+ && !resultPresented
657
+ && !deliveryCorrectionAttempted
658
+ && turn < maxTurns) {
659
+ deliveryCorrectionAttempted = true;
660
+ continue;
661
+ }
341
662
  // R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
342
663
  // 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
343
664
  // 走通用 warning(text/json 都落 stderr,不污染 json 那一行 stdout 契约)。
@@ -348,8 +669,15 @@ export async function* runQuery(userInput, options) {
348
669
  '可调大模型的 max_tokens 上限,或让模型分段输出。',
349
670
  };
350
671
  }
351
- const estimatedTokens = countMessagesTokens(history) + getToolsSchemaTokens();
672
+ // The final answer has already consumed the latest screenshot. Persist only a compact
673
+ // marker so the next turn does not resend a large screen image.
674
+ evictPriorVisionShots(history);
675
+ const estimatedTokens = countMessagesTokens(history) + getToolsSchemaTokens(activeTools);
352
676
  const usedTokens = Math.min(provider.contextWindow, Math.max(lastPromptTokens ?? 0, estimatedTokens));
677
+ yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('complete') };
678
+ const claimValidation = validateFinalClaims(assistantText, evidenceLedger.snapshot());
679
+ for (const warning of claimValidation.warnings)
680
+ yield { type: 'warning', message: warning };
353
681
  yield {
354
682
  type: 'context_usage',
355
683
  usedTokens,
@@ -357,8 +685,39 @@ export async function* runQuery(userInput, options) {
357
685
  remainingTokens: Math.max(0, provider.contextWindow - usedTokens),
358
686
  compactThreshold: getCompactThreshold(provider),
359
687
  source: lastPromptTokens === undefined ? 'estimated' : 'actual',
688
+ // 缓存观测只在有真实 usage 时才附带——估算路径没有缓存信息,
689
+ // 不能拿估算值冒充命中率(见 types.ts 的 context_usage 注释)。
690
+ ...(lastPromptTokens !== undefined && lastCachedPromptTokens !== undefined
691
+ ? { cachedPromptTokens: lastCachedPromptTokens }
692
+ : {}),
693
+ ...(lastPromptTokens !== undefined && lastCacheWriteTokens !== undefined
694
+ ? { cacheWriteTokens: lastCacheWriteTokens }
695
+ : {}),
696
+ ...(lastPromptTokens !== undefined && lastCacheMissTokens !== undefined
697
+ ? { cacheMissTokens: lastCacheMissTokens }
698
+ : {}),
699
+ promptCacheUsage: lastPromptCacheUsage,
360
700
  };
361
- yield { type: 'turn_done' };
701
+ if (formalDeliveryRequired && !resultPresented) {
702
+ const warning = '交付未完成:用户明确要求提供文件,但本轮没有成功提交可打开的文件结果。';
703
+ yield { type: 'warning', message: warning };
704
+ yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning };
705
+ }
706
+ else if (resultPresented && presentedResultUnverified) {
707
+ const warning = '结果已提交,但至少一个文件尚未通过结构化验证。';
708
+ yield { type: 'warning', message: warning };
709
+ yield { type: 'turn_done', completionStatus: 'delivery_unverified', warning };
710
+ }
711
+ else if (!claimValidation.verified) {
712
+ yield {
713
+ type: 'turn_done',
714
+ completionStatus: 'claims_unverified',
715
+ warning: claimValidation.warnings.join('\n'),
716
+ };
717
+ }
718
+ else {
719
+ yield { type: 'turn_done', completionStatus: 'completed' };
720
+ }
362
721
  return;
363
722
  }
364
723
  // 5. 执行工具调用
@@ -375,13 +734,49 @@ export async function* runQuery(userInput, options) {
375
734
  // 执行单个 tool call → WorkerOutcome(内部吞掉 abort/异常,永不 reject)。
376
735
  const runOne = async (tc, reportProgress) => {
377
736
  try {
737
+ if (stallGuard.shouldReject(tc)) {
738
+ return {
739
+ tc,
740
+ result: {
741
+ ok: false,
742
+ error: '检测到同一工具和参数已连续两次返回相同失败;第三次调用已在执行前拒绝,请改变输入或改用其他路径。',
743
+ recovery: 'change-input',
744
+ sensitivity: 'public',
745
+ },
746
+ };
747
+ }
748
+ // The model-visible catalog and the execution boundary are the same set.
749
+ // A provider may still emit a stale or fabricated tool call; return a
750
+ // recoverable result so the model can choose an available path.
751
+ if (!currentEffectiveToolSet.names.has(tc.function.name)) {
752
+ const deniedReason = tc.function.name === 'Agent'
753
+ ? '当前上下文不允许调用 Agent;请直接完成子任务或改用已提供的工具'
754
+ : `工具 ${tc.function.name} 不在当前宿主授权的有效工具集中,请改用已提供的工具继续任务`;
755
+ return {
756
+ tc,
757
+ result: {
758
+ ok: false,
759
+ error: deniedReason,
760
+ recovery: 'change-input',
761
+ sensitivity: 'public',
762
+ },
763
+ };
764
+ }
378
765
  // 工具执行闸门:宿主注入后覆盖所有工具,包括只读工具。deny → 把 reason 当
379
766
  // tool_result 回填,模型据此改道/如实报告,不抛异常不中断整轮。宿主异常一律 deny。
380
767
  const isReadOnly = getToolByName(tc.function.name)?.isReadOnly === true;
381
- if (options.toolGate) {
768
+ const effectiveToolGate = runtimeContext.toolGate ?? options.toolGate;
769
+ if (effectiveToolGate) {
382
770
  let decision;
383
771
  try {
384
- decision = await options.toolGate({ name: tc.function.name, argsJson: tc.function.arguments, isReadOnly, toolCallId: tc.id }, signal);
772
+ const gateDetails = getToolGateDetails(tc.function.name, tc.function.arguments, { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress });
773
+ decision = await effectiveToolGate({
774
+ name: tc.function.name,
775
+ argsJson: tc.function.arguments,
776
+ isReadOnly,
777
+ toolCallId: tc.id,
778
+ ...gateDetails,
779
+ }, signal);
385
780
  }
386
781
  catch {
387
782
  decision = { allow: false, reason: '工具执行审批失败(宿主异常)' };
@@ -393,7 +788,27 @@ export async function* runQuery(userInput, options) {
393
788
  options.onToolStart?.(tc.function.name, tc.id);
394
789
  // 浅拷贝填入本次调用的 toolCallId:进度按调用实例路由(并行同名工具不串扰)。
395
790
  // advisorCalls/agentCalls 是共享对象引用,计数仍全轮共享,不受拷贝影响。
396
- const result = await executeToolFn(tc.function.name, tc.function.arguments, signal, { ...toolCallContext, toolCallId: tc.id, reportProgress });
791
+ let result = await executeToolFn(tc.function.name, tc.function.arguments, signal, { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress });
792
+ if (tc.function.name === 'PresentResult'
793
+ && result.ok
794
+ && result.metadata?.kind === 'result_manifest'
795
+ && options.commitFormalDelivery) {
796
+ try {
797
+ await options.commitFormalDelivery({
798
+ toolCallId: tc.id,
799
+ turnId: userMessage.id ?? options.userMessageId ?? 'turn:unknown',
800
+ manifest: result.metadata.manifest,
801
+ });
802
+ }
803
+ catch (error) {
804
+ result = {
805
+ ok: false,
806
+ error: `正式交付未能可靠保存:${error.message}`,
807
+ recovery: 'retry',
808
+ sensitivity: 'local',
809
+ };
810
+ }
811
+ }
397
812
  options.onToolEnd?.(tc.id, false);
398
813
  return { tc, result };
399
814
  }
@@ -504,11 +919,19 @@ export async function* runQuery(userInput, options) {
504
919
  settled.push(o);
505
920
  }
506
921
  }
922
+ // 本回合工作量累计 → 上调顾问配额(只增不减;已用掉的次数不会因此回退)。
923
+ executedToolCalls += settled.length;
924
+ toolCallContext.advisorCalls.max = advisorQuotaFor(executedToolCalls);
507
925
  // 严格按 toolCallsByIndex 顺序把 tool 消息回填 history(settled 已保持该顺序)。
508
926
  let anyAborted = false;
509
927
  for (const { tc, result } of settled) {
928
+ evidenceLedger.record(tc.function.name, tc.function.arguments, result);
510
929
  if (!result.ok && result.error === 'aborted')
511
930
  anyAborted = true;
931
+ if (tc.function.name === 'PresentResult' && result.ok && result.metadata?.kind === 'result_manifest') {
932
+ resultPresented = true;
933
+ presentedResultUnverified ||= result.metadata.manifest.files.some((file) => file.verification !== 'passed');
934
+ }
512
935
  const rawContent = result.ok ? result.content : `Error: ${result.error}`;
513
936
  // 微压缩仅对成功结果走(失败结果通常很短,不需要压缩)
514
937
  const content = result.ok
@@ -523,6 +946,40 @@ export async function* runQuery(userInput, options) {
523
946
  id: crypto.randomUUID(),
524
947
  });
525
948
  }
949
+ // If the third identical observation is also the configured final turn, retain the
950
+ // established max_turns termination contract. The stall guard is valuable only when
951
+ // it can stop a failing path before the caller's explicit turn budget does.
952
+ const preemptedRepeatedFailure = settled.some(({ result }) => !result.ok && result.error.includes('第三次调用已在执行前拒绝'));
953
+ const stall = preemptedRepeatedFailure
954
+ ? ({
955
+ stalled: true,
956
+ reason: 'repeated_failure',
957
+ })
958
+ : turn + 1 < maxTurns
959
+ ? stallGuard.observe(settled.map((item) => item.tc), settled.map((item) => item.result), assistantText)
960
+ : { stalled: false };
961
+ if (stall.stalled) {
962
+ const message = stall.reason === 'repeated_failure'
963
+ ? '检测到同一工具以相同参数连续返回相同失败,已停止重复重试。'
964
+ : '检测到连续工具轮次没有产生新文本或新结果,已停止无进展循环。';
965
+ yield { type: 'warning', message };
966
+ yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('stalled') };
967
+ yield { type: 'error', error: message, code: 'stalled' };
968
+ return;
969
+ }
970
+ // 关键节点截图回填:把工具附带的实况截图作为一条带图 user 消息喂给(视觉)模型自我核验,
971
+ // 并 yield injected_message 让宿主在对话里即时展示这张实况截图。
972
+ // 纯增强:读图/编码等任何失败都静默跳过,绝不打断本轮(与工具「失败不抛异常」一致)。
973
+ let injectedShot = null;
974
+ try {
975
+ injectedShot = await injectVisionFollowups(history, settled, provider);
976
+ }
977
+ catch { /* 截图回填失败不影响主流程 */ }
978
+ // The history copy is intentionally downgraded after the turn. Emit a detached snapshot
979
+ // so TUI/Desktop stores do not see their already-rendered image mutate underneath them.
980
+ if (injectedShot) {
981
+ yield { type: 'injected_message', message: structuredClone(injectedShot) };
982
+ }
526
983
  if (anyAborted || signal?.aborted) {
527
984
  if (options.hasQueuedInput?.()) {
528
985
  pushQueuedInputMarker(history);
@@ -542,6 +999,7 @@ export async function* runQuery(userInput, options) {
542
999
  // 6. 继续 while 让模型看到 tool_result 后继续推理
543
1000
  }
544
1001
  // code='max_turns':撞最大轮数上限而终结,stop_reason='max_turns'
1002
+ yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('max_turns') };
545
1003
  yield {
546
1004
  type: 'error',
547
1005
  error: `达到最大轮数 ${maxTurns},提前结束(防止失控)。如果合理可以提高 maxTurns。`,
@@ -549,6 +1007,99 @@ export async function* runQuery(userInput, options) {
549
1007
  };
550
1008
  }
551
1009
  // ---------------- 辅助函数 ----------------
1010
+ /**
1011
+ * 易变上下文只存在于 provider 请求副本中,不会进入会话历史。
1012
+ */
1013
+ export const SYSTEM_REMINDER_OPEN = '<system-reminder>';
1014
+ export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
1015
+ /**
1016
+ * Attach transient context to the current turn's user message in a model-only
1017
+ * clone. Later tool-loop requests reuse the same decorated message, making each
1018
+ * serialized request a strict prefix extension of the previous one.
1019
+ */
1020
+ export function buildModelMessages(history, transientContext, currentUserMessageId) {
1021
+ const transient = transientContext?.trim();
1022
+ if (!transient)
1023
+ return history;
1024
+ let index = currentUserMessageId
1025
+ ? history.findIndex((message) => message.role === 'user' && message.id === currentUserMessageId)
1026
+ : -1;
1027
+ if (index < 0) {
1028
+ for (let cursor = history.length - 1; cursor >= 0; cursor--) {
1029
+ if (history[cursor]?.role === 'user') {
1030
+ index = cursor;
1031
+ break;
1032
+ }
1033
+ }
1034
+ }
1035
+ if (index < 0)
1036
+ return history;
1037
+ const target = history[index];
1038
+ if (target.role !== 'user')
1039
+ return history;
1040
+ const reminder = `${SYSTEM_REMINDER_OPEN}\n${transient}\n${SYSTEM_REMINDER_CLOSE}`;
1041
+ const content = typeof target.content === 'string'
1042
+ ? `${target.content}\n\n${reminder}`
1043
+ : [...target.content, { type: 'text', text: reminder }];
1044
+ const next = [...history];
1045
+ next[index] = { ...target, content };
1046
+ return next;
1047
+ }
1048
+ // 关键节点图像核验的合成消息标记:既是给模型的提示前缀,也是 evictPriorVisionShots 识别旧图消息的锚。
1049
+ export const VISION_SHOT_MARKER = '【实况图像】';
1050
+ /**
1051
+ * 历史标记。这套机制最初只有 WebBrowser 用得上,文案写死了「浏览器」;
1052
+ * 现在 Bash 的 ATTACH_IMAGE sentinel 也能产图(屏幕截图 / 生成的产物),文案必须中性。
1053
+ * 但**持久化会话里仍存着旧前缀的消息**,淘汰逻辑必须继续认它 ——
1054
+ * 否则老会话里的旧截图永远不会被降级,图片一路累积撑爆上下文。
1055
+ */
1056
+ const LEGACY_VISION_SHOT_MARKERS = ['【浏览器实况截图】'];
1057
+ const VISION_SHOT_PROMPT = `${VISION_SHOT_MARKER}上面是刚才工具执行后附带的真实图像(浏览器页面 / 屏幕截图 / 生成的产物)。请仔细看图核对:是否真的到达/呈现了你预期的状态?若与预期不符(空白/报错/弹窗遮挡/验证拦截/内容不对),不要假设成功,如实说明并调整下一步。`;
1058
+ /** 一条 user 消息的文本是否是「实况图像」合成消息(含历史前缀)。 */
1059
+ function isVisionShotText(text) {
1060
+ return (text.startsWith(VISION_SHOT_MARKER) ||
1061
+ LEGACY_VISION_SHOT_MARKERS.some((marker) => text.startsWith(marker)));
1062
+ }
1063
+ /**
1064
+ * 把本轮工具附带的实况截图(ToolResult.visionImages)组装成一条带图 user 消息喂给模型自我核验。
1065
+ * - 仅在模型明确支持识图时注入(provider.supportsVision === true);否则丢弃(不假装核验、也不给非视觉端点发图触发 400)。
1066
+ * - 注入前先把历史里旧的截图消息降级为纯文字占位(evictPriorVisionShots),保证上下文里最多一张实况图,
1067
+ * 避免图片累积撑爆上下文(呼应浏览器 ADR「高频帧只存内存、只持久化最终结果」)。
1068
+ * - **失败的工具结果同样回填**:出错现场那张图往往比成功时更有信息量(权限弹窗、
1069
+ * 验证拦截、意外的前台窗口)。此前这里要求 `ok === true`,逼得想附图的工具只能
1070
+ * 把失败伪装成成功——见 types.ts 里 ToolResult 那段注释。
1071
+ */
1072
+ async function injectVisionFollowups(history, settled, provider) {
1073
+ if (provider.supportsVision !== true)
1074
+ return null;
1075
+ const images = settled.flatMap((s) => s.result.visionImages ?? []);
1076
+ if (images.length === 0)
1077
+ return null;
1078
+ evictPriorVisionShots(history);
1079
+ // 工具可以给自己这批图配一句说明(visionNote)。内核只负责把它拼在通用提示之后,
1080
+ // 不解读内容——「这是一组 1Hz 连续帧」这类语义只有产图的工具知道。
1081
+ const notes = settled
1082
+ .flatMap((s) => (s.result.visionNote ? [s.result.visionNote] : []))
1083
+ .filter((note, index, all) => all.indexOf(note) === index);
1084
+ const text = notes.length > 0
1085
+ ? `${VISION_SHOT_PROMPT}\n\n${notes.join('\n\n')}`
1086
+ : VISION_SHOT_PROMPT;
1087
+ const content = await buildUserContent({ text, images });
1088
+ const msg = { role: 'user', content, timestamp: Date.now(), id: crypto.randomUUID() };
1089
+ history.push(msg);
1090
+ return msg;
1091
+ }
1092
+ /** 把历史里旧的实况图像 user 消息降级为纯文字占位、剔除其图片,释放上下文(只保留最近一张真图)。 */
1093
+ function evictPriorVisionShots(history) {
1094
+ for (const msg of history) {
1095
+ if (msg.role !== 'user' || !Array.isArray(msg.content))
1096
+ continue;
1097
+ const textPart = msg.content.find((p) => p.type === 'text');
1098
+ if (textPart?.type === 'text' && isVisionShotText(textPart.text)) {
1099
+ msg.content = `${VISION_SHOT_MARKER}(此前的实况图像已从上下文移除以节省空间。)`;
1100
+ }
1101
+ }
1102
+ }
552
1103
  /**
553
1104
  * 用户按ESC手动中断
554
1105
  */