@codespar/cli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/README.md +1 -0
  2. package/dist/surface.js +53 -1
  3. package/dist/surface.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/bills-agent/README.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  7. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  8. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  9. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  10. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  11. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  12. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  13. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  14. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  15. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  16. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  17. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  18. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  19. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  20. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  21. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  22. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  23. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  24. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  25. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  26. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  27. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  28. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  29. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  30. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  31. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  32. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  33. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  34. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  35. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  36. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  37. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  38. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  39. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  40. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  41. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  42. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  43. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  44. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  45. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  46. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  63. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  64. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  66. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  67. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  69. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  70. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  71. package/templates/checkout-agent/LICENSE +21 -0
  72. package/templates/checkout-agent/README.md +28 -0
  73. package/templates/checkout-agent/_gitignore +10 -0
  74. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  75. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  76. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  77. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  78. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  79. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  83. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  110. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  111. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  112. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  113. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  158. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  159. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  160. package/templates/checkout-agent/package.json +38 -0
  161. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  162. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  163. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  167. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  169. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  170. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  171. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  172. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  173. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  174. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  175. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  176. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  177. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  178. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  179. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  180. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  181. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  182. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  184. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  185. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  186. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  187. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  188. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  189. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  190. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  191. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  192. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  193. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  195. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  196. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  197. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  198. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  199. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  200. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  201. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  202. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  203. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  204. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  205. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  206. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  208. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  209. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  210. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  211. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  212. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  213. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  216. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  217. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  218. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  219. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  220. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  221. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  222. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  223. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  224. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  269. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  270. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  271. package/templates/checkout-agent/tsconfig.base.json +16 -0
  272. package/templates/checkout-agent/vitest.config.ts +12 -0
  273. package/templates/collections-agent/README.md +6 -1
  274. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  276. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  277. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  278. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  279. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  280. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  281. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  283. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  284. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  285. package/templates/collections-agent/package.json +1 -0
  286. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  287. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  288. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  289. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  290. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  291. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  292. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  293. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  294. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  295. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  296. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  297. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  298. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  299. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  300. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  301. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  302. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  303. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  304. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  305. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  306. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  307. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  308. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  309. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  310. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  311. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  312. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  313. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  314. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  315. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  316. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  317. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  318. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  319. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  320. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  321. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  322. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  339. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  340. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  342. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  343. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  345. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  346. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  347. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  348. package/templates/hello-agent/README.md +1 -1
  349. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  350. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  351. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  352. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  353. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  354. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  355. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  356. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  357. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  358. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  359. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  360. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  361. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  362. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  363. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  364. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  365. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  366. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  367. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  368. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  369. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  370. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  371. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  372. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  373. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  374. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  375. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  376. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  377. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  378. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  379. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  380. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  381. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  382. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  383. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  384. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  385. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  402. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  403. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  405. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  406. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  408. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  409. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  410. package/templates/kits.lock.json +29 -6
  411. package/templates/supplier-payments-agent/README.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  418. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  420. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  424. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  426. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  427. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  428. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  429. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  430. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  431. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  432. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  433. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  435. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  436. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  437. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  438. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  439. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  440. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  441. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  442. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  446. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  447. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  448. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  449. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  452. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  453. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  477. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  478. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  479. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  481. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,182 @@
