reactifact 0.6.1__tar.gz → 0.8.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 (205) hide show
  1. {reactifact-0.6.1 → reactifact-0.8.0}/PKG-INFO +46 -8
  2. reactifact-0.6.1/reactifact.egg-info/PKG-INFO → reactifact-0.8.0/README.md +37 -43
  3. {reactifact-0.6.1 → reactifact-0.8.0}/pyproject.toml +24 -3
  4. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/__init__.py +1 -1
  5. reactifact-0.8.0/reactifact/agent_tool.py +115 -0
  6. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/agents.py +11 -1
  7. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/branching.py +10 -9
  8. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/checkpoints.py +9 -2
  9. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/context.py +43 -3
  10. reactifact-0.8.0/reactifact/context_builder.py +157 -0
  11. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/effects.py +14 -4
  12. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/llm_agent.py +5 -1
  13. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/__init__.py +3 -0
  14. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/client.py +36 -7
  15. reactifact-0.8.0/reactifact/mcp/oauth.py +94 -0
  16. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/produce.py +14 -58
  17. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/__init__.py +18 -0
  18. reactifact-0.8.0/reactifact/recipes/plan_execute.py +207 -0
  19. reactifact-0.8.0/reactifact/recipes/reflection.py +262 -0
  20. reactifact-0.8.0/reactifact/recipes/supervisor.py +224 -0
  21. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/resources.py +25 -1
  22. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/runtime.py +60 -4
  23. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/scheduler.py +31 -0
  24. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tool_use.py +254 -17
  25. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/__init__.py +2 -0
  26. reactifact-0.8.0/reactifact/tracing/_otlp.py +59 -0
  27. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/langfuse.py +5 -38
  28. reactifact-0.8.0/reactifact/tracing/otlp.py +169 -0
  29. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/triggers.py +5 -5
  30. reactifact-0.8.0/reactifact/verify.py +161 -0
  31. reactifact-0.6.1/README.md → reactifact-0.8.0/reactifact.egg-info/PKG-INFO +81 -5
  32. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/SOURCES.txt +17 -0
  33. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/requires.txt +5 -0
  34. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_adaptive.py +88 -1
  35. reactifact-0.8.0/tests/test_agent_tool.py +190 -0
  36. reactifact-0.8.0/tests/test_canonical_ports.py +65 -0
  37. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_checkpoint.py +22 -0
  38. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_concurrency.py +56 -0
  39. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_consumes_produces.py +5 -20
  40. reactifact-0.8.0/tests/test_context_builder.py +165 -0
  41. reactifact-0.8.0/tests/test_incident_commander.py +114 -0
  42. reactifact-0.8.0/tests/test_mcp.py +348 -0
  43. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_medic_lab.py +31 -1
  44. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_produce_styles.py +4 -14
  45. reactifact-0.8.0/tests/test_recipe_plan_execute.py +106 -0
  46. reactifact-0.8.0/tests/test_recipe_reflection.py +124 -0
  47. reactifact-0.8.0/tests/test_recipe_supervisor.py +162 -0
  48. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_relations.py +10 -0
  49. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_tools.py +206 -1
  50. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_tracing.py +79 -0
  51. reactifact-0.8.0/tests/test_verify.py +129 -0
  52. reactifact-0.6.1/tests/test_mcp.py +0 -169
  53. {reactifact-0.6.1 → reactifact-0.8.0}/LICENSE +0 -0
  54. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/__main__.py +0 -0
  55. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/_extras.py +0 -0
  56. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/artifacts.py +0 -0
  57. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/budget.py +0 -0
  58. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/chat.py +0 -0
  59. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/__init__.py +0 -0
  60. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/branch.py +0 -0
  61. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/common.py +0 -0
  62. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/context.py +0 -0
  63. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/graph.py +0 -0
  64. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/replay.py +0 -0
  65. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/scenario.py +0 -0
  66. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/trace.py +0 -0
  67. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/commit.py +0 -0
  68. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/commit_log.py +0 -0
  69. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/consume.py +0 -0
  70. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/eval.py +0 -0
  71. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/events.py +0 -0
  72. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/interrupt.py +0 -0
  73. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/server.py +0 -0
  74. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/operations.py +0 -0
  75. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/patches.py +0 -0
  76. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/prompts.py +0 -0
  77. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/__init__.py +0 -0
  78. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/_retry.py +0 -0
  79. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/anthropic.py +0 -0
  80. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/azure.py +0 -0
  81. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/cerebras.py +0 -0
  82. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/chat.py +0 -0
  83. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/contracts.py +0 -0
  84. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/deepseek.py +0 -0
  85. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/fake.py +0 -0
  86. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/fireworks.py +0 -0
  87. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/gemini.py +0 -0
  88. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/github_models.py +0 -0
  89. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/groq.py +0 -0
  90. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/image.py +0 -0
  91. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/mistral.py +0 -0
  92. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/nvidia.py +0 -0
  93. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/ollama.py +0 -0
  94. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/openai.py +0 -0
  95. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/openrouter.py +0 -0
  96. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/perplexity.py +0 -0
  97. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/qwen.py +0 -0
  98. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/speech.py +0 -0
  99. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/together.py +0 -0
  100. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/video.py +0 -0
  101. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/xai.py +0 -0
  102. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/zai.py +0 -0
  103. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/py.typed +0 -0
  104. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/inputs.py +0 -0
  105. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/memory.py +0 -0
  106. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/resolve.py +0 -0
  107. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/rollback.py +0 -0
  108. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/search.py +0 -0
  109. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/skills.py +0 -0
  110. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/status.py +0 -0
  111. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/text.py +0 -0
  112. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/relations.py +0 -0
  113. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/replay.py +0 -0
  114. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/session.py +0 -0
  115. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/sources.py +0 -0
  116. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/streaming.py +0 -0
  117. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/structured.py +0 -0
  118. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/__init__.py +0 -0
  119. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/assertions.py +0 -0
  120. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/exceptions.py +0 -0
  121. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/fault.py +0 -0
  122. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/lab.py +0 -0
  123. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/mock.py +0 -0
  124. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/record.py +0 -0
  125. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/registry.py +0 -0
  126. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tools.py +0 -0
  127. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/models.py +0 -0
  128. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/postgres.py +0 -0
  129. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/store.py +0 -0
  130. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/templates/ui.html +0 -0
  131. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/templates/ui_run.html +0 -0
  132. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/tracer.py +0 -0
  133. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/web.py +0 -0
  134. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/viz.py +0 -0
  135. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/web.py +0 -0
  136. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/dependency_links.txt +0 -0
  137. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/entry_points.txt +0 -0
  138. {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/top_level.txt +0 -0
  139. {reactifact-0.6.1 → reactifact-0.8.0}/setup.cfg +0 -0
  140. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_anthropic_provider.py +0 -0
  141. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_artifacts.py +0 -0
  142. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_backbone.py +0 -0
  143. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_branching.py +0 -0
  144. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_budget.py +0 -0
  145. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_chat_web.py +0 -0
  146. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_checkpoints_concurrency.py +0 -0
  147. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_cli.py +0 -0
  148. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_cli_scenario.py +0 -0
  149. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_commit_log.py +0 -0
  150. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_devops.py +0 -0
  151. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_devops_web.py +0 -0
  152. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_effects.py +0 -0
  153. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_eval.py +0 -0
  154. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_forklab.py +0 -0
  155. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_forklab_web.py +0 -0
  156. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_friendly_api.py +0 -0
  157. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_gemini_provider.py +0 -0
  158. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_hitl.py +0 -0
  159. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_image_provider.py +0 -0
  160. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_invalidation.py +0 -0
  161. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_knowledge.py +0 -0
  162. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_knowledge_web.py +0 -0
  163. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_llm_ladder.py +0 -0
  164. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_medic_lab_web.py +0 -0
  165. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_multisource.py +0 -0
  166. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_openai_provider.py +0 -0
  167. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_patches.py +0 -0
  168. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_ports.py +0 -0
  169. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_prompts.py +0 -0
  170. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_provider_auth.py +0 -0
  171. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_provider_retry.py +0 -0
  172. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_providers_integration.py +0 -0
  173. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes.py +0 -0
  174. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_inputs.py +0 -0
  175. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_memory.py +0 -0
  176. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_skills.py +0 -0
  177. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_relation_graph.py +0 -0
  178. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_repair.py +0 -0
  179. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_repair_web.py +0 -0
  180. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_replay.py +0 -0
  181. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_research.py +0 -0
  182. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_resources.py +0 -0
  183. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_retry.py +0 -0
  184. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_runtime.py +0 -0
  185. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_runtime_errors.py +0 -0
  186. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sessions.py +0 -0
  187. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources.py +0 -0
  188. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources_search.py +0 -0
  189. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources_vector.py +0 -0
  190. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_speech_provider.py +0 -0
  191. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_streaming.py +0 -0
  192. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_structured.py +0 -0
  193. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_assertions.py +0 -0
  194. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_lab.py +0 -0
  195. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_mock.py +0 -0
  196. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_registry.py +0 -0
  197. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_vendor_factories.py +0 -0
  198. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_vendor_multimodal_factories.py +0 -0
  199. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_video_provider.py +0 -0
  200. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_view.py +0 -0
  201. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_viz.py +0 -0
  202. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_web_source.py +0 -0
  203. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_workspace.py +0 -0
  204. {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_workspace_with_sources.py +0 -0
  205. {reactifact-0.6.1 → reactifact-0.8.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,20 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.6.1
3
+ Version: 0.8.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
7
7
  Project-URL: Repository, https://github.com/bzdvdn/reactifact
8
- Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
8
+ Project-URL: Documentation, https://bzdvdn.github.io/reactifact/
9
9
  Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
10
10
  Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
11
- Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Typing :: Typed
19
21
  Requires-Python: >=3.11
20
22
  Description-Content-Type: text/markdown
@@ -34,16 +36,25 @@ Provides-Extra: pg
34
36
  Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
35
37
  Provides-Extra: mcp
36
38
  Requires-Dist: mcp>=2.2; extra == "mcp"
39
+ Provides-Extra: docs
40
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
41
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
42
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
37
43
  Dynamic: license-file
38
44
 
39
- # reactifact
45
+ <p align="center">
46
+ <img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
47
+ </p>
40
48
 
41
49
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
42
50
 
43
51
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
44
- [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
52
+ [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
53
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/bzdvdn/reactifact)
45
54
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
46
55
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
56
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bzdvdn/reactifact)
57
+ [![Docs](https://img.shields.io/badge/docs-bzdvdn.github.io%2Freactifact-blue)](https://bzdvdn.github.io/reactifact/)
47
58
 
48
59
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
49
60
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -56,6 +67,11 @@ react to events — there is no graph, no node pipeline.
56
67
 
57
68
  ![Left: a hand-wired fetch → verify → answer pipeline. Right: reactifact — search_agent and answer_agent each declare only what they consume and produce, wired together by Context, never each other.](docs/img/wiring.svg)
58
69
 
70
+ Two agents explore independently on their own forks and merge back automatically
71
+ — and when they disagree, reactifact refuses to merge silently:
72
+
73
+ ![forklab demo: two strategies (depth/breadth) investigate on separate forks and merge cleanly; a second run edits the same artifact on both forks and reactifact raises MergeConflict instead of guessing, then re-merges under an explicit policy.](docs/img/forklab-demo.gif)
74
+
59
75
  ```bash
60
76
  pip install reactifact
61
77
  ```
@@ -157,12 +173,33 @@ actual state.
157
173
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
158
174
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
159
175
  | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
176
+ | Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
177
+ | MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
178
+ | Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
160
179
 
161
180
  Reactive. Deterministic. Accountable.
162
181
 
163
182
  Full breakdown, including where reactifact is *not* the right choice:
164
183
  [docs/en/comparison.md](docs/en/comparison.md).
165
184
 
185
+ **Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
186
+ billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
187
+ offline. Edit *one* fact and see what actually reruns:
188
+
189
+ ```text
190
+ >>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
191
+ >>> LaborCost or Discount ever consumed.
192
+
193
+ LaborCost value=500.0 version=0 # untouched
194
+ Tax value=60.0 version=1 # recomputed
195
+ Discount value=25.0 version=0 # untouched
196
+ Total value=535.0 version=1 # recomputed
197
+ ```
198
+
199
+ 2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
200
+ because `Artifact.version` tracks real consumption, not a graph edge you drew
201
+ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
202
+
166
203
  ## Core primitives
167
204
 
168
205
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -170,7 +207,7 @@ Full breakdown, including where reactifact is *not* the right choice:
170
207
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
171
208
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
172
209
  - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
173
- - **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL, filesystem are equally first-class.
210
+ - **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
174
211
  - **Provenance** — every derived artifact links to what produced it
175
212
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
176
213
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -211,7 +248,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
211
248
  ```
212
249
 
213
250
  Classic-pattern ports run as one-liners too:
214
- `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
251
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
215
252
 
216
253
  ## Examples (in-repo, not shipped)
217
254
 
@@ -224,7 +261,7 @@ Classic-pattern ports run as one-liners too:
224
261
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
225
262
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
226
263
  - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
227
- - `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
264
+ - `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
228
265
 
229
266
  ## Documentation
230
267
 
@@ -236,6 +273,7 @@ Classic-pattern ports run as one-liners too:
236
273
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
237
274
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
238
275
  - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
276
+ - [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
239
277
 
240
278
  ## Development
241
279
 
@@ -1,49 +1,16 @@
1
- Metadata-Version: 2.4
2
- Name: reactifact
3
- Version: 0.6.1
4
- Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://github.com/bzdvdn/reactifact
7
- Project-URL: Repository, https://github.com/bzdvdn/reactifact
8
- Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
9
- Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
10
- Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Typing :: Typed
19
- Requires-Python: >=3.11
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- Requires-Dist: pydantic>=2.13.4
23
- Requires-Dist: httpx>=0.27
24
- Requires-Dist: python-dotenv>=1.0
25
- Provides-Extra: dev
26
- Requires-Dist: ruff>=0.8; extra == "dev"
27
- Requires-Dist: mypy>=1.11; extra == "dev"
28
- Requires-Dist: pytest>=9.1.1; extra == "dev"
29
- Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
30
- Provides-Extra: web
31
- Requires-Dist: fastapi>=0.115; extra == "web"
32
- Requires-Dist: uvicorn[standard]>=0.30; extra == "web"
33
- Provides-Extra: pg
34
- Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
35
- Provides-Extra: mcp
36
- Requires-Dist: mcp>=2.2; extra == "mcp"
37
- Dynamic: license-file
38
-
39
- # reactifact
1
+ <p align="center">
2
+ <img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
3
+ </p>
40
4
 
41
5
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
42
6
 
43
7
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
44
- [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
8
+ [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
9
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/bzdvdn/reactifact)
45
10
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
46
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
12
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bzdvdn/reactifact)
13
+ [![Docs](https://img.shields.io/badge/docs-bzdvdn.github.io%2Freactifact-blue)](https://bzdvdn.github.io/reactifact/)
47
14
 
48
15
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
49
16
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -56,6 +23,11 @@ react to events — there is no graph, no node pipeline.
56
23
 
57
24
  ![Left: a hand-wired fetch → verify → answer pipeline. Right: reactifact — search_agent and answer_agent each declare only what they consume and produce, wired together by Context, never each other.](docs/img/wiring.svg)
58
25
 
26
+ Two agents explore independently on their own forks and merge back automatically
27
+ — and when they disagree, reactifact refuses to merge silently:
28
+
29
+ ![forklab demo: two strategies (depth/breadth) investigate on separate forks and merge cleanly; a second run edits the same artifact on both forks and reactifact raises MergeConflict instead of guessing, then re-merges under an explicit policy.](docs/img/forklab-demo.gif)
30
+
59
31
  ```bash
60
32
  pip install reactifact
61
33
  ```
@@ -157,12 +129,33 @@ actual state.
157
129
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
158
130
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
159
131
  | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
132
+ | Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
133
+ | MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
134
+ | Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
160
135
 
161
136
  Reactive. Deterministic. Accountable.
162
137
 
163
138
  Full breakdown, including where reactifact is *not* the right choice:
164
139
  [docs/en/comparison.md](docs/en/comparison.md).
165
140
 
141
+ **Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
142
+ billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
143
+ offline. Edit *one* fact and see what actually reruns:
144
+
145
+ ```text
146
+ >>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
147
+ >>> LaborCost or Discount ever consumed.
148
+
149
+ LaborCost value=500.0 version=0 # untouched
150
+ Tax value=60.0 version=1 # recomputed
151
+ Discount value=25.0 version=0 # untouched
152
+ Total value=535.0 version=1 # recomputed
153
+ ```
154
+
155
+ 2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
156
+ because `Artifact.version` tracks real consumption, not a graph edge you drew
157
+ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
158
+
166
159
  ## Core primitives
167
160
 
168
161
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -170,7 +163,7 @@ Full breakdown, including where reactifact is *not* the right choice:
170
163
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
171
164
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
172
165
  - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
173
- - **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL, filesystem are equally first-class.
166
+ - **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
174
167
  - **Provenance** — every derived artifact links to what produced it
175
168
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
176
169
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -211,7 +204,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
211
204
  ```
212
205
 
213
206
  Classic-pattern ports run as one-liners too:
214
- `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
207
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
215
208
 
216
209
  ## Examples (in-repo, not shipped)
217
210
 
@@ -224,7 +217,7 @@ Classic-pattern ports run as one-liners too:
224
217
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
225
218
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
226
219
  - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
227
- - `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
220
+ - `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
228
221
 
229
222
  ## Documentation
230
223
 
@@ -236,6 +229,7 @@ Classic-pattern ports run as one-liners too:
236
229
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
237
230
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
238
231
  - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
232
+ - [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
239
233
 
240
234
  ## Development
241
235
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.6.1"
7
+ version = "0.8.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"
@@ -12,13 +12,15 @@ license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  keywords = ["agents", "llm", "ai-agents", "reactive", "provenance", "orchestration", "agentic"]
14
14
  classifiers = [
15
- "Development Status :: 3 - Alpha",
15
+ "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "Topic :: Software Development :: Libraries :: Python Modules",
18
18
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3.11",
21
21
  "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
22
24
  "Typing :: Typed",
23
25
  ]
24
26
  dependencies = [
@@ -30,7 +32,7 @@ dependencies = [
30
32
  [project.urls]
31
33
  Homepage = "https://github.com/bzdvdn/reactifact"
32
34
  Repository = "https://github.com/bzdvdn/reactifact"
33
- Documentation = "https://github.com/bzdvdn/reactifact/tree/master/docs"
35
+ Documentation = "https://bzdvdn.github.io/reactifact/"
34
36
  Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
35
37
 
36
38
  [project.optional-dependencies]
@@ -50,6 +52,11 @@ pg = [
50
52
  mcp = [
51
53
  "mcp>=2.2",
52
54
  ]
55
+ docs = [
56
+ "mkdocs>=1.6",
57
+ "mkdocs-material>=9.5",
58
+ "mkdocstrings[python]>=0.27",
59
+ ]
53
60
 
54
61
  # uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
55
62
  # deps by default (no `--extra` flag needed to boot the examples). Extras stay
@@ -74,6 +81,11 @@ pg = [
74
81
  mcp = [
75
82
  "mcp>=2.2",
76
83
  ]
84
+ docs = [
85
+ "mkdocs>=1.6",
86
+ "mkdocs-material>=9.5",
87
+ "mkdocstrings[python]>=0.27",
88
+ ]
77
89
 
78
90
  [tool.uv]
79
91
  default-groups = ["dev", "web", "mcp"]
@@ -93,6 +105,15 @@ reactifact = ["py.typed"]
93
105
  [tool.pytest.ini_options]
94
106
  testpaths = ["tests"]
95
107
 
108
+ # Enforced by CI's `pytest --cov=reactifact --cov-fail-under=85` (see
109
+ # .github/workflows/ci.yml). Current coverage is ~90%; 85% leaves headroom
110
+ # for `reactifact/tracing/postgres.py`, whose own test intentionally only
111
+ # checks the graceful-ImportError path without `psycopg` installed (`pg` is
112
+ # an opt-in extra with no external service in CI) rather than needing a
113
+ # per-file carve-out.
114
+ [tool.coverage.report]
115
+ fail_under = 85
116
+
96
117
  [tool.ruff]
97
118
  target-version = "py311"
98
119
  line-length = 88
@@ -48,7 +48,7 @@ from .session import Session, SessionStore
48
48
  from .tools import FunctionTool, Tool, ToolOutput, tool
49
49
  from .triggers import Trigger
50
50
 
51
- __version__ = "0.6.1"
51
+ __version__ = "0.8.0"
52
52
 
53
53
  __all__ = [
54
54
  "Agent",
@@ -0,0 +1,115 @@
1
+ """reactifact.agent_tool — a sub-agent as a `Tool` (delegation, §46).
2
+
3
+ `AgentAsTool` lets an `LLMAgent`/`HITLLMAgent` loop call another agent as an
4
+ ordinary tool: "delegate to X" runs a fresh, isolated nested `Runtime` to
5
+ completion and returns its final answer as the tool result. The delegating
6
+ loop sees only the final text — not the sub-agent's own tool calls, retries,
7
+ or internal reasoning.
8
+
9
+ Two deliberate constraints, not omissions:
10
+
11
+ - **Isolation is a fresh `Context`, not `context.branch()`.** The sub-agent
12
+ sees only the `query` it's given, nothing of the parent's history — no
13
+ prompt bloat from an unrelated conversation, and no accidental fork/merge
14
+ semantics (`branch()` exists for alternative-state exploration meant to be
15
+ merged back later, §39/§40; a one-shot delegate-and-discard call needs
16
+ neither). If a sub-agent genuinely needs to see the parent's state, build
17
+ its `Context` yourself and don't use this tool.
18
+ - **HITL sub-agents are rejected, not silently broken.** `Tool.execute` has
19
+ no channel back to a human — nothing would ever answer a `PendingQuestion`
20
+ the sub-agent raises inside its own isolated `Context`, and the nested run
21
+ would just finish with no `ToolAnswer`. `execute()` checks for this and
22
+ returns a `ToolOutput(error=...)` naming the unanswered question, instead
23
+ of returning empty text with no explanation.
24
+
25
+ Because `Tool.execute` receives only `args` (no `context`, see `tools.py`),
26
+ the sub-agent's `LLMProvider`/budget cannot be inherited implicitly from
27
+ whatever loop is calling this tool — pass them explicitly via `resources=`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections.abc import Callable
33
+ from typing import Any
34
+
35
+ from pydantic import BaseModel
36
+
37
+ from .agents import Agent
38
+ from .budget import Budget
39
+ from .context import Context
40
+ from .resources import RuntimeResources
41
+ from .runtime import Runtime
42
+ from .tool_use import ToolAnswer
43
+ from .tools import Tool, ToolOutput
44
+
45
+
46
+ class SubTask(BaseModel):
47
+ """Default input artifact for `AgentAsTool` — override via `input_type`
48
+ with any model that has a `text: str` field (the convention `ToolUse`'s
49
+ own `_goal` reads, `tool_use.py`)."""
50
+
51
+ text: str
52
+
53
+
54
+ class AgentAsTool(Tool):
55
+ """Wraps `agent_factory()` as a callable tool (see module docstring)."""
56
+
57
+ def __init__(
58
+ self,
59
+ *,
60
+ name: str,
61
+ description: str,
62
+ agent_factory: Callable[[], Agent],
63
+ resources: RuntimeResources,
64
+ input_type: type[BaseModel] = SubTask,
65
+ output_type: type[BaseModel] | None = None,
66
+ max_runs: int = 20,
67
+ destructive: bool = False,
68
+ ):
69
+ self.name = name
70
+ self.description = description
71
+ self.agent_factory = agent_factory
72
+ self.resources = resources
73
+ self.input_type = input_type
74
+ self.output_type = output_type or ToolAnswer
75
+ self.max_runs = max_runs
76
+ self.destructive = destructive
77
+ self.schema = {
78
+ "type": "object",
79
+ "properties": {
80
+ "query": {
81
+ "type": "string",
82
+ "description": "The task/question to delegate to the sub-agent.",
83
+ }
84
+ },
85
+ "required": ["query"],
86
+ }
87
+
88
+ async def execute(self, args: dict[str, Any]) -> ToolOutput:
89
+ query = str(args.get("query", ""))
90
+ sub_context = Context(resources=self.resources)
91
+ sub_agent = self.agent_factory()
92
+ sub_runtime = Runtime(
93
+ sub_context, agents=[sub_agent], budget=Budget(max_runs=self.max_runs)
94
+ )
95
+ sub_context.create(self.input_type(text=query))
96
+ await sub_runtime.arun()
97
+
98
+ if sub_context.has_pending_question():
99
+ pending = sub_context.latest_pending_question()
100
+ question = pending.data.question if pending is not None else ""
101
+ return ToolOutput(
102
+ error=(
103
+ f"sub-agent '{sub_agent.name}' needs clarification it wasn't "
104
+ f"given and can't ask for: {question!r}. Give '{self.name}' a "
105
+ "more complete query and try again."
106
+ )
107
+ )
108
+
109
+ outputs = sub_context.list_artifacts(self.output_type)
110
+ if not outputs:
111
+ return ToolOutput(
112
+ error=f"sub-agent '{sub_agent.name}' produced no {self.output_type.__name__}"
113
+ )
114
+ text = getattr(outputs[-1].data, "text", "") or ""
115
+ return ToolOutput(text=text)
@@ -90,7 +90,14 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
90
90
  return self._collect_inputs(context)
91
91
 
92
92
  def _collect_inputs(self, context: Context) -> list[Artifact[Any]]:
93
- """Collects all artifacts matching consumes and conditions."""
93
+ """Collects all artifacts matching consumes and conditions.
94
+
95
+ Ranking/truncation, if any, is a `Runtime`-level policy
96
+ (`context.resources.context_builder`, see `context_builder.py`), not
97
+ this agent's — applied here so both this and `collect_inputs()`
98
+ (used by the runtime for provenance) see the identical, already
99
+ built list.
100
+ """
94
101
  if not self.consumes:
95
102
  return []
96
103
  inputs: list[Artifact[Any]] = []
@@ -99,6 +106,9 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
99
106
  if c.condition:
100
107
  artifacts = [a for a in artifacts if c.condition(a)]
101
108
  inputs.extend(artifacts)
109
+ builder = context.resources.context_builder
110
+ if builder is not None:
111
+ inputs = builder.build(context, self, inputs)
102
112
  return inputs
103
113
 
104
114
  async def run(self, event: Event, context: Context) -> Patch | None:
@@ -5,8 +5,12 @@
5
5
  git-like state operations `Context.clone()`/`.branch()`/`.merge_from()`/
6
6
  `.merge()` delegate to — moved here so the git-like algorithm lives next to
7
7
  the concept it implements, not folded into `Context`'s general CRUD/relations/
8
- HITL surface. `BranchStore` only *persists* named forks so they survive a
9
- restart:
8
+ HITL surface. Application code should call the `Context` methods (see
9
+ [docs/en/branching.md](../docs/en/branching.md)); these module-level
10
+ functions are exported for building on `Context` without one already in
11
+ hand (e.g. `BranchStore`'s own load/merge path below), not as an equally
12
+ first-class alternative entry point. `BranchStore` only *persists* named
13
+ forks so they survive a restart:
10
14
 
11
15
  store = BranchStore(SQLiteKVBackend("sessions.sqlite3"))
12
16
  await store.save_branch(ctx_branch, session_id="demo", name="hypothesis-a")
@@ -59,6 +63,7 @@ def clone_context(source: Context) -> Context:
59
63
  new_ws._log = source._log.copy()
60
64
  new_ws._relations = source._relations.copy()
61
65
  new_ws._recompute_stale()
66
+ new_ws._reindex_by_type()
62
67
  return new_ws
63
68
 
64
69
 
@@ -91,13 +96,8 @@ def merge_context_from(target: Context, other: Context) -> None:
91
96
  operations.append(Update(other_id, new_data))
92
97
  else:
93
98
  new_data = other_artifact.data.model_copy(deep=True)
94
- # NOTE: no `id=other_id` — matches the pre-extraction behavior of
95
- # `Context.merge_from()` exactly (a merged-in artifact absent from
96
- # `target` gets a freshly minted id, not `other_id`). Not fixed
97
- # here — a real behavior change belongs in its own change, not a
98
- # pure extraction.
99
- target.create(new_data)
100
- operations.append(Create(new_data))
99
+ target.create(new_data, id=other_id)
100
+ operations.append(Create(new_data, id=other_id))
101
101
  for rel in other.relations():
102
102
  if (rel.source_id, rel.relation, rel.target_id) not in target._relations:
103
103
  target.link(rel.source_id, rel.relation, rel.target_id)
@@ -210,6 +210,7 @@ def merge_contexts(
210
210
  # `delete()` — resync `_stale` from scratch rather than risk it
211
211
  # drifting from the post-merge state.
212
212
  target._recompute_stale()
213
+ target._reindex_by_type()
213
214
 
214
215
 
215
216
  class BranchStore:
@@ -112,11 +112,18 @@ class FileBackend(CheckpointBackend):
112
112
  """File backend: state is stored in a single JSON file."""
113
113
 
114
114
  def __init__(self, path: str):
115
- self.path = path
115
+ self.path = Path(path)
116
116
 
117
117
  def _save_sync(self, data: dict[str, Any]) -> None:
118
- with open(self.path, "w", encoding="utf-8") as f:
118
+ # Atomic write (tmp + rename), same reasoning as `FileKVBackend._set_sync`:
119
+ # a direct write left a truncated/corrupt file on a process kill mid-write
120
+ # (OOM, deploy, kill -9) — the next `load()` would raise JSONDecodeError
121
+ # and the session would be unrecoverable.
122
+ self.path.parent.mkdir(parents=True, exist_ok=True)
123
+ tmp = self.path.with_suffix(self.path.suffix + ".tmp")
124
+ with open(tmp, "w", encoding="utf-8") as f:
119
125
  json.dump(data, f, indent=2)
126
+ tmp.replace(self.path)
120
127
 
121
128
  async def save(self, data: dict[str, Any]) -> None:
122
129
  await asyncio.to_thread(self._save_sync, data)