@arnilo/prism-tools 0.0.0-stage → 0.13.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 (331) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +28 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +545 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,623 @@
1
+ /**
2
+ * `ask_user_decision` tool: the model proposes choices with pros/cons and the host UI asks the
3
+ * person through `select` (options) or `input` (a custom answer).
4
+ *
5
+ * A host without a `ui` port suspends the run through the kernel path: the package's `tool.call`
6
+ * hook raises `{ ask }`, the tool's `elicitation` contract carries the answer schema, and
7
+ * `session.resume(decisions)` delivers the answer without executing the tool. No `prism-core`
8
+ * dependency and no host callback.
9
+ *
10
+ * Not included in `createCodingTools` / `createAllTools` / `createReadOnlyTools`.
11
+ */
12
+ import type { HostUi, JsonObject, ToolDefinition, ToolExecutionContext, ToolResult } from "@arnilo/prism";
13
+ import { CODING_OBSERVATION_EFFECT } from "./effects.js";
14
+ import { enforceExecutionPolicy } from "./execution-policy.js";
15
+ import { validateCodingLimit } from "./limits.js";
16
+
17
+ import type { ExecutionPolicy } from "./execution-policy.js";
18
+ export const ASK_USER_DECISION_TOOL_NAME = "ask_user_decision" as const;
19
+ export const ASK_USER_DECISION_SUSPEND_REASON = "ask_user_decision" as const;
20
+
21
+ /** Exactly three rationale bullets per side. */
22
+ export const ASK_USER_DECISION_RATIONALE_COUNT = 3 as const; // max pros/cons per option
23
+
24
+ export const DEFAULT_MAX_ASK_USER_DECISION_OPTIONS = 6;
25
+ export const HARD_MAX_ASK_USER_DECISION_OPTIONS = 16;
26
+ export const DEFAULT_MAX_ASK_USER_DECISION_QUESTION_BYTES = 2_048;
27
+ export const HARD_MAX_ASK_USER_DECISION_QUESTION_BYTES = 8_192;
28
+ export const DEFAULT_MAX_ASK_USER_DECISION_LABEL_BYTES = 512;
29
+ export const HARD_MAX_ASK_USER_DECISION_LABEL_BYTES = 2_048;
30
+ export const DEFAULT_MAX_ASK_USER_DECISION_BULLET_BYTES = 512;
31
+ export const HARD_MAX_ASK_USER_DECISION_BULLET_BYTES = 2_048;
32
+ /** Same ceiling as question text — free-text answers stay short. */
33
+ export const DEFAULT_MAX_ASK_USER_DECISION_CUSTOM_BYTES = DEFAULT_MAX_ASK_USER_DECISION_QUESTION_BYTES;
34
+ export const HARD_MAX_ASK_USER_DECISION_CUSTOM_BYTES = HARD_MAX_ASK_USER_DECISION_QUESTION_BYTES;
35
+
36
+ export type AskUserDecisionSelectionMode = "single" | "multiple";
37
+
38
+ export interface AskUserDecisionOption {
39
+ readonly id: string;
40
+ readonly label: string;
41
+ /** 0..3 advantages; the host shows whatever is given. */
42
+ readonly pros: readonly string[];
43
+ /** 0..3 disadvantages; the host shows whatever is given. */
44
+ readonly cons: readonly string[];
45
+ }
46
+
47
+ export interface AskUserDecisionRequest {
48
+ readonly question: string;
49
+ readonly options: readonly AskUserDecisionOption[];
50
+ readonly selectionMode: AskUserDecisionSelectionMode;
51
+ readonly allowCustom: boolean;
52
+ readonly toolCallId: string;
53
+ readonly sessionId?: string;
54
+ readonly runId?: string;
55
+ readonly signal?: AbortSignal;
56
+ }
57
+
58
+ /**
59
+ * Host answer shapes. Custom text is XOR with selection (v1): never both.
60
+ * `allowCustom` must be true for `{ customText }`.
61
+ */
62
+ export type AskUserDecisionAnswer =
63
+ | { readonly selectedId: string; readonly selectedIds?: never; readonly customText?: never }
64
+ | { readonly selectedIds: readonly string[]; readonly selectedId?: never; readonly customText?: never }
65
+ | { readonly selectedId: string; readonly selectedIds: readonly string[]; readonly customText?: never }
66
+ | { readonly customText: string; readonly selectedId?: never; readonly selectedIds?: never };
67
+
68
+ export type ResolvedAskUserDecisionAnswer =
69
+ | { readonly kind: "selection"; readonly selectedId: string; readonly selectedIds: readonly string[] }
70
+ | { readonly kind: "custom"; readonly customText: string };
71
+
72
+ /** The sentinel option value that opens the `input` prompt when `allowCustom` is set. */
73
+ export const ASK_USER_DECISION_CUSTOM_VALUE = "__prism_custom__" as const;
74
+
75
+ export interface AskUserDecisionToolOptions {
76
+ /** Host UI port; without it the tool never asks and the run suspends instead. */
77
+ readonly ui?: HostUi;
78
+ readonly executionPolicy?: ExecutionPolicy;
79
+ readonly maxOptions?: number;
80
+ readonly maxQuestionBytes?: number;
81
+ readonly maxLabelBytes?: number;
82
+ readonly maxBulletBytes?: number;
83
+ readonly maxCustomTextBytes?: number;
84
+ }
85
+
86
+ export interface ResolvedAskUserDecisionLimits {
87
+ readonly maxOptions: number;
88
+ readonly maxQuestionBytes: number;
89
+ readonly maxLabelBytes: number;
90
+ readonly maxBulletBytes: number;
91
+ readonly maxCustomTextBytes: number;
92
+ }
93
+
94
+ export function resolveAskUserDecisionLimits(
95
+ options?: Pick<AskUserDecisionToolOptions, "maxOptions" | "maxQuestionBytes" | "maxLabelBytes" | "maxBulletBytes" | "maxCustomTextBytes">,
96
+ ): ResolvedAskUserDecisionLimits {
97
+ return {
98
+ maxOptions: validateCodingLimit(
99
+ "maxOptions",
100
+ options?.maxOptions ?? DEFAULT_MAX_ASK_USER_DECISION_OPTIONS,
101
+ HARD_MAX_ASK_USER_DECISION_OPTIONS,
102
+ ),
103
+ maxQuestionBytes: validateCodingLimit(
104
+ "maxQuestionBytes",
105
+ options?.maxQuestionBytes ?? DEFAULT_MAX_ASK_USER_DECISION_QUESTION_BYTES,
106
+ HARD_MAX_ASK_USER_DECISION_QUESTION_BYTES,
107
+ ),
108
+ maxLabelBytes: validateCodingLimit(
109
+ "maxLabelBytes",
110
+ options?.maxLabelBytes ?? DEFAULT_MAX_ASK_USER_DECISION_LABEL_BYTES,
111
+ HARD_MAX_ASK_USER_DECISION_LABEL_BYTES,
112
+ ),
113
+ maxBulletBytes: validateCodingLimit(
114
+ "maxBulletBytes",
115
+ options?.maxBulletBytes ?? DEFAULT_MAX_ASK_USER_DECISION_BULLET_BYTES,
116
+ HARD_MAX_ASK_USER_DECISION_BULLET_BYTES,
117
+ ),
118
+ maxCustomTextBytes: validateCodingLimit(
119
+ "maxCustomTextBytes",
120
+ options?.maxCustomTextBytes ?? DEFAULT_MAX_ASK_USER_DECISION_CUSTOM_BYTES,
121
+ HARD_MAX_ASK_USER_DECISION_CUSTOM_BYTES,
122
+ ),
123
+ };
124
+ }
125
+
126
+ function errorResult(toolCallId: string, message: string): ToolResult {
127
+ return {
128
+ toolCallId,
129
+ name: ASK_USER_DECISION_TOOL_NAME,
130
+ content: [{ type: "text", text: message }],
131
+ error: { message },
132
+ };
133
+ }
134
+
135
+ function assertByteLimit(label: string, text: string, maxBytes: number): void {
136
+ const bytes = Buffer.byteLength(text, "utf8");
137
+ if (bytes < 1 || bytes > maxBytes) {
138
+ throw new Error(`${label} must be 1..${maxBytes} UTF-8 bytes`);
139
+ }
140
+ if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(text)) {
141
+ throw new Error(`${label} contains control characters`);
142
+ }
143
+ }
144
+
145
+ function parseBullets(value: unknown, label: string, maxBytes: number): string[] {
146
+ if (value === undefined || value === null) return [];
147
+ if (!Array.isArray(value) || value.length > ASK_USER_DECISION_RATIONALE_COUNT) {
148
+ throw new Error(`${label} must be 0..${ASK_USER_DECISION_RATIONALE_COUNT} strings`);
149
+ }
150
+ const out: string[] = [];
151
+ for (let i = 0; i < value.length; i++) {
152
+ const item = value[i];
153
+ if (typeof item !== "string") {
154
+ throw new Error(`${label}[${i}] must be a string`);
155
+ }
156
+ const trimmed = item.trim();
157
+ assertByteLimit(`${label}[${i}]`, trimmed, maxBytes);
158
+ out.push(trimmed);
159
+ }
160
+ return out;
161
+ }
162
+
163
+ function parseSelectionMode(value: unknown): AskUserDecisionSelectionMode {
164
+ if (value === undefined || value === null) return "single";
165
+ if (value === "single" || value === "multiple") return value;
166
+ throw new Error('selectionMode must be "single" or "multiple"');
167
+ }
168
+
169
+ function hasSelectionFields(answer: AskUserDecisionAnswer | null | undefined): boolean {
170
+ if (!answer) return false;
171
+ if (typeof answer.selectedId === "string" && answer.selectedId.trim() !== "") return true;
172
+ return Array.isArray(answer.selectedIds) && answer.selectedIds.length > 0;
173
+ }
174
+
175
+ /** Normalize host answer against mode + allowCustom. Exported for tests. */
176
+ export function resolveAskUserDecisionAnswer(
177
+ answer: AskUserDecisionAnswer | null | undefined,
178
+ selectionMode: AskUserDecisionSelectionMode,
179
+ options: readonly AskUserDecisionOption[],
180
+ gates: { readonly allowCustom: boolean; readonly maxCustomTextBytes: number },
181
+ ): ResolvedAskUserDecisionAnswer {
182
+ const customRaw = answer && typeof answer.customText === "string" ? answer.customText.trim() : "";
183
+ const hasCustom = customRaw.length > 0;
184
+ const hasSelection = hasSelectionFields(answer);
185
+
186
+ if (hasCustom && hasSelection) {
187
+ throw new Error("customText is mutually exclusive with selectedId/selectedIds");
188
+ }
189
+
190
+ if (hasCustom) {
191
+ if (!gates.allowCustom) throw new Error("customText rejected (allowCustom=false)");
192
+ assertByteLimit("customText", customRaw, gates.maxCustomTextBytes);
193
+ return { kind: "custom", customText: customRaw };
194
+ }
195
+
196
+ const byId = new Map(options.map((o) => [o.id, o]));
197
+ const rawIds: string[] = [];
198
+
199
+ if (answer && Array.isArray(answer.selectedIds)) {
200
+ for (const id of answer.selectedIds) {
201
+ if (typeof id !== "string") throw new Error("selectedIds entries must be strings");
202
+ rawIds.push(id.trim());
203
+ }
204
+ }
205
+ if (answer && typeof answer.selectedId === "string") {
206
+ const id = answer.selectedId.trim();
207
+ if (rawIds.length === 0) rawIds.push(id);
208
+ else if (!(rawIds.length === 1 && rawIds[0] === id)) {
209
+ throw new Error("selectedId and selectedIds disagree");
210
+ }
211
+ }
212
+
213
+ if (rawIds.length === 0) {
214
+ throw new Error(
215
+ gates.allowCustom
216
+ ? "ask() must return selectedId/selectedIds or customText"
217
+ : selectionMode === "multiple"
218
+ ? "ask() must return non-empty selectedIds"
219
+ : "ask() must return selectedId",
220
+ );
221
+ }
222
+
223
+ const seen = new Set<string>();
224
+ const selectedIds: string[] = [];
225
+ for (const id of rawIds) {
226
+ if (!byId.has(id)) throw new Error(`ask() returned unknown selectedId: ${id}`);
227
+ if (seen.has(id)) throw new Error(`duplicate selectedId: ${id}`);
228
+ seen.add(id);
229
+ selectedIds.push(id);
230
+ }
231
+
232
+ if (selectionMode === "single" && selectedIds.length !== 1) {
233
+ // Single accepts selectedIds only when length is exactly 1.
234
+ throw new Error("single selectionMode requires exactly one selected id");
235
+ }
236
+
237
+ return { kind: "selection", selectedIds, selectedId: selectedIds[0]! };
238
+ }
239
+
240
+ function parseAllowCustom(value: unknown): boolean {
241
+ if (value === undefined || value === null) return false;
242
+ if (typeof value !== "boolean") throw new Error("allowCustom must be a boolean");
243
+ return value;
244
+ }
245
+
246
+ /** Parse + validate model args into a bounded decision request. Exported for tests. */
247
+ export function parseAskUserDecisionArgs(
248
+ args: Record<string, unknown>,
249
+ limits: ResolvedAskUserDecisionLimits,
250
+ ): {
251
+ question: string;
252
+ options: AskUserDecisionOption[];
253
+ selectionMode: AskUserDecisionSelectionMode;
254
+ allowCustom: boolean;
255
+ } {
256
+ if (typeof args.question !== "string") {
257
+ throw new Error("question must be a string");
258
+ }
259
+ const question = args.question.trim();
260
+ assertByteLimit("question", question, limits.maxQuestionBytes);
261
+ const selectionMode = parseSelectionMode(args.selectionMode);
262
+ const allowCustom = parseAllowCustom(args.allowCustom);
263
+
264
+ if (!Array.isArray(args.options)) {
265
+ throw new Error("options must be an array");
266
+ }
267
+ if (args.options.length < 2) {
268
+ throw new Error("options must include at least 2 choices");
269
+ }
270
+ if (args.options.length > limits.maxOptions) {
271
+ throw new Error(`options exceeds maxOptions (${limits.maxOptions})`);
272
+ }
273
+
274
+ const seen = new Set<string>();
275
+ const options: AskUserDecisionOption[] = [];
276
+ for (let i = 0; i < args.options.length; i++) {
277
+ const raw = args.options[i];
278
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
279
+ throw new Error(`options[${i}] must be an object`);
280
+ }
281
+ const row = raw as Record<string, unknown>;
282
+ if (typeof row.id !== "string") throw new Error(`options[${i}].id must be a string`);
283
+ const id = row.id.trim();
284
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(id)) {
285
+ throw new Error(`options[${i}].id has invalid format`);
286
+ }
287
+ if (seen.has(id)) throw new Error(`duplicate option id: ${id}`);
288
+ seen.add(id);
289
+
290
+ if (typeof row.label !== "string") throw new Error(`options[${i}].label must be a string`);
291
+ const label = row.label.trim();
292
+ assertByteLimit(`options[${i}].label`, label, limits.maxLabelBytes);
293
+
294
+ options.push({
295
+ id,
296
+ label,
297
+ pros: parseBullets(row.pros, `options[${i}].pros`, limits.maxBulletBytes),
298
+ cons: parseBullets(row.cons, `options[${i}].cons`, limits.maxBulletBytes),
299
+ });
300
+ }
301
+ return { question, options, selectionMode, allowCustom };
302
+ }
303
+
304
+ /** How the host answered an `ask_user_decision` request, or that no UI could answer. */
305
+ export type AskUserDecisionUiOutcome =
306
+ | { readonly kind: "answered"; readonly result: ToolResult }
307
+ | { readonly kind: "declined"; readonly result: ToolResult }
308
+ | { readonly kind: "unavailable" };
309
+
310
+ /** Build the successful tool result for a resolved answer. */
311
+ function buildAnswerResult(
312
+ toolCallId: string,
313
+ resolved: ResolvedAskUserDecisionAnswer,
314
+ parsed: { question: string; options: AskUserDecisionOption[]; selectionMode: AskUserDecisionSelectionMode; allowCustom: boolean },
315
+ ): ToolResult {
316
+ if (resolved.kind === "custom") {
317
+ return {
318
+ toolCallId,
319
+ name: ASK_USER_DECISION_TOOL_NAME,
320
+ content: [{ type: "text", text: `User provided custom answer: ${resolved.customText}` }],
321
+ metadata: {
322
+ customText: resolved.customText,
323
+ selectionMode: parsed.selectionMode,
324
+ allowCustom: parsed.allowCustom,
325
+ question: parsed.question,
326
+ options: parsed.options,
327
+ },
328
+ };
329
+ }
330
+ const selected = resolved.selectedIds.map((id) => parsed.options.find((o) => o.id === id));
331
+ const labelText = selected.map((o) => `"${o?.label}" (id=${o?.id})`).join(", ");
332
+ return {
333
+ toolCallId,
334
+ name: ASK_USER_DECISION_TOOL_NAME,
335
+ content: [
336
+ {
337
+ type: "text",
338
+ text:
339
+ parsed.selectionMode === "multiple" ? `User selected ${selected.length} option(s): ${labelText}.` : `User selected ${labelText}.`,
340
+ },
341
+ ],
342
+ metadata: {
343
+ selectedId: resolved.selectedId,
344
+ selectedIds: resolved.selectedIds,
345
+ selectedLabels: selected.map((o) => o?.label),
346
+ selectionMode: parsed.selectionMode,
347
+ allowCustom: parsed.allowCustom,
348
+ question: parsed.question,
349
+ options: parsed.options,
350
+ },
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Ask through the host UI: `select` for the declared options (plus a custom sentinel when
356
+ * `allowCustom`), then `input` for a custom answer. A `dismissed` response yields a declined result;
357
+ * `unavailable` (or no UI) yields `kind: "unavailable"` so the caller can suspend the run instead of
358
+ * fabricating a choice. Exported so the `tool.call` gate and the tool share one path.
359
+ */
360
+ export async function askUserDecisionViaUi(
361
+ ui: HostUi | undefined,
362
+ parsed: { question: string; options: AskUserDecisionOption[]; selectionMode: AskUserDecisionSelectionMode; allowCustom: boolean },
363
+ limits: ResolvedAskUserDecisionLimits,
364
+ toolCallId: string,
365
+ ): Promise<AskUserDecisionUiOutcome> {
366
+ if (ui === undefined) return { kind: "unavailable" };
367
+ const selectOptions = parsed.options.map((option) => ({ value: option.id, label: option.label }));
368
+ if (parsed.allowCustom) selectOptions.push({ value: ASK_USER_DECISION_CUSTOM_VALUE, label: "Other (type a custom answer)" });
369
+ const response = await ui.request({
370
+ kind: "select",
371
+ title: parsed.question,
372
+ options: selectOptions,
373
+ ...(parsed.selectionMode === "multiple" ? { multiple: true } : {}),
374
+ detail: parsed.options.map((option) => `${option.label} (id=${option.id})`).join("; "),
375
+ });
376
+ if (response.status === "unavailable") return { kind: "unavailable" };
377
+ if (response.status === "dismissed") return { kind: "declined", result: errorResult(toolCallId, "the user declined to answer") };
378
+ const selected = typeof response.value === "string" ? [response.value] : [...response.value];
379
+ let answer: AskUserDecisionAnswer;
380
+ if (parsed.allowCustom && selected.includes(ASK_USER_DECISION_CUSTOM_VALUE)) {
381
+ const typed = await ui.request({ kind: "input", title: parsed.question, detail: "Type your answer." });
382
+ if (typed.status === "unavailable") return { kind: "unavailable" };
383
+ if (typed.status === "dismissed") return { kind: "declined", result: errorResult(toolCallId, "the user declined to answer") };
384
+ answer = { customText: typed.value };
385
+ } else {
386
+ answer = parsed.selectionMode === "multiple" ? { selectedIds: selected } : { selectedId: selected[0] ?? "" };
387
+ }
388
+ let resolved: ResolvedAskUserDecisionAnswer;
389
+ try {
390
+ resolved = resolveAskUserDecisionAnswer(answer, parsed.selectionMode, parsed.options, {
391
+ allowCustom: parsed.allowCustom,
392
+ maxCustomTextBytes: limits.maxCustomTextBytes,
393
+ });
394
+ } catch (error) {
395
+ return { kind: "declined", result: errorResult(toolCallId, error instanceof Error ? error.message : String(error)) };
396
+ }
397
+ return { kind: "answered", result: buildAnswerResult(toolCallId, resolved, parsed) };
398
+ }
399
+
400
+ /**
401
+ * Create the `ask_user_decision` tool. It asks through the supplied `host.ui`; without a UI the
402
+ * package's `tool.call` hook suspends the run instead, so `execute` never fabricates an answer.
403
+ */
404
+ export function createAskUserDecisionTool(options: AskUserDecisionToolOptions): ToolDefinition {
405
+ const limits = resolveAskUserDecisionLimits(options);
406
+ const ui = options?.ui;
407
+
408
+ return {
409
+ name: ASK_USER_DECISION_TOOL_NAME,
410
+ effect: CODING_OBSERVATION_EFFECT,
411
+ effects: [],
412
+ description:
413
+ "Ask the user to choose a direction when instructions are ambiguous and the choice matters. " +
414
+ "Provide 2+ options; each option may list up to 3 pros and up to 3 cons. " +
415
+ 'Use selectionMode "multiple" when several options may apply together; default is single choice. ' +
416
+ "Set allowCustom=true only when a short free-text alternative to the listed options is acceptable " +
417
+ "(custom answer is mutually exclusive with selecting option ids). " +
418
+ "Do not use for trivia, confirmations that are already clear, or when a single safe default exists.",
419
+ exclusive: true,
420
+ parameters: {
421
+ type: "object",
422
+ properties: {
423
+ question: {
424
+ type: "string",
425
+ description: "Clear decision question for the user",
426
+ },
427
+ selectionMode: {
428
+ type: "string",
429
+ enum: ["single", "multiple"],
430
+ description: "single (default) or multiple selection",
431
+ },
432
+ allowCustom: {
433
+ type: "boolean",
434
+ description: "When true, host may return customText instead of selecting option ids (XOR). Default false.",
435
+ },
436
+ options: {
437
+ type: "array",
438
+ minItems: 2,
439
+ maxItems: limits.maxOptions,
440
+ items: {
441
+ type: "object",
442
+ properties: {
443
+ id: {
444
+ type: "string",
445
+ description: "Stable option id returned when selected (e.g. keep_sqlite)",
446
+ },
447
+ label: {
448
+ type: "string",
449
+ description: "User-facing option label",
450
+ },
451
+ pros: {
452
+ type: "array",
453
+ minItems: 0,
454
+ maxItems: ASK_USER_DECISION_RATIONALE_COUNT,
455
+ items: { type: "string" },
456
+ description: "Up to 3 advantages of this option (optional)",
457
+ },
458
+ cons: {
459
+ type: "array",
460
+ minItems: 0,
461
+ maxItems: ASK_USER_DECISION_RATIONALE_COUNT,
462
+ items: { type: "string" },
463
+ description: "Up to 3 disadvantages of this option (optional)",
464
+ },
465
+ },
466
+ required: ["id", "label"],
467
+ additionalProperties: false,
468
+ },
469
+ description: `2..${limits.maxOptions} options with pros/cons`,
470
+ },
471
+ },
472
+ required: ["question", "options"],
473
+ additionalProperties: false,
474
+ } as JsonObject,
475
+ // Shared elicitation mapping (Phase 8): a durable gated run surfaces this call as a
476
+ // kind:"elicitation" pending decision; the answer payload resolves it without executing ask().
477
+ // The full UX payload (question + options with pros/cons) rides on the schema's extension key.
478
+ elicitation: (args) => {
479
+ let parsed: {
480
+ question: string;
481
+ options: AskUserDecisionOption[];
482
+ selectionMode: AskUserDecisionSelectionMode;
483
+ allowCustom: boolean;
484
+ };
485
+ try {
486
+ parsed = parseAskUserDecisionArgs(args as Record<string, unknown>, limits);
487
+ } catch {
488
+ return undefined; // malformed args fall back to plain tool approval; execute reports the parse error
489
+ }
490
+ return {
491
+ schema: {
492
+ ...askUserDecisionResumeSchema(parsed),
493
+ "x-prism-ask-user-decision": toAskUserDecisionSuspendData(parsed),
494
+ } as unknown as JsonObject,
495
+ reason: parsed.question,
496
+ validate: (payload) => {
497
+ resolveAskUserDecisionAnswer(payload as unknown as AskUserDecisionAnswer, parsed.selectionMode, parsed.options, {
498
+ allowCustom: parsed.allowCustom,
499
+ maxCustomTextBytes: limits.maxCustomTextBytes,
500
+ });
501
+ },
502
+ };
503
+ },
504
+ async execute(args, context: ToolExecutionContext): Promise<ToolResult> {
505
+ const toolCallId = context.toolCallId;
506
+ if (context.signal?.aborted) return errorResult(toolCallId, "Operation aborted");
507
+
508
+ let parsed: {
509
+ question: string;
510
+ options: AskUserDecisionOption[];
511
+ selectionMode: AskUserDecisionSelectionMode;
512
+ allowCustom: boolean;
513
+ };
514
+ try {
515
+ parsed = parseAskUserDecisionArgs(args as Record<string, unknown>, limits);
516
+ } catch (error) {
517
+ return errorResult(toolCallId, error instanceof Error ? error.message : String(error));
518
+ }
519
+
520
+ const policyCheck = await enforceExecutionPolicy(
521
+ options.executionPolicy,
522
+ {
523
+ kind: "ask_user_decision",
524
+ operation: "ask",
525
+ risk: "medium",
526
+ metadata: {
527
+ optionCount: parsed.options.length,
528
+ optionIds: parsed.options.map((o) => o.id),
529
+ selectionMode: parsed.selectionMode,
530
+ allowCustom: parsed.allowCustom,
531
+ sessionId: context.sessionId,
532
+ runId: context.runId,
533
+ signal: context.signal,
534
+ },
535
+ },
536
+ toolCallId,
537
+ ASK_USER_DECISION_TOOL_NAME,
538
+ );
539
+ if (!policyCheck.allowed) return policyCheck.result;
540
+
541
+ const outcome = await askUserDecisionViaUi(ui, parsed, limits, toolCallId);
542
+ if (outcome.kind === "unavailable") {
543
+ return errorResult(toolCallId, "the host is not available to answer; the run must suspend for an answer");
544
+ }
545
+ return outcome.result;
546
+ },
547
+ };
548
+ }
549
+
550
+ /** Durable decision payload for workflow suspension `data` (no AbortSignal / secrets). */
551
+ export interface AskUserDecisionSuspendData {
552
+ readonly question: string;
553
+ readonly options: readonly AskUserDecisionOption[];
554
+ readonly selectionMode: AskUserDecisionSelectionMode;
555
+ readonly allowCustom: boolean;
556
+ readonly toolCallId?: string;
557
+ readonly sessionId?: string;
558
+ readonly runId?: string;
559
+ }
560
+
561
+ /** JSON Schema describing resume `input` (= AskUserDecisionAnswer). */
562
+ export function askUserDecisionResumeSchema(
563
+ request: Pick<AskUserDecisionSuspendData, "selectionMode" | "allowCustom" | "options">,
564
+ ): JsonObject {
565
+ const optionIds = request.options.map((o) => o.id);
566
+ const selectionProps: JsonObject = {
567
+ selectedId: { type: "string", enum: optionIds },
568
+ selectedIds: {
569
+ type: "array",
570
+ minItems: 1,
571
+ maxItems: optionIds.length,
572
+ items: { type: "string", enum: optionIds },
573
+ },
574
+ };
575
+ if (!request.allowCustom) {
576
+ return {
577
+ type: "object",
578
+ additionalProperties: false,
579
+ properties: selectionProps,
580
+ // Host may send either field; tool/validator enforces mode + XOR with custom.
581
+ anyOf: [{ required: ["selectedId"] }, { required: ["selectedIds"] }],
582
+ };
583
+ }
584
+ return {
585
+ type: "object",
586
+ additionalProperties: false,
587
+ properties: {
588
+ ...selectionProps,
589
+ customText: {
590
+ type: "string",
591
+ minLength: 1,
592
+ maxLength: DEFAULT_MAX_ASK_USER_DECISION_CUSTOM_BYTES,
593
+ },
594
+ },
595
+ anyOf: [{ required: ["selectedId"] }, { required: ["selectedIds"] }, { required: ["customText"] }],
596
+ };
597
+ }
598
+
599
+ /**
600
+ * Normalize a suspend request into durable `AskUserDecisionSuspendData`.
601
+ * `allowCustom` follows the tool-path default: omitted/null becomes `false`
602
+ * (persisted data always carries a boolean); non-boolean values fail closed
603
+ * at accept time with an actionable message — never at resume time.
604
+ */
605
+ export function toAskUserDecisionSuspendData(request: {
606
+ readonly question: string;
607
+ readonly options: readonly AskUserDecisionOption[];
608
+ readonly selectionMode: AskUserDecisionSelectionMode;
609
+ readonly allowCustom?: boolean;
610
+ readonly toolCallId?: string;
611
+ readonly sessionId?: string;
612
+ readonly runId?: string;
613
+ }): AskUserDecisionSuspendData {
614
+ return {
615
+ question: request.question,
616
+ options: request.options,
617
+ selectionMode: request.selectionMode,
618
+ allowCustom: parseAllowCustom(request.allowCustom),
619
+ ...(request.toolCallId ? { toolCallId: request.toolCallId } : {}),
620
+ ...(request.sessionId ? { sessionId: request.sessionId } : {}),
621
+ ...(request.runId ? { runId: request.runId } : {}),
622
+ };
623
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Same-directory temp + rename for crash-safe UTF-8 file replacement.
3
+ * Custom WriteOperations/EditOperations should provide equivalent durability.
4
+ */
5
+ import { randomBytes } from "node:crypto";
6
+ import { rename as fsRename, unlink as fsUnlink, writeFile as fsWriteFile } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+
9
+ export async function atomicWriteUtf8File(targetPath: string, content: string, options?: { signal?: AbortSignal }): Promise<void> {
10
+ const dir = dirname(targetPath);
11
+ const tempPath = join(dir, `.prism-write-${randomBytes(8).toString("hex")}`);
12
+ try {
13
+ await fsWriteFile(tempPath, content, { encoding: "utf-8", signal: options?.signal });
14
+ if (options?.signal?.aborted) {
15
+ await fsUnlink(tempPath).catch(() => {});
16
+ throw new Error("Operation aborted");
17
+ }
18
+ await fsRename(tempPath, targetPath);
19
+ } catch (error) {
20
+ await fsUnlink(tempPath).catch(() => {});
21
+ throw error;
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import { open } from "node:fs/promises";
2
+
3
+ /** Read one regular-file snapshot no larger than `maxBytes`; always closes its handle. */
4
+ export async function readFileBounded(path: string, maxBytes: number, signal?: AbortSignal): Promise<Buffer> {
5
+ const handle = await open(path, "r");
6
+ try {
7
+ const size = (await handle.stat()).size;
8
+ if (size > maxBytes) throw new Error(`File exceeds ${maxBytes} byte limit`);
9
+ const buffer = Buffer.allocUnsafe(size);
10
+ let offset = 0;
11
+ while (offset < size) {
12
+ if (signal?.aborted) throw new Error("Operation aborted");
13
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
14
+ if (bytesRead === 0) break;
15
+ offset += bytesRead;
16
+ }
17
+ return buffer.subarray(0, offset);
18
+ } finally {
19
+ await handle.close();
20
+ }
21
+ }