@codespar/cli 0.17.0 → 0.18.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 (480) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/templates/bills-agent/README.md +1 -1
  4. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  5. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  7. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  8. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  9. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  10. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  11. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  12. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  13. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  14. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  15. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  16. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  17. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  18. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  19. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  20. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  21. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  22. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  23. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  24. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  25. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  26. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  27. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  28. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  29. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  30. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  31. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  32. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  33. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  34. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  35. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  36. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  37. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  38. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  39. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  40. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  41. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  42. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  43. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  44. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  45. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  46. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  52. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  53. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  59. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  60. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  61. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  62. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  63. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  64. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  65. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  66. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  67. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  69. package/templates/checkout-agent/LICENSE +21 -0
  70. package/templates/checkout-agent/README.md +28 -0
  71. package/templates/checkout-agent/_gitignore +10 -0
  72. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  73. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  74. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  75. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  76. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  77. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  78. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  79. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  83. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  84. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  108. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  109. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  110. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  111. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  112. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  113. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  141. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  146. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  147. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  156. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  157. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  158. package/templates/checkout-agent/package.json +38 -0
  159. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  160. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  161. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  162. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  163. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  167. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  168. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  169. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  170. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  171. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  172. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  173. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  174. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  175. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  176. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  177. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  178. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  179. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  180. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  181. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  182. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  184. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  185. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  186. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  187. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  188. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  189. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  190. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  191. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  192. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  193. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  195. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  196. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  197. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  198. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  199. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  200. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  201. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  202. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  203. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  204. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  205. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  206. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  208. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  209. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  210. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  211. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  212. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  213. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  216. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  217. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  218. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  219. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  220. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  221. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  222. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  223. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  224. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  260. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  261. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  267. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  268. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  269. package/templates/checkout-agent/tsconfig.base.json +16 -0
  270. package/templates/checkout-agent/vitest.config.ts +12 -0
  271. package/templates/collections-agent/README.md +6 -1
  272. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  273. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  274. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  276. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  277. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  278. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  279. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  280. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  281. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  283. package/templates/collections-agent/package.json +1 -0
  284. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  285. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  286. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  287. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  288. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  289. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  290. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  291. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  292. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  293. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  294. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  295. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  296. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  297. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  298. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  299. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  300. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  301. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  302. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  303. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  304. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  305. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  306. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  307. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  308. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  309. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  310. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  311. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  312. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  313. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  314. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  315. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  316. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  317. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  318. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  319. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  320. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  321. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  322. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  328. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  329. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  335. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  336. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  337. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  338. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  339. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  340. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  341. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  342. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  343. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  345. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  346. package/templates/hello-agent/README.md +1 -1
  347. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  348. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  349. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  350. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  351. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  352. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  353. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  354. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  355. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  356. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  357. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  358. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  359. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  360. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  361. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  362. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  363. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  364. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  365. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  366. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  367. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  368. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  369. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  370. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  371. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  372. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  373. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  374. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  375. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  376. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  377. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  378. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  379. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  380. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  381. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  382. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  383. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  384. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  385. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  391. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  392. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  398. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  399. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  400. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  401. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  402. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  403. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  404. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  405. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  406. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  408. package/templates/kits.lock.json +29 -6
  409. package/templates/supplier-payments-agent/README.md +1 -1
  410. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  411. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  416. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  417. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  418. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  420. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  424. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  425. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  426. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  427. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  428. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  429. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  430. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  431. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  432. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  433. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  435. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  437. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  438. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  439. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  440. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  441. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  442. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  443. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  446. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  447. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  451. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  452. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  453. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  473. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  474. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  477. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  478. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  479. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { createHmac, timingSafeEqual } from "node:crypto";
26
26
  import { createServer, type Server } from "node:http";
