reactifact 0.6.1__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.1/reactifact.egg-info → reactifact-0.7.0}/PKG-INFO +37 -6
  2. {reactifact-0.6.1 → reactifact-0.7.0}/README.md +35 -4
  3. {reactifact-0.6.1 → reactifact-0.7.0}/pyproject.toml +11 -2
  4. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/__init__.py +1 -1
  5. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/branching.py +8 -9
  6. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/effects.py +14 -4
  7. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/llm_agent.py +3 -1
  8. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/mcp/__init__.py +3 -0
  9. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/mcp/client.py +36 -7
  10. reactifact-0.7.0/reactifact/mcp/oauth.py +94 -0
  11. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/produce.py +14 -58
  12. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/resources.py +11 -1
  13. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/runtime.py +2 -2
  14. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/scheduler.py +31 -0
  15. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tool_use.py +126 -12
  16. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/__init__.py +2 -0
  17. reactifact-0.7.0/reactifact/tracing/_otlp.py +59 -0
  18. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/langfuse.py +5 -38
  19. reactifact-0.7.0/reactifact/tracing/otlp.py +169 -0
  20. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/triggers.py +5 -5
  21. {reactifact-0.6.1 → reactifact-0.7.0/reactifact.egg-info}/PKG-INFO +37 -6
  22. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact.egg-info/SOURCES.txt +4 -0
  23. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_adaptive.py +88 -1
  24. reactifact-0.7.0/tests/test_canonical_ports.py +65 -0
  25. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_consumes_produces.py +5 -20
  26. reactifact-0.7.0/tests/test_mcp.py +348 -0
  27. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_medic_lab.py +31 -1
  28. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_produce_styles.py +4 -14
  29. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_relations.py +10 -0
  30. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_tools.py +69 -1
  31. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_tracing.py +79 -0
  32. reactifact-0.6.1/tests/test_mcp.py +0 -169
  33. {reactifact-0.6.1 → reactifact-0.7.0}/LICENSE +0 -0
  34. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/__main__.py +0 -0
  35. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/_extras.py +0 -0
  36. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/agents.py +0 -0
  37. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/artifacts.py +0 -0
  38. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/budget.py +0 -0
  39. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/chat.py +0 -0
  40. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/checkpoints.py +0 -0
  41. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/__init__.py +0 -0
  42. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/branch.py +0 -0
  43. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/common.py +0 -0
  44. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/context.py +0 -0
  45. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/graph.py +0 -0
  46. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/replay.py +0 -0
  47. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/scenario.py +0 -0
  48. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/cli/trace.py +0 -0
  49. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/commit.py +0 -0
  50. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/commit_log.py +0 -0
  51. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/consume.py +0 -0
  52. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/context.py +0 -0
  53. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/eval.py +0 -0
  54. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/events.py +0 -0
  55. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/interrupt.py +0 -0
  56. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/mcp/server.py +0 -0
  57. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/operations.py +0 -0
  58. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/patches.py +0 -0
  59. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/prompts.py +0 -0
  60. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/__init__.py +0 -0
  61. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/_retry.py +0 -0
  62. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/anthropic.py +0 -0
  63. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/azure.py +0 -0
  64. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/cerebras.py +0 -0
  65. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/chat.py +0 -0
  66. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/contracts.py +0 -0
  67. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/deepseek.py +0 -0
  68. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/fake.py +0 -0
  69. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/fireworks.py +0 -0
  70. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/gemini.py +0 -0
  71. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/github_models.py +0 -0
  72. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/groq.py +0 -0
  73. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/image.py +0 -0
  74. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/mistral.py +0 -0
  75. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/nvidia.py +0 -0
  76. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/ollama.py +0 -0
  77. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/openai.py +0 -0
  78. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/openrouter.py +0 -0
  79. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/perplexity.py +0 -0
  80. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/qwen.py +0 -0
  81. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/speech.py +0 -0
  82. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/together.py +0 -0
  83. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/video.py +0 -0
  84. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/xai.py +0 -0
  85. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/providers/zai.py +0 -0
  86. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/py.typed +0 -0
  87. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/__init__.py +0 -0
  88. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/inputs.py +0 -0
  89. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/memory.py +0 -0
  90. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/resolve.py +0 -0
  91. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/rollback.py +0 -0
  92. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/search.py +0 -0
  93. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/skills.py +0 -0
  94. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/status.py +0 -0
  95. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/recipes/text.py +0 -0
  96. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/relations.py +0 -0
  97. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/replay.py +0 -0
  98. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/session.py +0 -0
  99. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/sources.py +0 -0
  100. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/streaming.py +0 -0
  101. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/structured.py +0 -0
  102. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/__init__.py +0 -0
  103. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/assertions.py +0 -0
  104. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/exceptions.py +0 -0
  105. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/fault.py +0 -0
  106. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/lab.py +0 -0
  107. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/mock.py +0 -0
  108. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/record.py +0 -0
  109. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/testing/registry.py +0 -0
  110. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tools.py +0 -0
  111. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/models.py +0 -0
  112. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/postgres.py +0 -0
  113. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/store.py +0 -0
  114. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/templates/ui.html +0 -0
  115. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/templates/ui_run.html +0 -0
  116. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/tracer.py +0 -0
  117. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/tracing/web.py +0 -0
  118. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/viz.py +0 -0
  119. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact/web.py +0 -0
  120. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact.egg-info/dependency_links.txt +0 -0
  121. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact.egg-info/entry_points.txt +0 -0
  122. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact.egg-info/requires.txt +0 -0
  123. {reactifact-0.6.1 → reactifact-0.7.0}/reactifact.egg-info/top_level.txt +0 -0
  124. {reactifact-0.6.1 → reactifact-0.7.0}/setup.cfg +0 -0
  125. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_anthropic_provider.py +0 -0
  126. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_artifacts.py +0 -0
  127. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_backbone.py +0 -0
  128. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_branching.py +0 -0
  129. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_budget.py +0 -0
  130. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_chat_web.py +0 -0
  131. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_checkpoint.py +0 -0
  132. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_checkpoints_concurrency.py +0 -0
  133. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_cli.py +0 -0
  134. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_cli_scenario.py +0 -0
  135. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_commit_log.py +0 -0
  136. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_concurrency.py +0 -0
  137. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_devops.py +0 -0
  138. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_devops_web.py +0 -0
  139. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_effects.py +0 -0
  140. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_eval.py +0 -0
  141. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_forklab.py +0 -0
  142. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_forklab_web.py +0 -0
  143. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_friendly_api.py +0 -0
  144. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_gemini_provider.py +0 -0
  145. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_hitl.py +0 -0
  146. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_image_provider.py +0 -0
  147. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_invalidation.py +0 -0
  148. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_knowledge.py +0 -0
  149. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_knowledge_web.py +0 -0
  150. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_llm_ladder.py +0 -0
  151. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_medic_lab_web.py +0 -0
  152. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_multisource.py +0 -0
  153. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_openai_provider.py +0 -0
  154. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_patches.py +0 -0
  155. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_ports.py +0 -0
  156. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_prompts.py +0 -0
  157. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_provider_auth.py +0 -0
  158. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_provider_retry.py +0 -0
  159. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_providers_integration.py +0 -0
  160. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_recipes.py +0 -0
  161. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_recipes_inputs.py +0 -0
  162. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_recipes_memory.py +0 -0
  163. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_recipes_skills.py +0 -0
  164. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_relation_graph.py +0 -0
  165. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_repair.py +0 -0
  166. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_repair_web.py +0 -0
  167. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_replay.py +0 -0
  168. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_research.py +0 -0
  169. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_resources.py +0 -0
  170. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_retry.py +0 -0
  171. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_runtime.py +0 -0
  172. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_runtime_errors.py +0 -0
  173. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_sessions.py +0 -0
  174. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_sources.py +0 -0
  175. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_sources_search.py +0 -0
  176. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_sources_vector.py +0 -0
  177. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_speech_provider.py +0 -0
  178. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_streaming.py +0 -0
  179. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_structured.py +0 -0
  180. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_testing_assertions.py +0 -0
  181. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_testing_lab.py +0 -0
  182. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_testing_mock.py +0 -0
  183. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_testing_registry.py +0 -0
  184. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_vendor_factories.py +0 -0
  185. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_vendor_multimodal_factories.py +0 -0
  186. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_video_provider.py +0 -0
  187. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_view.py +0 -0
  188. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_viz.py +0 -0
  189. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_web_source.py +0 -0
  190. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_workspace.py +0 -0
  191. {reactifact-0.6.1 → reactifact-0.7.0}/tests/test_workspace_with_sources.py +0 -0
  192. {reactifact-0.6.1 → reactifact-0.7.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.6.1
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
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/bzdvdn/reactifact
8
8
  Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
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
@@ -36,14 +36,18 @@ Provides-Extra: mcp
36
36
  Requires-Dist: mcp>=2.2; extra == "mcp"
37
37
  Dynamic: license-file
38
38
 
39
- # 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>
40
42
 
41
43
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
42
44
 
43
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)
44
47
  [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
45
48
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
46
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)
47
51
 
