one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -0,0 +1,632 @@
1
+ /**
2
+ * Requests on the wire as ledger deltas (design.md section 4.1). A request is
3
+ * split into its list (Anthropic and Chat Completions `messages`, Responses
4
+ * `input`, Google `contents`) and its head (everything else that is part of
5
+ * the prefix). Elements are stored once, content-addressed, with cache
6
+ * markers stripped: their placement moves every request and is the cache
7
+ * marker owner's job, not content.
8
+ *
9
+ * Pure: no pi imports.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { isObject, type RequestIdentity, type RequestRecordData, sameIdentity } from "./records.ts";
13
+
14
+ /** A provider request body as pi-ai built it. */
15
+ export type WirePayload = Record<string, unknown>;
16
+
17
+ /** A recorded request rebuilt from the branch: the list and head as sent, markers stripped. */
18
+ export interface RecordedRequest {
19
+ identity: RequestIdentity;
20
+ list: unknown[];
21
+ head: Record<string, unknown>;
22
+ /** Hashes of `list`, element by element. */
23
+ hashes: string[];
24
+ headHash: string;
25
+ }
26
+
27
+ /** The body key that holds the list for this API. */
28
+ export function listKey(api: string): "messages" | "input" | "contents" {
29
+ if (api.endsWith("-responses")) return "input";
30
+ if (api.startsWith("google-")) return "contents";
31
+ return "messages";
32
+ }
33
+
34
+ /** A Bedrock Converse `{ cachePoint }` block: a marker that is a whole list element. */
35
+ const isCachePoint = (value: unknown): boolean => isObject(value) && Object.keys(value).length === 1 && "cachePoint" in value;
36
+
37
+ /**
38
+ * Fields that hold the model's or a tool author's data, never a cache marker:
39
+ * a tool call's input (`input`, Google's `args`, Bedrock's `toolUse.input`, a
40
+ * Responses call's `arguments`) and a tool's schema. A `cache_control` key
41
+ * inside one is content.
42
+ */
43
+ const DATA_FIELDS: ReadonlySet<string> = new Set(["input", "args", "arguments", "input_schema", "inputSchema", "parameters"]);
44
+
45
+ /** `value` without cache markers (`cache_control`, Bedrock `cachePoint` blocks), deeply, except inside `DATA_FIELDS`. */
46
+ export function stripMarkers<T>(value: T): T {
47
+ if (Array.isArray(value)) return value.filter((item) => !isCachePoint(item)).map(stripMarkers) as T;
48
+ if (!isObject(value)) return value;
49
+ const out: Record<string, unknown> = {};
50
+ for (const [key, item] of Object.entries(value)) if (key !== "cache_control") out[key] = DATA_FIELDS.has(key) ? item : stripMarkers(item);
51
+ return out as T;
52
+ }
53
+
54
+ /** JSON with sorted keys and without cache markers, so neither key order nor a marker changes a hash; `undefined` fields drop out as in JSON. */
55
+ function canonical(value: unknown, data = false): string {
56
+ if (Array.isArray(value)) return `[${value.filter((item) => data || !isCachePoint(item)).map((item) => (item === undefined ? "null" : canonical(item, data))).join(",")}]`;
57
+ if (!isObject(value)) return JSON.stringify(value) ?? "null";
58
+ const fields = Object.keys(value)
59
+ .filter((key) => (data || key !== "cache_control") && value[key] !== undefined)
60
+ .sort()
61
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key], data || DATA_FIELDS.has(key))}`);
62
+ return `{${fields.join(",")}}`;
63
+ }
64
+
65
+ /** A stable content hash of `value`: object key order does not matter, cache markers are ignored. */
66
+ export function contentHash(value: unknown): string {
67
+ return createHash("sha256").update(canonical(value)).digest("hex");
68
+ }
69
+
70
+ /** Top-level output caps: a cache warm sends the same body with the cap at 1. */
71
+ const CAP_KEYS: ReadonlySet<string> = new Set(["max_tokens", "max_completion_tokens", "max_output_tokens", "maxTokens"]);
72
+
73
+ /** Fields nested one level down that are not content: Google's cap and abort signal, Bedrock's cap. */
74
+ const NESTED_OMIT: Record<string, readonly string[]> = {
75
+ config: ["maxOutputTokens", "abortSignal"],
76
+ inferenceConfig: ["maxTokens"],
77
+ };
78
+
79
+ /** The largest output cap pi's cache warmer sends: `maxTokens: 1`, which an API with a floor raises (Responses takes 16). */
80
+ const WARM_CAP = 16;
81
+
82
+ /** The payload's output caps, top-level and nested, wherever the API puts them. */
83
+ function outputCaps(payload: WirePayload): number[] {
84
+ const caps: number[] = [];
85
+ for (const [key, value] of Object.entries(payload)) {
86
+ if (CAP_KEYS.has(key) && typeof value === "number") caps.push(value);
87
+ const nested = NESTED_OMIT[key];
88
+ if (nested && isObject(value)) for (const field of nested) if (typeof value[field] === "number") caps.push(value[field] as number);
89
+ }
90
+ return caps;
91
+ }
92
+
93
+ /** Whether `payload` carries pi's cache-warm output cap. A real request can too (pi-ai clamps its cap to the window's room), so the caller also checks it is a call of its own. */
94
+ export function hasWarmCap(payload: WirePayload): boolean {
95
+ const caps = outputCaps(payload);
96
+ return caps.length > 0 && caps.every((cap) => cap <= WARM_CAP);
97
+ }
98
+
99
+ /** `body` with the request-only fields of `from` (its output caps, Google's abort signal) in place of its own. */
100
+ export function withRequestFields(body: WirePayload, from: WirePayload): WirePayload {
101
+ const out: WirePayload = { ...body };
102
+ for (const key of CAP_KEYS) {
103
+ if (key in from) out[key] = from[key];
104
+ else delete out[key];
105
+ }
106
+ for (const [key, fields] of Object.entries(NESTED_OMIT)) {
107
+ const source = from[key];
108
+ const target = out[key];
109
+ if (!isObject(source) || !isObject(target)) continue;
110
+ const merged: Record<string, unknown> = { ...target };
111
+ for (const field of fields) {
112
+ if (field in source) merged[field] = source[field];
113
+ else delete merged[field];
114
+ }
115
+ out[key] = merged;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** The head of a payload: every body field outside the list and the output cap, markers stripped. */
121
+ export function payloadHead(payload: WirePayload, api: string): Record<string, unknown> {
122
+ const list = listKey(api);
123
+ const head: Record<string, unknown> = {};
124
+ for (const [key, value] of Object.entries(payload)) {
125
+ if (key === list || CAP_KEYS.has(key)) continue;
126
+ const omit = NESTED_OMIT[key];
127
+ head[key] = omit && isObject(value) ? Object.fromEntries(Object.entries(value).filter(([field]) => !omit.includes(field))) : value;
128
+ }
129
+ return stripMarkers(head);
130
+ }
131
+
132
+ /** The payload's list, or an empty one when the body has none under this API's key. */
133
+ function payloadList(payload: WirePayload, api: string): unknown[] {
134
+ const list = payload[listKey(api)];
135
+ return Array.isArray(list) ? list : [];
136
+ }
137
+
138
+ /**
139
+ * `payload` as the ledger records it: its list and head, markers stripped,
140
+ * and their hashes. `prefix`: the payload's list starts with `base`'s first
141
+ * `end` elements (a splice's), so only the rest is stripped and hashed.
142
+ */
143
+ export function recordedRequest(payload: WirePayload, identity: RequestIdentity, prefix?: { base: RecordedRequest; end: number }): RecordedRequest {
144
+ const end = prefix?.end ?? 0;
145
+ const tail = stripMarkers(payloadList(payload, identity.api).slice(end));
146
+ const head = payloadHead(payload, identity.api);
147
+ const list = prefix ? prefix.base.list.slice(0, end).concat(tail) : tail;
148
+ const hashes = (prefix ? prefix.base.hashes.slice(0, end) : []).concat(tail.map(contentHash));
149
+ return { identity, list, head, hashes, headHash: contentHash(head) };
150
+ }
151
+
152
+ /**
153
+ * The record for `current`, or undefined when it adds nothing: the same list
154
+ * and head as `previous` (a pi cache warm or a provider retry re-running the
155
+ * hooks). `known` holds every hash already on the branch, so `blobs` carries
156
+ * only new bodies.
157
+ */
158
+ export function encodeRequest(
159
+ previous: RecordedRequest | undefined,
160
+ current: RecordedRequest,
161
+ known: { has(hash: string): boolean },
162
+ ): Omit<RequestRecordData, "v" | "by" | "consumed"> | undefined {
163
+ const { identity, list, head, hashes, headHash } = current;
164
+ let keep = 0;
165
+ if (previous) while (keep < hashes.length && keep < previous.hashes.length && hashes[keep] === previous.hashes[keep]) keep++;
166
+ const unchanged = previous && keep === hashes.length && keep === previous.hashes.length && headHash === previous.headHash && sameIdentity(identity, previous.identity);
167
+ if (unchanged) return undefined;
168
+
169
+ const blobs: Record<string, unknown> = {};
170
+ for (let i = keep; i < list.length; i++) if (!known.has(hashes[i])) blobs[hashes[i]] = list[i];
171
+ if (!known.has(headHash)) blobs[headHash] = head;
172
+ return { identity, keep, append: hashes.slice(keep), head: headHash, blobs };
173
+ }
174
+
175
+ /** APIs whose list starts with the system prompt as an instruction message (Chat Completions, Responses but not Codex, Mistral). */
176
+ const PROMPT_IN_LIST: ReadonlySet<string> = new Set(["openai-completions", "openai-responses", "azure-openai-responses", "mistral-conversations"]);
177
+
178
+ /**
179
+ * Whether `element` is a Responses `additional_tools` item: tools declared in
180
+ * the input. pi 1.4.0 and later declare a request's starting tools in one at
181
+ * the head of the input for models flagged for mid-conversation system
182
+ * messages and additional tools (findings §78); a `tool_search` load rides
183
+ * one at its load point on any version.
184
+ */
185
+ export function isToolsItem(element: unknown): element is Record<string, unknown> & { tools: unknown[] } {
186
+ return isObject(element) && element.type === "additional_tools" && Array.isArray(element.tools);
187
+ }
188
+
189
+ /** A Responses `additional_tools` item declaring `tools`, in the shape of `like` (pi's own item) when there is one. */
190
+ export function toolsItemFor(tools: unknown[], like?: Record<string, unknown>): Record<string, unknown> {
191
+ const { tools: _tools, ...shape } = like ?? { type: "additional_tools", role: "developer" };
192
+ return { ...shape, tools };
193
+ }
194
+
195
+ /**
196
+ * The `additional_tools` item a body declares its starting tools in (pi 1.4.0
197
+ * and later, findings §78), given its top-level `tools` and its list; undefined
198
+ * when it keeps them in a top-level array, or declares none at its head.
199
+ */
200
+ export function leadingToolsItem(tools: unknown, list: readonly unknown[]): (Record<string, unknown> & { tools: unknown[] }) | undefined {
201
+ const head = list[0];
202
+ return !Array.isArray(tools) && isToolsItem(head) ? head : undefined;
203
+ }
204
+
205
+ /** A plain instruction message: role system or developer, not a typed item such as `additional_tools`. */
206
+ const isInstruction = (element: unknown): boolean => isObject(element) && (element.role === "system" || element.role === "developer") && (element.type === undefined || element.type === "message");
207
+
208
+ /**
209
+ * How many leading elements of pi's list render its head: a Responses
210
+ * request's leading `additional_tools` item (pi 1.4.0 and later), then the
211
+ * system prompt where the API puts it in the list. A request that sends only
212
+ * its tail through pi still gets them, and the record already holds them.
213
+ * Read from the elements, never from pi's version: an element this does not
214
+ * recognize stays in the tail, which costs tokens but never a declaration.
215
+ */
216
+ export function preludeLength(list: readonly unknown[], api: string): number {
217
+ let at = api.endsWith("-responses") && isToolsItem(list[0]) ? 1 : 0;
218
+ if (PROMPT_IN_LIST.has(api) && isInstruction(list[at])) at++;
219
+ return at;
220
+ }
221
+
222
+ /**
223
+ * An element pi-ai appends to the end of every request and the next request
224
+ * replaces: Anthropic's directive-only effort message, the Responses
225
+ * `configuration_update` item. The next request's own copy comes before the
226
+ * reply it answers, so a recorded list ending in one drops it before the tail
227
+ * is appended.
228
+ */
229
+ export function isTrailer(element: unknown, api: string): boolean {
230
+ if (!isObject(element)) return false;
231
+ if (api === "anthropic-messages") return element.role === "system" && Array.isArray(element.content) && element.content.length === 0 && element.output_config !== undefined;
232
+ if (api.endsWith("-responses")) return element.type === "configuration_update";
233
+ return false;
234
+ }
235
+
236
+ /** A tool's name in any API's declaration shape, or undefined for a nameless entry (a Bedrock `cachePoint`). */
237
+ export function toolName(tool: unknown): string | undefined {
238
+ if (!isObject(tool)) return undefined;
239
+ if (typeof tool.name === "string") return tool.name;
240
+ if (isObject(tool.function) && typeof tool.function.name === "string") return tool.function.name;
241
+ if (isObject(tool.toolSpec) && typeof tool.toolSpec.name === "string") return tool.toolSpec.name;
242
+ return typeof tool.type === "string" && !("cachePoint" in tool) ? `type:${tool.type}` : undefined;
243
+ }
244
+
245
+ /** Google's function declarations, flattened out of its tool groups. */
246
+ export function functionDeclarations(groups: readonly unknown[]): unknown[] {
247
+ return groups.flatMap((group) => (isObject(group) && Array.isArray(group.functionDeclarations) ? group.functionDeclarations : []));
248
+ }
249
+
250
+ /** The object whose `tools` array declares a body's tools: Google's `config` (its tools are groups of function declarations), Bedrock's `toolConfig`, else the body itself. */
251
+ type ToolsHolder = "config" | "toolConfig" | "body";
252
+
253
+ const toolsHolder = (api: string): ToolsHolder => (api.startsWith("google-") ? "config" : api === "bedrock-converse-stream" ? "toolConfig" : "body");
254
+
255
+ /** The `tools` array of `body`'s `holder`, or undefined when there is none. */
256
+ function toolsIn(body: WirePayload, holder: ToolsHolder): unknown[] | undefined {
257
+ const at = holder === "body" ? body : body[holder];
258
+ return isObject(at) && Array.isArray(at.tools) ? at.tools : undefined;
259
+ }
260
+
261
+ /** `body` with `tools` as its `holder`'s tools array, the holder created when absent. */
262
+ function withToolsIn(body: WirePayload, holder: ToolsHolder, tools: unknown[]): WirePayload {
263
+ if (holder === "body") return { ...body, tools };
264
+ const at = body[holder];
265
+ return { ...body, [holder]: { ...(isObject(at) ? at : {}), tools } };
266
+ }
267
+
268
+ /** `body` with the tools array where `api` puts it mapped by `fn`: `body` itself when it has none or `fn` returns its input. */
269
+ function mapTools(body: WirePayload, api: string, fn: (tools: unknown[]) => unknown[]): WirePayload {
270
+ const holder = toolsHolder(api);
271
+ const tools = toolsIn(body, holder);
272
+ const mapped = tools && fn(tools);
273
+ return mapped === undefined || mapped === tools ? body : withToolsIn(body, holder, mapped);
274
+ }
275
+
276
+ /** Every declaration in a payload or head, whatever its API: `tools`, Bedrock `toolConfig.tools`, Google's function declarations. */
277
+ export function declarationsOf(payload: WirePayload): unknown[] {
278
+ return [...(toolsIn(payload, "body") ?? []), ...(toolsIn(payload, "toolConfig") ?? []), ...functionDeclarations(toolsIn(payload, "config") ?? [])];
279
+ }
280
+
281
+ /** The names a payload or head declares, in any API's shape; `eager` keeps only declarations not marked `defer_loading`. */
282
+ export function declaredToolNames(payload: WirePayload, { eager = false } = {}): Set<string> {
283
+ const names = new Set<string>();
284
+ for (const tool of declarationsOf(payload)) {
285
+ const name = eager && isObject(tool) && tool.defer_loading === true ? undefined : toolName(tool);
286
+ if (name !== undefined) names.add(name);
287
+ }
288
+ return names;
289
+ }
290
+
291
+ /** A declaration's input schema, in any API's shape: what pi validates a call's arguments against. */
292
+ function inputSchemaOf(tool: unknown): unknown {
293
+ if (!isObject(tool)) return undefined;
294
+ if (isObject(tool.function)) return tool.function.parameters;
295
+ if (isObject(tool.toolSpec)) return tool.toolSpec.inputSchema;
296
+ return tool.input_schema ?? tool.parameters ?? tool.parametersJsonSchema;
297
+ }
298
+
299
+ /** The live declaration with the recorded one's `defer_loading`: a load or a deferral moves that flag, never the tool. */
300
+ function withRecordedDeferral(live: unknown, recorded: unknown): unknown {
301
+ if (!isObject(live) || !isObject(recorded)) return live;
302
+ const { defer_loading: _live, ...rest } = live;
303
+ return recorded.defer_loading === undefined ? rest : { ...rest, defer_loading: recorded.defer_loading };
304
+ }
305
+
306
+ /**
307
+ * The recorded declarations in their recorded order and bytes, then the live
308
+ * ones the record lacks. A live declaration equal to its recorded one is kept
309
+ * as pi built it (its cache marker included); one that differs only in its
310
+ * text or its deferral takes the recorded bytes, so a description variant or a
311
+ * load never moves the tools array. One whose input schema changed (an
312
+ * upgrade, an MCP server's new schema) takes the live
313
+ * declaration, with the recorded deferral: pi validates and runs a call
314
+ * against the live schema, so the wire must offer that one. Nameless markers
315
+ * that end the live list (Bedrock `cachePoint`) end it.
316
+ */
317
+ export function mergeDeclarations(recorded: readonly unknown[], live: readonly unknown[], declaredElsewhere: ReadonlySet<string> = new Set()): unknown[] {
318
+ const liveByName = new Map<string, unknown>();
319
+ for (const tool of live) {
320
+ const name = toolName(tool);
321
+ if (name !== undefined && !liveByName.has(name)) liveByName.set(name, tool);
322
+ }
323
+ const out: unknown[] = [];
324
+ const seen = new Set<string>();
325
+ for (const tool of recorded) {
326
+ const name = toolName(tool);
327
+ if (name === undefined) continue;
328
+ seen.add(name);
329
+ const current = liveByName.get(name);
330
+ if (current !== undefined && contentHash(current) === contentHash(tool)) out.push(current);
331
+ else if (current !== undefined && contentHash(inputSchemaOf(current)) !== contentHash(inputSchemaOf(tool))) out.push(withRecordedDeferral(current, tool));
332
+ else out.push(structuredClone(tool));
333
+ }
334
+ for (const tool of live) {
335
+ const name = toolName(tool);
336
+ if (name === undefined || seen.has(name) || declaredElsewhere.has(name)) continue;
337
+ seen.add(name);
338
+ out.push(tool);
339
+ }
340
+ let trailing = live.length;
341
+ while (trailing > 0 && toolName(live[trailing - 1]) === undefined) trailing--;
342
+ return [...withToolsMarker(out, live), ...live.slice(trailing)];
343
+ }
344
+
345
+ /**
346
+ * An epoch's live declarations in the order the branch first declared them
347
+ * (`LedgerFold.toolOrder`), then the ones it never declared in pi's order;
348
+ * nameless trailing markers stay last. pi derives its tools from the
349
+ * transcript's tool deltas, where a changed definition is a removal and an
350
+ * addition (pi-ai `getCurrentTools`), so a tool whose text follows the model
351
+ * tier (bash, the description-form tools) moved to the end at every hop, and a
352
+ * hop back to the earlier model missed the prefix it had cached. The bytes stay
353
+ * pi's: only the order comes from the branch.
354
+ */
355
+ export function orderLikeBranch<T extends readonly unknown[]>(order: readonly string[], live: T): T | unknown[] {
356
+ const rank = new Map(order.map((name, index) => [name, index]));
357
+ let trailing = live.length;
358
+ while (trailing > 0 && toolName(live[trailing - 1]) === undefined) trailing--;
359
+ const named = live.slice(0, trailing).filter((tool) => toolName(tool) !== undefined);
360
+ const known = named.filter((tool) => rank.has(toolName(tool)!)).sort((a, b) => rank.get(toolName(a)!)! - rank.get(toolName(b)!)!);
361
+ const ordered = [...known, ...named.filter((tool) => !rank.has(toolName(tool)!))];
362
+ if (ordered.length === trailing && ordered.every((tool, index) => tool === live[index])) return live;
363
+ return [...withToolsMarker(ordered, live), ...live.slice(trailing)];
364
+ }
365
+
366
+ /** `payload` with its declarations in the branch's order (`orderLikeBranch`), in any API's shape. */
367
+ export function withBranchToolOrder(payload: WirePayload, order: readonly string[], api: string): WirePayload {
368
+ if (order.length === 0) return payload;
369
+ const reorder = (tools: unknown[]) => orderLikeBranch(order, tools);
370
+ const out = mapTools(payload, api, toolsHolder(api) === "config" ? (groups) => mapGroupDeclarations(groups, reorder) : reorder);
371
+ // A Responses request that declares its starting tools in a leading item.
372
+ const list = payloadList(out, api);
373
+ const head = api.endsWith("-responses") ? leadingToolsItem(out.tools, list) : undefined;
374
+ if (!head) return out;
375
+ const tools = reorder(head.tools);
376
+ return tools === head.tools ? out : { ...out, [listKey(api)]: [{ ...head, tools }, ...list.slice(1)] };
377
+ }
378
+
379
+ /** Google's tool groups with each group's function declarations mapped by `fn`: `groups` itself when `fn` returns every input. */
380
+ function mapGroupDeclarations(groups: unknown[], fn: (declarations: unknown[]) => unknown[]): unknown[] {
381
+ const out = groups.map((group) => {
382
+ if (!isObject(group) || !Array.isArray(group.functionDeclarations)) return group;
383
+ const declarations = fn(group.functionDeclarations);
384
+ return declarations === group.functionDeclarations ? group : { ...group, functionDeclarations: declarations };
385
+ });
386
+ return out.some((group, index) => group !== groups[index]) ? out : groups;
387
+ }
388
+
389
+ /** Google's recorded tool groups merged with the live ones: one group of the merged function declarations (none when empty), then the recorded groups without declarations. */
390
+ function mergeGroups(recorded: readonly unknown[], live: readonly unknown[]): unknown[] {
391
+ const merged = mergeDeclarations(functionDeclarations(recorded), functionDeclarations(live));
392
+ const others = recorded.filter((group) => !(isObject(group) && Array.isArray(group.functionDeclarations)));
393
+ return [...(merged.length ? [{ functionDeclarations: merged }] : []), ...others];
394
+ }
395
+
396
+ /**
397
+ * The merged declarations with pi's tools cache marker on the last one that is
398
+ * not deferred, where pi puts it: the merge can make another tool last, and
399
+ * Anthropic rejects a marker on a `defer_loading` tool.
400
+ */
401
+ function withToolsMarker(tools: unknown[], live: readonly unknown[]): unknown[] {
402
+ const marked = live.find((tool) => isObject(tool) && tool.cache_control !== undefined);
403
+ const mark = isObject(marked) ? marked.cache_control : undefined;
404
+ const out = tools.map((tool) => {
405
+ if (!isObject(tool) || tool.cache_control === undefined) return tool;
406
+ const { cache_control: _mark, ...rest } = tool;
407
+ return rest;
408
+ });
409
+ if (mark === undefined) return out;
410
+ const at = out.findLastIndex((tool) => isObject(tool) && tool.defer_loading !== true);
411
+ if (at !== -1) out[at] = { ...(out[at] as Record<string, unknown>), cache_control: mark };
412
+ return out;
413
+ }
414
+
415
+ /**
416
+ * `recorded` in place of `live` unless they match, then `live` (with pi's
417
+ * cache markers). pi's breakpoint carries over: the live last block's
418
+ * `cache_control`, or Bedrock's trailing `{ cachePoint }` blocks.
419
+ */
420
+ function recordedValue(recorded: unknown, live: unknown): unknown {
421
+ if (recorded === undefined) return live;
422
+ if (live !== undefined && contentHash(recorded) === contentHash(live)) return live;
423
+ const copy = structuredClone(recorded);
424
+ if (!Array.isArray(copy) || !Array.isArray(live)) return copy;
425
+ let content = live.length;
426
+ while (content > 0 && isCachePoint(live[content - 1])) content--;
427
+ const points = live.slice(content);
428
+ const lastLive = live[content - 1];
429
+ const mark = isObject(lastLive) ? lastLive.cache_control : undefined;
430
+ const last = copy.at(-1);
431
+ if (mark !== undefined && isObject(last)) last.cache_control = mark;
432
+ return [...copy, ...points];
433
+ }
434
+
435
+ /**
436
+ * The payload with the epoch's recorded system prompt, tools and cache key
437
+ * (OpenAI's `prompt_cache_key`) in place of pi's rendering, keeping every other field as pi built it. Tools pi declares
438
+ * that the record lacks are appended (a tool added mid-epoch); a recorded one
439
+ * pi no longer declares stays (Claude Code never removes a tool mid-conversation).
440
+ */
441
+ export function withRecordedHead(payload: WirePayload, head: Record<string, unknown>, api: string, declaredElsewhere: ReadonlySet<string> = new Set()): WirePayload {
442
+ const holder = toolsHolder(api);
443
+ const recordedTools = toolsIn(head, holder);
444
+ const withMergedTools = (out: WirePayload): WirePayload => {
445
+ if (!recordedTools) return out;
446
+ const live = toolsIn(payload, holder) ?? [];
447
+ return withToolsIn(out, holder, holder === "config" ? mergeGroups(recordedTools, live) : mergeDeclarations(recordedTools, live, declaredElsewhere));
448
+ };
449
+ if (holder === "config") {
450
+ const recorded = isObject(head.config) ? head.config : {};
451
+ const live = isObject(payload.config) ? payload.config : {};
452
+ const config: Record<string, unknown> = { ...live };
453
+ if ("systemInstruction" in recorded) config.systemInstruction = recordedValue(recorded.systemInstruction, live.systemInstruction);
454
+ return withMergedTools({ ...payload, config });
455
+ }
456
+ let out: WirePayload = { ...payload };
457
+ if (holder === "toolConfig") {
458
+ if ("system" in head) out.system = recordedValue(head.system, payload.system);
459
+ return withMergedTools(out);
460
+ }
461
+ // The cache key routes to the host holding the record's cache: a fork child,
462
+ // or a /fork, extends a request recorded under the parent session's key. Not
463
+ // on Codex, where pi-ai also sends the session's own id as the `session-id`
464
+ // header after every extension hook; a body key that disagrees with it read
465
+ // nothing live (findings §75).
466
+ const fields = api === "openai-codex-responses" ? ["system", "instructions"] : ["system", "instructions", "prompt_cache_key"];
467
+ for (const field of fields) if (field in head) out[field] = recordedValue(head[field], payload[field]);
468
+ out = withMergedTools(out);
469
+ // A beta an earlier request needed (a mid-conversation system message, a
470
+ // tool_addition) is still needed: that request's messages are in this one.
471
+ if (Array.isArray(head.betas)) {
472
+ const live = Array.isArray(payload.betas) ? payload.betas : [];
473
+ out.betas = [...head.betas, ...live.filter((beta) => !(head.betas as unknown[]).includes(beta))];
474
+ }
475
+ return out;
476
+ }
477
+
478
+ /** Whether `b`'s list starts with the same first `count` elements as `a`'s, element for element. */
479
+ export function samePrefix(a: WirePayload, b: WirePayload, key: string, count: number): boolean {
480
+ if (a === b) return true;
481
+ const left = a[key];
482
+ const right = b[key];
483
+ if (!Array.isArray(left) || !Array.isArray(right) || right.length < count) return false;
484
+ for (let i = 0; i < count; i++) if (left[i] !== right[i]) return false;
485
+ return true;
486
+ }
487
+
488
+ /** The append point of a recorded list: its length without the trailing directives the next request replaces (`isTrailer`). */
489
+ export function appendPoint(list: readonly unknown[], api: string): number {
490
+ let end = list.length;
491
+ while (end > 0 && isTrailer(list[end - 1], api)) end--;
492
+ return end;
493
+ }
494
+
495
+ /**
496
+ * The request that extends `base` inside its epoch: the base's list up to its
497
+ * append point, then pi's rendering of only the messages after the base's
498
+ * coverage (without the prompt element some APIs put first), under the
499
+ * recorded head. Elements above the append point come from the record, so
500
+ * nothing pi or an extension would render differently can change them.
501
+ */
502
+ export function spliceRequest(base: RecordedRequest, payload: WirePayload, api: string): WirePayload {
503
+ const list = payloadList(payload, api);
504
+ const tail = list.slice(preludeLength(list, api));
505
+ const at = appendPoint(base.list, api);
506
+ const out = underRecordedHead(base, payload, api, [...structuredClone(base.list.slice(0, at)), ...tail]);
507
+ return api.endsWith("-responses") && !Array.isArray(base.head.tools) ? withLiveDeclarations(out, payload, api, base, at) : out;
508
+ }
509
+
510
+ /**
511
+ * `list` under the record's head (`withRecordedHead`), with pi's declarations
512
+ * merged where the record keeps its tools. A Responses record with a top-level
513
+ * `tools` array merges pi's starting tools wherever pi put them (a pi 1.4.0
514
+ * head item on a session recorded by an earlier pi), so a new tool or a changed
515
+ * schema reaches the array. One that declares in its list takes no top-level
516
+ * `tools` from pi: an `additional_tools` item cannot replace a top-level tool,
517
+ * and a top-level array ahead of the input would miss the cache;
518
+ * `withLiveDeclarations` declares what it lacks.
519
+ */
520
+ function underRecordedHead(base: RecordedRequest, payload: WirePayload, api: string, list: unknown[]): WirePayload {
521
+ let live = payload;
522
+ if (api.endsWith("-responses")) {
523
+ const liveItem = leadingToolsItem(payload.tools, payloadList(payload, api));
524
+ if (Array.isArray(base.head.tools)) live = liveItem ? { ...payload, tools: liveItem.tools } : payload;
525
+ else if (leadingToolsItem(base.head.tools, base.list)) {
526
+ const { tools: _live, ...withoutTools } = payload;
527
+ live = withoutTools;
528
+ }
529
+ }
530
+ return { ...withRecordedHead(live, base.head, api, loadedInList(base.list)), [listKey(api)]: list };
531
+ }
532
+
533
+ /** Every declaration a list's own items make (`additional_tools`, `tool_search_output`), by name in list order; a later one replaces an earlier one, as the API reads them. */
534
+ function listDeclarations(list: readonly unknown[]): Map<string, unknown> {
535
+ const out = new Map<string, unknown>();
536
+ for (const item of list) {
537
+ if (!isObject(item) || (item.type !== "additional_tools" && item.type !== "tool_search_output") || !Array.isArray(item.tools)) continue;
538
+ for (const tool of item.tools) {
539
+ const name = toolName(tool);
540
+ if (name !== undefined) out.set(name, tool);
541
+ }
542
+ }
543
+ return out;
544
+ }
545
+
546
+ /** Every declaration a Responses body makes, by name, wherever it sits: its top-level `tools`, then its list's items. */
547
+ export function responsesDeclarations(payload: WirePayload, api: string): Map<string, unknown> {
548
+ const out = new Map<string, unknown>();
549
+ for (const tool of Array.isArray(payload.tools) ? payload.tools : []) {
550
+ const name = toolName(tool);
551
+ if (name !== undefined) out.set(name, tool);
552
+ }
553
+ for (const [name, tool] of listDeclarations(payloadList(payload, api))) out.set(name, tool);
554
+ return out;
555
+ }
556
+
557
+ /**
558
+ * `out` declaring every tool pi's own rendering (`live`) declares, for a
559
+ * Responses record without a top-level `tools` array (one with an array gets
560
+ * pi's tools through the head merge): a splice keeps the record's head and
561
+ * drops pi's, so a tool pi added mid-epoch, or one whose input schema changed,
562
+ * would otherwise go undeclared. It goes in an `additional_tools` item at the
563
+ * append point `at`, after the cached prefix; a later item replaces an earlier
564
+ * item's declaration of the same name.
565
+ */
566
+ function withLiveDeclarations(out: WirePayload, live: WirePayload, api: string, base: RecordedRequest, at: number): WirePayload {
567
+ const recordItem = leadingToolsItem(base.head.tools, base.list);
568
+ const liveItem = leadingToolsItem(live.tools, payloadList(live, api));
569
+ // The usual request: pi's head item is the record's, and every other
570
+ // declaration pi made rides the tail it sends.
571
+ if (recordItem && liveItem && contentHash(liveItem) === base.hashes[0]) return out;
572
+ const sent = responsesDeclarations(out, api);
573
+ const needed = [...responsesDeclarations(live, api)].filter(([name, tool]) => {
574
+ const current = sent.get(name);
575
+ return current === undefined || (recordItem !== undefined && contentHash(inputSchemaOf(current)) !== contentHash(inputSchemaOf(tool)));
576
+ });
577
+ if (needed.length === 0) return out;
578
+ const tools = needed.map(([, tool]) => stripMarkers(tool));
579
+ const list = payloadList(out, api);
580
+ return { ...out, [listKey(api)]: [...list.slice(0, at), toolsItemFor(tools, liveItem ?? recordItem), ...list.slice(at)] };
581
+ }
582
+
583
+ /**
584
+ * Every tool name a recorded request declares: its head's declarations, and
585
+ * for a Responses record the ones in its list (a leading `additional_tools`
586
+ * item, the items `tool_search` loads ride). `eager` drops head declarations
587
+ * marked `defer_loading`; a list declaration is always eager.
588
+ */
589
+ export function recordedToolNames(record: Pick<RecordedRequest, "head" | "list">, { eager = false } = {}): Set<string> {
590
+ return new Set([...declaredToolNames(record.head, { eager }), ...loadedInList(record.list)]);
591
+ }
592
+
593
+ /** The first `cache_control` marker on a list element or one of its content blocks. */
594
+ function listMarker(list: readonly unknown[]): unknown {
595
+ for (const element of list) {
596
+ if (!isObject(element)) continue;
597
+ if (element.cache_control !== undefined) return element.cache_control;
598
+ if (Array.isArray(element.content)) for (const block of element.content) if (isObject(block) && block.cache_control !== undefined) return block.cache_control;
599
+ }
600
+ return undefined;
601
+ }
602
+
603
+ /**
604
+ * The recorded request `base` as a body to send again, under `payload`'s
605
+ * request-only fields (a cache warm that outlived the instance that sent it:
606
+ * pi's own rendering of its context is the cut tail). pi's conversation
607
+ * marker goes back on the last block before the trailing directives, where pi
608
+ * put it.
609
+ */
610
+ export function recordedBody(base: RecordedRequest, payload: WirePayload, api: string): WirePayload {
611
+ const list = structuredClone(base.list);
612
+ const mark = listMarker(payloadList(payload, api));
613
+ const last = list[appendPoint(list, api) - 1];
614
+ if (mark !== undefined && isObject(last)) {
615
+ const blocks = Array.isArray(last.content) ? last.content : undefined;
616
+ const target = blocks?.at(-1);
617
+ if (isObject(target)) target.cache_control = mark;
618
+ else if (!blocks) last.cache_control = mark;
619
+ }
620
+ return withRequestFields(underRecordedHead(base, payload, api, list), payload);
621
+ }
622
+
623
+ /**
624
+ * The tools a Responses list declares itself: a leading `additional_tools`
625
+ * item (pi 1.4.0 and later), and the items a `tool_search` load rides (an
626
+ * `additional_tools` item, a `tool_search_output`). pi renders a loaded tool
627
+ * eager again once its load is in the recorded prefix, and the head must not
628
+ * declare it twice.
629
+ */
630
+ export function loadedInList(list: readonly unknown[]): Set<string> {
631
+ return new Set(listDeclarations(list).keys());
632
+ }