1
+ /**
2
+ * §46's three product gaps, as the channel now answers them, with no network:
3
+ * a status webhook is read (a `failed` of an outcome's message is that
4
+ * outcome NOT told), a tapped quick reply becomes a turn only as the intent a
5
+ * declared template gave it, and the registry carries a fallback for an
6
+ * outcome the kit has no copy for. The same behaviour against the emulator is
7
+ * in the repository's `test/whatsapp-emulator.integration.test.ts`, and the
8
+ * `check` rule that requires the fallback, asked of the shipped agents, in
9
+ * `test/whatsapp-fallback-check.test.ts`: both read this repository's other
10
+ * agents and scripts, which a template made from one agent does not carry.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import type { WhatsAppTemplate } from "@codespar/agent-core";
14
+ import { WhatsAppChannel } from "../src/channels/whatsapp/index.js";
15
+ import { buildSendRequest, parseInbound, type CloudApiConfig } from "../src/channels/whatsapp/cloud-api.js";
16
+ import type { ChannelBackend, ChannelLogLine, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../src/channels/types.js";
17
+
18
+ const CONTACT = "+5511987654321";
19
+ const NOW = new Date("2026-09-23T17:00:00Z");
20
+
21
+ /** A backend the test drives: it queues what the person does and hands the channel the statuses the test reports. */
22
+ class Scripted implements ChannelBackend {
23
+ readonly name = "scripted";
24
+ readonly live = false;
25
+ readonly delivered: OutboundBody[] = [];
26
+ private listener: ((s: StatusUpdate) => void) | undefined;
27
+ constructor(private readonly inbox: InboundMessage[] = []) {}
28
+ async open(): Promise<void> {}
29
+ async next(): Promise<InboundMessage | undefined> {
30
+ return this.inbox.shift();
31
+ }
32
+ async deliver(_to: string, body: OutboundBody): Promise<SentMessage> {
33
+ this.delivered.push(body);
34
+ return { id: `wamid.test.${this.delivered.length}`, state: "sent" };
35
+ }
36
+ onStatus(listener: (s: StatusUpdate) => void): void {
37
+ this.listener = listener;
38
+ }
39
+ report(status: StatusUpdate): void {
40
+ this.listener?.(status);
41
+ }
42
+ async close(): Promise<void> {}
43
+ }
44
+
45
+ const TEMPLATES: WhatsAppTemplate[] = [
46
+ { name: "acordo_quitado", language: "pt_BR", description: "paid", body: "Oi! {{1}} quitado." },
47
+ {
48
+ name: "acordo_cobranca_vencida",
49
+ language: "pt_BR",
50
+ description: "expired",
51
+ body: "Oi! A cobranca do {{1}} venceu.",
52
+ buttons: [
53
+ { id: "emitir_nova", title: "Emitir nova", intent: "quero que voce emita uma nova cobranca para o meu acordo" },
54
+ { id: "agora_nao", title: "Agora nao", intent: "agora nao quero uma nova cobranca" },
55
+ ],
56
+ },
57
+ { name: "atendimento_atualizacao", language: "pt_BR", description: "fallback", body: "Oi! Temos uma atualizacao.", fallback: true },
58
+ ];
59
+
60
+ function channel(inbox: InboundMessage[] = []) {
61
+ const backend = new Scripted(inbox);
62
+ const said: string[] = [];
63
+ const failed: ChannelLogLine[] = [];
64
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT, subject: "acordo-1042" }, now: () => NOW, templates: TEMPLATES, say: (l) => said.push(l), onDeliveryFailed: (l) => failed.push(l) });
65
+ return { ch, backend, said, failed };
66
+ }
67
+
68
+ const tap = (id: string, extra: Partial<InboundMessage["reply"]> = {}): InboundMessage => ({ id: `wamid.in.${id}`, from: CONTACT, text: "Emitir nova", timestamp: 1790190000, reply: { type: "button_reply", id, title: "Emitir nova", ...extra } });
69
+
70
+ describe("1. a status webhook is read, and a failed outcome is an outcome not told", () => {
71
+ it("parses statuses, with Meta's errors, out of the same delivery that carries messages", () => {
72
+ const parsed = parseInbound({
73
+ entry: [{ changes: [{ value: { statuses: [{ id: "wamid.a", status: "failed", timestamp: "1790190000", errors: [{ code: 131026, title: "Message undeliverable", error_data: { details: "not on whatsapp" } }] }, { id: "wamid.b", status: "read", timestamp: "1790190001" }, { id: "wamid.c", status: "deleted" }] } }] }],
74
+ });
75
+ expect(parsed.statuses).toEqual([
76
+ { message_id: "wamid.a", status: "failed", timestamp: 1790190000, errors: [{ code: 131026, title: "Message undeliverable", details: "not on whatsapp" }] },
77
+ { message_id: "wamid.b", status: "read", timestamp: 1790190001, errors: [] },
78
+ ]);
79
+ });
80
+
81
+ it("a failed status of a message that told an outcome is recorded, reaches the operator and the outcome's owner, and names the outcome", async () => {
82
+ const { ch, backend, said, failed } = channel();
83
+ await ch.open();
84
+ (ch as unknown as { session: { observeInbound(t: number): void } }).session.observeInbound(Math.floor(NOW.getTime() / 1000));
85
+ const sent = await ch.send({ kind: "text", text: "Recebemos, acordo quitado.", about: { execution_id: "exe_1", state: "settled" } });
86
+ backend.report({ message_id: sent.id, status: "failed", timestamp: 1790190100, errors: [{ code: 131026, details: "not on whatsapp" }] });
87
+ expect(ch.deliveryOf(sent.id)).toBe("failed");
88
+ const status = ch.log().find((l) => l.direction === "status")!;
89
+ expect(status).toMatchObject({ kind: "status", message_id: sent.id, state: "failed", about: { execution_id: "exe_1", state: "settled" }, errors: [{ code: 131026, details: "not on whatsapp" }] });
90
+ expect(failed).toEqual([status]);
91
+ expect(said.join("\n")).toContain("a pessoa NAO foi avisada");
92
+ });
93
+
94
+ it("a read is recorded and nothing more: not consent, not an acknowledgement, not a reply", async () => {
95
+ const { ch, backend, said, failed } = channel();
96
+ await ch.open();
97
+ (ch as unknown as { session: { observeInbound(t: number): void } }).session.observeInbound(Math.floor(NOW.getTime() / 1000));
98
+ const sent = await ch.send({ kind: "text", text: "Recebemos.", about: { execution_id: "exe_1", state: "settled" } });
99
+ backend.report({ message_id: sent.id, status: "read", timestamp: 1790190100, errors: [] });
100
+ expect(ch.log().filter((l) => l.direction === "status").map((l) => l.state)).toEqual(["read"]);
101
+ expect(failed).toEqual([]);
102
+ expect(said).toEqual([]);
103
+ // A read is not a turn either.
104
+ expect(await ch.next()).toBeUndefined();
105
+ });
106
+
107
+ it("a failure the provider reports BEFORE the send answered (the emulator posts statuses synchronously) is still tied to its outcome", async () => {
108
+ class Eager extends Scripted {
109
+ override async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
110
+ const sent = await super.deliver(to, body);
111
+ this.report({ message_id: sent.id, status: "failed", timestamp: 1, errors: [{ code: 131026 }] });
112
+ return sent;
113
+ }
114
+ }
115
+ const backend = new Eager();
116
+ const failed: ChannelLogLine[] = [];
117
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT }, now: () => NOW, templates: TEMPLATES, onDeliveryFailed: (l) => failed.push(l) });
118
+ await ch.open();
119
+ await ch.send({ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042"], about: { execution_id: "exe_2", state: "settled" } });
120
+ await new Promise((r) => setTimeout(r, 0));
121
+ expect(failed.map((l) => l.about)).toEqual([{ execution_id: "exe_2", state: "settled" }]);
122
+ });
123
+
124
+ it("a status about a message an EARLIER run sent is tied back to the outcome that run recorded", async () => {
125
+ const prior: ChannelLogLine = { at: NOW.toISOString(), direction: "out", contact: "+55*****4321", kind: "template", message_id: "wamid.old", state: "sent", about: { execution_id: "exe_old", state: "settled" } };
126
+ const backend = new Scripted();
127
+ const failed: ChannelLogLine[] = [];
128
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT }, now: () => NOW, templates: TEMPLATES, priorLines: [prior], onDeliveryFailed: (l) => failed.push(l) });
129
+ await ch.open();
130
+ backend.report({ message_id: "wamid.old", status: "failed", timestamp: 1, errors: [{ code: 131026 }] });
131
+ expect(failed[0]?.about).toEqual({ execution_id: "exe_old", state: "settled" });
132
+ });
133
+ });
134
+
135
+ describe("2. a tapped quick reply is a turn, as the intent a declared template gave it", () => {
136
+ it("parses a button_reply, a list_reply and a template's quick-reply button, id and context kept", () => {
137
+ const msg = (m: Record<string, unknown>) => ({ entry: [{ changes: [{ value: { messages: [{ from: "5511987654321", timestamp: "1", ...m }] } }] }] });
138
+ expect(parseInbound(msg({ id: "a", type: "interactive", interactive: { type: "button_reply", button_reply: { id: "emitir_nova", title: "Emitir nova" } }, context: { id: "wamid.offer" } })).messages[0]?.reply).toEqual({ type: "button_reply", id: "emitir_nova", title: "Emitir nova", context_id: "wamid.offer" });
139
+ expect(parseInbound(msg({ id: "b", type: "interactive", interactive: { type: "list_reply", list_reply: { id: "3x", title: "3x", description: "tres" } } })).messages[0]?.reply).toEqual({ type: "list_reply", id: "3x", title: "3x" });
140
+ expect(parseInbound(msg({ id: "c", type: "button", button: { payload: "emitir_nova", text: "Emitir nova" } })).messages[0]?.reply).toEqual({ type: "button", id: "emitir_nova", title: "Emitir nova" });
141
+ // What this channel cannot read is still named, not turned into an empty message.
142
+ expect(parseInbound(msg({ id: "d", type: "image" })).ignored).toEqual([{ id: "d", type: "image" }]);
143
+ expect(parseInbound(msg({ id: "e", type: "interactive", interactive: { type: "nfm_reply" } })).ignored).toEqual([{ id: "e", type: "interactive:nfm_reply" }]);
144
+ });
145
+
146
+ it("a declared id is a turn whose text is the declared intent, never the title", async () => {
147
+ const { ch } = channel([tap("emitir_nova")]);
148
+ const turn = await ch.next();
149
+ expect(turn?.text).toBe("quero que voce emita uma nova cobranca para o meu acordo");
150
+ const line = ch.log().at(-1)!;
151
+ expect(line).toMatchObject({ direction: "in", kind: "reply", reply: { id: "emitir_nova", title: "Emitir nova" }, text: "quero que voce emita uma nova cobranca para o meu acordo" });
152
+ });
153
+
154
+ it("an id no template declares is not a turn: recorded, told to the operator, and the next real turn is what comes back", async () => {
155
+ const typed: InboundMessage = { id: "wamid.in.text", from: CONTACT, text: "oi", timestamp: 1790190001 };
156
+ const { ch, said } = channel([tap("pagar_tudo", { title: "Pagar tudo" }), typed]);
157
+ const turn = await ch.next();
158
+ expect(turn?.text).toBe("oi");
159
+ expect(ch.log()[0]).toMatchObject({ kind: "reply", refused: { rule: "reply_not_offered" }, reply: { id: "pagar_tudo" } });
160
+ expect(said.join("\n")).toContain("reply_not_offered");
161
+ });
162
+
163
+ it("a tap that names a message this conversation did not send with that reply is not a turn either", async () => {
164
+ const { ch } = channel([tap("emitir_nova", { context_id: "wamid.somebody_else" })]);
165
+ expect(await ch.next()).toBeUndefined();
166
+ expect(ch.log()[0]?.refused?.rule).toBe("reply_not_offered");
167
+ });
168
+
169
+ it("a template goes out with the quick replies its declaration offers, and the log says which", async () => {
170
+ const { ch, backend } = channel();
171
+ await ch.send({ kind: "template", template: "acordo_cobranca_vencida", language: "pt_BR", variables: ["acordo-1042"] });
172
+ expect((backend.delivered[0] as { buttons?: unknown }).buttons).toEqual([{ id: "emitir_nova", title: "Emitir nova" }, { id: "agora_nao", title: "Agora nao" }]);
173
+ expect(ch.log()[0]?.offered).toEqual(["emitir_nova", "agora_nao"]);
174
+ const config: CloudApiConfig = { baseUrl: "http://sim", phoneNumberId: "1", accessToken: "t", verifyToken: "v", appSecret: "s", apiVersion: "v22.0", webhookPort: 0 };
175
+ const request = buildSendRequest(config, CONTACT, backend.delivered[0]!) as { body: string };
176
+ expect(JSON.parse(request.body).template.components).toEqual([
177
+ { type: "body", parameters: [{ type: "text", text: "acordo-1042" }] },
178
+ { type: "button", sub_type: "quick_reply", index: "0", parameters: [{ type: "payload", payload: "emitir_nova" }] },
179
+ { type: "button", sub_type: "quick_reply", index: "1", parameters: [{ type: "payload", payload: "agora_nao" }] },
180
+ ]);
181
+ });
182
+ });
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runs the WhatsApp Cloud API emulator the channel's `simulator` backend talks
4
+ * to: `dyvit-wa-sim`, published as `@dyvit/whatsapp-simulator-cli` (MIT) from
5
+ * https://github.com/fabianocruz/whatsapp-simulator. It is not ours and it is
6
+ * not a dependency of this workspace.
7
+ *
8
+ * WHY NOT A DEPENDENCY. It is a development tool. Nothing in `packages/` or
9
+ * `agents/` imports it, so `npm ci` has no reason to fetch it and a contributor
10
+ * who never runs the WhatsApp gate never downloads it. `npx` fetches the exact
11
+ * version into its own cache the first time it is needed; nothing is vendored,
12
+ * nothing is installed globally, and the repository's tree is untouched.
13
+ *
14
+ * WHY PINNED TO A VERSION. A moving `latest` would make our gate fail on
15
+ * somebody else's release. `--version` overrides, and there is no `latest`.
16
+ * The spec handed to `npx` is exact, so it cannot resolve anything else.
17
+ *
18
+ * Usage:
19
+ * node scripts/whatsapp-emulator.mjs start it (foreground)
20
+ * node scripts/whatsapp-emulator.mjs --prepare fetch only
21
+ * node scripts/whatsapp-emulator.mjs --version <v> a different release
22
+ */
23
+ import { spawn, spawnSync } from "node:child_process";
24
+ import { dirname, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
+
29
+ export const EMULATOR_PACKAGE = "@dyvit/whatsapp-simulator-cli";
30
+ /** The release the gate is measured against. Bump deliberately, never automatically. */
31
+ export const EMULATOR_VERSION = "0.3.0";
32
+ export const EMULATOR_PORT = 4290;
33
+ /** Must match `WHATSAPP_SIM_APP_SECRET`. A local development value, not a credential. */
34
+ export const EMULATOR_APP_SECRET = "dev";
35
+ /** Where the channel's own receiver listens; the emulator posts its webhooks there. */
36
+ export const EMULATOR_WEBHOOK_PORT = 4399;
37
+
38
+ function spec(version) {
39
+ return `${EMULATOR_PACKAGE}@${version}`;
40
+ }
41
+
42
+ /** Warms `npx`'s cache so the start below does not race a download. */
43
+ export function prepare(version = EMULATOR_VERSION, say = (l) => process.stderr.write(l + "\n")) {
44
+ say(`whatsapp emulator: fetching ${spec(version)}`);
45
+ const result = spawnSync("npx", ["--yes", spec(version), "--help"], { stdio: ["ignore", "ignore", "inherit"] });
46
+ if (result.error) throw result.error;
47
+ if (result.status !== 0) {
48
+ throw new Error(`could not fetch ${spec(version)} (npx exited ${result.status}). Is the version published, and is the network reachable?`);
49
+ }
50
+ return spec(version);
51
+ }
52
+
53
+ function serve(version, webhookUrl) {
54
+ const child = spawn(
55
+ "npx",
56
+ ["--yes", spec(version), "serve", "--port", String(EMULATOR_PORT), "--webhook", webhookUrl, "--app-secret", EMULATOR_APP_SECRET],
57
+ // Its own process group: `npx` is a wrapper around the real server, and a
58
+ // signal sent to the wrapper alone leaves the port held by an orphan.
59
+ { cwd: ROOT, stdio: "inherit", detached: true },
60
+ );
61
+ const stop = () => {
62
+ try {
63
+ process.kill(-child.pid, "SIGTERM");
64
+ } catch {
65
+ child.kill("SIGTERM");
66
+ }
67
+ };
68
+ process.once("SIGINT", stop);
69
+ process.once("SIGTERM", stop);
70
+ child.on("exit", (code) => process.exit(code ?? 0));
71
+ }
72
+
73
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
74
+ const argv = process.argv.slice(2);
75
+ const version = argv.includes("--version") ? argv[argv.indexOf("--version") + 1] : EMULATOR_VERSION;
76
+ const webhookUrl = argv.includes("--webhook") ? argv[argv.indexOf("--webhook") + 1] : `http://127.0.0.1:${EMULATOR_WEBHOOK_PORT}/`;
77
+ if (argv.includes("--prepare")) {
78
+ prepare(version);
79
+ process.stderr.write(`whatsapp emulator ready: ${spec(version)}\n`);
80
+ } else {
81
+ process.stderr.write(`whatsapp emulator: ${spec(version)} on :${EMULATOR_PORT}, webhooks to ${webhookUrl}\n`);
82
+ serve(version, webhookUrl);
83
+ }
84
+ }
@@ -1,7 +1,7 @@
1
1
  # {{name}}