48
52
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
49
53
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -56,6 +60,11 @@ react to events — there is no graph, no node pipeline.
56
60
 
57
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)
58
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
+
59
68
  ```bash
60
69
  pip install reactifact
61
70
  ```
@@ -157,12 +166,33 @@ actual state.
157
166
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
158
167
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
159
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` |
160
172
 
161
173
  Reactive. Deterministic. Accountable.
162
174
 
163
175
  Full breakdown, including where reactifact is *not* the right choice:
164
176
  [docs/en/comparison.md](docs/en/comparison.md).
165
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
+
166
196
  ## Core primitives
167
197
 
168
198
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -170,7 +200,7 @@ Full breakdown, including where reactifact is *not* the right choice:
170
200
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
171
201
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
172
202
  - **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.
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).
174
204
  - **Provenance** — every derived artifact links to what produced it
175
205
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
176
206
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -211,7 +241,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
211
241
  ```
212
242
 
213
243
  Classic-pattern ports run as one-liners too:
214
- `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`.
215
245
 
216
246
  ## Examples (in-repo, not shipped)
217
247
 
@@ -224,7 +254,7 @@ Classic-pattern ports run as one-liners too:
224
254
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
225
255
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
226
256
  - `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)).
257
+ - `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
228
258
 
229
259
  ## Documentation
