reactifact 0.6.0__tar.gz → 0.7.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 (192) hide show
  1. {reactifact-0.6.0 → reactifact-0.7.0}/PKG-INFO +61 -5
  2. reactifact-0.6.0/reactifact.egg-info/PKG-INFO → reactifact-0.7.0/README.md +47 -29
  3. {reactifact-0.6.0 → reactifact-0.7.0}/pyproject.toml +35 -3
  4. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/__init__.py +1 -1
  5. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/branching.py +8 -9
  6. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/effects.py +14 -4
  7. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/llm_agent.py +3 -1
  8. reactifact-0.7.0/reactifact/mcp/__init__.py +21 -0
  9. reactifact-0.7.0/reactifact/mcp/client.py +133 -0
  10. reactifact-0.7.0/reactifact/mcp/oauth.py +94 -0
  11. reactifact-0.7.0/reactifact/mcp/server.py +162 -0
  12. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/produce.py +14 -58
  13. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/resources.py +11 -1
  14. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/runtime.py +2 -2
  15. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/scheduler.py +31 -0
  16. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tool_use.py +126 -12
  17. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/__init__.py +2 -0
  18. reactifact-0.7.0/reactifact/tracing/_otlp.py +59 -0
  19. reactifact-0.7.0/reactifact/tracing/langfuse.py +194 -0
  20. reactifact-0.7.0/reactifact/tracing/otlp.py +169 -0
  21. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/templates/ui.html +1 -1
  22. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/triggers.py +5 -5
  23. reactifact-0.6.0/README.md → reactifact-0.7.0/reactifact.egg-info/PKG-INFO +85 -4
  24. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact.egg-info/SOURCES.txt +8 -0
  25. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact.egg-info/requires.txt +3 -0
  26. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_adaptive.py +88 -1
  27. reactifact-0.7.0/tests/test_canonical_ports.py +65 -0
  28. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_consumes_produces.py +5 -20
  29. reactifact-0.7.0/tests/test_mcp.py +348 -0
  30. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_medic_lab.py +31 -1
  31. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_produce_styles.py +4 -14
  32. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_relations.py +10 -0
  33. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_tools.py +69 -1
  34. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_tracing.py +119 -17
  35. reactifact-0.6.0/reactifact/tracing/langfuse.py +0 -125
  36. {reactifact-0.6.0 → reactifact-0.7.0}/LICENSE +0 -0
  37. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/__main__.py +0 -0
  38. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/_extras.py +0 -0
  39. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/agents.py +0 -0
  40. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/artifacts.py +0 -0
  41. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/budget.py +0 -0
  42. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/chat.py +0 -0
  43. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/checkpoints.py +0 -0
  44. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/__init__.py +0 -0
  45. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/branch.py +0 -0
  46. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/common.py +0 -0
  47. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/context.py +0 -0
  48. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/graph.py +0 -0
  49. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/replay.py +0 -0
  50. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/scenario.py +0 -0
  51. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/cli/trace.py +0 -0
  52. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/commit.py +0 -0
  53. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/commit_log.py +0 -0
  54. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/consume.py +0 -0
  55. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/context.py +0 -0
  56. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/eval.py +0 -0
  57. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/events.py +0 -0
  58. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/interrupt.py +0 -0
  59. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/operations.py +0 -0
  60. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/patches.py +0 -0
  61. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/prompts.py +0 -0
  62. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/__init__.py +0 -0
  63. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/_retry.py +0 -0
  64. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/anthropic.py +0 -0
  65. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/azure.py +0 -0
  66. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/cerebras.py +0 -0
  67. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/chat.py +0 -0
  68. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/contracts.py +0 -0
  69. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/deepseek.py +0 -0
  70. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/fake.py +0 -0
  71. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/fireworks.py +0 -0
  72. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/gemini.py +0 -0
  73. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/github_models.py +0 -0
  74. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/groq.py +0 -0
  75. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/image.py +0 -0
  76. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/mistral.py +0 -0
  77. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/nvidia.py +0 -0
  78. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/ollama.py +0 -0
  79. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/openai.py +0 -0
  80. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/openrouter.py +0 -0
  81. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/perplexity.py +0 -0
  82. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/qwen.py +0 -0
  83. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/speech.py +0 -0
  84. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/together.py +0 -0
  85. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/video.py +0 -0
  86. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/xai.py +0 -0
  87. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/providers/zai.py +0 -0
  88. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/py.typed +0 -0
  89. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/__init__.py +0 -0
  90. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/inputs.py +0 -0
  91. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/memory.py +0 -0
  92. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/resolve.py +0 -0
  93. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/rollback.py +0 -0
  94. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/search.py +0 -0
  95. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/skills.py +0 -0
  96. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/status.py +0 -0
  97. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/recipes/text.py +0 -0
  98. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/relations.py +0 -0
  99. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/replay.py +0 -0
  100. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/session.py +0 -0
  101. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/sources.py +0 -0
  102. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/streaming.py +0 -0
  103. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/structured.py +0 -0
  104. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/__init__.py +0 -0
  105. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/assertions.py +0 -0
  106. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/exceptions.py +0 -0
  107. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/fault.py +0 -0
  108. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/lab.py +0 -0
  109. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/mock.py +0 -0
  110. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/record.py +0 -0
  111. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/testing/registry.py +0 -0
  112. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tools.py +0 -0
  113. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/models.py +0 -0
  114. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/postgres.py +0 -0
  115. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/store.py +0 -0
  116. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/templates/ui_run.html +0 -0
  117. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/tracer.py +0 -0
  118. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/tracing/web.py +0 -0
  119. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/viz.py +0 -0
  120. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact/web.py +0 -0
  121. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact.egg-info/dependency_links.txt +0 -0
  122. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact.egg-info/entry_points.txt +0 -0
  123. {reactifact-0.6.0 → reactifact-0.7.0}/reactifact.egg-info/top_level.txt +0 -0
  124. {reactifact-0.6.0 → reactifact-0.7.0}/setup.cfg +0 -0
  125. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_anthropic_provider.py +0 -0
  126. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_artifacts.py +0 -0
  127. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_backbone.py +0 -0
  128. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_branching.py +0 -0
  129. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_budget.py +0 -0
  130. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_chat_web.py +0 -0
  131. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_checkpoint.py +0 -0
  132. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_checkpoints_concurrency.py +0 -0
  133. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_cli.py +0 -0
  134. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_cli_scenario.py +0 -0
  135. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_commit_log.py +0 -0
  136. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_concurrency.py +0 -0
  137. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_devops.py +0 -0
  138. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_devops_web.py +0 -0
  139. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_effects.py +0 -0
  140. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_eval.py +0 -0
  141. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_forklab.py +0 -0
  142. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_forklab_web.py +0 -0
  143. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_friendly_api.py +0 -0
  144. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_gemini_provider.py +0 -0
  145. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_hitl.py +0 -0
  146. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_image_provider.py +0 -0
  147. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_invalidation.py +0 -0
  148. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_knowledge.py +0 -0
  149. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_knowledge_web.py +0 -0
  150. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_llm_ladder.py +0 -0
  151. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_medic_lab_web.py +0 -0
  152. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_multisource.py +0 -0
  153. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_openai_provider.py +0 -0
  154. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_patches.py +0 -0
  155. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_ports.py +0 -0
  156. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_prompts.py +0 -0
  157. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_provider_auth.py +0 -0
  158. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_provider_retry.py +0 -0
  159. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_providers_integration.py +0 -0
  160. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_recipes.py +0 -0
  161. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_recipes_inputs.py +0 -0
  162. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_recipes_memory.py +0 -0
  163. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_recipes_skills.py +0 -0
  164. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_relation_graph.py +0 -0
  165. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_repair.py +0 -0
  166. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_repair_web.py +0 -0
  167. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_replay.py +0 -0
  168. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_research.py +0 -0
  169. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_resources.py +0 -0
  170. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_retry.py +0 -0
  171. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_runtime.py +0 -0
  172. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_runtime_errors.py +0 -0
  173. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_sessions.py +0 -0
  174. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_sources.py +0 -0
  175. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_sources_search.py +0 -0
  176. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_sources_vector.py +0 -0
  177. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_speech_provider.py +0 -0
  178. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_streaming.py +0 -0
  179. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_structured.py +0 -0
  180. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_testing_assertions.py +0 -0
  181. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_testing_lab.py +0 -0
  182. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_testing_mock.py +0 -0
  183. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_testing_registry.py +0 -0
  184. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_vendor_factories.py +0 -0
  185. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_vendor_multimodal_factories.py +0 -0
  186. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_video_provider.py +0 -0
  187. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_view.py +0 -0
  188. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_viz.py +0 -0
  189. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_web_source.py +0 -0
  190. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_workspace.py +0 -0
  191. {reactifact-0.6.0 → reactifact-0.7.0}/tests/test_workspace_with_sources.py +0 -0
  192. {reactifact-0.6.0 → reactifact-0.7.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,10 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
+ License-Expression: MIT
5
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
6
7
  Project-URL: Repository, https://github.com/bzdvdn/reactifact
7
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 :: 4 - Beta
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
8
19
  Requires-Python: >=3.11
9
20
  Description-Content-Type: text/markdown
10
21
  License-File: LICENSE
@@ -21,16 +32,22 @@ Requires-Dist: fastapi>=0.115; extra == "web"
21
32
  Requires-Dist: uvicorn[standard]>=0.30; extra == "web"
22
33
  Provides-Extra: pg
23
34
  Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
35
+ Provides-Extra: mcp
36
+ Requires-Dist: mcp>=2.2; extra == "mcp"
24
37
  Dynamic: license-file
25
38
 
26
- # reactifact
39
+ <p align="center">
40
+ <img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
41
+ </p>
27
42
 
28
43
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
29
44
 
30
45
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
46
+ [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
31
47
  [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
32
48
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
33
49
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
50
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bzdvdn/reactifact)
34
51
 
35
52
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
36
53
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -41,6 +58,13 @@ reactifact flips the model. You describe **what artifacts exist and what agents
41
58
  do with them**; the runtime derives what runs next from **state changes**. Agents
42
59
  react to events — there is no graph, no node pipeline.
43
60
 
61
+ ![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)
62
+
63
+ Two agents explore independently on their own forks and merge back automatically
64
+ — and when they disagree, reactifact refuses to merge silently:
65
+
66
+ ![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)
67
+
44
68
  ```bash
45
69
  pip install reactifact
46
70
  ```
@@ -107,6 +131,12 @@ print(answer.data.text) # "Refunds are available within 14 d
107
131
  print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
108
132
  ```
109
133
 
134
+ The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
135
+ harder, multi-source question (docs + a CSV) with a real computed number and
136
+ its sources, no LLM key required:
137
+
138
+ ![CLI demo: asking "how much does gpu cost in total?" — the runtime searches docs and a spreadsheet, computes the sum, verifies it, and answers with sources.](docs/img/knowledge-cli-demo.gif)
139
+
110
140
  ## How it works
111
141
 
112
142
  ```text
@@ -136,12 +166,33 @@ actual state.
136
166
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
137
167
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
138
168
  | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
169
+ | Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
170
+ | MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
171
+ | Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
139
172
 
140
173
  Reactive. Deterministic. Accountable.
141
174
 
142
175
  Full breakdown, including where reactifact is *not* the right choice:
143
176
  [docs/en/comparison.md](docs/en/comparison.md).
144
177
 
178
+ **Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
179
+ billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
180
+ offline. Edit *one* fact and see what actually reruns:
181
+
182
+ ```text
183
+ >>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
184
+ >>> LaborCost or Discount ever consumed.
185
+
186
+ LaborCost value=500.0 version=0 # untouched
187
+ Tax value=60.0 version=1 # recomputed
188
+ Discount value=25.0 version=0 # untouched
189
+ Total value=535.0 version=1 # recomputed
190
+ ```
191
+
192
+ 2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
193
+ because `Artifact.version` tracks real consumption, not a graph edge you drew
194
+ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
195
+
145
196
  ## Core primitives
146
197
 
147
198
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -149,7 +200,7 @@ Full breakdown, including where reactifact is *not* the right choice:
149
200
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
150
201
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
151
202
  - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
152
- - **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL, filesystem are equally first-class.
203
+ - **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).
153
204
  - **Provenance** — every derived artifact links to what produced it
154
205
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
155
206
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -160,6 +211,10 @@ Full breakdown, including where reactifact is *not* the right choice:
160
211
  fallbacks instead of hallucinated answers; the model reasons, never "knows".
161
212
  - **Observability** — every run traces agent spans, reads/writes, LLM calls,
162
213
  tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
214
+ - **MCP, both ways** — call any MCP server's tools as a `Tool`
215
+ (`mcp_stdio_tools`/`mcp_http_tools`), or expose your own `Tool`s and a live
216
+ `Context` as an MCP server (`create_mcp_server`) for Claude Desktop, Claude
217
+ Code, or another agent to call into (`mcp` extra).
163
218
  - **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
164
219
  - **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
165
220
  `ReplayLLM`, all for audit and safe alternative states.
@@ -186,7 +241,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
186
241
  ```
187
242
 
188
243
  Classic-pattern ports run as one-liners too:
189
- `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
244
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
190
245
 
191
246
  ## Examples (in-repo, not shipped)
192
247
 
@@ -199,7 +254,7 @@ Classic-pattern ports run as one-liners too:
199
254
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
200
255
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
201
256
  - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
202
- - `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
257
+ - `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
203
258
 
204
259
  ## Documentation
205
260
 
@@ -211,6 +266,7 @@ Classic-pattern ports run as one-liners too:
211
266
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
212
267
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
213
268
  - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
269
+ - [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
214
270
 
215
271
  ## Development
216
272
 
@@ -1,36 +1,15 @@
1
- Metadata-Version: 2.4
2
- Name: reactifact
3
- Version: 0.6.0
4
- Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
- Project-URL: Homepage, https://github.com/bzdvdn/reactifact
6
- Project-URL: Repository, https://github.com/bzdvdn/reactifact
7
- Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
8
- Requires-Python: >=3.11
9
- Description-Content-Type: text/markdown
10
- License-File: LICENSE
11
- Requires-Dist: pydantic>=2.13.4
12
- Requires-Dist: httpx>=0.27
13
- Requires-Dist: python-dotenv>=1.0
14
- Provides-Extra: dev
15
- Requires-Dist: ruff>=0.8; extra == "dev"
16
- Requires-Dist: mypy>=1.11; extra == "dev"
17
- Requires-Dist: pytest>=9.1.1; extra == "dev"
18
- Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
19
- Provides-Extra: web
20
- Requires-Dist: fastapi>=0.115; extra == "web"
21
- Requires-Dist: uvicorn[standard]>=0.30; extra == "web"
22
- Provides-Extra: pg
23
- Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
24
- Dynamic: license-file
25
-
26
- # 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>
27
4
 
28
5
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
29
6
 
30
7
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
8
+ [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
31
9
  [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
32
10
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
33
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)
34
13
 
35
14
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
36
15
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -41,6 +20,13 @@ reactifact flips the model. You describe **what artifacts exist and what agents
41
20
  do with them**; the runtime derives what runs next from **state changes**. Agents
42
21
  react to events — there is no graph, no node pipeline.
43
22
 
23
+ ![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)
24
+
25
+ Two agents explore independently on their own forks and merge back automatically
26
+ — and when they disagree, reactifact refuses to merge silently:
27
+
28
+ ![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)
29
+
44
30
  ```bash
45
31
  pip install reactifact
46
32
  ```
@@ -107,6 +93,12 @@ print(answer.data.text) # "Refunds are available within 14 d
107
93
  print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
108
94
  ```
109
95
 
96
+ The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
97
+ harder, multi-source question (docs + a CSV) with a real computed number and
98
+ its sources, no LLM key required:
99
+
100
+ ![CLI demo: asking "how much does gpu cost in total?" — the runtime searches docs and a spreadsheet, computes the sum, verifies it, and answers with sources.](docs/img/knowledge-cli-demo.gif)
101
+
110
102
  ## How it works
111
103
 
112
104
  ```text
@@ -136,12 +128,33 @@ actual state.
136
128
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
137
129
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
138
130
  | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
131
+ | Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
132
+ | MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
133
+ | Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
139
134
 
140
135
  Reactive. Deterministic. Accountable.
141
136
 
142
137
  Full breakdown, including where reactifact is *not* the right choice:
143
138
  [docs/en/comparison.md](docs/en/comparison.md).
144
139
 
140
+ **Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
141
+ billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
142
+ offline. Edit *one* fact and see what actually reruns:
143
+
144
+ ```text
145
+ >>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
146
+ >>> LaborCost or Discount ever consumed.
147
+
148
+ LaborCost value=500.0 version=0 # untouched
149
+ Tax value=60.0 version=1 # recomputed
150
+ Discount value=25.0 version=0 # untouched
151
+ Total value=535.0 version=1 # recomputed
152
+ ```
153
+
154
+ 2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
155
+ because `Artifact.version` tracks real consumption, not a graph edge you drew
156
+ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
157
+
145
158
  ## Core primitives
146
159
 
147
160
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -149,7 +162,7 @@ Full breakdown, including where reactifact is *not* the right choice:
149
162
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
150
163
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
151
164
  - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
152
- - **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL, filesystem are equally first-class.
165
+ - **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).
153
166
  - **Provenance** — every derived artifact links to what produced it
154
167
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
155
168
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -160,6 +173,10 @@ Full breakdown, including where reactifact is *not* the right choice:
160
173
  fallbacks instead of hallucinated answers; the model reasons, never "knows".
161
174
  - **Observability** — every run traces agent spans, reads/writes, LLM calls,
162
175
  tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
176
+ - **MCP, both ways** — call any MCP server's tools as a `Tool`
177
+ (`mcp_stdio_tools`/`mcp_http_tools`), or expose your own `Tool`s and a live
178
+ `Context` as an MCP server (`create_mcp_server`) for Claude Desktop, Claude
179
+ Code, or another agent to call into (`mcp` extra).
163
180
  - **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
164
181
  - **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
165
182
  `ReplayLLM`, all for audit and safe alternative states.
@@ -186,7 +203,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
186
203
  ```
187
204
 
188
205
  Classic-pattern ports run as one-liners too:
189
- `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
206
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
190
207
 
191
208
  ## Examples (in-repo, not shipped)
192
209
 
@@ -199,7 +216,7 @@ Classic-pattern ports run as one-liners too:
199
216
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
200
217
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
201
218
  - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
202
- - `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
219
+ - `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
203
220
 
204
221
  ## Documentation
205
222
 
@@ -211,6 +228,7 @@ Classic-pattern ports run as one-liners too:
211
228
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
212
229
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
213
230
  - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
231
+ - [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
214
232
 
215
233
  ## Development
216
234
 
@@ -4,10 +4,23 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.6.0"
7
+ version = "0.7.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"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = ["agents", "llm", "ai-agents", "reactive", "provenance", "orchestration", "agentic"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Typing :: Typed",
23
+ ]
11
24
  dependencies = [
12
25
  "pydantic>=2.13.4",
13
26
  "httpx>=0.27",
@@ -18,6 +31,7 @@ dependencies = [
18
31
  Homepage = "https://github.com/bzdvdn/reactifact"
19
32
  Repository = "https://github.com/bzdvdn/reactifact"
20
33
  Documentation = "https://github.com/bzdvdn/reactifact/tree/master/docs"
34
+ Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
21
35
 
22
36
  [project.optional-dependencies]
23
37
  dev = [
@@ -33,10 +47,16 @@ web = [
33
47
  pg = [
34
48
  "psycopg[binary]>=3.2",
35
49
  ]
50
+ mcp = [
51
+ "mcp>=2.2",
52
+ ]
36
53
 
37
54
  # uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
38
55
  # deps by default (no `--extra` flag needed to boot the examples). Extras stay
39
- # for PyPI (`pip install reactifact[web]`).
56
+ # for PyPI (`pip install reactifact[web]`). `mcp` joins the default groups too —
57
+ # unlike `pg`, its tests need no external service (in-memory MCP transport), so
58
+ # there's no reason not to run them for real in CI. `pg` stays opt-in; its own
59
+ # test only checks the graceful ImportError path without psycopg installed.
40
60
  [dependency-groups]
41
61
  dev = [
42
62
  "ruff>=0.8",
@@ -51,9 +71,12 @@ web = [
51
71
  pg = [
52
72
  "psycopg[binary]>=3.2",
53
73
  ]
74
+ mcp = [
75
+ "mcp>=2.2",
76
+ ]
54
77
 
55
78
  [tool.uv]
56
- default-groups = ["dev", "web"]
79
+ default-groups = ["dev", "web", "mcp"]
57
80
 
58
81
  # Публикация: в wheel едет только пакет `reactifact`; examples/tests остаются
59
82
  # в репозитории для демо и разработки, но не попадают в дистрибутив.
@@ -70,6 +93,15 @@ reactifact = ["py.typed"]
70
93
  [tool.pytest.ini_options]
71
94
  testpaths = ["tests"]
72
95
 
96
+ # Enforced by CI's `pytest --cov=reactifact --cov-fail-under=85` (see
97
+ # .github/workflows/ci.yml). Current coverage is ~90%; 85% leaves headroom
98
+ # for `reactifact/tracing/postgres.py`, whose own test intentionally only
99
+ # checks the graceful-ImportError path without `psycopg` installed (`pg` is
100
+ # an opt-in extra with no external service in CI) rather than needing a
101
+ # per-file carve-out.
102
+ [tool.coverage.report]
103
+ fail_under = 85
104
+
73
105
  [tool.ruff]
74
106
  target-version = "py311"
75
107
  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.0"
51
+ __version__ = "0.7.0"
52
52
 
53
53
  __all__ = [
54
54
  "Agent",
@@ -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")
@@ -91,13 +95,8 @@ def merge_context_from(target: Context, other: Context) -> None:
91
95
  operations.append(Update(other_id, new_data))
92
96
  else:
93
97
  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))
98
+ target.create(new_data, id=other_id)
99
+ operations.append(Create(new_data, id=other_id))
101
100
  for rel in other.relations():
102
101
  if (rel.source_id, rel.relation, rel.target_id) not in target._relations:
103
102
  target.link(rel.source_id, rel.relation, rel.target_id)
@@ -128,14 +128,24 @@ class Effects:
128
128
  def upsert(self, data: Any, *, id: str) -> Handle:
129
129
  """Explicit create-or-refresh: same effect as `create(data, id=id)`.
130
130
 
131
- Prefer this over `create(..., id=...)` when the artifact may already
132
- exist (e.g. a re-derived id like `f"answer:{qid}"`) it says at the
133
- call site that an update is an expected outcome, not a surprise.
131
+ Purely a call-site name: identical to `create(..., id=...)`, but says
132
+ at the call site that a refresh is an expected outcome, not a
133
+ surprise reach for it when the artifact may already exist (e.g. a
134
+ re-derived id like `f"answer:{qid}"`).
134
135
  """
135
136
  return self.create(data, id=id)
136
137
 
137
138
  def update(self, artifact: Artifact[Any], **fields: Any) -> Effects:
138
- """Bumps fields of an *existing* artifact (a new version)."""
139
+ """Bumps fields of an *existing* artifact (a new version).
140
+
141
+ Note the name means something different here than on `Patch.update`/
142
+ `Context.update`/`Artifact.update` (a full data replacement) — this is
143
+ the one intentional exception, matching `Patch.update_fields`
144
+ instead. `Effects` is the everyday authoring surface where "update
145
+ some fields" is the common case (§18 above), so it gets the short
146
+ name; the lower-level, less-used `Patch`/`Context`/`Artifact` surface
147
+ keeps `update` for the operation it's actually named after.
148
+ """
139
149
  new_data = artifact.data.model_copy(update=fields)
140
150
  self.operations.append(Update(artifact.id, new_data))
141
151
  return self
@@ -25,7 +25,7 @@ from .events import Event
25
25
  from .interrupt import PendingQuestion
26
26
  from .produce import Produce
27
27
  from .structured import SYSTEM_STRUCTURED, structured_llm
28
- from .tool_use import Observation, ToolAnswer, ToolUse, ToolUseHITL
28
+ from .tool_use import DeferredToolGroup, Observation, ToolAnswer, ToolUse, ToolUseHITL
29
29
  from .tools import Tool
30
30
 
31
31
 
@@ -98,6 +98,7 @@ class LLMAgent(Agent):
98
98
  max_steps: int = 8
99
99
  temperature: float | None = None
100
100
  max_tokens: int | None = None
101
+ deferred_tool_groups: Sequence[DeferredToolGroup] = ()
101
102
 
102
103
  def __init__(self, *, name: str | None = None, **kwargs: Any):
103
104
  llm_name = name or self.name or self.__class__.__name__.lower()
@@ -116,6 +117,7 @@ class LLMAgent(Agent):
116
117
  max_steps=self.max_steps,
117
118
  temperature=self.temperature,
118
119
  max_tokens=self.max_tokens,
120
+ deferred_tool_groups=self.deferred_tool_groups,
119
121
  ),
120
122
  Produce(ToolAnswer),
121
123
  *user_produces,
@@ -0,0 +1,21 @@
1
+ """MCP integration: talk to external MCP servers, or serve reactifact `Tool`s
2
+ and `Context` artifacts as one. Requires the `mcp` extra — nothing here is
3
+ imported by reactifact's core, so a bare `pip install reactifact` never pulls
4
+ in the `mcp` SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .client import MCPTool, mcp_http_tools, mcp_stdio_tools, mcp_tools
10
+ from .oauth import InMemoryTokenStorage, oauth_client_credentials
11
+ from .server import create_mcp_server
12
+
13
+ __all__ = [
14
+ "InMemoryTokenStorage",
15
+ "MCPTool",
16
+ "create_mcp_server",
17
+ "mcp_http_tools",
18
+ "mcp_stdio_tools",
19
+ "mcp_tools",
20
+ "oauth_client_credentials",
21
+ ]
@@ -0,0 +1,133 @@
1
+ """MCP client: turns tools exposed by an external MCP server into `Tool`s, so
2
+ `ToolUse`/`ToolUseHITL`/`LLMAgent` call remote MCP tools the same way they
3
+ call local ones — no separate code path for "MCP tool" vs. `@tool`. Requires
4
+ the `mcp` extra.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncGenerator
10
+ from contextlib import AsyncExitStack, asynccontextmanager
11
+ from typing import Any
12
+
13
+ from .._extras import require_extra
14
+ from ..tools import Tool, ToolOutput
15
+
16
+
17
+ class MCPTool(Tool):
18
+ """One tool from a connected `mcp.ClientSession`, wrapped as a `Tool`."""
19
+
20
+ def __init__(
21
+ self,
22
+ session: Any,
23
+ *,
24
+ name: str,
25
+ description: str,
26
+ schema: dict[str, Any],
27
+ destructive: bool = False,
28
+ ):
29
+ self._session = session
30
+ self.name = name
31
+ self.description = description
32
+ self.schema = schema
33
+ self.destructive = destructive
34
+
35
+ async def execute(self, args: dict[str, Any]) -> ToolOutput:
36
+ result = await self._session.call_tool(self.name, args)
37
+ return _to_tool_output(result)
38
+
39
+
40
+ def _to_tool_output(result: Any) -> ToolOutput:
41
+ texts = [block.text for block in result.content if getattr(block, "text", None)]
42
+ text = "\n".join(texts)
43
+ if result.is_error:
44
+ return ToolOutput(text=text, error=text or "MCP tool call failed")
45
+ data = dict(result.structured_content) if result.structured_content else {}
46
+ return ToolOutput(text=text, data=data)
47
+
48
+
49
+ async def mcp_tools(session: Any) -> list[Tool]:
50
+ """Lists tools on a connected, initialized `mcp.ClientSession` and wraps each as a `Tool`."""
51
+ listed = await session.list_tools()
52
+ return [
53
+ MCPTool(
54
+ session,
55
+ name=t.name,
56
+ description=t.description or t.name,
57
+ schema=t.input_schema,
58
+ destructive=bool(t.annotations and t.annotations.destructive_hint),
59
+ )
60
+ for t in listed.tools
61
+ ]
62
+
63
+
64
+ @asynccontextmanager
65
+ async def mcp_stdio_tools(
66
+ command: str,
67
+ args: list[str] | None = None,
68
+ *,
69
+ env: dict[str, str] | None = None,
70
+ ) -> AsyncGenerator[list[Tool], None]:
71
+ """Spawns an MCP server over stdio and yields its tools as `Tool`s.
72
+
73
+ Example: `mcp_stdio_tools("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])`.
74
+ The connection stays open for the `async with` block; tools called after
75
+ it exits will fail.
76
+ """
77
+ require_extra("mcp_stdio_tools", "mcp", "mcp")
78
+ from mcp.client.stdio import StdioServerParameters, stdio_client
79
+
80
+ from mcp import ClientSession
81
+
82
+ params = StdioServerParameters(command=command, args=args or [], env=env)
83
+ async with (
84
+ stdio_client(params) as (read, write),
85
+ ClientSession(read, write) as session,
86
+ ):
87
+ await session.initialize()
88
+ yield await mcp_tools(session)
89
+
90
+
91
+ @asynccontextmanager
92
+ async def mcp_http_tools(
93
+ url: str,
94
+ *,
95
+ headers: dict[str, str] | None = None,
96
+ auth: Any = None,
97
+ ) -> AsyncGenerator[list[Tool], None]:
98
+ """Connects to an MCP server over streamable HTTP and yields its tools as
99
+ `Tool`s.
100
+
101
+ Pass `headers` for servers that require a static credential (e.g.
102
+ `{"Authorization": "Bearer ..."}`), or `auth` (an `httpx2.Auth`) for a
103
+ server that requires OAuth — see `reactifact.mcp.oauth_client_credentials`
104
+ for the machine-to-machine case. The SDK's `streamable_http_client` has
105
+ no `headers=`/`auth=` kwarg of its own; the documented way is a
106
+ pre-configured client, which this builds (and owns/closes) with the same
107
+ recommended timeouts the SDK's own default client uses (30s
108
+ connect/write/pool, 300s read — a server may hold a response stream
109
+ open). Note this must be `httpx2.AsyncClient` (the MCP SDK's own httpx
110
+ fork/dependency, not plain `httpx` — `streamable_http_client` rejects
111
+ the wrong one at the type level), imported lazily here since it's only
112
+ guaranteed installed alongside the `mcp` extra.
113
+ """
114
+ require_extra("mcp_http_tools", "mcp", "mcp")
115
+ import httpx2
116
+ from mcp.client.streamable_http import streamable_http_client
117
+
118
+ from mcp import ClientSession
119
+
120
+ async with AsyncExitStack() as stack:
121
+ http_client = None
122
+ if headers is not None or auth is not None:
123
+ http_client = await stack.enter_async_context(
124
+ httpx2.AsyncClient(
125
+ headers=headers, auth=auth, timeout=httpx2.Timeout(30.0, read=300.0)
126
+ )
127
+ )
128
+ read, write = await stack.enter_async_context(
129
+ streamable_http_client(url, http_client=http_client)
130
+ )
131
+ session = await stack.enter_async_context(ClientSession(read, write))
132
+ await session.initialize()
133
+ yield await mcp_tools(session)