2
2
 
3
3
  Scaffolded by `codespar init --template hello-agent` from the `hello-agent` starter kit
4
- (https://github.com/codespar/agent-starter-kits at `8f130b7cdc30510c929e6f7ebb07184f763e6ce4`, agent 0.1.0).
4
+ (https://github.com/codespar/agent-starter-kits at `b766575306a71c9135c0d70e23d591e6523dc2f8`, agent 0.1.0).
5
5
 
6
6
  The worked example of the codespar-agent-builder skill: a read-only agent that reads the month's bills and cannot pay. Built from the five files, the packs and one kit module.
7
7
 
@@ -12,10 +12,23 @@ This package is the decisions. The RUNNER around them — the terminal channel,
12
12
 
13
13
  `ExecutionEngine.policy()` runs at draft, at a human's approval and again immediately before `executing`, inside the transaction that writes the outbox row. Mandate status, allowlist, per-transaction cap, the window cap (counting settled, in flight AND awaiting executions as reservations), `escalate_above` and the artifact's `items_hash` are all re-checked at the last gate. A person's approval satisfies `escalate_above` and nothing else.
14
14
 
15
+ ## What the artifact binds besides the items
16
+
17
+ `items_hash` covers the execution's items: payee, amount, currency and a receivable's due date. Two optional bindings cover what those fields cannot see, and each is **omitted, never null**, when it does not apply, so an artifact without them signs the same bytes it signed before they existed (`test/composition.test.ts` pins that against an artifact hashed on `main` before `composition` was added):
18
+
19
+ - `batch` — the set a line was approved inside (`batch_hash` over the ordered lines, the line's `index`, the `count`). One execution per line, N lines.
20
+ - `composition` — what ONE amount is made of (`{ ref, composition_hash, line_count }`, with `compositionHash` over the resolved lines). One execution, N lines behind it: a sale is one charge, so a cart of three lines is one item whose amount is the order's total. Two units at 100 and one unit at 200 are the same item and the same `items_hash`; they are not the same `composition_hash`. `compositionHash` uses `itemsHash`'s canonicalisation over `ref`, `quantity`, `unit_amount`, `amount` and `currency`, not `itemsHash` itself, because ten units at a 10% discount and nine at list price have the same line amount.
21
+
22
+ `checkApprovalArtifact` compares both, and the last gate sends a mismatch back to `awaiting_approval` with `items_hash_mismatch`, like a changed list. `ExecutionEngine.restate()` is how an OPEN execution (`awaiting_approval`, `approved`) follows the proposal behind it — a customer who changes the cart after the order was approved — without a transition: items, total, `items_hash` and composition are replaced, the state, the artifact and the idempotency key are kept, and the last gate does the refusing. The payees of a restatement cannot change.
23
+
24
+ ## What of the artifact reaches the receipt
25
+
26
+ Every spend carries `approval: { items_hash, batch_hash? }`, the artifact's own hashes (`spendApprovalOf`), and the API seals them into the receipt's chain as a link of their own (chain v4, ent#1670). Both rails refuse a spend without them before any call. `composition` is not sent: the API takes the two hashes and nothing else, and only charges carry a composition today. `receipt-chain.ts` recomputes a chain from the receipt read under the `chain_recipe` the key document publishes (RFC 8785 implemented locally, `node:crypto` only), and `verifyReceiptRead` holds the signature, the body and the sealed approval link against the artifact, in that order. It proves WHAT was approved to anybody holding the read; WHO approved stays in the artifact, under the local key. `docs/OPEN_QUESTIONS.md` §3 and §47.
27
+
15
28
  ## Reconcile never re-dispatches
16
29
 
17
30
  An execution left in `executing` is looked up on the rail, attempt by attempt. A recorded settled or failed outcome closes it; `in_flight`, `uncertain` or unknown leaves it in `executing` with `reason: rail_uncertain` for a human. The single exception is an outbox row still `pending` (the state changed, no call was ever made): those attempts go out once, under the same ids.
18
31
 
19
32
  ## Stubs
20
33
 
21
- `stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, and a read that does not answer is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
34
+ `stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, `org_paused: true` (the organization kill switch, ent#1648) refuses it whatever `status` says, and a read that does not answer, or answers without `org_paused`, is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@anthropic-ai/sdk": "0.128.0",
20
- "@codespar/sdk": "0.16.6",
20
+ "@codespar/sdk": "0.16.10",
21
21
  "yaml": "2.9.1",
22
22
  "zod": "3.25.76"
23
23
  }
@@ -16,6 +16,42 @@ export interface ToolContext {
16
16
  engine: ExecutionEngine;
17
17
  /** Fires when a payment tool created an execution; the channel decides what to do with it (ask, or run). */
18
18
  onExecution: (execution: Execution) => Promise<Execution>;
19
+ /**
20
+ * Fires ONCE for a batch, before its first line is drafted, when the channel
21
+ * can take one gesture for the whole list. Absent on a channel that decides
22
+ * line by line (the one-shot, the scenario runner), and then nothing changes:
23
+ * each line reaches `onExecution` and is decided there. The gesture never
24
+ * approves anything itself; every line is still drafted, still reaches
25
+ * `onExecution`, and still mints its own artifact.
26
+ */
27
+ onBatch?: (batch: BatchPresentation) => Promise<BatchGesture | undefined>;
28
+ }
29
+
30
+ /** What a person sees when a whole batch is put in front of them: the list, its total, and the hash of the list. */
31
+ export interface BatchPresentation {
32
+ ref: string;
33
+ label: string;
34
+ /** `batchHash` of the ordered lines. The gesture is a decision about THIS hash and no other. */
35
+ batch_hash: string;
36
+ count: number;
37
+ total_minor: number;
38
+ /** The total as the kit writes money. */
39
+ total: string;
40
+ lines: Array<{
41
+ index: number;
42
+ beneficiary: string;
43
+ amount_minor: number;
44
+ amount: string;
45
+ /** A line a previous run already covers is shown, and does not run again whatever the gesture says. */
46
+ status: "open" | "already_settled" | "in_progress" | "attempt_id_conflict";
47
+ }>;
48
+ }
49
+
50
+ /** The decision a person took on a presented batch: which positions they approved and which they vetoed. */
51
+ export interface BatchGesture {
52
+ batch_hash: string;
53
+ approved: number[];
54
+ vetoed: number[];
19
55
  }
20
56
 
21
57
  export type ToolHandler = (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
@@ -28,6 +64,7 @@ export interface AgentLoopOptions {
28
64
  bundle: ProofBundle;
29
65
  engine: ExecutionEngine;
30
66
  onExecution: ToolContext["onExecution"];
67
+ onBatch?: ToolContext["onBatch"] | undefined;
31
68
  /** A hard stop on tool round-trips per turn, so a looping model cannot run forever. */
32
69
  maxSteps?: number;
33
70
  clock?: () => Date;
@@ -91,7 +128,7 @@ export class AgentLoop {
91
128
  }
92
129
 
93
130
  private async dispatch(call: ToolCall, executions: Execution[]): Promise<{ refused: boolean; content: unknown }> {
94
- const { bundle, handlers, engine, onExecution } = this.options;
131
+ const { bundle, handlers, engine, onExecution, onBatch } = this.options;
95
132
  bundle.transcript({ at: this.now(), kind: "tool_call", tool_call_id: call.id, name: call.name, input: call.input });
96
133
 
97
134
  if (!this.allowed.has(call.name)) {
@@ -110,6 +147,7 @@ export class AgentLoop {
110
147
  executions.push(settledOrNot);
111
148
  return settledOrNot;
112
149
  },
150
+ ...(onBatch ? { onBatch } : {}),
113
151
  });
114
152
  return { refused: false, content };
115
153
  } catch (err) {
@@ -19,30 +19,39 @@
19
19
  * There is no receipt object for a receivable; the paid charge as the API
20
20
  * reports it is what the bundle keeps, marked `kind: "charge"` and unsealed.
21
21
  */
22
- import type { ApiClient } from "@codespar/sdk";
22
+ import type { ApiClient, ApiOperation, ApiSuccess } from "@codespar/sdk";
23
23
  import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
24
24
  import type { Actor, ChargeInstrument } from "../types.js";
25
- import { describeApiError, isUncertain } from "./client.js";
25
+ import { describeApiError, isUncertain, type ApiErrorCode } from "./client.js";
26
26
 
27
- /** The `/v1/charges` answer, as the SDK types it. */
28
- export interface ChargeView {
29
- id: string | null;
30
- status: string;
31
- local_status: string;
32
- status_conflict: boolean;
33
- method: string;
34
- currency: string;
35
- amount: number;
36
- amount_minor: number;
37
- due_date: string | null;
38
- payable: boolean;
39
- boleto_bar_code: string | null;
40
- boleto_bank_line: string | null;
41
- pix_copy_paste: string | null;
42
- credit_correlation_armed: boolean;
43
- payment_in_flight: boolean;
44
- settlement: "confirmed" | "pending" | "unconfirmable" | null;
45
- issuance_unconfirmed: boolean;
27
+ /** The `/v1/charges/{chargeId}` answer, as the SDK's generated OpenAPI types it. The create answers the same shape, and the compiler holds them together. */
28
+ export type ChargeView = ApiSuccess<ApiOperation<"/v1/charges/{chargeId}", "get">>;
29
+
30
+ /**
31
+ * Documented as the read's 409. The create does not document it, and the
32
+ * create's error path below still treats it as uncertain, which is the safe
33
+ * reading of a code the route is not known to send (OPEN_QUESTIONS §8).
34
+ */
35
+ const ISSUANCE_UNCONFIRMED: ApiErrorCode<ApiOperation<"/v1/charges/{chargeId}", "get">> = "issuance_unconfirmed";
36
+
37
+ /** The read's other 409: the reference matched more than one charge. Terminal for that reference; the API answers by the charge id. */
38
+ const REFERENCE_AMBIGUOUS: ApiErrorCode<ApiOperation<"/v1/charges/{chargeId}", "get">> = "charge_reference_ambiguous";
39
+
40
+ type ChargeRead =
41
+ | { kind: "view"; view: ChargeView }
42
+ | { kind: "missing" }
43
+ | { kind: "in_flight" }
44
+ | { kind: "failed"; outcome: Extract<RailOutcome, { status: "failed" }> }
45
+ | { kind: "uncertain"; outcome: Extract<RailOutcome, { status: "uncertain" }> };
46
+
47
+ /**
48
+ * When the charge settled, if the answer says so. `settled_at` is a column of
49
+ * the charge row, but the SDK's type for the read does not carry it, so this
50
+ * is the one field here read outside the type (OPEN_QUESTIONS §8).
51
+ */
52
+ function settledAt(view: ChargeView): string | undefined {
53
+ const value: unknown = (view as Record<string, unknown>)["settled_at"];
54
+ return typeof value === "string" && value !== "" ? value : undefined;
46
55
  }
47
56
 
48
57
  export function instrumentOf(view: ChargeView): ChargeInstrument {
@@ -82,7 +91,7 @@ export class CodeSparChargeRail implements PaymentRail {
82
91
  if (!payment.consumer_id) return { status: "failed", code: "consumer_id_required", message: "a receivable settles into the principal's account; the policy names no consumer_id" };
83
92
  let view: ChargeView;
84
93
  try {
85
- view = (await this.api.post("/v1/charges", {
94
+ view = await this.api.post("/v1/charges", {
86
95
  body: {
87
96
  consumer_id: payment.consumer_id,
88
97
  amount: payment.amount_minor / 100,
@@ -93,13 +102,13 @@ export class CodeSparChargeRail implements PaymentRail {
93
102
  due_date: payment.due_date,
94
103
  idempotency_key: payment.attempt_id,
95
104
  },
96
- })) as ChargeView;
105
+ });
97
106
  } catch (err) {
98
107
  const failure = describeApiError(err);
99
- if (isUncertain(failure) || failure.code === "issuance_unconfirmed") return { status: "uncertain", code: failure.code, message: failure.message };
108
+ if (isUncertain(failure) || failure.code === ISSUANCE_UNCONFIRMED) return { status: "uncertain", code: failure.code, message: failure.message };
100
109
  return { status: "failed", code: failure.code, message: failure.message };
101
110
  }
102
- if (view.issuance_unconfirmed || !view.id) return { status: "uncertain", code: "issuance_unconfirmed", message: "the issuer's answer to the create was lost; the key is reserved and a later read resolves it" };
111
+ if (view.issuance_unconfirmed || !view.id) return { status: "uncertain", code: ISSUANCE_UNCONFIRMED, message: "the issuer's answer to the create was lost; the key is reserved and a later read resolves it" };
103
112
  if (view.amount_minor !== payment.amount_minor) {
104
113
  // The API understood another amount than the one approved. The receivable exists; withdraw it rather than leave a debt nobody approved.
105
114
  const withdrawn = await this.withdraw(view.id);
@@ -107,7 +116,7 @@ export class CodeSparChargeRail implements PaymentRail {
107
116
  }
108
117
  const outcome = outcomeOf(view);
109
118
  // `in_flight` is excluded above (`issuance_unconfirmed` / no id); the create answers a state, never "still running".
110
- return outcome.status === "in_flight" ? { status: "uncertain", code: "issuance_unconfirmed", message: "the create answered no readable state" } : outcome;
119
+ return outcome.status === "in_flight" ? { status: "uncertain", code: ISSUANCE_UNCONFIRMED, message: "the create answered no readable state" } : outcome;
111
120
  }
112
121
 
113
122
  /**
@@ -119,37 +128,48 @@ export class CodeSparChargeRail implements PaymentRail {
119
128
  const byId = transactionId ? await this.read(transactionId) : { kind: "missing" as const };
120
129
  if (byId.kind === "view") return outcomeOf(byId.view);
121
130
  if (byId.kind === "in_flight") return { status: "in_flight" };
122
- if (byId.kind === "uncertain") return byId.outcome;
131
+ if (byId.kind === "failed" || byId.kind === "uncertain") return byId.outcome;
123
132
  const byKey = await this.read(attemptId);
124
133
  if (byKey.kind === "view") return outcomeOf(byKey.view);
125
134
  if (byKey.kind === "in_flight") return { status: "in_flight" };
126
- if (byKey.kind === "uncertain") return byKey.outcome;
135
+ if (byKey.kind === "failed" || byKey.kind === "uncertain") return byKey.outcome;
127
136
  return undefined;
128
137
  }
129
138
 
130
- private async read(chargeRef: string): Promise<{ kind: "view"; view: ChargeView } | { kind: "missing" } | { kind: "in_flight" } | { kind: "uncertain"; outcome: Extract<RailOutcome, { status: "uncertain" }> }> {
139
+ /**
140
+ * A 409 is branched on its code, never on the status alone: only `issuance_unconfirmed` means "still issuing".
141
+ * `charge_reference_ambiguous` is terminal for the reference and fails explicitly instead of being polled forever,
142
+ * and a 409 whose code this kit does not read is a failure too.
143
+ */
144
+ private async read(chargeRef: string): Promise<ChargeRead> {
131
145
  try {
132
- const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId: chargeRef } })) as ChargeView;
146
+ const view = await this.api.get("/v1/charges/{chargeId}", { path: { chargeId: chargeRef } });
133
147
  return { kind: "view", view };
134
148
  } catch (err) {
135
149
  const failure = describeApiError(err);
136
150
  if (failure.status === 404) return { kind: "missing" };
137
- if (failure.status === 409) return { kind: "in_flight" };
151
+ if (failure.status === 409) {
152
+ if (failure.code === ISSUANCE_UNCONFIRMED) return { kind: "in_flight" };
153
+ const message =
154
+ failure.code === REFERENCE_AMBIGUOUS
155
+ ? `the reference ${chargeRef} matches more than one charge, and the API answers for the charge id only; which one is this attempt's is not assumed, so reconcile it by the charge id`
156
+ : `GET /v1/charges/${chargeRef} answered 409 with a code this kit does not read (${failure.code}); it is not read as a charge still issuing`;
157
+ return { kind: "failed", outcome: { status: "failed", code: failure.code, message } };
158
+ }
138
159
  return { kind: "uncertain", outcome: { status: "uncertain", code: failure.code, message: failure.message } };
139
160
  }
140
161
  }
141
162
 
142
163
  async receipt(chargeId: string, actor: Actor): Promise<RailReceipt | undefined> {
143
164
  try {
144
- const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId } })) as ChargeView;
145
- const raw = view as ChargeView & { settled_at?: string };
165
+ const view = await this.api.get("/v1/charges/{chargeId}", { path: { chargeId } });
146
166
  return {
147
167
  receipt_id: chargeId,
148
168
  kind: "charge",
149
169
  state: view.local_status === "settled" ? "paid" : view.local_status,
150
170
  mandate: { id: "n/a" },
151
171
  // The read names no buyer, so the payee is not on this record; the execution's item names the debtor.
152
- payment: { amount_minor: view.amount_minor, payee: null, attempt_id: chargeId, money_moved: false, sandbox: true, at: raw.settled_at ?? new Date().toISOString() },
172
+ payment: { amount_minor: view.amount_minor, payee: null, attempt_id: chargeId, money_moved: false, sandbox: true, at: settledAt(view) ?? new Date().toISOString() },
153
173
  chain: null,
154
174
  receipt_sig: null,
155
175
  receipt_sig_ed25519: null,
@@ -3,7 +3,7 @@
3
3
  * the `csk_test_` prefix before the client is built, so no live key ever
4
4
  * reaches the network from this repository.
5
5
  */
6
- import { ApiClient, CodesparApiError, TimeoutError } from "@codespar/sdk";
6
+ import { ApiClient, CodesparApiError, TimeoutError, type ApiOperation, type ApiResponse } from "@codespar/sdk";
7
7
  import { assertTestKey } from "../secrets.js";
8
8
 
9
9
  export const DEFAULT_BASE_URL = "https://api.codespar.dev";
@@ -32,15 +32,38 @@ export interface ApiFailure {
32
32
  body?: unknown;
33
33
  }
34
34
 
35
+ /**
36
+ * The `error.code` values an operation documents on its non-2xx answers, read
37
+ * from the SDK's generated types. A code the kit branches on is declared
38
+ * against this, so a code the API stops documenting fails `tsc`.
39
+ */
40
+ export type ApiErrorCode<Op> = Extract<ApiResponse<Op>, { ok: false }>["data"] extends infer D ? (D extends { error: { code: infer C } } ? C : never) : never;
41
+
42
+ /** The codes both spend routes document; the rail takes either. */
43
+ export type SpendErrorCode = ApiErrorCode<ApiOperation<"/v1/consumers/mandates/{id}/spend", "post">> & ApiErrorCode<ApiOperation<"/v1/consumer-payments/execute", "post">>;
44
+
45
+ const UNCERTAIN_CODES: readonly SpendErrorCode[] = ["psp_dispatch_uncertain", "psp_attempt_uncertain"];
46
+
47
+ /** The thrown error's body is `unknown` to the SDK, whatever the route documents; this reads `{ error: { code, message } }` off it without a cast. */
48
+ function errorEnvelope(body: unknown): { code?: string; message?: string } {
49
+ if (!body || typeof body !== "object" || !("error" in body)) return {};
50
+ const error = body.error;
51
+ if (!error || typeof error !== "object") return {};
52
+ return {
53
+ ...("code" in error && typeof error.code === "string" ? { code: error.code } : {}),
54
+ ...("message" in error && typeof error.message === "string" ? { message: error.message } : {}),
55
+ };
56
+ }
57
+
35
58
  /** Normalise the SDK's two error classes into one readable shape without leaking the body into logs. */
36
59
  export function describeApiError(err: unknown): ApiFailure {
37
60
  if (err instanceof TimeoutError) return { status: 0, code: "timeout", message: `request timed out after ${err.timeoutMs}ms` };
38
61
  if (err instanceof CodesparApiError) {
39
- const body = err.body as { error?: { code?: string; message?: string } } | undefined;
62
+ const envelope = errorEnvelope(err.body);
40
63
  return {
41
64
  status: err.status,
42
- code: err.code ?? body?.error?.code ?? (err.status === 0 ? "network" : `http_${err.status}`),
43
- message: body?.error?.message ?? err.message,
65
+ code: err.code ?? envelope.code ?? (err.status === 0 ? "network" : `http_${err.status}`),
66
+ message: envelope.message ?? err.message,
44
67
  body: err.body,
45
68
  };
46
69
  }
@@ -49,5 +72,5 @@ export function describeApiError(err: unknown): ApiFailure {
49
72
 
50
73
  /** 5xx, timeouts and `psp_dispatch_uncertain` mean "the money may have moved". */
51
74
  export function isUncertain(failure: ApiFailure): boolean {
52
- return failure.status === 0 || failure.status >= 500 || failure.code === "psp_dispatch_uncertain" || failure.code === "psp_attempt_uncertain";
75
+ return failure.status === 0 || failure.status >= 500 || UNCERTAIN_CODES.some((code) => code === failure.code);
53
76
  }