27
- import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage } from "../types.js";
27
+ import type { ChannelBackend, DeliveryState, InboundMessage, InboundReply, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
28
28
 
29
29
  export interface CloudApiConfig {
30
30
  /**
@@ -140,23 +140,36 @@ interface CloudApiEnvelope {
140
140
  value?: {
141
141
  messaging_product?: string;
142
142
  metadata?: { phone_number_id?: string };
143
- messages?: Array<{ id?: string; from?: string; timestamp?: string; type?: string; text?: { body?: string } }>;
144
- statuses?: Array<{ id?: string; status?: string }>;
143
+ messages?: Array<{
144
+ id?: string;
145
+ from?: string;
146
+ timestamp?: string;
147
+ type?: string;
148
+ text?: { body?: string };
149
+ interactive?: { type?: string; button_reply?: { id?: string; title?: string }; list_reply?: { id?: string; title?: string } };
150
+ button?: { payload?: string; text?: string };
151
+ context?: { id?: string };
152
+ }>;
153
+ statuses?: Array<{ id?: string; status?: string; timestamp?: string; errors?: Array<{ code?: number; title?: string; message?: string; error_data?: { details?: string } }> }>;
145
154
  };
146
155
  }>;
147
156
  }>;
148
157
  }
149
158
 
150
159
  /**
151
- * The text messages inside one delivery. A delivery carries several entries,
152
- * each with several changes, each with several messages — and also status
153
- * updates, which are not messages. Anything that is not a `text` is dropped
154
- * with its id named: this channel reads words, and an image or a location the
155
- * agent cannot read must not be silently treated as an empty message.
160
+ * What one delivery carries. A delivery holds several entries, each with
161
+ * several changes, each with messages and with STATUS updates about messages
162
+ * we sent. A message is read when it is text, or a tapped quick reply (an
163
+ * interactive `button_reply` / `list_reply`, or a template's `button`), which
164
+ * keeps its id — the channel decides whether that id means anything. Anything
165
+ * else (an image, a location) is dropped with its id named: this channel
166
+ * reads words and taps, and an unreadable message must not become an empty
167
+ * turn.
156
168
  */
157
- export function parseInbound(payload: unknown): { messages: InboundMessage[]; ignored: Array<{ id: string; type: string }> } {
169
+ export function parseInbound(payload: unknown): { messages: InboundMessage[]; statuses: StatusUpdate[]; ignored: Array<{ id: string; type: string }> } {
158
170
  const envelope = payload as CloudApiEnvelope;
159
171
  const messages: InboundMessage[] = [];
172
+ const statuses: StatusUpdate[] = [];
160
173
  const ignored: Array<{ id: string; type: string }> = [];
161
174
  for (const entry of envelope.entry ?? []) {
162
175
  for (const change of entry.changes ?? []) {
@@ -164,15 +177,51 @@ export function parseInbound(payload: unknown): { messages: InboundMessage[]; ig
164
177
  const id = message.id;
165
178
  const from = message.from;
166
179
  if (!id || !from) continue;
180
+ const base = { id, from: from.startsWith("+") ? from : `+${from}`, timestamp: Number(message.timestamp ?? 0) };
181
+ const reply = replyOf(message);
182
+ if (reply) {
183
+ messages.push({ ...base, text: reply.title, reply: { ...reply, ...(message.context?.id ? { context_id: message.context.id } : {}) } });
184
+ continue;
185
+ }
167
186
  if (message.type !== "text" || !message.text?.body) {
168
- ignored.push({ id, type: message.type ?? "unknown" });
187
+ ignored.push({ id, type: message.type === "interactive" ? `interactive:${message.interactive?.type ?? "unknown"}` : message.type ?? "unknown" });
169
188
  continue;
170
189
  }
171
- messages.push({ id, from: from.startsWith("+") ? from : `+${from}`, text: message.text.body, timestamp: Number(message.timestamp ?? 0) });
190
+ messages.push({ ...base, text: message.text.body });
191
+ }
192
+ for (const status of change.value?.statuses ?? []) {
193
+ if (!status.id || !isDeliveryState(status.status)) continue;
194
+ statuses.push({
195
+ message_id: status.id,
196
+ status: status.status,
197
+ timestamp: Number(status.timestamp ?? 0),
198
+ errors: (status.errors ?? []).map((e) => ({
199
+ ...(typeof e.code === "number" ? { code: e.code } : {}),
200
+ ...(e.title ?? e.message ? { title: e.title ?? e.message } : {}),
201
+ ...(e.error_data?.details ? { details: e.error_data.details } : {}),
202
+ })),
203
+ });
172
204
  }
173
205
  }
174
206
  }
175
- return { messages, ignored };
207
+ return { messages, statuses, ignored };
208
+ }
209
+
210
+ type CloudMessage = NonNullable<NonNullable<NonNullable<NonNullable<CloudApiEnvelope["entry"]>[number]["changes"]>[number]["value"]>["messages"]>[number];
211
+
212
+ function replyOf(message: CloudMessage): Omit<InboundReply, "context_id"> | undefined {
213
+ if (message.type === "interactive") {
214
+ const kind = message.interactive?.type;
215
+ const picked = kind === "button_reply" ? message.interactive?.button_reply : kind === "list_reply" ? message.interactive?.list_reply : undefined;
216
+ if ((kind === "button_reply" || kind === "list_reply") && picked?.id) return { type: kind, id: picked.id, title: picked.title ?? picked.id };
217
+ return undefined;
218
+ }
219
+ if (message.type === "button" && message.button?.payload) return { type: "button", id: message.button.payload, title: message.button.text ?? message.button.payload };
220
+ return undefined;
221
+ }
222
+
223
+ function isDeliveryState(value: unknown): value is DeliveryState {
224
+ return value === "sent" || value === "delivered" || value === "read" || value === "failed";
176
225
  }
177
226
 
178
227
  export interface SendRequest {
@@ -200,10 +249,6 @@ export function buildSendRequest(config: CloudApiConfig, to: string, body: Outbo
200
249
  method: "POST" as const,
201
250
  headers: { authorization: `Bearer ${config.accessToken}`, "content-type": "application/json" },
202
251
  };
203
- // `to` loses its `+`: the Cloud API documents the number in international
204
- // format without one, and the emulator keys its conversation on the literal
205
- // string, so a `+` here and none on the inbound side splits one person into
206
- // two conversations (measured; docs/OPEN_QUESTIONS.md §46c).
207
252
  const recipient = { messaging_product: "whatsapp", recipient_type: "individual", to: toGraphNumber(to) };
208
253
  switch (body.kind) {
209
254
  case "text":
@@ -219,7 +264,7 @@ export function buildSendRequest(config: CloudApiConfig, to: string, body: Outbo
219
264
  template: {
220
265
  name: body.template,
221
266
  language: { code: body.language },
222
- ...(body.variables.length ? { components: [{ type: "body", parameters: body.variables.map((text) => ({ type: "text", text })) }] } : {}),
267
+ ...templateComponents(body),
223
268
  },
224
269
  }),
225
270
  };
@@ -228,6 +273,49 @@ export function buildSendRequest(config: CloudApiConfig, to: string, body: Outbo
228
273
  }
229
274
  }
230
275
 
276
+ /**
277
+ * A template's parameters as Meta takes them: the body's variables, and one
278
+ * `quick_reply` button component per declared reply, whose `payload` is the
279
+ * reply id — what comes back as `button.payload` when the person taps it.
280
+ */
281
+ function templateComponents(body: Extract<OutboundBody, { kind: "template" }>): { components?: unknown[] } {
282
+ const components: unknown[] = [];
283
+ if (body.variables.length) components.push({ type: "body", parameters: body.variables.map((text) => ({ type: "text", text })) });
284
+ for (const [index, button] of (body.buttons ?? []).entries()) components.push({ type: "button", sub_type: "quick_reply", index: String(index), parameters: [{ type: "payload", payload: button.id }] });
285
+ return components.length ? { components } : {};
286
+ }
287
+
288
+ /** Meta's error for a free-form message outside the 24 hours after the person's last one. */
289
+ export const WINDOW_CLOSED_ERROR = 131047;
290
+
291
+ /**
292
+ * What a non-2xx from `/messages` means, read from Meta's error body.
293
+ *
294
+ * One code gets its own rule: 131047 is the provider saying the 24-hour
295
+ * window is shut, which is the SAME rule the channel enforces before a send
296
+ * and is answered the same way — with a template. It reaches here only when
297
+ * the two clocks disagree (ours read the window open, the provider's read it
298
+ * shut), which at the boundary is a race and not a bug, so a caller has to be
299
+ * able to tell it from a refusal it cannot do anything about.
300
+ */
301
+ export function providerRefusal(status: number, body: string): { rule: string; detail: string } {
302
+ let error: { code?: unknown; message?: unknown; error_data?: { details?: unknown } } | undefined;
303
+ try {
304
+ error = (JSON.parse(body) as { error?: typeof error }).error;
305
+ } catch {
306
+ /* an error without Meta's envelope: the status is all there is */
307
+ }
308
+ const details = typeof error?.error_data?.details === "string" ? error.error_data.details : undefined;
309
+ if (error?.code === WINDOW_CLOSED_ERROR) {
310
+ return {
311
+ rule: "session_window_closed",
312
+ detail: `the provider refused the free-form message (${status}/${WINDOW_CLOSED_ERROR}): its 24h window is shut${details ? ` — ${details}` : ""}`,
313
+ };
314
+ }
315
+ const code = typeof error?.code === "number" ? `/${error.code}` : "";
316
+ return { rule: "provider_refused", detail: `${status}${code} from the Cloud API${details ? ` — ${details}` : ""}` };
317
+ }
318
+
231
319
  /* ── the impure half: one fetch over a request the above built ── */
232
320
 
233
321
  /** `+5511987654321` -> `5511987654321`, the only form the Graph API takes. */
@@ -249,7 +337,10 @@ export class WhatsAppCloudApi implements ChannelBackend {
249
337
  readonly live: boolean;
250
338
  private server: Server | undefined;
251
339
  private readonly queue: InboundMessage[] = [];
340
+ /** Message ids already handed on. Meta delivers at-least-once, so the same message can arrive twice. */
341
+ private readonly seen = new Set<string>();
252
342
  private waiting: ((m: InboundMessage | undefined) => void) | undefined;
343
+ private statusListener: ((status: StatusUpdate) => void) | undefined;
253
344
  private closed = false;
254
345
 
255
346
  constructor(private readonly options: CloudApiOptions) {
@@ -287,15 +378,16 @@ export class WhatsAppCloudApi implements ChannelBackend {
287
378
  res.writeHead(401, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad_signature" }));
288
379
  return;
289
380
  }
290
- let parsed: { messages: InboundMessage[]; ignored: Array<{ id: string; type: string }> };
381
+ let parsed: ReturnType<typeof parseInbound>;
291
382
  try {
292
383
  parsed = parseInbound(JSON.parse(raw));
293
384
  } catch {
294
385
  res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "invalid_json" }));
295
386
  return;
296
387
  }
297
- for (const ignored of parsed.ignored) say(`[whatsapp] ignored ${ignored.type} message ${ignored.id}: this channel reads text`);
388
+ for (const ignored of parsed.ignored) say(`[whatsapp] ignored ${ignored.type} message ${ignored.id}: this channel reads text and taps`);
298
389
  for (const message of parsed.messages) this.push(message);
390
+ for (const status of parsed.statuses) this.statusListener?.(status);
299
391
  // Always 200 once read: Meta retries on anything else, and a duplicate is not an error.
300
392
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ received: parsed.messages.length }));
301
393
  });
