@falai/agent 2.6.1 → 2.7.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 (320) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/MemoryAdapter.js +29 -33
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  5. package/dist/adapters/PostgreSQLAdapter.js +16 -11
  6. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  7. package/dist/adapters/RedisAdapter.d.ts +1 -0
  8. package/dist/adapters/RedisAdapter.d.ts.map +1 -1
  9. package/dist/adapters/RedisAdapter.js +75 -25
  10. package/dist/adapters/RedisAdapter.js.map +1 -1
  11. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  12. package/dist/adapters/SQLiteAdapter.js +7 -29
  13. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  14. package/dist/adapters/sessionRow.d.ts +22 -0
  15. package/dist/adapters/sessionRow.d.ts.map +1 -0
  16. package/dist/adapters/sessionRow.js +48 -0
  17. package/dist/adapters/sessionRow.js.map +1 -0
  18. package/dist/cjs/adapters/MemoryAdapter.js +29 -33
  19. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  20. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  21. package/dist/cjs/adapters/PostgreSQLAdapter.js +16 -11
  22. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/RedisAdapter.d.ts +1 -0
  24. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
  25. package/dist/cjs/adapters/RedisAdapter.js +75 -25
  26. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  27. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  28. package/dist/cjs/adapters/SQLiteAdapter.js +7 -29
  29. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  30. package/dist/cjs/adapters/sessionRow.d.ts +22 -0
  31. package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
  32. package/dist/cjs/adapters/sessionRow.js +52 -0
  33. package/dist/cjs/adapters/sessionRow.js.map +1 -0
  34. package/dist/cjs/core/Agent.d.ts +8 -0
  35. package/dist/cjs/core/Agent.d.ts.map +1 -1
  36. package/dist/cjs/core/Agent.js +40 -2
  37. package/dist/cjs/core/Agent.js.map +1 -1
  38. package/dist/cjs/core/AutoChainExecutor.d.ts +8 -18
  39. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
  40. package/dist/cjs/core/AutoChainExecutor.js +23 -26
  41. package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
  42. package/dist/cjs/core/CompactionEngine.d.ts +14 -1
  43. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
  44. package/dist/cjs/core/CompactionEngine.js +30 -6
  45. package/dist/cjs/core/CompactionEngine.js.map +1 -1
  46. package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
  47. package/dist/cjs/core/FlowRouter.js +32 -10
  48. package/dist/cjs/core/FlowRouter.js.map +1 -1
  49. package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -1
  50. package/dist/cjs/core/ResponseGenerationError.js +3 -5
  51. package/dist/cjs/core/ResponseGenerationError.js.map +1 -1
  52. package/dist/cjs/core/ResponseModal.d.ts +29 -0
  53. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  54. package/dist/cjs/core/ResponseModal.js +141 -18
  55. package/dist/cjs/core/ResponseModal.js.map +1 -1
  56. package/dist/cjs/core/ResponsePipeline.d.ts +36 -6
  57. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  58. package/dist/cjs/core/ResponsePipeline.js +208 -73
  59. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  60. package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -1
  61. package/dist/cjs/core/SessionFinalizer.js +30 -3
  62. package/dist/cjs/core/SessionFinalizer.js.map +1 -1
  63. package/dist/cjs/core/SessionManager.d.ts +10 -1
  64. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  65. package/dist/cjs/core/SessionManager.js +43 -16
  66. package/dist/cjs/core/SessionManager.js.map +1 -1
  67. package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
  68. package/dist/cjs/core/SignalProcessor.js +5 -77
  69. package/dist/cjs/core/SignalProcessor.js.map +1 -1
  70. package/dist/cjs/core/Step.d.ts.map +1 -1
  71. package/dist/cjs/core/Step.js +50 -2
  72. package/dist/cjs/core/Step.js.map +1 -1
  73. package/dist/cjs/core/StepLifecycle.d.ts +16 -6
  74. package/dist/cjs/core/StepLifecycle.d.ts.map +1 -1
  75. package/dist/cjs/core/StepLifecycle.js +97 -14
  76. package/dist/cjs/core/StepLifecycle.js.map +1 -1
  77. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  78. package/dist/cjs/core/StreamingToolExecutor.js +28 -4
  79. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  80. package/dist/cjs/core/ToolLoopExecutor.d.ts +5 -1
  81. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
  82. package/dist/cjs/core/ToolLoopExecutor.js +155 -63
  83. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  84. package/dist/cjs/core/ToolManager.d.ts +1 -1
  85. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  86. package/dist/cjs/core/ToolManager.js +40 -17
  87. package/dist/cjs/core/ToolManager.js.map +1 -1
  88. package/dist/cjs/core/flow-namespace.d.ts +15 -0
  89. package/dist/cjs/core/flow-namespace.d.ts.map +1 -1
  90. package/dist/cjs/core/flow-namespace.js +22 -0
  91. package/dist/cjs/core/flow-namespace.js.map +1 -1
  92. package/dist/cjs/index.d.ts +4 -1
  93. package/dist/cjs/index.d.ts.map +1 -1
  94. package/dist/cjs/index.js +5 -2
  95. package/dist/cjs/index.js.map +1 -1
  96. package/dist/cjs/providers/AnthropicProvider.d.ts +10 -3
  97. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  98. package/dist/cjs/providers/AnthropicProvider.js +54 -73
  99. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  100. package/dist/cjs/providers/GeminiProvider.d.ts +9 -3
  101. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  102. package/dist/cjs/providers/GeminiProvider.js +24 -68
  103. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  104. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +9 -0
  105. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  106. package/dist/cjs/providers/OpenAICompatibleProvider.js +101 -74
  107. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
  108. package/dist/cjs/providers/OpenAIProvider.d.ts +1 -1
  109. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  110. package/dist/cjs/providers/OpenAIProvider.js +1 -1
  111. package/dist/cjs/providers/errorClassification.d.ts +7 -0
  112. package/dist/cjs/providers/errorClassification.d.ts.map +1 -1
  113. package/dist/cjs/providers/errorClassification.js +27 -0
  114. package/dist/cjs/providers/errorClassification.js.map +1 -1
  115. package/dist/cjs/types/agent.d.ts +26 -0
  116. package/dist/cjs/types/agent.d.ts.map +1 -1
  117. package/dist/cjs/types/flow.d.ts +49 -32
  118. package/dist/cjs/types/flow.d.ts.map +1 -1
  119. package/dist/cjs/types/index.d.ts +1 -1
  120. package/dist/cjs/types/index.d.ts.map +1 -1
  121. package/dist/cjs/types/index.js.map +1 -1
  122. package/dist/cjs/types/tool.d.ts +2 -0
  123. package/dist/cjs/types/tool.d.ts.map +1 -1
  124. package/dist/cjs/types/tool.js.map +1 -1
  125. package/dist/cjs/utils/index.d.ts +4 -4
  126. package/dist/cjs/utils/index.d.ts.map +1 -1
  127. package/dist/cjs/utils/index.js +10 -1
  128. package/dist/cjs/utils/index.js.map +1 -1
  129. package/dist/cjs/utils/retry.d.ts +99 -1
  130. package/dist/cjs/utils/retry.d.ts.map +1 -1
  131. package/dist/cjs/utils/retry.js +140 -3
  132. package/dist/cjs/utils/retry.js.map +1 -1
  133. package/dist/cjs/utils/serialize.d.ts +17 -0
  134. package/dist/cjs/utils/serialize.d.ts.map +1 -1
  135. package/dist/cjs/utils/serialize.js +33 -0
  136. package/dist/cjs/utils/serialize.js.map +1 -1
  137. package/dist/cjs/utils/session.d.ts +21 -1
  138. package/dist/cjs/utils/session.d.ts.map +1 -1
  139. package/dist/cjs/utils/session.js +36 -4
  140. package/dist/cjs/utils/session.js.map +1 -1
  141. package/dist/core/Agent.d.ts +8 -0
  142. package/dist/core/Agent.d.ts.map +1 -1
  143. package/dist/core/Agent.js +40 -2
  144. package/dist/core/Agent.js.map +1 -1
  145. package/dist/core/AutoChainExecutor.d.ts +8 -18
  146. package/dist/core/AutoChainExecutor.d.ts.map +1 -1
  147. package/dist/core/AutoChainExecutor.js +23 -26
  148. package/dist/core/AutoChainExecutor.js.map +1 -1
  149. package/dist/core/CompactionEngine.d.ts +14 -1
  150. package/dist/core/CompactionEngine.d.ts.map +1 -1
  151. package/dist/core/CompactionEngine.js +30 -6
  152. package/dist/core/CompactionEngine.js.map +1 -1
  153. package/dist/core/FlowRouter.d.ts.map +1 -1
  154. package/dist/core/FlowRouter.js +32 -10
  155. package/dist/core/FlowRouter.js.map +1 -1
  156. package/dist/core/ResponseGenerationError.d.ts.map +1 -1
  157. package/dist/core/ResponseGenerationError.js +3 -5
  158. package/dist/core/ResponseGenerationError.js.map +1 -1
  159. package/dist/core/ResponseModal.d.ts +29 -0
  160. package/dist/core/ResponseModal.d.ts.map +1 -1
  161. package/dist/core/ResponseModal.js +142 -19
  162. package/dist/core/ResponseModal.js.map +1 -1
  163. package/dist/core/ResponsePipeline.d.ts +36 -6
  164. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  165. package/dist/core/ResponsePipeline.js +208 -73
  166. package/dist/core/ResponsePipeline.js.map +1 -1
  167. package/dist/core/SessionFinalizer.d.ts.map +1 -1
  168. package/dist/core/SessionFinalizer.js +31 -4
  169. package/dist/core/SessionFinalizer.js.map +1 -1
  170. package/dist/core/SessionManager.d.ts +10 -1
  171. package/dist/core/SessionManager.d.ts.map +1 -1
  172. package/dist/core/SessionManager.js +44 -17
  173. package/dist/core/SessionManager.js.map +1 -1
  174. package/dist/core/SignalProcessor.d.ts.map +1 -1
  175. package/dist/core/SignalProcessor.js +5 -77
  176. package/dist/core/SignalProcessor.js.map +1 -1
  177. package/dist/core/Step.d.ts.map +1 -1
  178. package/dist/core/Step.js +50 -2
  179. package/dist/core/Step.js.map +1 -1
  180. package/dist/core/StepLifecycle.d.ts +16 -6
  181. package/dist/core/StepLifecycle.d.ts.map +1 -1
  182. package/dist/core/StepLifecycle.js +97 -14
  183. package/dist/core/StepLifecycle.js.map +1 -1
  184. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  185. package/dist/core/StreamingToolExecutor.js +28 -4
  186. package/dist/core/StreamingToolExecutor.js.map +1 -1
  187. package/dist/core/ToolLoopExecutor.d.ts +5 -1
  188. package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
  189. package/dist/core/ToolLoopExecutor.js +155 -63
  190. package/dist/core/ToolLoopExecutor.js.map +1 -1
  191. package/dist/core/ToolManager.d.ts +1 -1
  192. package/dist/core/ToolManager.d.ts.map +1 -1
  193. package/dist/core/ToolManager.js +41 -18
  194. package/dist/core/ToolManager.js.map +1 -1
  195. package/dist/core/flow-namespace.d.ts +15 -0
  196. package/dist/core/flow-namespace.d.ts.map +1 -1
  197. package/dist/core/flow-namespace.js +22 -0
  198. package/dist/core/flow-namespace.js.map +1 -1
  199. package/dist/index.d.ts +4 -1
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +2 -1
  202. package/dist/index.js.map +1 -1
  203. package/dist/providers/AnthropicProvider.d.ts +10 -3
  204. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  205. package/dist/providers/AnthropicProvider.js +56 -75
  206. package/dist/providers/AnthropicProvider.js.map +1 -1
  207. package/dist/providers/GeminiProvider.d.ts +9 -3
  208. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  209. package/dist/providers/GeminiProvider.js +26 -70
  210. package/dist/providers/GeminiProvider.js.map +1 -1
  211. package/dist/providers/OpenAICompatibleProvider.d.ts +9 -0
  212. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  213. package/dist/providers/OpenAICompatibleProvider.js +103 -76
  214. package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
  215. package/dist/providers/OpenAIProvider.d.ts +1 -1
  216. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  217. package/dist/providers/OpenAIProvider.js +1 -1
  218. package/dist/providers/errorClassification.d.ts +7 -0
  219. package/dist/providers/errorClassification.d.ts.map +1 -1
  220. package/dist/providers/errorClassification.js +26 -0
  221. package/dist/providers/errorClassification.js.map +1 -1
  222. package/dist/types/agent.d.ts +26 -0
  223. package/dist/types/agent.d.ts.map +1 -1
  224. package/dist/types/flow.d.ts +49 -32
  225. package/dist/types/flow.d.ts.map +1 -1
  226. package/dist/types/index.d.ts +1 -1
  227. package/dist/types/index.d.ts.map +1 -1
  228. package/dist/types/index.js.map +1 -1
  229. package/dist/types/tool.d.ts +2 -0
  230. package/dist/types/tool.d.ts.map +1 -1
  231. package/dist/types/tool.js.map +1 -1
  232. package/dist/utils/index.d.ts +4 -4
  233. package/dist/utils/index.d.ts.map +1 -1
  234. package/dist/utils/index.js +3 -3
  235. package/dist/utils/index.js.map +1 -1
  236. package/dist/utils/retry.d.ts +99 -1
  237. package/dist/utils/retry.d.ts.map +1 -1
  238. package/dist/utils/retry.js +137 -3
  239. package/dist/utils/retry.js.map +1 -1
  240. package/dist/utils/serialize.d.ts +17 -0
  241. package/dist/utils/serialize.d.ts.map +1 -1
  242. package/dist/utils/serialize.js +31 -0
  243. package/dist/utils/serialize.js.map +1 -1
  244. package/dist/utils/session.d.ts +21 -1
  245. package/dist/utils/session.d.ts.map +1 -1
  246. package/dist/utils/session.js +33 -4
  247. package/dist/utils/session.js.map +1 -1
  248. package/docs/concepts/architecture.md +3 -3
  249. package/docs/concepts/directives.md +1 -1
  250. package/docs/guides/error-handling.md +46 -45
  251. package/docs/guides/flow-control.md +8 -1
  252. package/docs/guides/instructions.md +15 -6
  253. package/docs/guides/persistence.md +12 -5
  254. package/docs/guides/streaming.md +10 -0
  255. package/docs/migration/README.md +4 -0
  256. package/docs/migration/v2-3-to-v2-4.md +4 -0
  257. package/docs/migration/v2-6-to-v2-7.md +246 -0
  258. package/docs/reference/adapters.md +15 -1
  259. package/docs/reference/create-agent.md +28 -0
  260. package/docs/reference/directive.md +1 -1
  261. package/docs/reference/errors.md +29 -31
  262. package/docs/reference/providers.md +23 -19
  263. package/docs/reference/step.md +28 -21
  264. package/docs/reference/tool.md +14 -5
  265. package/docs/start/02-first-agent.md +8 -4
  266. package/docs/start/03-collect-data.md +19 -10
  267. package/examples/01-quickstart.ts +1 -1
  268. package/examples/02-data-extraction.ts +1 -1
  269. package/examples/03-tools.ts +1 -1
  270. package/examples/04-instructions.ts +1 -1
  271. package/examples/05-branching.ts +1 -1
  272. package/examples/06-flow-control.ts +3 -3
  273. package/examples/07-streaming.ts +1 -1
  274. package/examples/08-persistence.ts +1 -1
  275. package/examples/09-signals.ts +1 -1
  276. package/package.json +2 -2
  277. package/src/adapters/MemoryAdapter.ts +29 -33
  278. package/src/adapters/PostgreSQLAdapter.ts +23 -18
  279. package/src/adapters/RedisAdapter.ts +81 -34
  280. package/src/adapters/SQLiteAdapter.ts +10 -31
  281. package/src/adapters/sessionRow.ts +57 -0
  282. package/src/core/Agent.ts +48 -2
  283. package/src/core/AutoChainExecutor.ts +37 -50
  284. package/src/core/CompactionEngine.ts +40 -6
  285. package/src/core/FlowRouter.ts +38 -12
  286. package/src/core/ResponseGenerationError.ts +3 -6
  287. package/src/core/ResponseModal.ts +185 -19
  288. package/src/core/ResponsePipeline.ts +259 -85
  289. package/src/core/SessionFinalizer.ts +35 -5
  290. package/src/core/SessionManager.ts +55 -21
  291. package/src/core/SignalProcessor.ts +5 -85
  292. package/src/core/Step.ts +79 -2
  293. package/src/core/StepLifecycle.ts +125 -22
  294. package/src/core/StreamingToolExecutor.ts +31 -6
  295. package/src/core/ToolLoopExecutor.ts +200 -74
  296. package/src/core/ToolManager.ts +45 -18
  297. package/src/core/flow-namespace.ts +29 -0
  298. package/src/index.ts +4 -1
  299. package/src/providers/AnthropicProvider.ts +83 -131
  300. package/src/providers/GeminiProvider.ts +42 -116
  301. package/src/providers/OpenAICompatibleProvider.ts +132 -133
  302. package/src/providers/OpenAIProvider.ts +2 -2
  303. package/src/providers/errorClassification.ts +34 -0
  304. package/src/types/agent.ts +25 -0
  305. package/src/types/flow.ts +37 -39
  306. package/src/types/index.ts +1 -0
  307. package/src/types/tool.ts +2 -0
  308. package/src/utils/index.ts +22 -3
  309. package/src/utils/retry.ts +251 -2
  310. package/src/utils/serialize.ts +38 -0
  311. package/src/utils/session.ts +41 -4
  312. package/dist/cjs/core/DirectiveBus.d.ts +0 -88
  313. package/dist/cjs/core/DirectiveBus.d.ts.map +0 -1
  314. package/dist/cjs/core/DirectiveBus.js +0 -196
  315. package/dist/cjs/core/DirectiveBus.js.map +0 -1
  316. package/dist/core/DirectiveBus.d.ts +0 -88
  317. package/dist/core/DirectiveBus.d.ts.map +0 -1
  318. package/dist/core/DirectiveBus.js +0 -192
  319. package/dist/core/DirectiveBus.js.map +0 -1
  320. package/src/core/DirectiveBus.ts +0 -248
