@armadra/agent 0.0.0-stage → 0.2.1

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 (407) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +426 -2
  4. package/dist/agent/agent.d.ts +56 -0
  5. package/dist/agent/agent.js +129 -0
  6. package/dist/agent/loop.d.ts +51 -0
  7. package/dist/agent/loop.js +158 -0
  8. package/dist/agent/queue.d.ts +25 -0
  9. package/dist/agent/queue.js +55 -0
  10. package/dist/agent/retry.d.ts +22 -0
  11. package/dist/agent/retry.js +92 -0
  12. package/dist/agent/schema.d.ts +27 -0
  13. package/dist/agent/schema.js +186 -0
  14. package/dist/agent/session-cache.d.ts +100 -0
  15. package/dist/agent/session-cache.js +511 -0
  16. package/dist/agent/session-compaction.d.ts +49 -0
  17. package/dist/agent/session-compaction.js +251 -0
  18. package/dist/agent/session-core.d.ts +103 -0
  19. package/dist/agent/session-core.js +7 -0
  20. package/dist/agent/session-run.d.ts +55 -0
  21. package/dist/agent/session-run.js +246 -0
  22. package/dist/agent/session-state.d.ts +39 -0
  23. package/dist/agent/session-state.js +118 -0
  24. package/dist/agent/session-subagent.d.ts +54 -0
  25. package/dist/agent/session-subagent.js +157 -0
  26. package/dist/agent/session-sync.d.ts +20 -0
  27. package/dist/agent/session-sync.js +104 -0
  28. package/dist/agent/session-tools.d.ts +24 -0
  29. package/dist/agent/session-tools.js +277 -0
  30. package/dist/agent/session.d.ts +117 -0
  31. package/dist/agent/session.js +484 -0
  32. package/dist/agent/system-prompt.d.ts +49 -0
  33. package/dist/agent/system-prompt.js +121 -0
  34. package/dist/agent/tool-runner.d.ts +64 -0
  35. package/dist/agent/tool-runner.js +309 -0
  36. package/dist/agent/transform.d.ts +26 -0
  37. package/dist/agent/transform.js +171 -0
  38. package/dist/agent/types.d.ts +378 -0
  39. package/dist/agent/types.js +21 -0
  40. package/dist/ai/apis/anthropic-messages.d.ts +26 -0
  41. package/dist/ai/apis/anthropic-messages.js +214 -0
  42. package/dist/ai/apis/anthropic-request.d.ts +42 -0
  43. package/dist/ai/apis/anthropic-request.js +282 -0
  44. package/dist/ai/apis/api.d.ts +28 -0
  45. package/dist/ai/apis/api.js +102 -0
  46. package/dist/ai/apis/cache-params.d.ts +48 -0
  47. package/dist/ai/apis/cache-params.js +163 -0
  48. package/dist/ai/apis/google-generative-ai.d.ts +32 -0
  49. package/dist/ai/apis/google-generative-ai.js +249 -0
  50. package/dist/ai/apis/google-request.d.ts +40 -0
  51. package/dist/ai/apis/google-request.js +255 -0
  52. package/dist/ai/apis/openai-compat.d.ts +35 -0
  53. package/dist/ai/apis/openai-compat.js +142 -0
  54. package/dist/ai/apis/openai-completions.d.ts +28 -0
  55. package/dist/ai/apis/openai-completions.js +246 -0
  56. package/dist/ai/apis/openai-request.d.ts +35 -0
  57. package/dist/ai/apis/openai-request.js +303 -0
  58. package/dist/ai/apis/openai-responses-request.d.ts +52 -0
  59. package/dist/ai/apis/openai-responses-request.js +266 -0
  60. package/dist/ai/apis/openai-responses.d.ts +28 -0
  61. package/dist/ai/apis/openai-responses.js +303 -0
  62. package/dist/ai/apis/shared.d.ts +56 -0
  63. package/dist/ai/apis/shared.js +179 -0
  64. package/dist/ai/cache/economics.d.ts +24 -0
  65. package/dist/ai/cache/economics.js +69 -0
  66. package/dist/ai/cache/fingerprint.d.ts +17 -0
  67. package/dist/ai/cache/fingerprint.js +39 -0
  68. package/dist/ai/cache/miss.d.ts +49 -0
  69. package/dist/ai/cache/miss.js +96 -0
  70. package/dist/ai/cache/reporting.d.ts +39 -0
  71. package/dist/ai/cache/reporting.js +102 -0
  72. package/dist/ai/cache/types.d.ts +94 -0
  73. package/dist/ai/cache/types.js +19 -0
  74. package/dist/ai/cache/warmer.d.ts +94 -0
  75. package/dist/ai/cache/warmer.js +224 -0
  76. package/dist/ai/context.d.ts +35 -0
  77. package/dist/ai/context.js +114 -0
  78. package/dist/ai/cost.d.ts +21 -0
  79. package/dist/ai/cost.js +60 -0
  80. package/dist/ai/event-stream.d.ts +33 -0
  81. package/dist/ai/event-stream.js +101 -0
  82. package/dist/ai/fake/fake-provider.d.ts +68 -0
  83. package/dist/ai/fake/fake-provider.js +278 -0
  84. package/dist/ai/fake/fake-script.d.ts +69 -0
  85. package/dist/ai/fake/fake-script.js +142 -0
  86. package/dist/ai/http.d.ts +54 -0
  87. package/dist/ai/http.js +212 -0
  88. package/dist/ai/json-partial.d.ts +17 -0
  89. package/dist/ai/json-partial.js +243 -0
  90. package/dist/ai/overflow.d.ts +22 -0
  91. package/dist/ai/overflow.js +61 -0
  92. package/dist/ai/providers/auth.d.ts +71 -0
  93. package/dist/ai/providers/auth.js +202 -0
  94. package/dist/ai/providers/builtin.d.ts +23 -0
  95. package/dist/ai/providers/builtin.js +148 -0
  96. package/dist/ai/providers/catalog-data.d.ts +5 -0
  97. package/dist/ai/providers/catalog-data.js +19 -0
  98. package/dist/ai/providers/catalog.d.ts +28 -0
  99. package/dist/ai/providers/catalog.js +166 -0
  100. package/dist/ai/providers/registry.d.ts +83 -0
  101. package/dist/ai/providers/registry.js +305 -0
  102. package/dist/ai/sse.d.ts +32 -0
  103. package/dist/ai/sse.js +140 -0
  104. package/dist/ai/thinking.d.ts +42 -0
  105. package/dist/ai/thinking.js +101 -0
  106. package/dist/ai/types.d.ts +428 -0
  107. package/dist/ai/types.js +19 -0
  108. package/dist/bundle/ama-sandbox.cjs +324 -0
  109. package/dist/bundle/ama.cjs +25770 -0
  110. package/dist/bundle.d.ts +9 -0
  111. package/dist/bundle.js +15 -0
  112. package/dist/cli/args.d.ts +82 -0
  113. package/dist/cli/args.js +381 -0
  114. package/dist/cli/bootstrap.d.ts +18 -0
  115. package/dist/cli/bootstrap.js +398 -0
  116. package/dist/cli/compose-providers.d.ts +37 -0
  117. package/dist/cli/compose-providers.js +104 -0
  118. package/dist/cli/compose-session.d.ts +69 -0
  119. package/dist/cli/compose-session.js +370 -0
  120. package/dist/cli/compose-store.d.ts +24 -0
  121. package/dist/cli/compose-store.js +125 -0
  122. package/dist/cli/compose.d.ts +106 -0
  123. package/dist/cli/compose.js +206 -0
  124. package/dist/cli/default-model.d.ts +16 -0
  125. package/dist/cli/default-model.js +24 -0
  126. package/dist/cli/deps.d.ts +205 -0
  127. package/dist/cli/deps.js +12 -0
  128. package/dist/cli/exit-codes.d.ts +29 -0
  129. package/dist/cli/exit-codes.js +40 -0
  130. package/dist/cli/main.d.ts +26 -0
  131. package/dist/cli/main.js +165 -0
  132. package/dist/cli/runtime.d.ts +90 -0
  133. package/dist/cli/runtime.js +8 -0
  134. package/dist/cli/startup-screen.d.ts +15 -0
  135. package/dist/cli/startup-screen.js +56 -0
  136. package/dist/cli/startup-steps.d.ts +35 -0
  137. package/dist/cli/startup-steps.js +197 -0
  138. package/dist/cli/subcommands/auth.d.ts +12 -0
  139. package/dist/cli/subcommands/auth.js +91 -0
  140. package/dist/cli/subcommands/config.d.ts +44 -0
  141. package/dist/cli/subcommands/config.js +201 -0
  142. package/dist/cli/subcommands/context.d.ts +21 -0
  143. package/dist/cli/subcommands/context.js +41 -0
  144. package/dist/cli/subcommands/doctor.d.ts +11 -0
  145. package/dist/cli/subcommands/doctor.js +246 -0
  146. package/dist/cli/subcommands/models-cache-probe.d.ts +34 -0
  147. package/dist/cli/subcommands/models-cache-probe.js +190 -0
  148. package/dist/cli/subcommands/models-discover.d.ts +40 -0
  149. package/dist/cli/subcommands/models-discover.js +204 -0
  150. package/dist/cli/subcommands/models.d.ts +40 -0
  151. package/dist/cli/subcommands/models.js +144 -0
  152. package/dist/cli/subcommands/sessions.d.ts +11 -0
  153. package/dist/cli/subcommands/sessions.js +100 -0
  154. package/dist/codemode/capability.d.ts +37 -0
  155. package/dist/codemode/capability.js +55 -0
  156. package/dist/codemode/declarations.d.ts +44 -0
  157. package/dist/codemode/declarations.js +111 -0
  158. package/dist/codemode/host-side.d.ts +59 -0
  159. package/dist/codemode/host-side.js +253 -0
  160. package/dist/codemode/modes.d.ts +23 -0
  161. package/dist/codemode/modes.js +40 -0
  162. package/dist/codemode/protocol.d.ts +91 -0
  163. package/dist/codemode/protocol.js +114 -0
  164. package/dist/codemode/sandbox-entry.d.ts +25 -0
  165. package/dist/codemode/sandbox-entry.js +323 -0
  166. package/dist/codemode/store.d.ts +26 -0
  167. package/dist/codemode/store.js +53 -0
  168. package/dist/codemode/tool.d.ts +105 -0
  169. package/dist/codemode/tool.js +283 -0
  170. package/dist/compaction/branch-summary.d.ts +30 -0
  171. package/dist/compaction/branch-summary.js +79 -0
  172. package/dist/compaction/breaker.d.ts +32 -0
  173. package/dist/compaction/breaker.js +65 -0
  174. package/dist/compaction/cut-point.d.ts +23 -0
  175. package/dist/compaction/cut-point.js +70 -0
  176. package/dist/compaction/estimate.d.ts +30 -0
  177. package/dist/compaction/estimate.js +114 -0
  178. package/dist/compaction/prune-tier.d.ts +31 -0
  179. package/dist/compaction/prune-tier.js +83 -0
  180. package/dist/compaction/serialize.d.ts +20 -0
  181. package/dist/compaction/serialize.js +115 -0
  182. package/dist/compaction/summarize-tier.d.ts +111 -0
  183. package/dist/compaction/summarize-tier.js +305 -0
  184. package/dist/config/auth-file.d.ts +39 -0
  185. package/dist/config/auth-file.js +92 -0
  186. package/dist/config/context-files.d.ts +30 -0
  187. package/dist/config/context-files.js +77 -0
  188. package/dist/config/load.d.ts +45 -0
  189. package/dist/config/load.js +97 -0
  190. package/dist/config/merge.d.ts +73 -0
  191. package/dist/config/merge.js +248 -0
  192. package/dist/config/paths.d.ts +38 -0
  193. package/dist/config/paths.js +108 -0
  194. package/dist/config/profile.d.ts +27 -0
  195. package/dist/config/profile.js +65 -0
  196. package/dist/config/schema.d.ts +34 -0
  197. package/dist/config/schema.js +413 -0
  198. package/dist/config/trust.d.ts +42 -0
  199. package/dist/config/trust.js +122 -0
  200. package/dist/config/types.d.ts +172 -0
  201. package/dist/config/types.js +27 -0
  202. package/dist/config/write.d.ts +10 -0
  203. package/dist/config/write.js +25 -0
  204. package/dist/errors.d.ts +28 -0
  205. package/dist/errors.js +30 -0
  206. package/dist/hooks/config.d.ts +49 -0
  207. package/dist/hooks/config.js +102 -0
  208. package/dist/hooks/dispatcher.d.ts +41 -0
  209. package/dist/hooks/dispatcher.js +104 -0
  210. package/dist/hooks/matcher.d.ts +22 -0
  211. package/dist/hooks/matcher.js +108 -0
  212. package/dist/hooks/protocol.d.ts +33 -0
  213. package/dist/hooks/protocol.js +240 -0
  214. package/dist/hooks/runner.d.ts +30 -0
  215. package/dist/hooks/runner.js +143 -0
  216. package/dist/hooks/types.d.ts +147 -0
  217. package/dist/hooks/types.js +20 -0
  218. package/dist/host/api-impl.d.ts +75 -0
  219. package/dist/host/api-impl.js +173 -0
  220. package/dist/host/loader.d.ts +31 -0
  221. package/dist/host/loader.js +147 -0
  222. package/dist/host/types.d.ts +169 -0
  223. package/dist/host/types.js +14 -0
  224. package/dist/host.d.ts +5 -0
  225. package/dist/host.js +4 -0
  226. package/dist/index.d.ts +41 -0
  227. package/dist/index.js +28 -0
  228. package/dist/modes/commands-core.d.ts +44 -0
  229. package/dist/modes/commands-core.js +171 -0
  230. package/dist/modes/interactive/approval-dialog.d.ts +48 -0
  231. package/dist/modes/interactive/approval-dialog.js +202 -0
  232. package/dist/modes/interactive/commands.d.ts +41 -0
  233. package/dist/modes/interactive/commands.js +202 -0
  234. package/dist/modes/interactive/completion.d.ts +53 -0
  235. package/dist/modes/interactive/completion.js +157 -0
  236. package/dist/modes/interactive/interactive-mode.d.ts +46 -0
  237. package/dist/modes/interactive/interactive-mode.js +487 -0
  238. package/dist/modes/interactive/key-dispatch.d.ts +29 -0
  239. package/dist/modes/interactive/key-dispatch.js +107 -0
  240. package/dist/modes/interactive/line/line-editor.d.ts +50 -0
  241. package/dist/modes/interactive/line/line-editor.js +232 -0
  242. package/dist/modes/interactive/line/line-mode.d.ts +20 -0
  243. package/dist/modes/interactive/line/line-mode.js +216 -0
  244. package/dist/modes/interactive/line/line-render.d.ts +35 -0
  245. package/dist/modes/interactive/line/line-render.js +171 -0
  246. package/dist/modes/interactive/line/paste-state.d.ts +26 -0
  247. package/dist/modes/interactive/line/paste-state.js +72 -0
  248. package/dist/modes/interactive/message-view.d.ts +77 -0
  249. package/dist/modes/interactive/message-view.js +269 -0
  250. package/dist/modes/interactive/pickers.d.ts +42 -0
  251. package/dist/modes/interactive/pickers.js +115 -0
  252. package/dist/modes/interactive/startup-ui.d.ts +28 -0
  253. package/dist/modes/interactive/startup-ui.js +238 -0
  254. package/dist/modes/interactive/status-bar.d.ts +48 -0
  255. package/dist/modes/interactive/status-bar.js +139 -0
  256. package/dist/modes/interactive/tool-view.d.ts +95 -0
  257. package/dist/modes/interactive/tool-view.js +344 -0
  258. package/dist/modes/print/json-event.d.ts +15 -0
  259. package/dist/modes/print/json-event.js +39 -0
  260. package/dist/modes/print/print-mode.d.ts +15 -0
  261. package/dist/modes/print/print-mode.js +96 -0
  262. package/dist/modes/rpc/commands.d.ts +40 -0
  263. package/dist/modes/rpc/commands.js +258 -0
  264. package/dist/modes/rpc/jsonl.d.ts +16 -0
  265. package/dist/modes/rpc/jsonl.js +55 -0
  266. package/dist/modes/rpc/rpc-mode.d.ts +22 -0
  267. package/dist/modes/rpc/rpc-mode.js +153 -0
  268. package/dist/modes/session-report.d.ts +55 -0
  269. package/dist/modes/session-report.js +278 -0
  270. package/dist/modes/shared.d.ts +13 -0
  271. package/dist/modes/shared.js +52 -0
  272. package/dist/modes/startup-ui-text.d.ts +16 -0
  273. package/dist/modes/startup-ui-text.js +103 -0
  274. package/dist/permissions/broker.d.ts +47 -0
  275. package/dist/permissions/broker.js +107 -0
  276. package/dist/permissions/dangerous.d.ts +47 -0
  277. package/dist/permissions/dangerous.js +370 -0
  278. package/dist/permissions/pipeline.d.ts +51 -0
  279. package/dist/permissions/pipeline.js +181 -0
  280. package/dist/permissions/preview.d.ts +43 -0
  281. package/dist/permissions/preview.js +463 -0
  282. package/dist/permissions/rules.d.ts +58 -0
  283. package/dist/permissions/rules.js +249 -0
  284. package/dist/permissions/types.d.ts +100 -0
  285. package/dist/permissions/types.js +18 -0
  286. package/dist/rpc.d.ts +171 -0
  287. package/dist/rpc.js +13 -0
  288. package/dist/sdk.d.ts +98 -0
  289. package/dist/sdk.js +195 -0
  290. package/dist/session/manager.d.ts +70 -0
  291. package/dist/session/manager.js +278 -0
  292. package/dist/session/migrate.d.ts +19 -0
  293. package/dist/session/migrate.js +53 -0
  294. package/dist/session/projection.d.ts +50 -0
  295. package/dist/session/projection.js +180 -0
  296. package/dist/session/store.d.ts +47 -0
  297. package/dist/session/store.js +210 -0
  298. package/dist/session/tree.d.ts +21 -0
  299. package/dist/session/tree.js +88 -0
  300. package/dist/session/types.d.ts +199 -0
  301. package/dist/session/types.js +18 -0
  302. package/dist/skills/discover.d.ts +60 -0
  303. package/dist/skills/discover.js +160 -0
  304. package/dist/skills/expand.d.ts +35 -0
  305. package/dist/skills/expand.js +46 -0
  306. package/dist/skills/frontmatter.d.ts +27 -0
  307. package/dist/skills/frontmatter.js +157 -0
  308. package/dist/skills/index-prompt.d.ts +13 -0
  309. package/dist/skills/index-prompt.js +42 -0
  310. package/dist/skills/templates.d.ts +52 -0
  311. package/dist/skills/templates.js +177 -0
  312. package/dist/tools/bash.d.ts +49 -0
  313. package/dist/tools/bash.js +222 -0
  314. package/dist/tools/edit-fuzzy.d.ts +36 -0
  315. package/dist/tools/edit-fuzzy.js +129 -0
  316. package/dist/tools/edit.d.ts +38 -0
  317. package/dist/tools/edit.js +241 -0
  318. package/dist/tools/file-mutex.d.ts +8 -0
  319. package/dist/tools/file-mutex.js +29 -0
  320. package/dist/tools/glob.d.ts +44 -0
  321. package/dist/tools/glob.js +217 -0
  322. package/dist/tools/grep.d.ts +39 -0
  323. package/dist/tools/grep.js +179 -0
  324. package/dist/tools/ignore.d.ts +40 -0
  325. package/dist/tools/ignore.js +174 -0
  326. package/dist/tools/ls.d.ts +21 -0
  327. package/dist/tools/ls.js +96 -0
  328. package/dist/tools/output-accumulator.d.ts +48 -0
  329. package/dist/tools/output-accumulator.js +106 -0
  330. package/dist/tools/paths.d.ts +20 -0
  331. package/dist/tools/paths.js +58 -0
  332. package/dist/tools/presets.d.ts +71 -0
  333. package/dist/tools/presets.js +128 -0
  334. package/dist/tools/process-tree.d.ts +35 -0
  335. package/dist/tools/process-tree.js +125 -0
  336. package/dist/tools/read.d.ts +28 -0
  337. package/dist/tools/read.js +181 -0
  338. package/dist/tools/registry.d.ts +43 -0
  339. package/dist/tools/registry.js +106 -0
  340. package/dist/tools/shell.d.ts +38 -0
  341. package/dist/tools/shell.js +91 -0
  342. package/dist/tools/task.d.ts +29 -0
  343. package/dist/tools/task.js +107 -0
  344. package/dist/tools/todo.d.ts +28 -0
  345. package/dist/tools/todo.js +107 -0
  346. package/dist/tools/truncate.d.ts +44 -0
  347. package/dist/tools/truncate.js +175 -0
  348. package/dist/tools/types.d.ts +135 -0
  349. package/dist/tools/types.js +19 -0
  350. package/dist/tools/write.d.ts +20 -0
  351. package/dist/tools/write.js +84 -0
  352. package/dist/tui/ansi.d.ts +29 -0
  353. package/dist/tui/ansi.js +420 -0
  354. package/dist/tui/component.d.ts +38 -0
  355. package/dist/tui/component.js +13 -0
  356. package/dist/tui/components/box.d.ts +26 -0
  357. package/dist/tui/components/box.js +65 -0
  358. package/dist/tui/components/container.d.ts +14 -0
  359. package/dist/tui/components/container.js +39 -0
  360. package/dist/tui/components/editor-buffer.d.ts +67 -0
  361. package/dist/tui/components/editor-buffer.js +300 -0
  362. package/dist/tui/components/editor-paste.d.ts +29 -0
  363. package/dist/tui/components/editor-paste.js +64 -0
  364. package/dist/tui/components/editor.d.ts +114 -0
  365. package/dist/tui/components/editor.js +530 -0
  366. package/dist/tui/components/key-value.d.ts +29 -0
  367. package/dist/tui/components/key-value.js +48 -0
  368. package/dist/tui/components/loader.d.ts +35 -0
  369. package/dist/tui/components/loader.js +71 -0
  370. package/dist/tui/components/markdown.d.ts +90 -0
  371. package/dist/tui/components/markdown.js +462 -0
  372. package/dist/tui/components/meter.d.ts +29 -0
  373. package/dist/tui/components/meter.js +47 -0
  374. package/dist/tui/components/overlay.d.ts +24 -0
  375. package/dist/tui/components/overlay.js +52 -0
  376. package/dist/tui/components/select-list.d.ts +57 -0
  377. package/dist/tui/components/select-list.js +179 -0
  378. package/dist/tui/components/spacer.d.ts +11 -0
  379. package/dist/tui/components/spacer.js +16 -0
  380. package/dist/tui/components/text.d.ts +30 -0
  381. package/dist/tui/components/text.js +65 -0
  382. package/dist/tui/keybindings.d.ts +66 -0
  383. package/dist/tui/keybindings.js +120 -0
  384. package/dist/tui/keys.d.ts +38 -0
  385. package/dist/tui/keys.js +275 -0
  386. package/dist/tui/stdin-buffer.d.ts +41 -0
  387. package/dist/tui/stdin-buffer.js +206 -0
  388. package/dist/tui/terminal.d.ts +79 -0
  389. package/dist/tui/terminal.js +153 -0
  390. package/dist/tui/theme.d.ts +31 -0
  391. package/dist/tui/theme.js +199 -0
  392. package/dist/tui/tui.d.ts +103 -0
  393. package/dist/tui/tui.js +363 -0
  394. package/dist/tui/vt-screen.d.ts +46 -0
  395. package/dist/tui/vt-screen.js +251 -0
  396. package/dist/tui.d.ts +30 -0
  397. package/dist/tui.js +31 -0
  398. package/dist/version.d.ts +8 -0
  399. package/dist/version.js +24 -0
  400. package/docs/codemode.md +102 -0
  401. package/docs/hooks.md +196 -0
  402. package/docs/host-api.md +161 -0
  403. package/docs/providers.md +316 -0
  404. package/docs/rpc.md +253 -0
  405. package/docs/session-format.md +119 -0
  406. package/docs/tui.md +168 -0
  407. package/package.json +101 -4