@@ -309,6 +401,13 @@ export class WhatsAppCloudApi implements ChannelBackend {
309
401
  }
310
402
 
311
403
  private push(message: InboundMessage): void {
404
+ // A redelivery is the same message, not a second turn: answering it twice
405
+ // would put two replies in front of the person for one thing they said.
406
+ if (this.seen.has(message.id)) {
407
+ this.options.say(`[whatsapp] duplicate delivery of ${message.id} dropped`);
408
+ return;
409
+ }
410
+ this.seen.add(message.id);
312
411
  if (this.waiting) {
313
412
  const resolve = this.waiting;
314
413
  this.waiting = undefined;
@@ -318,6 +417,10 @@ export class WhatsAppCloudApi implements ChannelBackend {
318
417
  this.queue.push(message);
319
418
  }
320
419
 
420
+ onStatus(listener: (status: StatusUpdate) => void): void {
421
+ this.statusListener = listener;
422
+ }
423
+
321
424
  async next(): Promise<InboundMessage | undefined> {
322
425
  if (this.closed) return undefined;
323
426
  const queued = this.queue.shift();
@@ -335,9 +438,7 @@ export class WhatsAppCloudApi implements ChannelBackend {
335
438
  const doFetch = this.options.fetchImpl ?? fetch;
336
439
  const response = await doFetch(request.url, { method: request.method, headers: request.headers, body: request.body });
337
440
  const text = await response.text();
338
- if (!response.ok) {
339
- return { id: "", state: "failed", refused: { rule: "provider_refused", detail: `${response.status} from the Cloud API` } };
340
- }
441
+ if (!response.ok) return { id: "", state: "failed", refused: providerRefusal(response.status, text) };
341
442
  let id = "";
342
443
  try {
343
444
  id = String((JSON.parse(text) as { messages?: Array<{ id?: string }> }).messages?.[0]?.id ?? "");
@@ -26,8 +26,8 @@
26
26
  * `npm run whatsapp:emulator` is the command.
27
27
  */
28
28
  import type { ConversationScript } from "@codespar/agent-core";
29
- import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage } from "../types.js";
30
- import { toGraphNumber, WhatsAppCloudApi, type CloudApiOptions } from "./cloud-api.js";
29
+ import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
30
+ import { WhatsAppCloudApi, type CloudApiOptions } from "./cloud-api.js";
31
31
 
32
32
  export const EMULATOR_ENV = {
33
33
  url: "WHATSAPP_SIM_URL",
@@ -93,18 +93,23 @@ export class EmulatorDriver {
93
93
  return this.post("/_sim/clock", { advance_hours: hours });
94
94
  }
95
95
 
96
- /** The person writes. The emulator answers with a signed inbound webhook to our receiver. */
97
- inbound(options: { phoneNumberId: string; from: string; text: string; sentAt?: Date }): Promise<unknown> {
96
+ /** The person writes, or taps a quick reply. The emulator answers with a signed inbound webhook to our receiver. */
97
+ inbound(options: { phoneNumberId: string; from: string; text: string; sentAt?: Date } | { phoneNumberId: string; from: string; reply: { id: string; title: string }; sentAt?: Date }): Promise<unknown> {
98
98
  return this.post("/_sim/inbound", {
99
99
  phone_number_id: options.phoneNumberId,
100
- // Without the `+`, matching what `deliver` sends as `to`: the emulator keys
101
- // its conversation on the literal string, so the two forms are two people.
102
- from: toGraphNumber(options.from),
103
- text: options.text,
100
+ from: options.from,
101
+ ...("reply" in options
102
+ ? { type: "interactive", interactive: { type: "button_reply", button_reply: { id: options.reply.id, title: options.reply.title } } }
103
+ : { text: options.text }),
104
104
  ...(options.sentAt ? { sent_at: options.sentAt.toISOString() } : {}),
105
105
  });
106
106
  }
107
107
 
108
+ /** Reports a delivery status for a message the business sent, as the provider would: `read`, or `failed` with a reason. */
109
+ status(options: { status: "read" | "failed"; messageId: string; reason?: string }): Promise<unknown> {
110
+ return this.post("/_sim/status", { status: options.status, message_id: options.messageId, ...(options.reason ? { reason: options.reason } : {}) });
111
+ }
112
+
108
113
  /** The priced timeline, which is the emulator's own reason for existing. Read for the console, never asserted on. */
109
114
  state(key: string): Promise<{ priced?: { total?: number; currency?: string } } | undefined> {
110
115
  return this.call(`/_sim/state?key=${encodeURIComponent(key)}`).then(
@@ -156,20 +161,21 @@ export class WhatsAppEmulator implements ChannelBackend {
156
161
 
157
162
  async next(): Promise<InboundMessage | undefined> {
158
163
  if (this.closed) return undefined;
159
- const text = await this.nextText();
160
- if (text === undefined) return undefined;
161
- // The person writes THROUGH the emulator: it mints the message id and posts
162
- // the signed webhook our receiver verifies, so a scripted turn takes the
163
- // same path a real one would.
164
- await this.options.driver.inbound({
165
- phoneNumberId: this.options.config.phoneNumberId,
166
- from: this.options.conversation.contact,
167
- text,
168
- });
164
+ const turn = await this.nextTurn();
165
+ if (turn === undefined) return undefined;
166
+ // The person writes (or taps) THROUGH the emulator: it mints the message id
167
+ // and posts the signed webhook our receiver verifies, so a scripted turn
168
+ // takes the same path a real one would.
169
+ const who = { phoneNumberId: this.options.config.phoneNumberId, from: this.options.conversation.contact };
170
+ await this.options.driver.inbound(typeof turn === "string" ? { ...who, text: turn } : { ...who, reply: turn });
169
171
  return this.api.next();
170
172
  }
171
173
 
172
- private async nextText(): Promise<string | undefined> {
174
+ onStatus(listener: (status: StatusUpdate) => void): void {
175
+ this.api.onStatus(listener);
176
+ }
177
+
178
+ private async nextTurn(): Promise<string | { id: string; title: string } | undefined> {
173
179
  const script = this.options.script;
174
180
  if (script) {
175
181
  const turn = script.turns[this.turn];
@@ -178,8 +184,12 @@ export class WhatsAppEmulator implements ChannelBackend {
178
184
  // The person's own pause, on the emulator's clock. A conversation that
179
185
  // happens over days is what closes the 24-hour window.
180
186
  if (turn.after_seconds > 0) await this.options.driver.advanceHours(turn.after_seconds / 3600);
181
- return turn.text;
187
+ return turn.reply ? { id: turn.reply.id, title: turn.reply.title ?? turn.reply.id } : turn.text;
182
188
  }
189
+ return this.nextText();
190
+ }
191
+
192
+ private async nextText(): Promise<string | undefined> {
183
193
  if (!this.options.ask) return undefined;
184
194
  let answer: string;
185
195
  try {
@@ -13,9 +13,9 @@
13
13
  * caller and have no path to this object at all — not a check, a shape: there
14
14
  * is no method here that an operator line could be handed to.
15
15
  */
16
- import type { ProofBundle, WhatsAppTemplate } from "@codespar/agent-core";
16
+ import { declaredReplies, type ProofBundle, type QuickReply, type WhatsAppTemplate } from "@codespar/agent-core";
17
17
  import { checkOutbound, type HoursRule, type RuleContext } from "../rules.js";
18
- import type { Channel, ChannelBackend, ChannelLogLine, Conversation, InboundMessage, OutboundBody, SentMessage } from "../types.js";
18
+ import type { Channel, ChannelBackend, ChannelLogLine, Conversation, DeliveryState, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
19
19
  import { maskContact } from "../contact.js";
20
20
  import { SessionWindow, type SessionState } from "./session.js";
21
21
 
@@ -44,6 +44,14 @@ export interface WhatsAppChannelOptions {
44
44
  * something in front of the person — that is `send`, and it is the only one.
45
45
  */
46
46
  render?: ((line: string) => void) | undefined;
47
+ /**
48
+ * The provider reported that a message the agent sent was NOT delivered.
49
+ * Called with the status line, which names the outcome the message told
50
+ * when it told one — that outcome was not told, whatever the send answered.
51
+ */
52
+ onDeliveryFailed?: ((line: ChannelLogLine) => void) | undefined;
53
+ /** Lines an earlier run wrote for this conversation, so a status about a message it sent can be tied back to what that message was. */
54
+ priorLines?: ReadonlyArray<ChannelLogLine> | undefined;
47
55
  }
48
56
 
49
57
  export class WhatsAppChannel implements Channel {
@@ -52,10 +60,21 @@ export class WhatsAppChannel implements Channel {
52
60
  private readonly session: SessionWindow;
53
61
  private readonly lines: ChannelLogLine[] = [];
54
62
  private lastInbound: InboundMessage | undefined;
63
+ private readonly replies: Map<string, QuickReply>;
64
+ /** The latest state the provider reported per message id, this process. */
65
+ private readonly delivery = new Map<string, DeliveryState>();
66
+ /**
67
+ * Failures reported for a message not yet in the log. A provider can post
68
+ * the status before the send that minted the id has answered (the emulator
69
+ * does, synchronously); the failure is acted on once the message is logged
70
+ * and it is known what the message was.
71
+ */
72
+ private readonly earlyFailures = new Map<string, ChannelLogLine>();
55
73
 
56
74
  constructor(private readonly options: WhatsAppChannelOptions) {
57
75
  this.conversation = options.conversation;
58
76
  this.session = new SessionWindow(options.templates ?? [], options.session);
77
+ this.replies = declaredReplies(options.templates ?? []);
59
78
  }
60
79
 
61
80
  get backend(): string {
@@ -87,33 +106,125 @@ export class WhatsAppChannel implements Channel {
87
106
  return this.session.remainingSeconds(this.options.now());
88
107
  }
89
108
 
109
+ /** The registry's fallback template, the one sent for an outcome the kit has no copy for. */
110
+ fallbackTemplate(): string | undefined {
111
+ return (this.options.templates ?? []).find((t) => t.fallback)?.name;
112
+ }
113
+
90
114
  /** What the agent declared about a template it is about to send. The language is the registry's, never the sender's guess. */
91
115
  declaredTemplate(name: string): WhatsAppTemplate | undefined {
92
116
  return this.session.declared(name);
93
117
  }
94
118
 
95
119
  async open(): Promise<void> {
120
+ this.options.backend.onStatus?.((status) => this.observeStatus(status));
96
121
  await this.options.backend.open();
97
122
  }
98
123
 
124
+ /**
125
+ * The person's next turn. A typed message is the turn. A TAPPED quick reply
126
+ * is a turn only when its id is one a declared template offers, and then the
127
+ * turn is that reply's declared intent, never the button's title and never
128
+ * anything the model makes of it. A tap on an id nobody declared — or on a
129
+ * message this conversation did not send, when the provider names the
130
+ * message — is recorded, reported to the operator and skipped: a button the
131
+ * agent never offered has no meaning to hand on.
132
+ */
99
133
  async next(): Promise<InboundMessage | undefined> {
100
- const message = await this.options.backend.next();
101
- if (!message) return undefined;
102
- this.lastInbound = message;
103
- this.session.observeInbound(message.timestamp);
104
- this.record({
134
+ for (;;) {
135
+ const message = await this.options.backend.next();
136
+ if (!message) return undefined;
137
+ // Any inbound, tap or text, reopens the 24-hour window: the person wrote.
138
+ this.lastInbound = message;
139
+ this.session.observeInbound(message.timestamp);
140
+ const base = {
141
+ at: this.options.now().toISOString(),
142
+ direction: "in" as const,
143
+ contact: maskContact(message.from),
144
+ message_id: message.id,
145
+ state: "delivered" as const,
146
+ // The provider's clock, which is the only one the 24-hour window is
147
+ // counted on. A later run reads the window back from here.
148
+ provider_timestamp: message.timestamp,
149
+ };
150
+ if (!message.reply) {
151
+ this.record({ ...base, kind: "text", text: message.text });
152
+ return message;
153
+ }
154
+ const tapped = { type: message.reply.type, id: message.reply.id, title: message.reply.title };
155
+ const declared = this.replies.get(message.reply.id);
156
+ const refusal = !declared
157
+ ? { rule: "reply_not_offered", detail: `the person tapped ${message.reply.id}, which no template this agent declares offers; it is not a turn` }
158
+ : message.reply.context_id && !this.offeredOn(message.reply.context_id, message.reply.id)
159
+ ? { rule: "reply_not_offered", detail: `the tap names message ${message.reply.context_id}, which this conversation did not send with ${message.reply.id}` }
160
+ : undefined;
161
+ if (refusal) {
162
+ this.record({ ...base, kind: "reply", reply: tapped, refused: refusal });
163
+ this.options.say?.(` [whatsapp] toque ignorado (${refusal.rule}): ${refusal.detail}`);
164
+ continue;
165
+ }
166
+ this.record({ ...base, kind: "reply", reply: tapped, text: declared!.intent });
167
+ return { ...message, text: declared!.intent };
168
+ }
169
+ }
170
+
171
+ /** Whether the message the provider named is one this conversation sent carrying that reply. */
172
+ private offeredOn(messageId: string, replyId: string): boolean {
173
+ return [...(this.options.priorLines ?? []), ...this.lines].some((l) => l.direction === "out" && l.message_id === messageId && (l.offered ?? []).includes(replyId));
174
+ }
175
+
176
+ /**
177
+ * A status webhook. Recorded in the conversation; a `failed` is also the
178
+ * operator's business and, when the message told an outcome, the outcome's:
179
+ * the provider just said the person never got it. A `read` is recorded and
180
+ * nothing more — it says a device displayed the message, which is not
181
+ * consent, not acknowledgement of a debt or an order, and not a reply.
182
+ */
183
+ private observeStatus(status: StatusUpdate): void {
184
+ const sent = [...(this.options.priorLines ?? []), ...this.lines].find((l) => l.direction === "out" && l.message_id === status.message_id);
185
+ this.delivery.set(status.message_id, status.status);
186
+ const line: ChannelLogLine = {
105
187
  at: this.options.now().toISOString(),
106
- direction: "in",
107
- contact: maskContact(message.from),
108
- kind: "text",
109
- message_id: message.id,
110
- state: "delivered",
111
- text: message.text,
112
- // The provider's clock, which is the only one the 24-hour window is
113
- // counted on. A later run reads the window back from here.
114
- provider_timestamp: message.timestamp,
115
- });
116
- return message;
188
+ direction: "status",
189
+ contact: maskContact(this.conversation.contact),
190
+ kind: "status",
191
+ message_id: status.message_id,
192
+ state: status.status,
193
+ provider_timestamp: status.timestamp,
194
+ ...(sent?.about ? { about: sent.about } : {}),
195
+ ...(status.errors.length ? { errors: status.errors } : {}),
196
+ };
197
+ this.record(line);
198
+ if (status.status !== "failed") return;
199
+ if (!sent) {
200
+ this.earlyFailures.set(status.message_id, line);
201
+ return;
202
+ }
203
+ this.reportFailure(line);
204
+ }
205
+
206
+ private reportFailure(line: ChannelLogLine): void {
207
+ const status = { message_id: line.message_id, errors: line.errors ?? [] };
208
+ const codes = status.errors.map((e) => `${e.code ?? "?"}${e.details ? ` ${e.details}` : ""}`).join("; ") || "no error given";
209
+ this.options.say?.(
210
+ ` [operador] ENTREGA FALHOU da mensagem ${status.message_id} (${codes})${line.about ? ` — era o aviso de ${line.about.execution_id} (${line.about.state}): a pessoa NAO foi avisada` : ""}`,
211
+ );
212
+ this.options.onDeliveryFailed?.(line);
213
+ }
214
+
215
+ /** The latest status the provider reported for a message this process saw a status for. */
216
+ deliveryOf(messageId: string): DeliveryState | undefined {
217
+ return this.delivery.get(messageId);
218
+ }
219
+
220
+ /**
221
+ * Waits for status webhooks that may still be in flight. A provider reports
222
+ * a failure after the send answered, and a process that exits first never
223
+ * hears it; this is the short, bounded wait a caller that has just told an
224
+ * outcome gives the provider before it reports the outcome as told.
225
+ */
226
+ async settle(ms: number): Promise<void> {
227
+ if (ms > 0) await new Promise((resolve) => setTimeout(resolve, ms));
117
228
  }
118
229
 
119
230
  async send(body: OutboundBody): Promise<SentMessage> {
@@ -134,9 +245,13 @@ export class WhatsAppChannel implements Channel {
134
245
  return sent;
135
246
  }
136
247
 
137
- const sent = await this.options.backend.deliver(to, body);
248
+ // A template carries the quick replies its declaration offers, and only those.
249
+ const buttons = body.kind === "template" ? this.session.declared(body.template)?.buttons : undefined;
250
+ const outgoing: OutboundBody = body.kind === "template" && buttons?.length ? { ...body, buttons: buttons.map((b) => ({ id: b.id, title: b.title })) } : body;
251
+ const sent = await this.options.backend.deliver(to, outgoing);
138
252
  if (sent.refused) this.options.say?.(` [whatsapp] recusado pelo backend (${sent.refused.rule}): ${sent.refused.detail}`);
139
- this.logOutbound(body, sent);
253
+ if (sent.refused?.rule === "session_window_closed") this.session.observeProviderShut();
254
+ this.logOutbound(outgoing, sent);
140
255
  return sent;
141
256
  }
142
257
 
@@ -154,6 +269,12 @@ export class WhatsAppChannel implements Channel {
154
269
  }
155
270
 
156
271
  private logOutbound(body: OutboundBody, sent: SentMessage): void {
272
+ const early = sent.id ? this.earlyFailures.get(sent.id) : undefined;
273
+ if (early) {
274
+ this.earlyFailures.delete(sent.id);
275
+ const about = (body.kind === "text" || body.kind === "template") && body.about ? body.about : undefined;
276
+ queueMicrotask(() => this.reportFailure({ ...early, ...(about ? { about } : {}) }));
277
+ }
157
278
  this.record({
158
279
  at: this.options.now().toISOString(),
159
280
  direction: "out",
@@ -163,6 +284,8 @@ export class WhatsAppChannel implements Channel {
163
284
  state: sent.state,
164
285
  ...(textOf(body) !== undefined ? { text: textOf(body)! } : {}),
165
286
  ...(sent.refused ? { refused: sent.refused } : {}),
287
+ ...((body.kind === "text" || body.kind === "template") && body.about ? { about: body.about } : {}),
288
+ ...(body.kind === "template" && body.buttons?.length ? { offered: body.buttons.map((b) => b.id) } : {}),
166
289
  });
167
290
  }
168
291
 
@@ -193,6 +316,8 @@ export class WhatsAppChannel implements Channel {
193
316
 
194
317
  /** One console line for one message. The refusals are visible: a message nobody got is part of the conversation's story. */
195
318
  function draw(line: ChannelLogLine): string {
319
+ if (line.direction === "status") return `(status ${line.message_id}: ${line.state}${line.errors?.length ? ` ${line.errors.map((e) => e.code).join(",")}` : ""})`;
320
+ if (line.kind === "reply") return `${line.contact}: [toque: ${line.reply?.title ?? "?"}]${line.refused ? ` (ignorado: ${line.refused.rule})` : ` -> ${line.text ?? ""}`}`;
196
321
  const who = line.direction === "in" ? `${line.contact}:` : "loja:";
197
322
  const what =
198
323
  line.kind === "instrument"
@@ -214,7 +339,7 @@ function textOf(body: OutboundBody): string | undefined {
214
339
  case "media":
215
340
  return body.caption;
216
341
  case "template":
217
- return `[template ${body.template}] ${body.variables.join(" | ")}`;
342
+ return `[template ${body.template}] ${body.variables.join(" | ")}${body.buttons?.length ? ` [${body.buttons.map((b) => b.title).join("] [")}]` : ""}`;
218
343
  }
219
344
  }
220
345
 
@@ -15,9 +15,9 @@ import type { ConversationScript, ProofBundle } from "@codespar/agent-core";
15
15
  import type { Agent } from "../../agent.js";
16
16
  import type { Setup } from "../../setup.js";
17
17
  import { knownSubjects, loadTemplates } from "../index.js";
18
- import type { ChannelBackend, Conversation } from "../types.js";
18
+ import type { ChannelBackend, ChannelLogLine, Conversation } from "../types.js";
19
19
  import { WhatsAppChannel } from "./index.js";
20
- import { toGraphNumber, WhatsAppCloudApi, loadCloudApiConfig, type CloudApiConfig } from "./cloud-api.js";
20
+ import { WhatsAppCloudApi, loadCloudApiConfig, type CloudApiConfig } from "./cloud-api.js";
21
21
  import { EmulatorDriver, EMULATOR_DEFAULTS, EMULATOR_ENV, WhatsAppEmulator } from "./emulator.js";
22
22
  import type { SessionState } from "./session.js";
23
23
 
@@ -52,6 +52,41 @@ export interface OpenWhatsAppOptions {
52
52
  session?: SessionState | undefined;
53
53
  /** Draws the conversation on the operator's console. */
54
54
  render?: ((line: string) => void) | undefined;
55
+ /** What earlier runs wrote for this conversation: a status about one of their messages is tied back to it. */
56
+ priorLines?: ReadonlyArray<ChannelLogLine> | undefined;
57
+ }
58
+
59
+ /**
60
+ * How long a command that has just told an outcome waits for status webhooks
61
+ * before it reports the outcome as told. A provider reports a failed delivery
62
+ * AFTER the send answered, and a process that has exited hears nothing; this
63
+ * bounds the wait. `WHATSAPP_STATUS_GRACE_MS` overrides it (0 turns it off).
64
+ */
65
+ export function statusGraceMs(env: NodeJS.ProcessEnv = process.env): number {
66
+ const raw = Number(env["WHATSAPP_STATUS_GRACE_MS"]);
67
+ return Number.isFinite(raw) && raw >= 0 && env["WHATSAPP_STATUS_GRACE_MS"]?.trim() ? raw : 1000;
68
+ }
69
+
70
+ /** The cursor that says an outcome's message came back `failed`, so a later poll reports it as not told rather than as told. */
71
+ export function undeliveredCursor(executionId: string, state: string): string {
72
+ return `undelivered:${executionId}:${state}`;
73
+ }
74
+
75
+ /**
76
+ * A `failed` status is an event in the proof bundle, always. When the message
77
+ * told an outcome it is `message.debtor.failed`, and the cursor above is taken:
78
+ * the cursor that says the person was told (`markTold`) stays held — a failed
79
+ * delivery is not retried by itself, because the usual cause (not a WhatsApp
80
+ * number, blocked) fails again — but nothing reports that outcome as told.
81
+ */
82
+ export function recordDeliveryFailure(s: Setup, line: ChannelLogLine): void {
83
+ const payload = { message_id: line.message_id, errors: line.errors ?? [], channel: "whatsapp" };
84
+ if (!line.about) {
85
+ s.engine.note("message.failed", null, payload);
86
+ return;
87
+ }
88
+ s.store.setCursor(undeliveredCursor(line.about.execution_id, line.about.state), JSON.stringify(payload));
89
+ s.engine.note("message.debtor.failed", line.about.execution_id, { ...payload, state: line.about.state });
55
90
  }
56
91
 
57
92
  export interface OpenedWhatsApp {
@@ -102,7 +137,7 @@ export function buildWhatsApp(options: OpenWhatsAppOptions): OpenedWhatsApp | {
102
137
  webhookPort: Number(env[EMULATOR_ENV.webhookPort]?.trim() || EMULATOR_DEFAULTS.webhookPort),
103
138
  };
104
139
  driver = new EmulatorDriver(url);
105
- sessionKey = `${config.phoneNumberId}:${toGraphNumber(options.conversation.contact)}`;
140
+ sessionKey = `${config.phoneNumberId}:${options.conversation.contact}`;
106
141
  backend = new WhatsAppEmulator({
107
142
  config,
108
143
  conversation: { contact: options.conversation.contact },
@@ -129,6 +164,8 @@ export function buildWhatsApp(options: OpenWhatsAppOptions): OpenedWhatsApp | {
129
164
  say,
130
165
  render: options.render ?? say,
131
166
  ...(options.session ? { session: options.session } : {}),
167
+ ...(options.priorLines ? { priorLines: options.priorLines } : {}),
168
+ onDeliveryFailed: (line) => recordDeliveryFailure(s, line),
132
169
  });
133
170
 
134
171
  return { channel, ...(driver ? { driver } : {}), ...(sessionKey ? { sessionKey } : {}) };