@@ -27,13 +27,17 @@ The format applies to framework-thrown errors. If you write a custom tool or hoo
27
27
  |-------|-------------|------------|
28
28
  | `FlowConfigurationError` | Misconfigured agent: duplicate ids, unknown `collect` keys, malformed `Directive`, branch targets that don't resolve, auto-step cycles, function on `when`. | Don't. Surface in CI. |
29
29
  | `DataValidationError` | A user message produces a value that violates the declared `schema`. | Re-prompt the user for the offending fields. |
30
- | `ToolExecutionError` | A tool handler throws, all retries fail, or `validateInput` cannot correct invalid args. | User-friendly message, optional retry, optional `agent.dispatch` to a recovery flow. |
31
- | `ResponseGenerationError` | Anything inside the turn fails — provider call, parsing, persistence. Wraps the underlying error in `details.originalError`. | Inspect `details.originalError` first; backoff retry, fall back, or surface a soft failure. |
30
+ | `ToolExecutionError` | The framework's config-driven tools (data enrichment / API call / computation configs) fail after internal retries. A plain handler that throws does **not** throw this it soft-fails a failure result back to the model instead (see Pattern 3). | User-friendly message, optional retry, optional `agent.dispatch` to a recovery flow. |
31
+ | `ResponseGenerationError` | Anything inside the turn fails — provider call, parsing, persistence. Wraps the underlying error in `details.originalError`, also reachable as the native `err.cause`. | Inspect `.cause` first; backoff retry, fall back, or surface a soft failure. |
32
32
  | `ProviderError` | A provider call fails terminally — retries and `backupModels` exhausted. Normalized `code` across all vendors; original SDK error on `cause`. | Match on `code`: backoff for `rate_limited` / `overloaded`, fix credentials for `auth`. |
