reactifact 0.15.3__tar.gz → 0.16.0__tar.gz

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 (321) hide show
  1. {reactifact-0.15.3/reactifact.egg-info → reactifact-0.16.0}/PKG-INFO +1 -1
  2. {reactifact-0.15.3 → reactifact-0.16.0}/pyproject.toml +1 -1
  3. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/__init__.py +3 -2
  4. reactifact-0.16.0/reactifact/_persistence.py +78 -0
  5. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/agents.py +3 -0
  6. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/branching.py +28 -5
  7. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/chat.py +67 -17
  8. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/checkpoints.py +104 -8
  9. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/consume.py +52 -2
  10. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/context.py +185 -14
  11. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/online.py +2 -1
  12. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/metrics.py +20 -2
  13. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/native_tool_use.py +97 -1
  14. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/__init__.py +8 -0
  15. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/memory.py +48 -10
  16. reactifact-0.16.0/reactifact/recipes/tool_loop.py +436 -0
  17. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/resources.py +13 -7
  18. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/runtime.py +57 -7
  19. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/session.py +40 -6
  20. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tool_use.py +2 -2
  21. reactifact-0.16.0/reactifact/tools.py +351 -0
  22. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/__init__.py +2 -0
  23. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/langfuse.py +68 -0
  24. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/models.py +4 -0
  25. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/otlp.py +57 -0
  26. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/postgres.py +14 -4
  27. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/store.py +12 -4
  28. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/templates/app.css +5 -0
  29. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/templates/ui_run.html +52 -5
  30. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/tracer.py +20 -11
  31. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/triggers.py +19 -1
  32. {reactifact-0.15.3 → reactifact-0.16.0/reactifact.egg-info}/PKG-INFO +1 -1
  33. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact.egg-info/SOURCES.txt +3 -0
  34. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_chat_memory.py +58 -0
  35. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_consumes_produces.py +65 -0
  36. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_devops.py +10 -0
  37. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_metrics.py +26 -0
  38. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_public_api.py +5 -0
  39. reactifact-0.16.0/tests/test_recipe_tool_loop.py +479 -0
  40. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipes_memory.py +53 -0
  41. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_repair_web.py +9 -4
  42. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_request.py +62 -0
  43. reactifact-0.16.0/tests/test_session_append.py +222 -0
  44. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_token_budget.py +43 -0
  45. reactifact-0.16.0/tests/test_trace_ui.py +33 -0
  46. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_tracing.py +118 -0
  47. reactifact-0.15.3/reactifact/recipes/tool_loop.py +0 -190
  48. reactifact-0.15.3/reactifact/tools.py +0 -131
  49. reactifact-0.15.3/tests/test_recipe_tool_loop.py +0 -160
  50. {reactifact-0.15.3 → reactifact-0.16.0}/LICENSE +0 -0
  51. {reactifact-0.15.3 → reactifact-0.16.0}/README.md +0 -0
  52. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/__main__.py +0 -0
  53. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/_extras.py +0 -0
  54. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/_httpx.py +0 -0
  55. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/a2a/__init__.py +0 -0
  56. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/a2a/client.py +0 -0
  57. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/a2a/models.py +0 -0
  58. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/a2a/remote.py +0 -0
  59. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/a2a/server.py +0 -0
  60. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/agent_tool.py +0 -0
  61. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/artifacts.py +0 -0
  62. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/audit.py +0 -0
  63. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/authz.py +0 -0
  64. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/budget.py +0 -0
  65. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cache.py +0 -0
  66. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/__init__.py +0 -0
  67. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/branch.py +0 -0
  68. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/common.py +0 -0
  69. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/context.py +0 -0
  70. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/eval.py +0 -0
  71. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/graph.py +0 -0
  72. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/replay.py +0 -0
  73. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/scenario.py +0 -0
  74. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/skills.py +0 -0
  75. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/cli/trace.py +0 -0
  76. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/commit.py +0 -0
  77. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/commit_log.py +0 -0
  78. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/context_builder.py +0 -0
  79. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/effects.py +0 -0
  80. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/__init__.py +0 -0
  81. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/dataset.py +0 -0
  82. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/evaluators.py +0 -0
  83. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/judge.py +0 -0
  84. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/runner.py +0 -0
  85. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/eval/scoring.py +0 -0
  86. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/events.py +0 -0
  87. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/guardrails.py +0 -0
  88. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/interrupt.py +0 -0
  89. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/llm_agent.py +0 -0
  90. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/logging.py +0 -0
  91. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/mcp/__init__.py +0 -0
  92. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/mcp/client.py +0 -0
  93. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/mcp/oauth.py +0 -0
  94. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/mcp/server.py +0 -0
  95. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/operations.py +0 -0
  96. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/patches.py +0 -0
  97. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/pricing.py +0 -0
  98. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/produce.py +0 -0
  99. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/prompts.py +0 -0
  100. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/__init__.py +0 -0
  101. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/_retry.py +0 -0
  102. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/anthropic.py +0 -0
  103. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/azure.py +0 -0
  104. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/cerebras.py +0 -0
  105. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/chat.py +0 -0
  106. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/contracts.py +0 -0
  107. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/deepseek.py +0 -0
  108. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/fake.py +0 -0
  109. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/fireworks.py +0 -0
  110. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/gemini.py +0 -0
  111. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/github_models.py +0 -0
  112. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/groq.py +0 -0
  113. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/image.py +0 -0
  114. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/mistral.py +0 -0
  115. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/nvidia.py +0 -0
  116. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/ollama.py +0 -0
  117. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/openai.py +0 -0
  118. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/openrouter.py +0 -0
  119. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/perplexity.py +0 -0
  120. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/qwen.py +0 -0
  121. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/speech.py +0 -0
  122. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/together.py +0 -0
  123. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/video.py +0 -0
  124. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/xai.py +0 -0
  125. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/providers/zai.py +0 -0
  126. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/py.typed +0 -0
  127. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/__init__.py +0 -0
  128. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/_shared.py +0 -0
  129. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/agent.py +0 -0
  130. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/chat.py +0 -0
  131. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/models.py +0 -0
  132. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/rag.py +0 -0
  133. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quick/tools_agent.py +0 -0
  134. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/quota.py +0 -0
  135. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/cleanup.py +0 -0
  136. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/conversation.py +0 -0
  137. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/identity.py +0 -0
  138. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/inputs.py +0 -0
  139. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/plan_execute.py +0 -0
  140. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/reflection.py +0 -0
  141. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/resolve.py +0 -0
  142. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/rollback.py +0 -0
  143. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/search.py +0 -0
  144. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/skills.py +0 -0
  145. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/status.py +0 -0
  146. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/supervisor.py +0 -0
  147. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/recipes/text.py +0 -0
  148. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/redaction.py +0 -0
  149. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/relations.py +0 -0
  150. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/replay.py +0 -0
  151. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/request.py +0 -0
  152. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/routing.py +0 -0
  153. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/scheduler.py +0 -0
  154. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact/SKILL.md +0 -0
  155. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  156. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  157. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  158. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  159. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  160. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  161. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  162. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  163. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  164. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  165. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  166. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  167. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  168. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  169. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  170. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/sources.py +0 -0
  171. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/streaming.py +0 -0
  172. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/structured.py +0 -0
  173. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/__init__.py +0 -0
  174. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/assertions.py +0 -0
  175. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/exceptions.py +0 -0
  176. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/fault.py +0 -0
  177. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/golden.py +0 -0
  178. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/lab.py +0 -0
  179. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/mock.py +0 -0
  180. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/pytest_plugin.py +0 -0
  181. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/record.py +0 -0
  182. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/testing/registry.py +0 -0
  183. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/_otlp.py +0 -0
  184. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/columns.py +0 -0
  185. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/templates/sessions.html +0 -0
  186. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/templates/ui.html +0 -0
  187. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/tracing/web.py +0 -0
  188. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/types.py +0 -0
  189. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/verify.py +0 -0
  190. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/viz.py +0 -0
  191. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact/web.py +0 -0
  192. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact.egg-info/dependency_links.txt +0 -0
  193. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact.egg-info/entry_points.txt +0 -0
  194. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact.egg-info/requires.txt +0 -0
  195. {reactifact-0.15.3 → reactifact-0.16.0}/reactifact.egg-info/top_level.txt +0 -0
  196. {reactifact-0.15.3 → reactifact-0.16.0}/setup.cfg +0 -0
  197. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_a2a.py +0 -0
  198. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_a2a_example.py +0 -0
  199. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_a2a_interop.py +0 -0
  200. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_absent_consume.py +0 -0
  201. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_adaptive.py +0 -0
  202. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_agent_tool.py +0 -0
  203. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_anthropic_provider.py +0 -0
  204. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_artifacts.py +0 -0
  205. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_audit.py +0 -0
  206. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_authz.py +0 -0
  207. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_backbone.py +0 -0
  208. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_branching.py +0 -0
  209. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_budget.py +0 -0
  210. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_cache.py +0 -0
  211. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_canonical_ports.py +0 -0
  212. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_chat_example.py +0 -0
  213. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_chat_outbox.py +0 -0
  214. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_chat_web.py +0 -0
  215. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_checkpoint.py +0 -0
  216. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_checkpoints_concurrency.py +0 -0
  217. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_cli.py +0 -0
  218. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_cli_scenario.py +0 -0
  219. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_commit_log.py +0 -0
  220. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_compaction.py +0 -0
  221. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_concurrency.py +0 -0
  222. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_context_builder.py +0 -0
  223. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_correlated_consume.py +0 -0
  224. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_debounce.py +0 -0
  225. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_determinism.py +0 -0
  226. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_devops_online.py +0 -0
  227. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_devops_web.py +0 -0
  228. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_effects.py +0 -0
  229. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_eval.py +0 -0
  230. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_eval_dataset.py +0 -0
  231. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_eval_judge.py +0 -0
  232. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_eval_online.py +0 -0
  233. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_eval_trajectory.py +0 -0
  234. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_fintech_audit.py +0 -0
  235. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_forklab.py +0 -0
  236. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_forklab_web.py +0 -0
  237. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_friendly_api.py +0 -0
  238. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_gemini_provider.py +0 -0
  239. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_golden.py +0 -0
  240. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_guardrails.py +0 -0
  241. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_hitl.py +0 -0
  242. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_image_provider.py +0 -0
  243. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_incident_commander.py +0 -0
  244. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_invalidation.py +0 -0
  245. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_join_consume.py +0 -0
  246. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_knowledge.py +0 -0
  247. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_knowledge_web.py +0 -0
  248. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_llm_ladder.py +0 -0
  249. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_logging.py +0 -0
  250. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_loop_bound_client.py +0 -0
  251. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_mcp.py +0 -0
  252. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_medic_lab.py +0 -0
  253. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_medic_lab_web.py +0 -0
  254. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_multisource.py +0 -0
  255. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_native_tool_use.py +0 -0
  256. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_openai_provider.py +0 -0
  257. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_outbox.py +0 -0
  258. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_patches.py +0 -0
  259. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_pending_actions.py +0 -0
  260. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_ports.py +0 -0
  261. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_produce_styles.py +0 -0
  262. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_produce_trigger.py +0 -0
  263. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_prompts.py +0 -0
  264. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_provider_auth.py +0 -0
  265. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_provider_retry.py +0 -0
  266. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_providers_integration.py +0 -0
  267. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_quick.py +0 -0
  268. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_quota.py +0 -0
  269. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_reacts_to.py +0 -0
  270. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipe_conversation.py +0 -0
  271. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipe_plan_execute.py +0 -0
  272. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipe_reflection.py +0 -0
  273. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipe_supervisor.py +0 -0
  274. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipes.py +0 -0
  275. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipes_inputs.py +0 -0
  276. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_recipes_skills.py +0 -0
  277. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_redaction.py +0 -0
  278. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_relation_graph.py +0 -0
  279. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_relations.py +0 -0
  280. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_repair.py +0 -0
  281. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_replay.py +0 -0
  282. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_repo_agent.py +0 -0
  283. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_research.py +0 -0
  284. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_resources.py +0 -0
  285. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_retry.py +0 -0
  286. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_routing.py +0 -0
  287. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_runtime.py +0 -0
  288. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_runtime_errors.py +0 -0
  289. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_scheduler_semantics.py +0 -0
  290. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_sessions.py +0 -0
  291. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_shutdown.py +0 -0
  292. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_skills.py +0 -0
  293. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_sources.py +0 -0
  294. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_sources_search.py +0 -0
  295. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_sources_vector.py +0 -0
  296. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_speech_provider.py +0 -0
  297. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_starter_app.py +0 -0
  298. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_streaming.py +0 -0
  299. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_structured.py +0 -0
  300. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_support_copilot.py +0 -0
  301. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_assertions.py +0 -0
  302. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_golden.py +0 -0
  303. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_lab.py +0 -0
  304. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_mock.py +0 -0
  305. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_provenance.py +0 -0
  306. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_pytest_plugin.py +0 -0
  307. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_testing_registry.py +0 -0
  308. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_tools.py +0 -0
  309. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_trace_columns.py +0 -0
  310. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_trace_tags.py +0 -0
  311. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_type_registry.py +0 -0
  312. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_vendor_factories.py +0 -0
  313. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_vendor_multimodal_factories.py +0 -0
  314. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_verify.py +0 -0
  315. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_video_provider.py +0 -0
  316. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_view.py +0 -0
  317. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_viz.py +0 -0
  318. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_web_source.py +0 -0
  319. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_workspace.py +0 -0
  320. {reactifact-0.15.3 → reactifact-0.16.0}/tests/test_workspace_with_sources.py +0 -0
  321. {reactifact-0.15.3 → reactifact-0.16.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.15.3
3
+ Version: 0.16.0
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.15.3"
7
+ version = "0.16.0"
8
8
  description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -47,11 +47,11 @@ from .request import current_request
47
47
  from .resources import ResourceKey, ResourceScope, RuntimeResources
48
48
  from .runtime import Runtime
49
49
  from .session import Session, SessionStore
50
- from .tools import FunctionTool, Tool, ToolOutput, tool
50
+ from .tools import FunctionTool, Tool, ToolError, ToolOutput, tool
51
51
  from .triggers import Trigger
52
52
  from .types import register as register_type
53
53
 
54
- __version__ = "0.15.3"
54
+ __version__ = "0.16.0"
55
55
 
56
56
  __all__ = [
57
57
  "Agent",
@@ -92,6 +92,7 @@ __all__ = [
92
92
  "Session",
93
93
  "SessionStore",
94
94
  "Tool",
95
+ "ToolError",
95
96
  "ToolOutput",
96
97
  "Trigger",
97
98
  "Unlink",
@@ -0,0 +1,78 @@
1
+ """Internal: append-only persistence helpers shared by session/branch stores.
2
+
3
+ Not public API — `SessionStore`/`BranchStore` use these when the backend
4
+ implements `AppendBackend` (SQLite/Postgres). A save writes a full snapshot on
5
+ the first save, after a compaction, or after a rollback; otherwise it appends
6
+ only the new commits plus the current pending-event queue.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, cast
12
+
13
+ from .checkpoints import AppendBackend
14
+ from .commit import Commit
15
+ from .context import Context
16
+ from .events import Event
17
+
18
+ #: Default number of appended saves before a snapshot rewrite. Bounds load
19
+ #: replay (and log size) for long-lived sessions without an explicit
20
+ #: `context.compact()`.
21
+ DEFAULT_COMPACT_EVERY = 100
22
+
23
+
24
+ async def save_append(
25
+ backend: AppendBackend,
26
+ key: str,
27
+ context: Context,
28
+ *,
29
+ compact_every: int | None = DEFAULT_COMPACT_EVERY,
30
+ ) -> None:
31
+ persisted, persisted_compacted = context.persisted_state(key)
32
+ needs_snapshot = (
33
+ persisted < 0
34
+ or persisted > context.version
35
+ or context.compacted_at > persisted_compacted
36
+ or await backend.get(key) is None
37
+ )
38
+ if (
39
+ not needs_snapshot
40
+ and compact_every is not None
41
+ and await backend.entry_count(key) + 1 >= compact_every
42
+ ):
43
+ needs_snapshot = True
44
+ if needs_snapshot:
45
+ await backend.set(key, cast(dict[str, Any], context.to_dict()))
46
+ await backend.clear_entries(key)
47
+ context.mark_persisted(key=key)
48
+ return
49
+ new_commits = [
50
+ commit.to_dict()
51
+ for commit in context.commit_log()
52
+ if (commit.context_version or 0) > persisted
53
+ ]
54
+ await backend.append(
55
+ key,
56
+ {
57
+ "commits": new_commits,
58
+ "events": [event.to_dict() for event in context.pending_events()],
59
+ },
60
+ )
61
+ context.mark_persisted(key=key)
62
+
63
+
64
+ async def load_append(backend: AppendBackend, key: str) -> Context | None:
65
+ snapshot = await backend.get(key)
66
+ if snapshot is None:
67
+ return None
68
+ context = Context.from_dict(snapshot)
69
+ for entry in await backend.entries(key):
70
+ for commit_dict in entry.get("commits", []):
71
+ context.apply_commit(Commit.from_dict(commit_dict), reindex=False)
72
+ if "events" in entry:
73
+ context.replace_events(
74
+ [Event.from_dict(event) for event in entry["events"]]
75
+ )
76
+ context.rebuild_indices()
77
+ context.mark_persisted(key=key)
78
+ return context
@@ -13,6 +13,7 @@ from .effects import current_effects
13
13
  from .events import Event, EventType
14
14
  from .patches import Patch
15
15
  from .produce import Produce, ProduceCall
16
+ from .tools import reset_current_produce, set_current_produce
16
17
  from .triggers import Trigger
17
18
 
18
19
  #: Per-task log of the produces that ran for the current agent execution:
@@ -190,9 +191,11 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
190
191
  call = ProduceCall(
191
192
  context=context, inputs=inputs, event=event, trigger=trigger
192
193
  )
194
+ produce_token = set_current_produce(p.name)
193
195
  try:
194
196
  await p.produce(call)
195
197
  finally:
198
+ reset_current_produce(produce_token)
196
199
  # Record the produce even when it raises — a failing produce is
197
200
  # exactly what a trace should show (the span is then the error
198
201
  # span, with this produce's partial operations/latency).
@@ -29,8 +29,9 @@ from typing import Any
29
29
 
30
30
  from pydantic import BaseModel
31
31
 
32
+ from ._persistence import DEFAULT_COMPACT_EVERY, load_append, save_append
32
33
  from .artifacts import Artifact
33
- from .checkpoints import KVBackend
34
+ from .checkpoints import AppendBackend, KVBackend
34
35
  from .commit import Commit
35
36
  from .context import Context
36
37
  from .interrupt import PendingAction
@@ -242,10 +243,22 @@ def merge_contexts(
242
243
 
243
244
 
244
245
  class BranchStore:
245
- """Persists named branches (`branch:<session>:<name>`) over a KV backend."""
246
+ """Persists named branches (`branch:<session>:<name>`) over a KV backend.
246
247
 
247
- def __init__(self, backend: KVBackend):
248
+ With an `AppendBackend` (SQLite/Postgres) branches use the same
249
+ snapshot + append-only commit log as sessions, keyed per branch name.
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ backend: KVBackend,
255
+ *,
256
+ compact_every: int | None = DEFAULT_COMPACT_EVERY,
257
+ ):
258
+ if compact_every is not None and compact_every < 1:
259
+ raise ValueError("compact_every must be >= 1 or None")
248
260
  self.backend = backend
261
+ self.compact_every = compact_every
249
262
 
250
263
  @staticmethod
251
264
  def _key(session_id: str, name: str) -> str:
@@ -255,11 +268,21 @@ class BranchStore:
255
268
  self, context: Context, *, session_id: str, name: str
256
269
  ) -> None:
257
270
  """Saves a branch (including its fork base snapshot, §40)."""
258
- await context.to_kv(self.backend, self._key(session_id, name))
271
+ key = self._key(session_id, name)
272
+ if isinstance(self.backend, AppendBackend):
273
+ await save_append(
274
+ self.backend, key, context, compact_every=self.compact_every
275
+ )
276
+ return
277
+ await context.to_kv(self.backend, key)
278
+ context.mark_persisted(key=key)
259
279
 
260
280
  async def load_branch(self, session_id: str, name: str) -> Context | None:
261
281
  """Loads a branch, or None if it does not exist."""
262
- return await Context.from_kv(self.backend, self._key(session_id, name))
282
+ key = self._key(session_id, name)
283
+ if isinstance(self.backend, AppendBackend):
284
+ return await load_append(self.backend, key)
285
+ return await Context.from_kv(self.backend, key)
263
286
 
264
287
  async def list_branches(self, session_id: str) -> list[str]:
265
288
  prefix = f"branch:{session_id}:"
@@ -37,6 +37,7 @@ from .budget import Budget
37
37
  from .consume import Consume
38
38
  from .context import Context
39
39
  from .events import Event
40
+ from .request import reset_request, set_request
40
41
  from .resources import RuntimeResources
41
42
  from .runtime import Dispatcher, DispatchErrorHandler, Runtime
42
43
  from .session import Session, SessionStore
@@ -101,8 +102,12 @@ async def run_message(
101
102
  (devops-style clarify) pass their own. Also accepts the 3-arg shape
102
103
  `create_message(ctx, text, session_id) -> msg_id` for a hook that needs
103
104
  the current turn's `session_id` (e.g. to stamp it onto the created
104
- artifact) — detected from the callable's own arity, so the 2-arg shape
105
- keeps working unchanged.
105
+ artifact) and the 4-arg shape `create_message(ctx, text, session_id,
106
+ request) -> msg_id` for per-request metadata (a category, tenant, user)
107
+ — detected from the callable's own arity, so the 2-arg shape keeps
108
+ working unchanged. During the hook `ctx.request` is the same mapping
109
+ passed as `request=`, so a 2/3-arg hook can read it too without a
110
+ `ContextVar` of its own.
106
111
 