230
260
 
@@ -236,6 +266,7 @@ Classic-pattern ports run as one-liners too:
236
266
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
237
267
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
238
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.
239
270
 
240
271
  ## Development
241
272
 
@@ -1,11 +1,15 @@
1
- # 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>
2
4
 
3
5
  **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
4
6
 
5
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)
6
9
  [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
7
10
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
8
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)
9
13
 
10
14
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
11
15
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -18,6 +22,11 @@ react to events — there is no graph, no node pipeline.
18
22
 
19
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)
20
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
+
21
30
  ```bash
22
31
  pip install reactifact
23
32
  ```
@@ -119,12 +128,33 @@ actual state.
119
128
  | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
120
129
  | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
121
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` |
122
134
 
123
135
  Reactive. Deterministic. Accountable.
124
136
 
125
137
  Full breakdown, including where reactifact is *not* the right choice:
126
138
  [docs/en/comparison.md](docs/en/comparison.md).
127
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
+
128
158
  ## Core primitives
129
159
 
130
160
  - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
@@ -132,7 +162,7 @@ Full breakdown, including where reactifact is *not* the right choice:
132
162
  - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
133
163
  - **Patch** — the compiled, validated change-set applied as one atomic commit.
134
164
  - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
135
- - **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).
136
166
  - **Provenance** — every derived artifact links to what produced it
137
167
  (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
138
168
  - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
@@ -173,7 +203,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
173
203
  ```
174
204
 
175
205
  Classic-pattern ports run as one-liners too:
176
- `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`.
177
207
 
178
208
  ## Examples (in-repo, not shipped)
179
209
 
@@ -186,7 +216,7 @@ Classic-pattern ports run as one-liners too:
186
216
  - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