@@ -0,0 +1,278 @@
1
+ /**
2
+ * 会话报告与缓存提示文本(第三波 §1.10)。[W3-C2]
3
+ *
4
+ * line 模式、交互模式与 RPC 共用的纯文本层,不含渲染:
5
+ * - `/session`:会话、模型、消息、用量、上下文 + 「缓存」段(`describeSession`);
6
+ * - `/cache`:只有缓存段(`describeCache`);`/cache fingerprint`:最近一次真实请求的前缀指纹;
7
+ * - 消息区 / stderr 的一行提示:缓存未命中(只提示 ≥ 20k token 或 ≥ $0.10 的那次)、
8
+ * 上下文跨越 70% / 90%(`cache.missNotices` 为 false 时都不提示,统计不受影响)。
9
+ *
10
+ * 两列对齐用 B9a-2 的 `KeyValue`(按足够宽的宽度渲染再去行尾空白,结果是纯文本)。
11
+ */
12
+ import { AgentSessionImpl } from "../agent/session.js";
13
+ import { MISS_NOTICE_TOKENS, MISS_NOTICE_USD } from "../ai/cache/miss.js";
14
+ import { KeyValue } from "../tui/components/key-value.js";
15
+ const REPORT_WIDTH = 240;
16
+ /** `950`、`38.2k`、`150k`、`1.05M`(比状态栏多一位精度,面板与提示用)。 */
17
+ export function formatTokenCount(count) {
18
+ const trim = (text) => text.replace(/\.?0+$/, "");
19
+ if (count < 1000)
20
+ return String(Math.round(count));
21
+ if (count < 100_000)
22
+ return `${trim((count / 1000).toFixed(1))}k`;
23
+ if (count < 1_000_000)
24
+ return `${Math.round(count / 1000)}k`;
25
+ return `${trim((count / 1_000_000).toFixed(2))}M`;
26
+ }
27
+ export function formatUsd(cost) {
28
+ if (cost === undefined)
29
+ return "$?";
30
+ return cost >= 0.01 || cost === 0 ? `$${cost.toFixed(2)}` : `$${cost.toFixed(3)}`;
31
+ }
32
+ export function formatPercent(rate) {
33
+ return rate === undefined ? "—" : `${Math.round(rate * 100)}%`;
34
+ }
35
+ /** `45s`、`2m 10s`、`7 分钟`(≥ 10 分钟只到分钟)。 */
36
+ export function formatDuration(ms) {
37
+ const seconds = Math.max(0, Math.round(ms / 1000));
38
+ if (seconds < 60)
39
+ return `${seconds}s`;
40
+ const minutes = Math.floor(seconds / 60);
41
+ if (minutes >= 10)
42
+ return `${minutes}m`;
43
+ const rest = seconds % 60;
44
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
45
+ }
46
+ export function reportingLabel(reporting) {
47
+ return reporting === "reported" ? "reported" : reporting === "silent" ? "未报告" : "未知";
48
+ }
49
+ const REASON_LABELS = {
50
+ prefix_changed: "前缀变化",
51
+ model_changed: "切换模型",
52
+ idle: "空闲超时",
53
+ subtask: "子任务",
54
+ evicted: "服务端淘汰",
55
+ };
56
+ /** 未命中原因的短语(提示行括号里那段)。 */
57
+ export function missReasonText(miss) {
58
+ const minutes = Math.max(1, Math.round(miss.idleMs / 60_000));
59
+ switch (miss.reason) {
60
+ case "idle":
61
+ return `空闲 ${minutes} 分钟后`;
62
+ case "subtask":
63
+ return `子任务运行 ${minutes} 分钟后`;
64
+ case "model_changed":
65
+ return "切换模型后";
66
+ case "prefix_changed":
67
+ return miss.detail === "tools"
68
+ ? "工具表变化"
69
+ : miss.detail === "system"
70
+ ? "系统提示变化"
71
+ : "前缀变化";
72
+ case "evicted":
73
+ return "服务端已淘汰";
74
+ }
75
+ }
76
+ export function shouldNotifyMiss(miss) {
77
+ return (miss.missedTokens >= MISS_NOTICE_TOKENS ||
78
+ (miss.missedCost !== undefined && miss.missedCost >= MISS_NOTICE_USD));
79
+ }
80
+ /** `缓存未命中(空闲 7 分钟后):重计费 38.2k token(约 $0.11)`。 */
81
+ export function cacheMissNotice(miss) {
82
+ const cost = miss.missedCost === undefined ? "" : `(约 ${formatUsd(miss.missedCost)})`;
83
+ return `缓存未命中(${missReasonText(miss)}):重计费 ${formatTokenCount(miss.missedTokens)} token${cost}`;
84
+ }
85
+ /** `上下文已用 72%,约剩 9 回合(按最近 5 回合均值)`;估不出回合时给余量 token。 */
86
+ export function contextPressureNotice(event) {
87
+ const head = `上下文已用 ${Math.round(event.percent)}%`;
88
+ if (event.estimatedTurnsLeft !== undefined)
89
+ return `${head},约剩 ${event.estimatedTurnsLeft} 回合(按最近 5 回合均值)`;
90
+ if (event.remainingTokens !== undefined)
91
+ return `${head},余量 ${formatTokenCount(event.remainingTokens)} token`;
92
+ return head;
93
+ }
94
+ /**
95
+ * 缓存事件 → 消息区 / stderr 的一行提示;不提示时 undefined。`enabled` = `cache.missNotices`。
96
+ * 未命中只提示超过门槛的那次;70% 为 warn、90% 为 error;保温事件不进消息区(状态栏 ♨)。
97
+ */
98
+ export function cacheEventNotice(event, enabled) {
99
+ if (!enabled)
100
+ return undefined;
101
+ if (event.type === "cache_miss") {
102
+ const { type: _type, ...miss } = event;
103
+ return shouldNotifyMiss(miss) ? { level: "warn", text: cacheMissNotice(miss) } : undefined;
104
+ }
105
+ if (event.type === "context_pressure") {
106
+ return { level: event.threshold >= 90 ? "error" : "warn", text: contextPressureNotice(event) };
107
+ }
108
+ return undefined;
109
+ }
110
+ /** 保温请求发出后的一行(line 模式只在 `AMA_LOG=info|debug` 时写 stderr)。 */
111
+ export function warmSentNotice(event) {
112
+ if (event.phase !== "sent")
113
+ return undefined;
114
+ const read = event.usage === undefined ? "" : `读 ${formatTokenCount(event.usage.cacheRead)} token`;
115
+ const cost = event.cost === undefined ? "" : `${read !== "" ? "," : ""}${formatUsd(event.cost)}`;
116
+ return `缓存保温已刷新${read + cost !== "" ? `(${read}${cost})` : ""}`;
117
+ }
118
+ /** 消息区 / stderr 是否提示未命中与上下文余量(`cache.missNotices`,缺省 true)。 */
119
+ export function cacheNoticesEnabled(session) {
120
+ return session instanceof AgentSessionImpl ? session.cache.cacheSettings.missNotices : true;
121
+ }
122
+ const STOP_REASONS = {
123
+ retention_none: "缓存保留为 none",
124
+ payload_replaced: "请求体被 onPayload 替换",
125
+ thinking_budget: "思考预算随 max_tokens,不可重放",
126
+ reporting_unknown: "端点还没报过缓存",
127
+ reporting_silent: "端点不报缓存",
128
+ no_ttl: "模型目录没有缓存 TTL",
129
+ ttl_too_short: "缓存 TTL 太短",
130
+ max_duration: "已达保温时长上限",
131
+ late: "计时器迟到",
132
+ off: "已关闭",
133
+ stale: "上下文已变",
134
+ declined: "宿主否决",
135
+ error: "保温请求失败",
136
+ no_cache_hits: "连续保温零命中",
137
+ no_price: "缺价格,经济性不可算",
138
+ below_min_savings: "期望节省低于门槛",
139
+ };
140
+ export function warmStopText(reason) {
141
+ if (reason === undefined)
142
+ return "已停止";
143
+ return `已停止:${STOP_REASONS[reason] ?? reason}`;
144
+ }
145
+ /** `streaming · 下次 2m 10s · 期望节省 $0.18 ≥ $0.05 · 已发 2 次 $0.01`。 */
146
+ export function warmingText(status, now, minSavingsUsd) {
147
+ if (status.mode === "off")
148
+ return "off";
149
+ const parts = [status.mode];
150
+ if (status.state === "scheduled") {
151
+ if (status.nextWarmAt !== undefined)
152
+ parts.push(`下次 ${formatDuration(status.nextWarmAt - now)}`);
153
+ if (status.expectedSavingsUsd !== undefined) {
154
+ const floor = minSavingsUsd === undefined ? "" : ` ≥ ${formatUsd(minSavingsUsd)}`;
155
+ parts.push(`期望节省 ${formatUsd(status.expectedSavingsUsd)}${floor}`);
156
+ }
157
+ }
158
+ else if (status.state === "stopped")
159
+ parts.push(warmStopText(status.reason));
160
+ else
161
+ parts.push("待下一次请求");
162
+ if (status.sent !== undefined && status.sent > 0)
163
+ parts.push(`已发 ${status.sent} 次 ${formatUsd(status.costUsd)}`);
164
+ return parts.join(" · ");
165
+ }
166
+ function missesText(cache) {
167
+ if (cache.misses.count === 0)
168
+ return "0 次";
169
+ const by = Object.entries(cache.misses.byReason)
170
+ .filter(([, n]) => n !== undefined && n > 0)
171
+ .map(([reason, n]) => `${REASON_LABELS[reason] ?? reason} ${n}`)
172
+ .join(" · ");
173
+ const cost = cache.reBilledUsd === undefined ? "$?" : formatUsd(cache.reBilledUsd);
174
+ return `${cache.misses.count} 次,重计费 ${formatTokenCount(cache.reBilledTokens)} token ≈ ${cost}${by !== "" ? `(${by})` : ""}`;
175
+ }
176
+ /** 缓存段的键值行(`/session` 与 `/cache` 共用)。 */
177
+ export function cacheRows(session, now = Date.now()) {
178
+ const stats = session.getStats();
179
+ const t = stats.tokens;
180
+ const prompt = t.input + t.cacheRead + t.cacheWrite;
181
+ const uncached = t.input + t.cacheWrite;
182
+ const readShare = prompt > 0 ? `(${formatPercent(t.cacheRead / prompt)})` : " ";
183
+ const write = t.cacheWrite > 0 ? `(其中写入 ${formatTokenCount(t.cacheWrite)})` : "";
184
+ const rows = [
185
+ {
186
+ key: "输入",
187
+ value: `${formatTokenCount(prompt)} = 缓存读 ${formatTokenCount(t.cacheRead)}${readShare}+ 未缓存 ${formatTokenCount(uncached)}${write}`,
188
+ },
189
+ ];
190
+ const cache = stats.cache;
191
+ if (cache === undefined) {
192
+ rows.push({ key: "命中率", value: `会话 ${formatPercent(stats.cacheHitRate)}` });
193
+ return rows;
194
+ }
195
+ rows.push({ key: "报告状态", value: reportingLabel(cache.reporting) });
196
+ if (cache.reporting === "reported") {
197
+ rows.push({
198
+ key: "命中率",
199
+ value: `最近 ${formatPercent(cache.lastHitRate)} · 会话 ${formatPercent(cache.hitRate)}`,
200
+ });
201
+ }
202
+ rows.push({ key: "未命中", value: missesText(cache) });
203
+ const minSavings = session instanceof AgentSessionImpl ? session.cache.cacheSettings.minSavingsUsd : undefined;
204
+ rows.push({ key: "保温", value: warmingText(cache.warming, now, minSavings) });
205
+ if (stats.contextPercent !== undefined) {
206
+ const remaining = cache.contextRemainingTokens === undefined
207
+ ? ""
208
+ : `,余量 ≈ ${formatTokenCount(cache.contextRemainingTokens)} token`;
209
+ const turns = cache.estimatedTurnsLeft === undefined ? "" : ` ≈ ${cache.estimatedTurnsLeft} 回合`;
210
+ rows.push({ key: "上下文", value: `${Math.round(stats.contextPercent)}%${remaining}${turns}` });
211
+ }
212
+ if (cache.subagents !== undefined) {
213
+ const sub = cache.subagents;
214
+ const rebill = sub.reBilledTokens > 0 ? `,重计费 ${formatTokenCount(sub.reBilledTokens)} token` : "";
215
+ rows.push({
216
+ key: "子任务",
217
+ value: `${sub.count} 个会话,命中率 ${formatPercent(sub.hitRate)}${rebill}`,
218
+ });
219
+ }
220
+ return rows;
221
+ }
222
+ /** 键值行 → 纯文本(键列对齐、去行尾空白)。 */
223
+ export function renderRows(rows, indent = "") {
224
+ return new KeyValue(rows)
225
+ .render(REPORT_WIDTH)
226
+ .map((line) => (line === "" ? "" : `${indent}${line}`.replace(/\s+$/, "")));
227
+ }
228
+ export function describeCache(session, now = Date.now()) {
229
+ return ["缓存", ...renderRows(cacheRows(session, now), " ")].join("\n");
230
+ }
231
+ export function describeSession(session, now = Date.now()) {
232
+ const state = session.state;
233
+ const stats = session.getStats();
234
+ const model = state.model === undefined ? "?" : `${state.model.provider}/${state.model.id}`;
235
+ const t = stats.tokens;
236
+ const percent = stats.contextPercent === undefined ? "?" : `${Math.round(stats.contextPercent)}`;
237
+ const rows = [
238
+ {
239
+ key: "会话",
240
+ value: `${state.sessionId}${state.sessionFile !== undefined ? `(${state.sessionFile})` : "(未落盘)"}`,
241
+ },
242
+ {
243
+ key: "模型",
244
+ value: `${model} · 思考 ${state.thinkingLevel} · 权限 ${state.permissionMode}`,
245
+ },
246
+ {
247
+ key: "消息",
248
+ value: `用户 ${stats.userMessages} · 助手 ${stats.assistantMessages} · 工具调用 ${stats.toolCalls}`,
249
+ },
250
+ {
251
+ key: "用量",
252
+ value: `输入 ${t.input} · 输出 ${t.output} · 缓存读 ${t.cacheRead} · 缓存写 ${t.cacheWrite}` +
253
+ (stats.cost !== undefined ? ` · $${stats.cost.toFixed(4)}` : " · $?"),
254
+ },
255
+ {
256
+ key: "上下文",
257
+ value: `${stats.contextTokens ?? "?"} / ${stats.contextWindow ?? "?"}(${percent}%)`,
258
+ },
259
+ ];
260
+ return [...renderRows(rows), describeCache(session, now)].join("\n");
261
+ }
262
+ /** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型)。 */
263
+ export function describeFingerprint(session) {
264
+ if (!(session instanceof AgentSessionImpl))
265
+ return "当前会话不提供前缀指纹";
266
+ const record = session.cache.lastTurn;
267
+ if (record === undefined)
268
+ return "还没有真实请求(指纹在第一次请求后记录)";
269
+ const f = record.fingerprint;
270
+ return [
271
+ "前缀指纹(最近一次请求)",
272
+ ...renderRows([
273
+ { key: "system", value: f.system },
274
+ { key: "tools", value: f.tools },
275
+ { key: "model", value: f.model },
276
+ ], " "),
277
+ ].join("\n");
278
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 模式层共用的小工具:最后一条助手消息、信号处理、错误文本。[B6]
3
+ */
4
+ import type { AgentSession } from "../agent/types.js";
5
+ import type { AssistantMessage } from "../ai/types.js";
6
+ export declare function lastAssistant(session: AgentSession): AssistantMessage | undefined;
7
+ export declare function errorText(error: unknown): string;
8
+ /**
9
+ * 装 SIGINT / SIGTERM:调用 `onSignal(退出码)`;返回卸载函数。只在当前模式运行期间生效
10
+ * (设计 §11.1 第 1 步:信号交给当前模式)。
11
+ */
12
+ export declare function onTerminationSignals(onSignal: (exitCode: number) => void): () => void;
13
+ export declare function onStdoutClosed(onClosed: () => void): () => void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 模式层共用的小工具:最后一条助手消息、信号处理、错误文本。[B6]
3
+ */
4
+ import { ExitCode } from "../cli/exit-codes.js";
5
+ export function lastAssistant(session) {
6
+ for (let i = session.messages.length - 1; i >= 0; i--) {
7
+ const message = session.messages[i];
8
+ if (message?.role === "assistant")
9
+ return message;
10
+ }
11
+ return undefined;
12
+ }
13
+ export function errorText(error) {
14
+ return error instanceof Error ? error.message : String(error);
15
+ }
16
+ /**
17
+ * 装 SIGINT / SIGTERM:调用 `onSignal(退出码)`;返回卸载函数。只在当前模式运行期间生效
18
+ * (设计 §11.1 第 1 步:信号交给当前模式)。
19
+ */
20
+ export function onTerminationSignals(onSignal) {
21
+ const sigint = () => onSignal(ExitCode.Sigint);
22
+ const sigterm = () => onSignal(ExitCode.Sigterm);
23
+ process.on("SIGINT", sigint);
24
+ process.on("SIGTERM", sigterm);
25
+ return () => {
26
+ process.off("SIGINT", sigint);
27
+ process.off("SIGTERM", sigterm);
28
+ };
29
+ }
30
+ /**
31
+ * stdout 被下游提前关闭(`| head`)时不当作未捕获异常:调用 onClosed 并吞掉 EPIPE。
32
+ * 进程内只装一个监听器、留到进程结束(之后的写入也可能再报 EPIPE);返回的函数只摘掉回调——
33
+ * 同一进程里多次运行 print 模式(SDK、测试)不会累积监听器。
34
+ */
35
+ const stdoutClosedHandlers = new Set();
36
+ let stdoutErrorListening = false;
37
+ export function onStdoutClosed(onClosed) {
38
+ if (!stdoutErrorListening) {
39
+ stdoutErrorListening = true;
40
+ process.stdout.on("error", (error) => {
41
+ if (error.code !== "EPIPE")
42
+ throw error;
43
+ for (const handler of [...stdoutClosedHandlers])
44
+ handler();
45
+ });
46
+ }
47
+ const handler = () => onClosed();
48
+ stdoutClosedHandlers.add(handler);
49
+ return () => {
50
+ stdoutClosedHandlers.delete(handler);
51
+ };
52
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 启动期问答的文本回退(`--no-tui`、stdout 非 TTY、`TERM=dumb`):问题写 stderr,从 stdin 读一行。
3
+ * 与 `interactive/startup-ui.ts` 同一组 `InteractiveUi` 回调,但不加载终端组件库。[W3-B9a-1]
4
+ *
5
+ * 读一行只消费到换行为止,多读的部分 `unshift` 回流里,留给随后的行式界面;EOF = 取消。
6
+ */
7
+ import type { InteractiveUi } from "../cli/deps.js";
8
+ export interface TextUiIo {
9
+ stdin: NodeJS.ReadableStream & {
10
+ readableEnded?: boolean;
11
+ };
12
+ write(text: string): void;
13
+ }
14
+ /** 读一行(不含行尾);EOF 且无内容返回 undefined。 */
15
+ export declare function readOneLine(stdin: TextUiIo["stdin"]): Promise<string | undefined>;
16
+ export declare function createTextStartupUi(io: TextUiIo): Required<InteractiveUi>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * 启动期问答的文本回退(`--no-tui`、stdout 非 TTY、`TERM=dumb`):问题写 stderr,从 stdin 读一行。
3
+ * 与 `interactive/startup-ui.ts` 同一组 `InteractiveUi` 回调,但不加载终端组件库。[W3-B9a-1]
4
+ *
5
+ * 读一行只消费到换行为止,多读的部分 `unshift` 回流里,留给随后的行式界面;EOF = 取消。
6
+ */
7
+ import { existsSync, statSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ /** 读一行(不含行尾);EOF 且无内容返回 undefined。 */
10
+ export function readOneLine(stdin) {
11
+ if (stdin.readableEnded === true)
12
+ return Promise.resolve(undefined);
13
+ return new Promise((done) => {
14
+ let buffer = "";
15
+ const finish = (line, rest) => {
16
+ stdin.off("data", onData);
17
+ stdin.off("end", onEnd);
18
+ stdin.pause();
19
+ if (rest !== "")
20
+ stdin.unshift(Buffer.from(rest));
21
+ done(line);
22
+ };
23
+ const onData = (chunk) => {
24
+ buffer += chunk.toString();
25
+ const at = buffer.indexOf("\n");
26
+ if (at >= 0)
27
+ finish(buffer.slice(0, at).replace(/\r$/, ""), buffer.slice(at + 1));
28
+ };
29
+ const onEnd = () => finish(buffer === "" ? undefined : buffer, "");
30
+ stdin.on("data", onData);
31
+ stdin.on("end", onEnd);
32
+ stdin.resume();
33
+ });
34
+ }
35
+ /** 按编号或值(前缀唯一)选一项;空行 / 无匹配 / EOF 返回 undefined。 */
36
+ function choose(answer, values) {
37
+ const text = answer?.trim() ?? "";
38
+ if (text === "")
39
+ return undefined;
40
+ const index = Number(text);
41
+ if (Number.isInteger(index) && index >= 1 && index <= values.length)
42
+ return values[index - 1];
43
+ if (values.includes(text))
44
+ return text;
45
+ const matches = values.filter((value) => value.startsWith(text));
46
+ return matches.length === 1 ? matches[0] : undefined;
47
+ }
48
+ function isDirectory(path) {
49
+ try {
50
+ return statSync(path).isDirectory();
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ export function createTextStartupUi(io) {
57
+ const ask = async (lines, question) => {
58
+ io.write([...lines, question].join("\n"));
59
+ return readOneLine(io.stdin);
60
+ };
61
+ const list = (values, labels) => values.map((_, i) => ` ${i + 1}. ${labels[i]}`);
62
+ return {
63
+ async promptTrust(cwd, resources) {
64
+ const shown = resources.slice(0, 6).map((p) => ` ${p}`);
65
+ if (resources.length > shown.length)
66
+ shown.push(` …另有 ${resources.length - shown.length} 项`);
67
+ const answer = await ask([`信任这个目录的项目资源?${cwd}`, ...shown], "[y 仅本次 / a 信任并记住 / N 不信任] ");
68
+ const key = answer?.trim().toLowerCase();
69
+ if (key === "a")
70
+ return { trusted: true, remember: true };
71
+ return { trusted: key === "y" || key === "yes", remember: false };
72
+ },
73
+ async pickSession(items) {
74
+ if (items.length === 0)
75
+ return undefined;
76
+ const sorted = [...items].sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
77
+ const ids = sorted.map((item) => item.id);
78
+ const labels = sorted.map((item) => `${item.id.slice(0, 8)} ${(item.name ?? item.firstPrompt ?? "").replace(/\s+/g, " ").slice(0, 60)}`);
79
+ return choose(await ask(["恢复哪个会话?", ...list(ids, labels)], "编号或 id:"), ids);
80
+ },
81
+ async pickModel(providers, reason) {
82
+ const refs = [];
83
+ for (const provider of providers.list())
84
+ for (const model of provider.models)
85
+ refs.push(`${provider.id}/${model.id}`);
86
+ if (refs.length === 0)
87
+ return undefined;
88
+ const answer = await ask([reason, "选择模型:", ...list(refs, refs)], "编号或 provider/id:");
89
+ return choose(answer, refs);
90
+ },
91
+ async askCwd(missing) {
92
+ const answer = (await ask([`会话的工作目录不存在:${missing}`], "替代目录(空行取消):"))?.trim();
93
+ if (answer === undefined || answer === "")
94
+ return undefined;
95
+ const abs = resolve(answer);
96
+ if (!existsSync(abs) || !isDirectory(abs)) {
97
+ io.write(`不是目录:${abs}\n`);
98
+ return undefined;
99
+ }
100
+ return abs;
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ApprovalBroker 链(设计 §6.3 第 4 步、§7.1)。[B3]
3
+ *
4
+ * - 回答者顺序:宿主 broker(`HostApi.approvals.setBroker`)→ UI broker(TUI 对话框 / RPC 客户端 /
5
+ * SDK 回调)→ 无人值守 deny。回答者返回 undefined 表示交给下一个;抛错按 deny(fail-safe)。
6
+ * - 超时(缺省 10 min)→ deny;父 signal abort → deny。
7
+ * - 审批**串行化**:同一时刻只有一个请求在等回答(task 子 Agent 的审批排到父对话框之后)。
8
+ * - `allow_session` → 调 `pipeline.rememberForSession()` 后放行。
9
+ * - `authorize()`:管线判定 + 需要时走 broker,给 tool-runner(B2)一个单入口。
10
+ */
11
+ import type { ApprovalBroker, ApprovalDecision, ApprovalRequest, PermissionCheckInput, PermissionPipelineApi, PermissionVerdict } from "./types.js";
12
+ export declare const DEFAULT_APPROVAL_TIMEOUT_MS: number;
13
+ export interface BrokerChainOptions {
14
+ timeoutMs?: number;
15
+ onRequested?(request: ApprovalRequest): void;
16
+ onResolved?(request: ApprovalRequest, decision: ApprovalDecision, by: AnsweredBy): void;
17
+ log?(level: "debug" | "info" | "warn", message: string): void;
18
+ }
19
+ export type AnsweredBy = "host" | "ui" | "timeout" | "aborted" | "unattended" | "error";
20
+ export declare class ApprovalBrokerChain {
21
+ private readonly options;
22
+ private host;
23
+ private ui;
24
+ private queue;
25
+ constructor(options?: BrokerChainOptions);
26
+ setHostBroker(broker: ApprovalBroker | undefined): void;
27
+ setUiBroker(broker: ApprovalBroker | undefined): void;
28
+ /** 有没有可能回答的人;没有则调用方应按无人值守处理。 */
29
+ hasResponder(): boolean;
30
+ /** 串行排队后询问;总会给出决定。 */
31
+ ask(request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalDecision>;
32
+ private askNow;
33
+ }
34
+ export interface AuthorizeResult {
35
+ allowed: boolean;
36
+ verdict: PermissionVerdict;
37
+ /** 走了 broker 时的回答。 */
38
+ decision?: ApprovalDecision;
39
+ /** 拒绝时给 tool_result 的说明。 */
40
+ message?: string;
41
+ }
42
+ export interface AuthorizeOptions {
43
+ signal: AbortSignal;
44
+ requestId?: string;
45
+ }
46
+ /** 管线 + broker 的单入口。`input.unattended` 由调用方按「是否有回答者」等决定。 */
47
+ export declare function authorize(pipeline: PermissionPipelineApi, chain: ApprovalBrokerChain, input: PermissionCheckInput, options: AuthorizeOptions): Promise<AuthorizeResult>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ApprovalBroker 链(设计 §6.3 第 4 步、§7.1)。[B3]
3
+ *
4
+ * - 回答者顺序:宿主 broker(`HostApi.approvals.setBroker`)→ UI broker(TUI 对话框 / RPC 客户端 /
5
+ * SDK 回调)→ 无人值守 deny。回答者返回 undefined 表示交给下一个;抛错按 deny(fail-safe)。
6
+ * - 超时(缺省 10 min)→ deny;父 signal abort → deny。
7
+ * - 审批**串行化**:同一时刻只有一个请求在等回答(task 子 Agent 的审批排到父对话框之后)。
8
+ * - `allow_session` → 调 `pipeline.rememberForSession()` 后放行。
9
+ * - `authorize()`:管线判定 + 需要时走 broker,给 tool-runner(B2)一个单入口。
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ export const DEFAULT_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
13
+ export class ApprovalBrokerChain {
14
+ options;
15
+ host;
16
+ ui;
17
+ queue = Promise.resolve();
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ }
21
+ setHostBroker(broker) {
22
+ this.host = broker;
23
+ }
24
+ setUiBroker(broker) {
25
+ this.ui = broker;
26
+ }
27
+ /** 有没有可能回答的人;没有则调用方应按无人值守处理。 */
28
+ hasResponder() {
29
+ return this.host !== undefined || this.ui !== undefined;
30
+ }
31
+ /** 串行排队后询问;总会给出决定。 */
32
+ ask(request, signal) {
33
+ const run = this.queue.then(() => this.askNow(request, signal));
34
+ this.queue = run.catch(() => undefined);
35
+ return run;
36
+ }
37
+ async askNow(request, signal) {
38
+ this.options.onRequested?.(request);
39
+ const finish = (decision, by) => {
40
+ this.options.onResolved?.(request, decision, by);
41
+ return decision;
42
+ };
43
+ if (signal.aborted)
44
+ return finish("deny", "aborted");
45
+ const timeout = new AbortController();
46
+ const timer = setTimeout(() => timeout.abort(), this.options.timeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS);
47
+ const combined = AbortSignal.any([signal, timeout.signal]);
48
+ const stopped = new Promise((resolve) => {
49
+ if (combined.aborted)
50
+ resolve("stopped");
51
+ combined.addEventListener("abort", () => resolve("stopped"), { once: true });
52
+ });
53
+ try {
54
+ const responders = [
55
+ ["host", this.host],
56
+ ["ui", this.ui],
57
+ ];
58
+ for (const [by, broker] of responders) {
59
+ if (!broker)
60
+ continue;
61
+ let answer;
62
+ try {
63
+ answer = await Promise.race([broker.ask(request, combined), stopped]);
64
+ }
65
+ catch (err) {
66
+ this.options.log?.("warn", `approval broker (${by}) failed: ${err.message}`);
67
+ return finish("deny", "error");
68
+ }
69
+ if (answer === "stopped")
70
+ return finish("deny", signal.aborted ? "aborted" : "timeout");
71
+ if (answer === "allow" || answer === "deny" || answer === "allow_session") {
72
+ return finish(answer, by);
73
+ }
74
+ }
75
+ return finish("deny", "unattended");
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ }
80
+ }
81
+ }
82
+ /** 管线 + broker 的单入口。`input.unattended` 由调用方按「是否有回答者」等决定。 */
83
+ export async function authorize(pipeline, chain, input, options) {
84
+ const verdict = pipeline.check(input);
85
+ if (verdict.decision === "allow")
86
+ return { allowed: true, verdict };
87
+ if (verdict.decision === "deny") {
88
+ return { allowed: false, verdict, message: verdict.message ?? "Permission denied" };
89
+ }
90
+ const reason = verdict.approvalReason ?? "mode";
91
+ const request = {
92
+ requestId: options.requestId ?? randomUUID(),
93
+ toolName: input.toolName,
94
+ input: input.input,
95
+ reason,
96
+ };
97
+ const hookReason = reason === "hook" ? (input.hookReason ?? verdict.message) : undefined;
98
+ if (hookReason !== undefined)
99
+ request.hookReason = hookReason;
100
+ const decision = await chain.ask(request, options.signal);
101
+ if (decision === "allow_session")
102
+ pipeline.rememberForSession(input.toolName, input.input);
103
+ if (decision === "deny") {
104
+ return { allowed: false, verdict, decision, message: "The tool call was not approved." };
105
+ }
106
+ return { allowed: true, verdict, decision };
107
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 危险命令表(设计 §7.2;v1 §7.1 的表 + `git branch -D`、`npm publish`、`docker system prune -a`、
3
+ * `shutdown/reboot`)。[B3]
4
+ *
5
+ * 命中 → 管线第 ② 步 ask(无人值守 deny),allow 规则不能越过。识别按段进行:命令先按
6
+ * `&&`、`||`、`;`、`|`、`&`、换行切段(引号内不切),每段剥掉前导的环境赋值与
7
+ * `sudo / command / exec / nohup / time / env` 等前缀后看命令名;`sh -c '…'`、`eval …`、`xargs …`、`find -exec … ;` 里的
8
+ * 命令递归识别,嵌套超过 {@link MAX_NESTING} 层按危险处理。整条命令另外做跨段检查
9
+ * (`curl … | sh`、fork 炸弹)。每条规则都有正例与反例测试(dangerous.test.ts)。
10
+ */
11
+ export interface DangerousRule {
12
+ id: string;
13
+ description: string;
14
+ /** 段级检查:`argv` 是剥掉前缀后的词,`segment` 是原段文本。 */
15
+ segment?(argv: readonly string[], segment: string): boolean;
16
+ /** 整条命令检查。 */
17
+ whole?(command: string): boolean;
18
+ }
19
+ export interface DangerousMatch {
20
+ id: string;
21
+ description: string;
22
+ }
23
+ /**
24
+ * 分词:按未加引号的空白切;单引号内原样,双引号内反斜杠只转义 `\`、`"`、`$`、反引号,引号外反斜杠转义下一个字符;
25
+ * 相邻的引号段与普通字符拼成一个词(`'a'"b"c` → `abc`)。不做变量与通配展开。
26
+ */
27
+ export declare function shellWords(segment: string): string[];
28
+ /** 剥前缀;`keepSudo` 为假时连 sudo 一并剥掉。 */
29
+ export declare function commandWords(segment: string, keepSudo?: boolean): string[];
30
+ export declare const DANGEROUS_RULES: readonly DangerousRule[];
31
+ /** 嵌套层数上限:`sh -c` / `eval` 等每包一层计一层,超过按危险处理。 */
32
+ export declare const MAX_NESTING = 3;
33
+ /**
34
+ * 段内嵌套的命令文本(剥过前缀的 argv):`sh / bash / zsh / dash / ksh -c '…'` 的字符串参数;
35
+ * `eval` 其余词以空格拼接;`xargs` 要执行的命令;`find -exec … ;` 里的命令。
36
+ */
37
+ export declare function nestedCommands(argv: readonly string[]): string[];
38
+ export interface NestedCommands {
39
+ /** 各层嵌套命令文本(不含最外层),按出现顺序。 */
40
+ commands: string[];
41
+ /** 是否有超过 {@link MAX_NESTING} 层的嵌套(其内容未展开)。 */
42
+ tooDeep: boolean;
43
+ }
44
+ /** 整条命令里所有嵌套的命令文本,供 allow / deny 规则与会话记忆逐层核对。 */
45
+ export declare function collectNestedCommands(command: string): NestedCommands;
46
+ /** 第一条命中的危险规则;无则 undefined。嵌套命令({@link nestedCommands})递归识别。 */
47
+ export declare function matchDangerous(command: string, depth?: number): DangerousMatch | undefined;