33
33
  | `SessionConflictError` | A session save carries a stale `version` — another writer persisted the session after this one loaded it. | Reload the session and retry the turn. |
34
34
  | `NotImplementedError` | A reserved option is set to a value this version does not support (e.g. `routerMode: 'embedding'` in v2.0). | Use a supported value. Same posture as `FlowConfigurationError`. |
35
35
 
36
- `FlowConfigurationError`, `ToolExecutionError`, `ProviderError`, `SessionConflictError`, and `NotImplementedError` are exported from `@falai/agent` and matchable with `instanceof`. `DataValidationError` and `ResponseGenerationError` are internal — match them by `error.name`.
36
+ `FlowConfigurationError`, `ToolExecutionError`, `ProviderError`, `SessionConflictError`, `ResponseGenerationError`, and `NotImplementedError` are exported from `@falai/agent` and matchable with `instanceof`. Only `DataValidationError` is internal — match it by `error.name`.
37
+
38
+ How errors reach your `catch` depends on the class. `ProviderError` and `SessionConflictError` propagate **bare** out of `respond()` — no wrapping — so you can branch on them directly. Everything else thrown inside a turn arrives wrapped in a `ResponseGenerationError` whose native `.cause` (and `details.originalError`) holds the original. On the streaming path, errors are not thrown at all: they land on the terminal chunk's `error` field, wrapped the same way.
39
+
40
+ The provider stack classifies every failure as retriable or deterministic before spending budget: rate limits, overload, timeouts, and network faults retry with backoff; auth failures, invalid requests, and caller aborts fail fast because retrying would only repeat the outcome. By the time an error reaches your code, that machinery is exhausted.
37
41
 
38
42
  One framing that makes recovery simpler: **a failed turn has no effect**. If `respond()` or `stream()` throws mid-turn, the in-memory session is rolled back to its pre-turn snapshot (the user message added by `chat()`/`stream()` before the turn is retained), and persisted state is whatever the previous turn saved. There is no partially mutated session to repair — retrying the turn is always safe.