107
112
  `status_kinds` selects which progress event kinds are forwarded as `status`
108
113
  frames (default: only `status`; e.g. tool-announcing demos also forward
@@ -115,10 +120,17 @@ async def run_message(
115
120
  forwarded = set(status_kinds)
116
121
  ctx = runtime.context
117
122
  try:
118
- if create_message is not None:
119
- msg_id = _call_create_message(create_message, ctx, text, session_id)
120
- else:
121
- msg_id = ctx.create(user_message(text=text, session_id=session_id)).id
123
+ token = set_request(request) if request is not None else None
124
+ try:
125
+ if create_message is not None:
126
+ msg_id = _call_create_message(
127
+ create_message, ctx, text, session_id, request
128
+ )
129
+ else:
130
+ msg_id = ctx.create(user_message(text=text, session_id=session_id)).id
131
+ finally:
132
+ if token is not None:
133
+ reset_request(token)
122
134
  except Exception:
123
135
  logger.exception("chat.run_message: failed to enter the turn")
124
136
  yield ChatEvent(
@@ -217,11 +229,20 @@ def _resolve_with_session(value: Any, session_id: str) -> Any:
217
229
 
218
230
 
219
231
  def _call_create_message(
220
- create_message: Callable[..., str], ctx: Context, text: str, session_id: str
232
+ create_message: Callable[..., str],
233
+ ctx: Context,
234
+ text: str,
235
+ session_id: str,
236
+ request: Mapping[str, Any] | None = None,
221
237
  ) -> str:
222
- """Calls `create_message` with `session_id` appended when it declares a
223
- third parameter for it (`(ctx, text, session_id) -> msg_id`), alongside
224
- the pre-existing two-arg shape (`(ctx, text) -> msg_id`)."""
238
+ """Calls `create_message` with the widest shape it declares.
239
+
240
+ Four params get `(ctx, text, session_id, request)`, three get
241
+ `(ctx, text, session_id)`, and the pre-existing two-arg shape gets
242
+ `(ctx, text)`. Detected from arity so old hooks keep working unchanged.
243
+ """
244
+ if _accepts_arg(create_message, 4):
245
+ return create_message(ctx, text, session_id, request)
225
246
  if _accepts_arg(create_message, 3):
226
247
  return create_message(ctx, text, session_id)
227
248
  return create_message(ctx, text)
@@ -274,9 +295,16 @@ class ChatMemory:
274
295
  `message_type` (needed for `keep`/`summarize`) is any model with
275
296
  `role`/`text`; for compaction-only it isn't needed at all, so
276
297
  `ChatMemory(compact_commits=…)` is a complete policy on its own.
298
+
299
+ `message_types` (or a sequence passed as `message_type`) bounds a
300
+ heterogeneous conversation — the natural `UserQuery`/`Answer` pair, or a
301
+ marker base like `ChatMessage` whose concrete types inherit from it. The
302
+ memory agent wakes on every listed type (and, for a marker base, on its
303
+ subclasses), so one window covers the whole thread.
277
304
  """
278
305
 
279
- message_type: type[BaseModel] | None = None
306
+ message_type: type[BaseModel] | Sequence[type[BaseModel]] | None = None
307
+ message_types: Sequence[type[BaseModel]] | None = None
280
308
  keep: int | None = None
281
309
  summarize: Callable[[Context, str, list[Any]], Awaitable[str | None]] | None = None
282
310
  summary_type: type[BaseModel] | None = None
@@ -284,6 +312,20 @@ class ChatMemory:
284
312
  digest_id: str = "digest"
285
313
  compact_commits: int | None = None
286
314
 
315
+ def resolved_message_types(self) -> tuple[type[BaseModel], ...]:
316
+ """The message types to bound, normalized from the two fields."""
317
+ if self.message_types is not None:
318
+ types = tuple(self.message_types)
319
+ elif self.message_type is None:
320
+ return ()
321
+ elif isinstance(self.message_type, type):
322
+ types = (self.message_type,)
323
+ else:
324
+ types = tuple(self.message_type)
325
+ if not types:
326
+ raise ValueError("message_type must name at least one artifact type")
327
+ return types
328
+
287
329
 
288
330
  class ChatAssistant:
289
331
  """Session-persisted chat over the runtime, for the canonical contract.
@@ -298,7 +340,9 @@ class ChatAssistant:
298
340
  its provider's HTTP client doesn't leak. Pass a plain `RuntimeResources`
299
341
  instance instead when you want one shared, long-lived provider across
300
342
  turns/sessions — that instance is never closed automatically; close it
301
- yourself at real shutdown.
343
+ yourself at real shutdown. It is safe to share: the runtime installs
344
+ per-turn budget/tracing wrappers on a shallow copy, so concurrent turns do
345
+ not mutate the shared instance.
302
346
 
303
347
  `session_save_policy=` passes straight through to `Runtime` — `"per_turn"`
304
348
  trades finer crash-resilience granularity (a save at every generation
@@ -434,12 +478,17 @@ class ChatAssistant:
434
478
 
435
479
  produce: Any
436
480
  if memory.summarize is not None:
437
- if memory.message_type is None or memory.summary_type is None:
481
+ if memory.summary_type is None:
482
+ raise ValueError(
483
+ "ChatMemory.summarize needs message_type and summary_type"
484
+ )
485
+ message_types = memory.resolved_message_types()
486
+ if not message_types:
438
487
  raise ValueError(
439
488
  "ChatMemory.summarize needs message_type and summary_type"
440
489
  )
441
490
  produce = RollingDigestSummarizer(
442
- message_type=memory.message_type,
491
+ message_type=message_types,
443
492
  artifact_type=memory.summary_type,
444
493
  summarize=memory.summarize,
445
494
  build=_digest_build(memory.summary_type, memory.build_summary),
@@ -447,15 +496,16 @@ class ChatAssistant:
447
496
  digest_id=memory.digest_id,
448
497
  )
449
498
  elif memory.keep is not None:
450
- if memory.message_type is None:
499
+ message_types = memory.resolved_message_types()
500
+ if not message_types:
451
501
  raise ValueError("ChatMemory.keep needs message_type")
452
- produce = WindowPruner(memory.message_type, keep=memory.keep)
502
+ produce = WindowPruner(message_types, keep=memory.keep)
453
503
  else:
454
504
  return [] # compaction-only: no message-level memory agent
455
505
  return [
456
506
  create_agent(
457
507
  "memory",
458
- consumes=[Consume(memory.message_type)],
508
+ consumes=[Consume.by_base(t) for t in message_types],
459
509
  produces=[produce],
460
510
  )
461
511
  ]
@@ -48,6 +48,36 @@ class KVBackend(ABC):
48
48
  """Releases held resources (a pooled connection, ...). No-op by default."""
49
49
 
50
50
 
51
+ class AppendBackend(KVBackend):
52
+ """KV backend that can append commit entries without rewriting the snapshot.
53
+
54
+ `SessionStore` uses this when available to persist a session as
55
+ *snapshot + append-only commit log* instead of rewriting the whole Context
56
+ on every commit. Backends without it keep the full-blob behavior.
57
+
58
+ Entries are opaque JSON dicts; `SessionStore` writes one per save with the
59
+ new commits and the current pending-event queue. `clear_entries` is used
60
+ when the snapshot is rewritten (first save, compaction, rollback).
61
+ """
62
+
63
+ @abstractmethod
64
+ async def append(self, key: str, entry: dict[str, Any]) -> None: ...
65
+
66
+ @abstractmethod
67
+ async def entries(self, key: str) -> list[dict[str, Any]]: ...
68
+
69
+ @abstractmethod
70
+ async def clear_entries(self, key: str) -> None: ...
71
+
72
+ async def entry_count(self, key: str) -> int:
73
+ """Number of appended entries for `key`.
74
+
75
+ Backends with a native `COUNT(*)` should override this; the default
76
+ loads the entries and counts them.
77
+ """
78
+ return len(await self.entries(key))
79
+
80
+
51
81
  class InMemoryKVBackend(KVBackend):
52
82
  """Dict-backed KV store — no files, no external service.
53
83
 
@@ -178,9 +208,9 @@ class _AsyncSQLite:
178
208
  writers across processes via the database file).
179
209
  """
180
210
 
181
- def __init__(self, db_path: str, init_sql: str):
211
+ def __init__(self, db_path: str, init_sql: str | tuple[str, ...]):
182
212
  self.db_path = db_path
183
- self._init_sql = init_sql
213
+ self._init_sql = (init_sql,) if isinstance(init_sql, str) else init_sql
184
214
  self._conn: sqlite3.Connection | None = None
185
215
  self._lock = asyncio.Lock()
186
216
 
@@ -197,7 +227,8 @@ class _AsyncSQLite:
197
227
  try:
198
228
  conn.execute("PRAGMA journal_mode=WAL")
199
229
  conn.execute("PRAGMA busy_timeout=5000")
200
- conn.execute(self._init_sql)
230
+ for statement in self._init_sql:
231
+ conn.execute(statement)
201
232
  conn.commit()
202
233
  return conn
203
234
  except sqlite3.OperationalError:
@@ -227,15 +258,23 @@ class _AsyncSQLite:
227
258
  self._conn = None
228
259
 
229
260
 
230
- class SQLiteKVBackend(KVBackend):
231
- """SQLite KV backend: table kv_entries(key PRIMARY KEY, data JSON)."""
261
+ class SQLiteKVBackend(AppendBackend):
262
+ """SQLite KV backend: snapshot in `kv_entries`, commit log in
263
+ `append_entries` (snapshot + append-only commits for sessions)."""
232
264
 
233
265
  def __init__(self, db_path: str):
234
266
  self.db_path = db_path
235
267
  self._sql = _AsyncSQLite(
236
268
  db_path,
237
- "CREATE TABLE IF NOT EXISTS kv_entries "
238
- "(key TEXT PRIMARY KEY, data TEXT NOT NULL)",
269
+ (
270
+ "CREATE TABLE IF NOT EXISTS kv_entries "
271
+ "(key TEXT PRIMARY KEY, data TEXT NOT NULL)",
272
+ "CREATE TABLE IF NOT EXISTS append_entries "
273
+ "(seq INTEGER PRIMARY KEY AUTOINCREMENT, "
274
+ "key TEXT NOT NULL, data TEXT NOT NULL)",
275
+ "CREATE INDEX IF NOT EXISTS idx_append_entries_key "
276
+ "ON append_entries(key, seq)",
277
+ ),
239
278
  )
240
279
 
241
280
  async def set(self, key: str, data: dict[str, Any]) -> None:
@@ -252,11 +291,33 @@ class SQLiteKVBackend(KVBackend):
252
291
 
253
292
  async def delete(self, key: str) -> None:
254
293
  await self._sql.execute("DELETE FROM kv_entries WHERE key = ?", (key,))
294
+ await self._sql.execute("DELETE FROM append_entries WHERE key = ?", (key,))
255
295
 
256
296
  async def keys(self) -> list[str]:
257
297
  rows = await self._sql.execute("SELECT key FROM kv_entries")
258
298
  return [cast(str, r[0]) for r in rows]
259
299
 
300
+ async def append(self, key: str, entry: dict[str, Any]) -> None:
301
+ await self._sql.execute(
302
+ "INSERT INTO append_entries (key, data) VALUES (?, ?)",
303
+ (key, json.dumps(entry)),
304
+ )
305
+
306
+ async def entries(self, key: str) -> list[dict[str, Any]]:
307
+ rows = await self._sql.execute(
308
+ "SELECT data FROM append_entries WHERE key = ? ORDER BY seq", (key,)
309
+ )
310
+ return [cast(dict[str, Any], json.loads(r[0])) for r in rows]
311
+
312
+ async def clear_entries(self, key: str) -> None:
313
+ await self._sql.execute("DELETE FROM append_entries WHERE key = ?", (key,))
314
+
315
+ async def entry_count(self, key: str) -> int:
316
+ rows = await self._sql.execute(
317
+ "SELECT COUNT(*) FROM append_entries WHERE key = ?", (key,)
318
+ )
319
+ return int(rows[0][0]) if rows else 0
320
+
260
321
  async def aclose(self) -> None:
261
322
  await self._sql.aclose()
262
323
 
@@ -288,7 +349,7 @@ class SQLiteBackend(CheckpointBackend):
288
349
  await self._sql.aclose()
289
350
 
290
351
 
291
- class PostgreSQLKVBackend(KVBackend):
352
+ class PostgreSQLKVBackend(AppendBackend):
292
353
  """PostgreSQL KV backend (`kv_entries(key, data)`) — behind the `pg` extra.
293
354
 
294
355
  Like the SQLite KV backend, but shared across processes: a natural store
@@ -327,6 +388,15 @@ class PostgreSQLKVBackend(KVBackend):
327
388
  "CREATE TABLE IF NOT EXISTS kv_entries "
328
389
  "(key TEXT PRIMARY KEY, data TEXT NOT NULL)"
329
390
  )
391
+ await cur.execute(
392
+ "CREATE TABLE IF NOT EXISTS append_entries "
393
+ "(seq BIGSERIAL PRIMARY KEY, "
394
+ "key TEXT NOT NULL, data TEXT NOT NULL)"
395
+ )
396
+ await cur.execute(
397
+ "CREATE INDEX IF NOT EXISTS idx_append_entries_key "
398
+ "ON append_entries(key, seq)"
399
+ )
330
400
  await conn.commit()
331
401
  self._conn = conn
332
402
  return conn
@@ -384,11 +454,37 @@ class PostgreSQLKVBackend(KVBackend):
384
454
 
385
455
  async def delete(self, key: str) -> None:
386
456
  await self._run("DELETE FROM kv_entries WHERE key = %s", (key,))
457
+ await self._run("DELETE FROM append_entries WHERE key = %s", (key,))
387
458
 
388
459
  async def keys(self) -> list[str]:
389
460
  rows = await self._run("SELECT key FROM kv_entries", fetch="fetchall")
390
461
  return [cast(str, r[0]) for r in rows]
391
462
 
463
+ async def append(self, key: str, entry: dict[str, Any]) -> None:
464
+ await self._run(
465
+ "INSERT INTO append_entries (key, data) VALUES (%s, %s)",
466
+ (key, json.dumps(entry)),
467
+ )
468
+
469
+ async def entries(self, key: str) -> list[dict[str, Any]]:
470
+ rows = await self._run(
471
+ "SELECT data FROM append_entries WHERE key = %s ORDER BY seq",
472
+ (key,),
473
+ fetch="fetchall",
474
+ )
475
+ return [cast(dict[str, Any], json.loads(r[0])) for r in rows]
476
+
477
+ async def clear_entries(self, key: str) -> None:
478
+ await self._run("DELETE FROM append_entries WHERE key = %s", (key,))
479
+
480
+ async def entry_count(self, key: str) -> int:
481
+ row = await self._run(
482
+ "SELECT COUNT(*) FROM append_entries WHERE key = %s",
483
+ (key,),
484
+ fetch="fetchone",
485
+ )
486
+ return int(row[0]) if row is not None else 0
487
+
392
488
  async def aclose(self) -> None:
393
489
  async with self._lock:
394
490
  await self._reset()
@@ -28,6 +28,14 @@ class Consume:
28
28
  subclass that overrides `run()` directly and has no `consumes` at all to
29
29
  attach a condition to; see `Agent.triggers`'s own docstring).
30
30
 
31
+ `include_subclasses` (default `False`): wake on artifacts whose type is a
32
+ subclass of `artifact_type`, not only the exact type — the declarative
33
+ form of a marker base / protocol base (`Consume(ChatMessage,
34
+ include_subclasses=True)` fires for `UserQuery` and `Answer`). `collect()`
35
+ already reads by `issubclass` (`Context.list_artifacts`), so the flag only
36
+ affects triggering. Opt-in so an agent subscribed to both a base and a
37
+ concrete type never wakes twice for the same event.
38
+
31
39
  `debounce` (default `False`): when several events in the same
32
40
  generation would each independently wake this agent via this `Consume`
33
41
  (a fan-out step creating five `Evidence` artifacts in one commit, five
@@ -51,6 +59,7 @@ class Consume:
51
59
  )
52
60
  wakes: bool = True
53
61
  debounce: bool = False
62
+ include_subclasses: bool = False
54
63
 
55
64
  def __init__(
56
65
  self,
@@ -60,6 +69,7 @@ class Consume:
60
69
  *,
61
70
  wakes: bool | None = None,
62
71
  debounce: bool | None = None,
72
+ include_subclasses: bool | None = None,
63
73
  ):
64
74
  self.artifact_type = artifact_type or self.__class__.artifact_type
65
75
  if self.artifact_type is None:
@@ -75,6 +85,11 @@ class Consume:
75
85
  )
76
86
  self.wakes = wakes if wakes is not None else self.__class__.wakes
77
87
  self.debounce = debounce if debounce is not None else self.__class__.debounce
88
+ self.include_subclasses = (
89
+ include_subclasses
90
+ if include_subclasses is not None
91
+ else self.__class__.include_subclasses
92
+ )
78
93
  if self.debounce and not self.wakes:
79
94
  raise ValueError(
80
95
  "Consume(debounce=True, wakes=False) is meaningless: debounce "
@@ -93,7 +108,11 @@ class Consume:
93
108
  return []
94
109
  return [
95
110
  Trigger(
96
- event_type, self.artifact_type, self.condition, debounce=self.debounce
111
+ event_type,
112
+ self.artifact_type,
113
+ self.condition,
114
+ debounce=self.debounce,
115
+ include_subclasses=self.include_subclasses,
97
116
  )
98
117
  for event_type in self.event_types
99
118
  ]
@@ -132,6 +151,31 @@ class Consume:
132
151
  debounce=debounce,
133
152
  )
134
153
 
154
+ @classmethod
155
+ def by_base(
156
+ cls,
157
+ artifact_type: ArtifactType,
158
+ event_types: Sequence[EventType] = (
159
+ EventType.ARTIFACT_CREATED,
160
+ EventType.ARTIFACT_UPDATED,
161
+ ),
162
+ *,
163
+ wakes: bool = True,
164
+ debounce: bool = False,
165
+ ) -> Consume:
166
+ """Creates a Consume that also wakes on subclasses of `artifact_type`.
167
+
168
+ Sugar for `Consume(artifact_type, include_subclasses=True)` — e.g. a
169
+ marker base `ChatMessage` with concrete `UserQuery`/`Answer` artifacts.
170
+ """
171
+ return cls(
172
+ artifact_type,
173
+ event_types=event_types,
174
+ wakes=wakes,
175
+ debounce=debounce,
176
+ include_subclasses=True,
177
+ )
178
+
135
179
  @classmethod
136
180
  def by_field(
137
181
  cls,
@@ -380,8 +424,14 @@ def consume(
380
424
  *,
381
425
  wakes: bool = True,
382
426
  debounce: bool = False,
427
+ include_subclasses: bool = False,
383
428
  ) -> Consume:
384
429
  """Factory for quickly creating a Consume."""
385
430
  return Consume(
386
- artifact_type, condition, event_types, wakes=wakes, debounce=debounce
431
+ artifact_type,
432
+ condition,
433
+ event_types,
434
+ wakes=wakes,
435
+ debounce=debounce,
436
+ include_subclasses=include_subclasses,
387
437
  )