187
217
  - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
188
218
  - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
189
- - `{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)).
190
220
 
191
221
  ## Documentation
192
222
 
@@ -198,6 +228,7 @@ Classic-pattern ports run as one-liners too:
198
228
  - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
199
229
  - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
200
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.
201
232
 
202
233
  ## Development
203
234
 
@@ -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.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"
@@ -12,7 +12,7 @@ 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",
@@ -93,6 +93,15 @@ reactifact = ["py.typed"]
93
93
  [tool.pytest.ini_options]
94
94
  testpaths = ["tests"]
95
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
+
96
105
  [tool.ruff]
97
106
  target-version = "py311"
98
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.1"
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,
@@ -7,12 +7,15 @@ in the `mcp` SDK.
7
7
  from __future__ import annotations
8
8
 
9
9
  from .client import MCPTool, mcp_http_tools, mcp_stdio_tools, mcp_tools
10
+ from .oauth import InMemoryTokenStorage, oauth_client_credentials
10
11
  from .server import create_mcp_server
11
12
 
12
13
  __all__ = [
14
+ "InMemoryTokenStorage",
13
15
  "MCPTool",
14
16
  "create_mcp_server",
15
17
  "mcp_http_tools",
16
18
  "mcp_stdio_tools",
17
19
  "mcp_tools",
20
+ "oauth_client_credentials",
18
21
  ]
@@ -7,7 +7,7 @@ the `mcp` extra.
7
7
  from __future__ import annotations
8
8
 
9
9
  from collections.abc import AsyncGenerator
10
- from contextlib import asynccontextmanager
10
+ from contextlib import AsyncExitStack, asynccontextmanager
11
11
  from typing import Any
12
12
 
13
13
  from .._extras import require_extra
@@ -89,16 +89,45 @@ async def mcp_stdio_tools(
89
89
 
90
90
 
91
91
  @asynccontextmanager
92
- async def mcp_http_tools(url: str) -> AsyncGenerator[list[Tool], None]:
93
- """Connects to an MCP server over streamable HTTP and yields its tools as `Tool`s."""
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
+ """
94
114
  require_extra("mcp_http_tools", "mcp", "mcp")
115
+ import httpx2
95
116
  from mcp.client.streamable_http import streamable_http_client
96
117
 
97
118
  from mcp import ClientSession
98
119
 
99
- async with (
100
- streamable_http_client(url) as (read, write),
101
- ClientSession(read, write) as session,
102
- ):
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))
103
132
  await session.initialize()
104
133
  yield await mcp_tools(session)
@@ -0,0 +1,94 @@
1
+ """OAuth 2.1 for the MCP client: machine-to-machine auth (client_credentials)
2
+ for MCP servers that require it.
3
+
4
+ Scoped to `client_credentials` only — the flow that needs no browser or
5
+ human consent, which covers an agent authenticating to a server on its own
6
+ behalf. The authorization-code flow (a person granting consent through a
7
+ browser redirect) needs a `redirect_handler`/`callback_handler` wired to
8
+ whatever hosts the app (CLI, a script, a web app), which is host-specific
9
+ plumbing outside a headless library's scope; use
10
+ `mcp.client.auth.OAuthClientProvider` directly if you need it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Literal
16
+
17
+ from .._extras import require_extra
18
+
19
+
20
+ class InMemoryTokenStorage:
21
+ """The MCP SDK's `TokenStorage` protocol, held in process memory only.
22
+
23
+ Tokens (and refreshes) live only as long as this object — nothing is
24
+ written to disk. Fine for a long-running process reusing one provider
25
+ across calls; pass your own `TokenStorage` implementation instead if you
26
+ need tokens to survive a restart.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self._tokens: Any = None
31
+ self._client_info: Any = None
32
+
33
+ async def get_tokens(self) -> Any:
34
+ return self._tokens
35
+
36
+ async def set_tokens(self, tokens: Any) -> None:
37
+ self._tokens = tokens
38
+
39
+ async def get_client_info(self) -> Any:
40
+ return self._client_info
41
+
42
+ async def set_client_info(self, client_info: Any) -> None:
43
+ self._client_info = client_info
44
+
45
+
46
+ def oauth_client_credentials(
47
+ server_url: str,
48
+ *,
49
+ client_id: str,
50
+ client_secret: str,
51
+ issuer: str,
52
+ scope: str | None = None,
53
+ token_endpoint_auth_method: Literal[
54
+ "client_secret_basic", "client_secret_post"
55
+ ] = "client_secret_basic",
56
+ storage: Any = None,
57
+ ) -> Any:
58
+ """Builds an `httpx2.Auth` that authenticates to `server_url` via OAuth's
59
+ `client_credentials` grant — pass it as `auth=` to `mcp_http_tools`.
60
+
61
+ `issuer` names the authorization server `client_id`/`client_secret`
62
+ belong to: the token request is only ever built from that server's
63
+ discovered metadata, so a compromised or misconfigured MCP server can't
64
+ redirect the credential exchange elsewhere. Requires the `mcp` extra.
65
+
66
+ from reactifact.mcp import mcp_http_tools, oauth_client_credentials
67
+
68
+ auth = oauth_client_credentials(
69
+ "https://mcp.example.com",
70
+ client_id="...",
71
+ client_secret="...",
72
+ issuer="https://auth.example.com",
73
+ )
74
+ async with mcp_http_tools("https://mcp.example.com", auth=auth) as tools:
75
+ ...
76
+
77
+ `storage` defaults to a fresh `InMemoryTokenStorage()` (tokens live only
78
+ for this process) — pass your own `TokenStorage` to persist/reuse tokens
79
+ across runs.
80
+ """
81
+ require_extra("oauth_client_credentials", "mcp", "mcp")
82
+ from mcp.client.auth.extensions.client_credentials import (
83
+ ClientCredentialsOAuthProvider,
84
+ )
85
+
86
+ return ClientCredentialsOAuthProvider(
87
+ server_url,
88
+ storage=storage if storage is not None else InMemoryTokenStorage(),
89
+ client_id=client_id,
90
+ client_secret=client_secret,
91
+ token_endpoint_auth_method=token_endpoint_auth_method,
92
+ scope=scope,
93
+ issuer=issuer,
94
+ )
@@ -13,14 +13,8 @@ for one of these first:
13
13
  decorator, both signatures recognized by parameter name — pick this for a
14
14
  short, one-off produce with no class ceremony.
15
15
 
16
- Two other things `Produce` accepts are *not* on that list on purpose:
17
-
18
- - `Produce(Model, factory=fn)` — a bare two-argument-factory constructor
19
- kwarg, **deprecated** (raises `DeprecationWarning`). It predates the
20
- `@produce` decorator, only supports `(context, inputs[, event]) -> Model |
21
- list | Patch | None`, and can't see the effects slot at all — strictly
22
- weaker than `@produce(Model)`, which covers the same signature plus
23
- `effects`. Kept only so old code doesn't break; port it to `@produce`.
16
+ One other thing `Produce`/`Agent` accepts is *not* on that list on purpose:
17
+
24
18
  - Overriding `Agent.run(self, event, context) -> Patch | None` directly,
25
19
  bypassing `Produce` entirely (though not necessarily `effects` — the
26
20
  runtime still merges whatever `current_effects()` collected during the
@@ -36,7 +30,6 @@ from __future__ import annotations
36
30
 
37
31
  import asyncio
38
32
  import inspect
39
- import warnings
40
33
  from collections.abc import Callable
41
34
  from typing import Any, Generic, TypeVar, get_args, get_origin
42
35
 
@@ -71,43 +64,13 @@ class Produce(Generic[TOut]):
71
64
  return
72
65
 
73
66
  artifact_type: ArtifactType | None = None
74
- factory: Callable[..., Any] | None = None
75
67
 
76
- def __init__(
77
- self,
78
- artifact_type: ArtifactType | None = None,
79
- factory: Callable[..., Any] | None = None,
80
- ):
68
+ def __init__(self, artifact_type: ArtifactType | None = None):
81
69
  self.artifact_type = artifact_type or self.__class__.artifact_type
82
70
  if self.artifact_type is None:
83
71
  raise ValueError(
84
72
  "artifact_type must be provided either as class attribute or constructor argument"
85
73
  )
86
- self.factory = factory if factory is not None else self.__class__.factory
87
- if self.factory is not None:
88
- warnings.warn(
89
- "Produce(..., factory=...) is deprecated: it only supports "
90
- "(context, inputs[, event]) -> Model | list | Patch | None and "
91
- "can't see the effects slot (no `effects` param support). Use "
92
- "the @produce(Type) decorator instead — same return-style "
93
- "signature, plus optional effects/event params when you need "
94
- "them. See the reactifact.produce module docstring.",
95
- DeprecationWarning,
96
- stacklevel=2,
97
- )
98
- self._accepts_event = False
99
- if self.factory is not None:
100
- try:
101
- signature = inspect.signature(self.factory)
102
- except (TypeError, ValueError):
103
- signature = None
104
- if signature is not None:
105
- positional = [
106
- p
107
- for p in signature.parameters.values()
108
- if not p.kind & inspect.Parameter.VAR_POSITIONAL
109
- ]
110
- self._accepts_event = len(positional) >= 3
111
74
 
112
75
  @property
113
76
  def effects(self) -> Effects:
@@ -132,29 +95,22 @@ class Produce(Generic[TOut]):
132
95
  inputs: list[Artifact[Any]],
133
96
  event: Event | None = None,
134
97
  ) -> None:
135
- """Runs the factory and writes its effect into the slot (§24).
98
+ """No-op by default (§24).
136
99
 
137
- Subclass-style overrides write `self.effects.*` and return None;
138
- a `None` return means "no work". (The progress nest the old
139
- `Patch | None` returnthe runtime compiles the slot now.)
100
+ Subclass-style overrides write `self.effects.*` and return None; a
101
+ `None` return means "no work". A bare `Produce(Model)` (no override)
102
+ is a valid, deliberate no-op used e.g. to widen an agent's allowed
103
+ `Create` types when the actual write happens via `self.effects.ask(...)`
104
+ in another produce (see `examples/supervisor`).
140
105
  """
141
- if self.factory is None:
142
- return None
143
-
144
- if self._accepts_event:
145
- result = self.factory(context, inputs, event)
146
- else:
147
- result = self.factory(context, inputs)
148
- if asyncio.iscoroutine(result):
149
- result = await result
150
-
151
- self._apply_result(result)
106
+ return None
152
107
 
153
108
  def _apply_result(self, result: Any) -> None:
154
- """Writes a factory result into the effect slot (§24).
109
+ """Writes a `@produce`-decorated function's return-style result into
110
+ the effect slot (§24).
155
111
 
156
- `None` — nothing; a model or a list of models — creates; a Patch — the
157
- factory-level legacy escape (its operations are appended to the effects).
112
+ `None` — nothing; a model or a list of models — creates; a Patch — a
113
+ legacy escape (its operations are appended to the effects).
158
114
  """
159
115
  if result is None:
160
116
  return
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from .providers import EmbeddingProvider, LLMProvider
6
6
  from .sources import Source
7
7
 
8
+ if TYPE_CHECKING:
9
+ from .budget import Budget
10
+
8
11
 
9
12
  class RuntimeResources:
10
13
  def __init__(
@@ -18,6 +21,13 @@ class RuntimeResources:
18
21
  self.embedder = embedder
19
22
  self.sources = sources or {}
20
23
  self.additional = additional
24
+ # Set by Runtime per turn (not a constructor param — the runtime, not
25
+ # the caller, owns these): the active Budget and its wall-clock
26
+ # deadline, read back by ToolUse's own inner loop (§ tool_use.py) to
27
+ # enforce the tool-call/time budget between its own round-trips, not
28
+ # just at the top-level Runtime._budget_exhausted check.
29
+ self.budget: Budget | None = None
30
+ self.budget_deadline: float | None = None
21
31
 
22
32
  def get_source(self, source_id: str) -> Source | None:
23
33
  return self.sources.get(source_id)