39
43
 
@@ -43,32 +47,35 @@ Wrap every `agent.respond` and `agent.respondStream` call site. Narrow by class,
43
47
 
44
48
  ```typescript
45
49
  import {
46
- FlowConfigurationError,
50
+ ProviderError,
51
+ SessionConflictError,
47
52
  ToolExecutionError,
48
- NotImplementedError,
53
+ ResponseGenerationError,
49
54
  } from "@falai/agent";
50
55
 
51
56
  try {
52
57
  const response = await agent.respond({ history, session });
53
58
  return response.message;
54
59
  } catch (err) {
55
- if (err instanceof FlowConfigurationError) throw err; // bugbubble up
56
- if (err instanceof NotImplementedError) throw err; // config bug — bubble up
57
- if (err instanceof ToolExecutionError) {
58
- log.warn({ toolId: err.toolId, cause: err.cause }, err.message);
60
+ // Typed operational errors propagate bare from respond() narrow directly.
61
+ if (err instanceof SessionConflictError) throw err; // see Pattern 5
62
+ if (err instanceof ProviderError) throw err; // see Pattern 4
63
+
64
+ // Everything else thrown inside the turn arrives wrapped in a
65
+ // ResponseGenerationError; unwrap via `.cause` and narrow the original.
66
+ const cause = err instanceof ResponseGenerationError ? err.cause : err;
67
+ if (cause instanceof ToolExecutionError) {
68
+ log.warn({ toolId: cause.toolId }, cause.message);
59
69
  return "Sorry — that action failed. Try again in a moment.";
60
70
  }
61
- if (err instanceof Error && err.name === "DataValidationError") {
71
+ if (cause instanceof Error && cause.name === "DataValidationError") {
62
72
  return "I need a couple of details cleared up — let's try that again.";
63
73
  }
64
- if (err instanceof Error && err.name === "ResponseGenerationError") {
65
- return "I'm having trouble reaching the model. Please retry.";
66
- }
67
74
  throw err;
68
75
  }
69
76
  ```
70
77
 
71
- The two unrecoverable classes `FlowConfigurationError` and `NotImplementedError`get rethrown so the process crashes loudly. The other three return a graceful message. Anything unexpected falls through to the outer rethrow.
78
+ The two bare classes rethrow to dedicated recovery paths. Wrapped errors narrow through `.cause` — a `ToolExecutionError` gets a soft message, a `DataValidationError` a clarifying question (Pattern 2). Anything unexpected falls through to the outer rethrow. Configuration bugs (`FlowConfigurationError`, `NotImplementedError`) belong in CI, not the request path — see Pattern 6.
72
79
 
73
80
  ## Pattern 2: DataValidationError → re-prompt
74
81
 
@@ -76,8 +83,9 @@ A `DataValidationError` carries an `errors: ValidationError[]` array naming the
76
83
 
77
84
  ```typescript
78
85
  } catch (err) {
79
- if (err instanceof Error && err.name === "DataValidationError") {
80
- const fields = (err as { errors: { path: string; message: string }[] }).errors
86
+ const cause = err instanceof ResponseGenerationError ? err.cause : err;
87
+ if (cause instanceof Error && cause.name === "DataValidationError") {
88
+ const fields = (cause as { errors: { path: string; message: string }[] }).errors
81
89
  .map((e) => `${e.path} (${e.message})`)
82
90
  .join(", ");
83
91
  return `I couldn't read these from your message: ${fields}. Mind clarifying?`;
@@ -88,15 +96,18 @@ A `DataValidationError` carries an `errors: ValidationError[]` array naming the
88
96
 
89
97
  The framework does not retry by itself — the next user turn is the retry. Keep `session` alive between turns so the partial collection survives.
90
98
 
91
- ## Pattern 3: ToolExecutionError message, retry, or dispatch
99
+ ## Pattern 3: tool failures — soft-fail, then message, retry, or dispatch
92
100
 
93
- Tool failures have three shapes. Pick by what the failing tool was doing.
101
+ First, what does *not* happen: a crashed handler does not crash the turn. When a tool handler throws — or a gate like `checkPermissions` denies the call — the executor catches it and feeds the model a failure result (`{"success": false, "error": "..."}`) as the tool's output. The model sees the failure honestly and can apologize, retry, or route around it; the turn continues. A thrown `ToolExecutionError` reaching your `catch` comes from the framework's config-driven tools (data enrichment, API call, computation configs), not from an ordinary handler.
102
+
103
+ When you do catch one, three shapes exist. Pick by what the failing tool was doing. In each, unwrap through `.cause` first — config-tool failures surface wrapped, like everything else from inside the turn.
94
104
 
95
105
  **Soft failure — speak and continue.** A read-only tool that timed out: tell the user, let the conversation move on.
96
106
 
97
107
  ```typescript
98
- if (err instanceof ToolExecutionError) {
99
- log.warn({ toolId: err.toolId }, err.message);
108
+ const cause = err instanceof ResponseGenerationError ? err.cause : err;
109
+ if (cause instanceof ToolExecutionError) {
110
+ log.warn({ toolId: cause.toolId }, cause.message);
100
111
  return "I couldn't fetch that just now. Want to try again?";
101
112
  }
102
113
  ```
@@ -108,7 +119,8 @@ for (let attempt = 0; attempt < 2; attempt++) {
108
119
  try {
109
120
  return (await agent.respond({ history, session })).message;
110
121
  } catch (err) {
111
- if (err instanceof ToolExecutionError && attempt === 0) {
122
+ const cause = err instanceof ResponseGenerationError ? err.cause : err;
123
+ if (cause instanceof ToolExecutionError && attempt === 0) {
112
124
  await new Promise((r) => setTimeout(r, 250));
113
125
  continue;
114
126
  }
@@ -120,8 +132,9 @@ for (let attempt = 0; attempt < 2; attempt++) {
120
132
  **Recovery flow.** Hard failures — a payment tool that hit a permanent denial — should redirect into a recovery flow rather than echo the error. Dispatch a directive against the session so the *next* turn enters the recovery flow.
121
133
 
122
134
  ```typescript
123
- if (err instanceof ToolExecutionError && err.toolId === "charge_card") {
124
- await agent.dispatch(session, { goTo: "PaymentRecovery" });
135
+ const cause = err instanceof ResponseGenerationError ? err.cause : err;
136
+ if (cause instanceof ToolExecutionError && cause.toolId === "charge_card") {
137
+ await agent.dispatch({ goTo: "PaymentRecovery" }, session);
125
138
  return "I hit a snag with that payment. Let me walk you through it.";
126
139
  }
127
140
  ```
@@ -130,34 +143,23 @@ The dispatched [`Directive`](../reference/directive.md) lands in `session.pendin
130
143
 
131
144
  ## Pattern 4: ProviderError → match the code, backoff or fallback provider
132
145
 
133
- Terminal provider failures throw `ProviderError` with a normalized `code` (`rate_limited`, `overloaded`, `auth`, `invalid_request`, `schema_rejected`, `timeout`, `network`, `unknown`) — the same shape whether you run Gemini, OpenAI, Anthropic, OpenRouter, or DeepSeek. Inside a turn it arrives wrapped in `ResponseGenerationError`; unwrap via `details.originalError`. Retry with backoff for transient codes; fall back to a second provider only if the primary keeps failing.
146
+ Terminal provider failures throw `ProviderError` with a normalized `code` (`rate_limited`, `overloaded`, `auth`, `invalid_request`, `schema_rejected`, `timeout`, `network`, `unknown`) — the same shape whether you run Gemini, OpenAI, Anthropic, OpenRouter, or DeepSeek. From `respond()` it propagates **bare** narrow with `instanceof` directly, no unwrapping. On the streaming path it arrives on the terminal chunk's `error`, wrapped in a `ResponseGenerationError` whose `.cause` holds the original.
147
+
148
+ Before an error ever reaches you, the provider stack has classified it: transient failures (`rate_limited`, `overloaded`, `timeout`, `network`) consumed the retry budget and any `backupModels`; deterministic ones (`auth`, `invalid_request`, caller aborts) failed fast without burning retries. So by catch time, retrying the same call is usually pointless — fall back to a second provider or surface the failure.
134
149
 
135
150
  ```typescript
136
151
  import { ProviderError } from "@falai/agent";
137
152
 
138
- function asProviderError(err: unknown): ProviderError | undefined {
139
- if (err instanceof ProviderError) return err;
140
- if (err instanceof Error && err.name === "ResponseGenerationError") {
141
- const original = (err as { details?: { originalError?: unknown } }).details?.originalError;
142
- if (original instanceof ProviderError) return original;
143
- }
144
- return undefined;
145
- }
146
-
147
153
  async function respondWithFallback(history: HistoryItem[], session: Session) {
148
154
  for (let attempt = 0; attempt < 3; attempt++) {
149
155
  try {
150
156
  return await primaryAgent.respond({ history, session });
151
157
  } catch (err) {
152
- const providerError = asProviderError(err);
153
- if (providerError?.code === "auth" || providerError?.code === "invalid_request") {
158
+ if (!(err instanceof ProviderError)) throw err;
159
+ if (err.code === "auth" || err.code === "invalid_request") {
154
160
  throw err; // config bug — retrying won't help
155
161
  }
156
- if (providerError) {
157
- await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
158
- continue;
159
- }
160
- throw err;
162
+ await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
161
163
  }
162
164
  }
163
165
  return fallbackAgent.respond({ history, session });
@@ -173,13 +175,12 @@ With a durable adapter, every save is a compare-and-swap on the session's `versi
173
175
  The error names `sessionId`, `expectedVersion`, and `actualVersion`. The recovery is mechanical: reload the session, retry the turn. The failed turn rolled back, so there is nothing to clean up.
174
176
 
175
177
  ```typescript
176
- import { SessionConflictError } from "@falai/agent";
178
+ import { ResponseGenerationError, SessionConflictError } from "@falai/agent";
177
179
 
178
180
  function isSessionConflict(err: unknown): boolean {
179
- if (err instanceof SessionConflictError) return true;
180
- if (err instanceof Error && err.name === "ResponseGenerationError") {
181
- const original = (err as { details?: { originalError?: unknown } }).details?.originalError;
182
- return original instanceof SessionConflictError;
181
+ if (err instanceof SessionConflictError) return true; // bare from respond()
182
+ if (err instanceof ResponseGenerationError) {
183
+ return err.cause instanceof SessionConflictError; // wrapped on the stream path
183
184
  }
184
185
  return false;
185
186
  }
@@ -351,7 +351,14 @@ await agent.dispatch({ goTo: "Billing", reply: "Transferring you now." }, sessio
351
351
  The call validates the directive (`flow.validate`), confirms any
352
352
  `goTo`-named flow exists (throws `FlowConfigurationError` if not),
353
353
  strips pre-LLM-only fields, writes `pendingDirective` onto the
354
- session, and persists if an adapter is configured.
354
+ session, and returns the updated session. With a persistence adapter
355
+ configured (and `autoSave` on — the default), dispatch also persists
356
+ immediately, so a webhook's redirect survives even if the next turn
357
+ runs in a different process; the save compare-and-swaps on the session
358
+ version, so a stale copy throws `SessionConflictError` rather than
359
+ clobbering another writer. Without an adapter (or with
360
+ `autoSave: false`) the directive is memory-only until the next turn's
361
+ auto-save — persisting sooner is then the caller's job.
355
362
 
356
363
  `pendingDirective` is **single-shot** — consumed exactly once and
357
364
  cleared. Calling `dispatch` again before the next turn overwrites the
@@ -98,7 +98,9 @@ The format is fixed. The kind prefix is always present (defaulting to `[should]`
98
98
  Every `respond()` call returns an `appliedInstructions` array listing exactly which instructions were rendered into that turn's prompt. The set is deterministic — it comes from the prompt composer, not the model — so you can use it for observability, audits, and tests. For an instruction with `when`, inclusion means the conditional instruction reached the model; it does not claim that the model judged the condition true:
99
99
 
100
100
  ```typescript
101
- const response = await agent.respond("I want to book a room.");
101
+ const response = await agent.respond({
102
+ history: [{ role: "user", content: "I want to book a room." }],
103
+ });
102
104
 
103
105
  for (const a of response.appliedInstructions ?? []) {
104
106
  console.log(`${a.scope}${a.scopeRef ? `:${a.scopeRef}` : ""} → ${a.id}`);
@@ -113,7 +115,9 @@ Each `AppliedInstruction` carries the firing instruction's `id`, the originating
113
115
  The same array lands on the final chunk of `respondStream`:
114
116
 
115
117
  ```typescript
116
- for await (const chunk of agent.respondStream("I want to book a room.")) {
118
+ for await (const chunk of agent.respondStream({
119
+ history: [{ role: "user", content: "I want to book a room." }],
120
+ })) {
117
121
  if (chunk.done) {
118
122
  console.log("rendered:", chunk.appliedInstructions);
119
123
  }
@@ -189,11 +193,16 @@ Because the set is deterministic, you can assert against it directly:
189
193
  ```typescript
190
194
  import { test, expect } from "bun:test";
191
195
 
196
+ // Seed the condition state at construction — `initialData` pre-populates
197
+ // session.data before the first turn.
198
+ const agent = createAgent({
199
+ /* ...same scaffold, with the payment step configured as above... */
200
+ initialData: { lastChargeStatus: "declined" },
201
+ });
202
+
192
203
  test("payment step renders the no-retry rule when the card was declined", async () => {
193
- const response = await agent.respond("Try again.", {
194
- sessionId: "s_1",
195
- initialContext: {},
196
- initialData: { lastChargeStatus: "declined" },
204
+ const response = await agent.respond({
205
+ history: [{ role: "user", content: "Try again." }],
197
206
  });
198
207
 
199
208
  const ids = response.appliedInstructions?.map(a => a.id) ?? [];
@@ -120,6 +120,14 @@ const response = await agent.respond({
120
120
 
121
121
  `getOrCreate` returns the stored `SessionState` (collected data, flow position, `pendingDirective`, signals state) — or creates a fresh session with that id if nothing exists yet. Unknown ids are not an error path; they are the start of a new conversation pinned to that id.
122
122
 
123
+ If you load a persisted session blob yourself — a custom cache, a queue payload, a row fetched outside the adapter's normal path — pass it through [`restoreSession`](../reference/adapters.md) instead of hand-shaping a `SessionState`. It is the named inverse of `createPersistedState`: it takes the persisted slice and returns a fully-shaped `SessionState`, including a completed-flow blob whose collected state must survive the round trip.
124
+
125
+ ```typescript
126
+ import { restoreSession } from "@falai/agent";
127
+
128
+ const session = restoreSession(await myCache.get(sessionId));
129
+ ```
130
+
123
131
  A practical id shape: `<userId>:<threadId>`. Keep it stable across restarts and replicas. The adapter does the rest.
124
132
 
125
133
  ## Recipe 3: Redis for fast, ephemeral sessions
@@ -162,13 +170,12 @@ If you need an archive of every session and message (audit logs, search, analyti
162
170
  Once sessions span processes, two writers can race on one id — parallel webhooks, a double-send from a chat widget, two browser tabs. Every save is a compare-and-swap on the session's `version` (incremented on each save): the loser's save throws `SessionConflictError` instead of silently overwriting the winner's state. The error carries `sessionId`, `expectedVersion`, and `actualVersion`; the recovery is mechanical — reload, retry.
163
171
 
164
172
  ```typescript
165
- import { SessionConflictError } from "@falai/agent";
173
+ import { ResponseGenerationError, SessionConflictError } from "@falai/agent";
166
174
 
167
175
  function isSessionConflict(err: unknown): boolean {
168
- if (err instanceof SessionConflictError) return true;
169
- if (err instanceof Error && err.name === "ResponseGenerationError") {
170
- const original = (err as { details?: { originalError?: unknown } }).details?.originalError;
171
- return original instanceof SessionConflictError;
176
+ if (err instanceof SessionConflictError) return true; // bare from respond()
177
+ if (err instanceof ResponseGenerationError) {
178
+ return err.cause instanceof SessionConflictError; // wrapped on the stream path
172
179
  }
173
180
  return false;
174
181
  }
@@ -136,4 +136,14 @@ For a Stop button, store the `controller` reference for the active stream on the
136
136
 
137
137
  If the turn fails — the generator surfaces an error chunk — it has no lasting effect: the in-memory session rolls back to its pre-turn snapshot (the user message added by `stream()` before the turn is retained), and persisted state is whatever the previous turn saved. Retrying is always safe.
138
138
 
139
+ ## Reliability: retries, backups, and the first-chunk deadline
140
+
141
+ Streaming inherits the same resilience machinery as non-streaming calls — provider `retryConfig` and `backupModels` — with stream-specific rules:
142
+
143
+ - **Retry only before the first chunk.** A stream that fails before yielding anything (a connection error, an empty completion, a stalled open) is retried on the same model up to `retryConfig.retries` times. Once a delta has reached your renderer, the stream is committed: any later failure propagates instead of retrying, so a retry can never emit a token your consumer has already seen.
144
+ - **First-chunk deadline.** `retryConfig.timeout` doubles as the time-to-first-token budget. If the provider opens a stream but produces no first chunk within it, the attempt counts as failed and the retry and backup machinery takes over. Only the first chunk is bounded — later chunks are unbounded, so a long-but-healthy stream is never cut off.
145
+ - **Transparent backup-model switch.** With `backupModels` configured, a model that fails mid-stream — even after deltas were delivered — falls through to the next model, whose chunks flow through unchanged. The switchover happens between chunks; no error reaches the consumer unless every model fails.
146
+
147
+ These knobs live on the provider constructor, not on `respondStream` — see [Providers](../reference/providers.md) for `retryConfig` and `backupModels`.
148
+
139
149
  **Next:** [Errors](./error-handling.md)
@@ -7,6 +7,10 @@ order: 99
7
7
 
8
8
  # Migration
9
9
 
10
+ Upgrading from `2.6.x`? The v2.7 guide covers the consumer-fit release surface — `message`/`allowedFlows` turn parameters, `endedFlows` and `metadata.tokensUsed`, provider `client` injection, and the exported `restoreSession` — plus the behavior changes to know: the default 400-message history bound, finalize-before-persist ordering, soft-failing tools, bare typed-error propagation, and session load failures that now throw.
11
+
12
+ [Read the v2.6 → v2.7 migration guide](./v2-6-to-v2-7.md)
13
+
10
14
  Upgrading from `2.3.x`? The v2.4 guide covers the concurrency-safety and provider-layer changes — required `AiProvider.capabilities`, normalized `ProviderError`, optimistic session locking with `SessionConflictError`, the `unknown` generic defaults, and the internals removed from the public barrel — with before/after code and per-adapter notes.
11
15
 
12
16
  [Read the v2.3 → v2.4 migration guide](./v2-3-to-v2-4.md)
@@ -109,6 +109,8 @@ try {
109
109
  ```
110
110
 
111
111
  > **Note:** when the failure surfaces through `agent.respond(...)`, it is wrapped in `ResponseGenerationError` like every other turn failure — the `ProviderError` is then on `details.originalError`. Code calling a provider directly sees the `ProviderError` itself.
112
+ >
113
+ > **v2.7 update:** this wrapping no longer happens — `ProviderError` now propagates **bare** out of `respond()` (`instanceof` survives). See the [v2.6 → v2.7 guide](./v2-6-to-v2-7.md#7-errors-propagate-typed-and-bare).
112
114
 
113
115
  ---
114
116
 
@@ -144,6 +146,8 @@ try {
144
146
 
145
147
  `SessionConflictError` carries `sessionId`, `expectedVersion`, and `actualVersion`. Recommended handling: reload the session, retry the operation.
146
148
 
149
+ > **v2.7 update:** the unwrapping fallback below is no longer needed — `SessionConflictError` (like `ProviderError`) now propagates bare out of `respond()`. The direct `instanceof` check is sufficient. See the [v2.6 → v2.7 guide](./v2-6-to-v2-7.md#7-errors-propagate-typed-and-bare).
150
+
147
151
  ### What you need to migrate, per adapter
148
152
 
149
153
  | Adapter | Action |
@@ -0,0 +1,246 @@
1
+ ---
2
+ title: "v2.6 → v2.7 migration"
3
+ description: "What changed in v2.7: per-turn message/allowedFlows params, the default history bound, finalize-before-persist ordering, soft-failing tools, bare error propagation, and provider client injection."
4
+ type: migration
5
+ order: 3
6
+ ---
7
+
8
+ # v2.6 → v2.7 Migration
9
+
10
+ **Version:** 2.7.0 — Consumer-fit release surface and correctness batch (tool directives end-to-end, hook desugaring, retry classification, error propagation)
11
+
12
+ ## Summary
13
+
14
+ v2.7 is mostly **additive**: `respond()` / `respondStream()` gain `message` and `allowedFlows` parameters and return richer observability (`endedFlows`, `metadata.tokensUsed`), providers accept a pre-configured SDK `client`, and `restoreSession` joins the public barrel. Four behavior changes are worth calling out even though none changes a type signature: the default history bound is now active, step `finalize` runs before persistence, crashed tools report failure to the model instead of throwing, and session load failures propagate.
15
+
16
+ If your integration passes explicit histories and catches errors broadly, the upgrade is usually zero-code. The sections below cover each case.
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ 1. [New turn parameters: `message` and `allowedFlows`](#1-new-turn-parameters-message-and-allowedflows)
23
+ 2. [`AgentResponse` gains `endedFlows` and `metadata.tokensUsed`](#2-agentresponse-gains-endedflows-and-metadatatokensused)
24
+ 3. [Default history bound is active (400)](#3-default-history-bound-is-active-400)
25
+ 4. [Step `finalize` now runs BEFORE persistence](#4-step-finalize-now-runs-before-persistence)
26
+ 5. [Crashed tools soft-fail to the model](#5-crashed-tools-soft-fail-to-the-model)
27
+ 6. [Tool directives work end-to-end](#6-tool-directives-work-end-to-end)
28
+ 7. [Errors propagate typed and bare](#7-errors-propagate-typed-and-bare)
29
+ 8. [Step hooks: shorthand and `hooks.*` both run](#8-step-hooks-shorthand-and-hooks-both-run)
30
+ 9. [Provider `client` injection and retry classification](#9-provider-client-injection-and-retry-classification)
31
+ 10. [`restoreSession` exported](#10-restoresession-exported)
32
+ 11. [Behavioral changes to be aware of](#11-behavioral-changes-to-be-aware-of)
33
+
34
+ ---
35
+
36
+ ## 1. New turn parameters: `message` and `allowedFlows`
37
+
38
+ Both `respond()` and `respondStream()` accept two new optional fields on the shared params object.
39
+
40
+ **`message`** — pass the user's turn instead of appending it to the history yourself. The engine appends it to what the model sees this turn *and* records it on the returned session's history, then appends the assistant reply on top. Callers who hold sessions no longer maintain history arrays by hand.
41
+
42
+ **`allowedFlows`** — restricts this turn's *routing* candidates to the given flow ids/titles. Directive targets (`goTo` etc.) still resolve against the full registry. Use for entry-pin funnels instead of cloning or filtering agents.
43
+
44
+ ```typescript
45
+ // ─── v2.6: caller owns the user turn ───
46
+ history.push({ role: "user", content: userInput });
47
+ const response = await agent.respond({ history });
48
+
49
+ // ─── v2.7: the engine owns appending ───
50
+ const response = await agent.respond({
51
+ history,
52
+ message: userInput,
53
+ allowedFlows: ["order"], // optional routing pin
54
+ });
55
+ // response.session.history already carries user + assistant turns
56
+ ```
57
+
58
+ Existing calls that pass pre-built `history` arrays keep working unchanged.
59
+
60
+ ---
61
+
62
+ ## 2. `AgentResponse` gains `endedFlows` and `metadata.tokensUsed`
63
+
64
+ `respond()` can now tell you directly which flows left their active position during the turn:
65
+
66
+ ```typescript
67
+ interface EndedFlow {
68
+ flowId: string;
69
+ title?: string;
70
+ reason: StoppedReason; // 'completed' | 'last_step' | 'goto' | …
71
+ }
72
+
73
+ interface AgentResponse<TData> {
74
+ // …existing fields…
75
+ endedFlows?: EndedFlow[]; // completions, redirects, resets
76
+ metadata?: { tokensUsed?: number }; // provider-reported usage for the primary generation call
77
+ }
78
+ ```
79
+
80
+ Before, you re-derived exits from `executedSteps` plus session-cursor inspection; `endedFlows` removes the guesswork. `metadata.tokensUsed` is present only when the provider reports usage, and covers only the turn's primary generation call (routing and extraction sub-calls are not included). On streaming turns, provider usage rides on chunk `metadata`; stream chunks do not carry `endedFlows`.
81
+
82
+ ---
83
+
84
+ ## 3. Default history bound is active (400)
85
+
86
+ A hard bound now applies to `session.history` even when you configure no compaction: entries beyond `maxHistoryMessages` (default **400**) are trimmed at end-of-turn finalize — and on interim auto-saves — always keeping whole assistant/tool pairs together. Each trim logs a warning recommending compaction for summarization.
87
+
88
+ ```typescript
89
+ const agent = createAgent({
90
+ // …
91
+ maxHistoryMessages: 1000, // raise the bound…
92
+ // …or configure compaction for summarization instead of truncation:
93
+ // compaction: { maxTokens: 8000 },
94
+ });
95
+ ```
96
+
97
+ Set `maxHistoryMessages: 0` to disable bounding entirely (previous unbounded behavior). Long-running `chat()` / `stream()` sessions can no longer grow until the provider context limit bricks them.
98
+
99
+ ---
100
+
101
+ ## 4. Step `finalize` now runs BEFORE persistence
102
+
103
+ The step `finalize` hook previously ran after the auto-save, so state writes it made could miss the persisted row when the conversation ended on that turn. The order inside end-of-turn finalization is now:
104
+
105
+ 1. History bound + deterministic compaction
106
+ 2. **`finalize` hook** — its state writes land on this turn's session
107
+ 3. **Auto-save to persistence**
108
+ 4. Live-session sync
109
+
110
+ No code change is needed — but hooks that "didn't stick" across restarts now do, and a control-flow directive returned by `finalize` queues for the start of the next turn as before.
111
+
112
+ ---
113
+
114
+ ## 5. Crashed tools soft-fail to the model
115
+
116
+ A tool handler that throws used to surface as an error escaping `respond()`. It is now reported **to the model** as a failed tool result — a `role: "tool"` message shaped `{"success":false,"error":"…"}` — so the model can react to the failed call instead of the turn dying (or worse, a fabricated success confirming an action that never happened). Unknown tool names behave the same way.
117
+
118
+ ```typescript
119
+ // The turn survives; the model sees the failure and can apologize/retry.
120
+ handler: () => {
121
+ throw new Error("upstream API is down");
122
+ },
123
+ ```
124
+
125
+ If you relied on catching handler crashes out of `respond()`, catch them inside the handler instead and return `{ success: false, error }` — same signal to the model, explicit in your code.
126
+
127
+ ---
128
+
129
+ ## 6. Tool directives work end-to-end
130
+
131
+ Directives emitted by tools now reliably reach the engine (previously they could be collected and dropped):
132
+
133
+ - `ctx.dispatch(directive)` mid-handler and returning `{ directive }` are equivalent.
134
+ - State fields (`dataUpdate`, `contextUpdate`) apply immediately.
135
+ - A `reply` directive short-circuits the remaining tool loop — its verbatim text becomes the final message with no follow-up LLM call.
136
+ - Control-flow fields queue on `session.pendingDirective` and steer the next turn (same deferred semantics as `agent.dispatch()`).
137
+
138
+ See [Tool → Directive wiring](../reference/tool.md#directive-wiring-and-turn-semantics).
139
+
140
+ ---
141
+
142
+ ## 7. Errors propagate typed and bare
143
+
144
+ Two related changes to how turn failures reach your `catch`:
145
+
146
+ - **`ProviderError` propagates bare out of `respond()`** — `instanceof ProviderError` works directly; no unwrapping through `ResponseGenerationError.details.originalError`. Same for `SessionConflictError`.
147
+ - **`ResponseGenerationError` is exported** and exposes the original error on the native `.cause` (as well as `details.originalError`). All other turn failures still wrap into it.
148
+
149
+ ```typescript
150
+ // ─── v2.6: unwrap through details ───
151
+ catch (err) {
152
+ const original = (err as { details?: { originalError?: unknown } }).details?.originalError;
153
+ if (original instanceof ProviderError) { /* … */ }
154
+ }
155
+
156
+ // ─── v2.7: instanceof survives ───
157
+ import { ProviderError, ResponseGenerationError } from "@falai/agent";
158
+
159
+ catch (err) {
160
+ if (err instanceof ProviderError) {
161
+ // err.code, err.provider, err.cause — branch directly
162
+ }
163
+ if (err instanceof ResponseGenerationError) {
164
+ // err.cause is the original error
165
+ }
166
+ }
167
+ ```
168
+
169
+ On streaming turns, errors arrive wrapped as `ResponseGenerationError` on the final chunk's `error` field (with the original on `.cause`) rather than thrown.
170
+
171
+ ---
172
+
173
+ ## 8. Step hooks: shorthand and `hooks.*` both run
174
+
175
+ Declaring a top-level `prepare`/`finalize` alongside `hooks.prepare`/`hooks.finalize` used to silently pick one. Now **both run** — the shorthand first, then the hook — with their directive returns merged via Algorithm 4. One new validation: combining a *tool-form* handler (a tool id or `Tool` object) with a function `hooks.*` entry throws `FlowConfigurationError` at construction, since a single position cannot compose a tool reference with a function.
176
+
177
+ ```typescript
178
+ {
179
+ id: "enrich",
180
+ prepare: (context, data) => ({ dataUpdate: { tier: lookupTier(context, data?.email) } }),
181
+ hooks: {
182
+ // ALSO runs — after the shorthand above, results merged
183
+ prepare: ({ data }) => {
184
+ if (data.tier === "blocked") return { halt: true, reply: "Account on hold." };
185
+ },
186
+ },
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ## 9. Provider `client` injection and retry classification
193
+
194
+ **`AnthropicProvider` and `GeminiProvider` accept a `client` option** — a pre-configured SDK client that overrides the internally-constructed one. Intended for tests injecting scripted transports; production callers should keep passing `apiKey`.
195
+
196
+ Retry semantics are now uniform and documented: deterministic failures (auth 401/403, invalid request 400/404/422, caller aborts) fail fast without burning the retry budget; retriable failures are rate limits, overloads, timeouts, and network faults. `retryConfig.timeout` also bounds time-to-first-token on streaming calls, so a stream that opens and then stalls is treated as failed and retried before the first delta is committed.
197
+
198
+ ---
199
+
200
+ ## 10. `restoreSession` exported
201
+
202
+ `restoreSession<TData>(state)` joins the public barrel as the canonical inverse of `createPersistedState(session)` — restore a persisted blob verbatim (collected data and completed-flow history survive the round trip). Prefer it over the ambiguous `createSession(state)` overload in custom persistence code.
203
+
204
+ ```typescript
205
+ import { createPersistedState, restoreSession } from "@falai/agent";
206
+
207
+ await store.put(id, JSON.stringify(createPersistedState(session)));
208
+ const restored = restoreSession<MyData>(JSON.parse(await store.get(id)));
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 11. Behavioral changes to be aware of
214
+
215
+ Not breaking in the type sense, but observable at runtime:
216
+
217
+ - **`SessionManager` load failures propagate.** A failed `sessionRepository.findById` (transient DB error) used to be swallowed and fall through to creating a blank session — which then saved over the existing row, erasing the conversation. The error now throws out of `getOrCreate()` / the first turn. A *missing* row still creates a new session as before.
218
+ - **`MemoryAdapter` state writes now match the SQL adapters.** `updateStatus` / `updateCollectedData` / `updateFlowStep` bump `version` + `updatedAt` exactly like SQLite/PostgreSQL (previously Memory mutated in place without either), and `incrementMessageCount` refreshes timestamps without moving `version`. If you asserted on `MemoryAdapter` versions in your own tests, they may shift by design; all adapters are pinned to one contract in `tests/adapter-contract.test.ts`.
219
+ - **`agent.dispatch` persists when an adapter is configured.** Previously the queued directive was memory-only until the next turn's auto-save — lost if the process ended first (webhook/cron callers). With an adapter + autoSave (the defaults) dispatch now saves immediately through the per-session save queue, so the directive survives process boundaries, and a stale session copy throws `SessionConflictError` instead of silently clobbering another writer. No adapter, or `autoSave: false`: unchanged memory-only behavior.
220
+ - **Failed-turn rollback is unchanged**, but combined with §7 more failures now arrive as typed instances you can branch on rather than wrapped messages to string-match.
221
+
222
+ ---
223
+
224
+ ## Verification
225
+
226
+ After migrating, run the type checker — it will flag any code that depended on `ResponseGenerationError` being name-matched only, or on provider options that have since widened:
227
+
228
+ ```bash
229
+ bun run typecheck
230
+ ```
231
+
232
+ To confirm the behavior changes land as described, the relevant suites are:
233
+
234
+ ```bash
235
+ bun test tests/consumer-fit-api.test.ts # message/allowedFlows, endedFlows, bare error propagation
236
+ bun test tests/tool-loop-correctness.test.ts # soft-failing tools
237
+ bun test tests/directive-wiring.test.ts # tool directives + finalize-before-persist ordering
238
+ ```
239
+
240
+ ## Cross-References
241
+
242
+ - [createAgent reference](../reference/create-agent.md) — `maxHistoryMessages`, turn parameters, response surface
243
+ - [Providers reference](../reference/providers.md) — `client` option and retry classification
244
+ - [Errors reference](../reference/errors.md) — bare `ProviderError`, exported `ResponseGenerationError`
245
+ - [Tool reference](../reference/tool.md) — directive wiring and soft-fail semantics
246
+ - [Adapters reference](../reference/adapters.md) — `createPersistedState` / `restoreSession`
@@ -73,7 +73,7 @@ Both are **required columns** on every adapter's session schema. v1 schemas had
73
73
 
74
74
  ## Optimistic locking
75
75
 
76
- Every session row carries a `version: number` (on `SessionData` / `SessionState`), incremented by the repository on every update. `SessionRepository.update()` takes an optional compare-and-swap guard:
76
+ Every session row carries a `version: number` (on `SessionData` / `SessionState`), incremented by the repository on every update. One deliberate exception: `incrementMessageCount` is bookkeeping — it refreshes the timestamps but never moves `version`, because count bumps run alongside `saveSessionState`'s compare-and-swap on every turn and would otherwise manufacture false conflicts. The state-writer helpers (`updateStatus`, `updateCollectedData`, `updateFlowStep`) go through `update()` and do bump it, identically on every built-in adapter. `SessionRepository.update()` takes an optional compare-and-swap guard:
77
77
 
78
78
  ```typescript
79
79
  interface SessionUpdateOptions {
@@ -126,6 +126,20 @@ await agent.respond({ history: [{ role: "user", content: "Hi again" }] });
126
126
 
127
127
  The engine looks the id up via `sessionRepository.findById`, hydrates the full `SessionState` (including `pendingDirective` and `signals`), and continues the turn against the restored state. Unknown ids create a new session with that id — there's no "not found" error path.
128
128
 
129
+ ### Session helpers for custom persistence code
130
+
131
+ When you read or write session rows outside the agent (admin tooling, batch jobs), two exported helpers keep the round trip lossless:
132
+
133
+ - `createPersistedState(session)` — strips transient pre-LLM directive fields (`appendPrompt`, `injectTools`, `halt`) before writing; call it (or an adapter that already does) before serializing.
134
+ - `restoreSession<TData>(state)` — the canonical inverse: rebuilds a `SessionState` from a persisted blob verbatim, so collected data and completed-flow history survive the round trip. Prefer it over the ambiguous `createSession(state)` overload.
135
+
136
+ ```typescript
137
+ import { createPersistedState, restoreSession } from "@falai/agent";
138
+
139
+ await store.put(id, JSON.stringify(createPersistedState(session)));
140
+ const session = restoreSession<MyData>(JSON.parse(await store.get(id)));
141
+ ```
142
+
129
143
  ## MemoryAdapter
130
144
 
131
145
  The implicit default. Omit `persistence` entirely to use it; instantiate explicitly only when you want to inspect or clear the store from tests.