reactifact 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. {reactifact-0.7.0/reactifact.egg-info → reactifact-0.8.0}/PKG-INFO +10 -3
  2. {reactifact-0.7.0 → reactifact-0.8.0}/README.md +2 -1
  3. {reactifact-0.7.0 → reactifact-0.8.0}/pyproject.toml +14 -2
  4. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/__init__.py +1 -1
  5. reactifact-0.8.0/reactifact/agent_tool.py +115 -0
  6. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/agents.py +11 -1
  7. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/branching.py +2 -0
  8. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/checkpoints.py +9 -2
  9. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/context.py +43 -3
  10. reactifact-0.8.0/reactifact/context_builder.py +157 -0
  11. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/llm_agent.py +2 -0
  12. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/__init__.py +18 -0
  13. reactifact-0.8.0/reactifact/recipes/plan_execute.py +207 -0
  14. reactifact-0.8.0/reactifact/recipes/reflection.py +262 -0
  15. reactifact-0.8.0/reactifact/recipes/supervisor.py +224 -0
  16. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/resources.py +14 -0
  17. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/runtime.py +58 -2
  18. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tool_use.py +129 -6
  19. reactifact-0.8.0/reactifact/verify.py +161 -0
  20. {reactifact-0.7.0 → reactifact-0.8.0/reactifact.egg-info}/PKG-INFO +10 -3
  21. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/SOURCES.txt +13 -0
  22. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/requires.txt +5 -0
  23. reactifact-0.8.0/tests/test_agent_tool.py +190 -0
  24. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_checkpoint.py +22 -0
  25. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_concurrency.py +56 -0
  26. reactifact-0.8.0/tests/test_context_builder.py +165 -0
  27. reactifact-0.8.0/tests/test_incident_commander.py +114 -0
  28. reactifact-0.8.0/tests/test_recipe_plan_execute.py +106 -0
  29. reactifact-0.8.0/tests/test_recipe_reflection.py +124 -0
  30. reactifact-0.8.0/tests/test_recipe_supervisor.py +162 -0
  31. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_tools.py +137 -0
  32. reactifact-0.8.0/tests/test_verify.py +129 -0
  33. {reactifact-0.7.0 → reactifact-0.8.0}/LICENSE +0 -0
  34. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/__main__.py +0 -0
  35. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/_extras.py +0 -0
  36. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/artifacts.py +0 -0
  37. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/budget.py +0 -0
  38. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/chat.py +0 -0
  39. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/__init__.py +0 -0
  40. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/branch.py +0 -0
  41. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/common.py +0 -0
  42. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/context.py +0 -0
  43. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/graph.py +0 -0
  44. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/replay.py +0 -0
  45. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/scenario.py +0 -0
  46. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/trace.py +0 -0
  47. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/commit.py +0 -0
  48. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/commit_log.py +0 -0
  49. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/consume.py +0 -0
  50. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/effects.py +0 -0
  51. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/eval.py +0 -0
  52. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/events.py +0 -0
  53. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/interrupt.py +0 -0
  54. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/__init__.py +0 -0
  55. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/client.py +0 -0
  56. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/oauth.py +0 -0
  57. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/server.py +0 -0
  58. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/operations.py +0 -0
  59. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/patches.py +0 -0
  60. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/produce.py +0 -0
  61. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/prompts.py +0 -0
  62. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/__init__.py +0 -0
  63. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/_retry.py +0 -0
  64. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/anthropic.py +0 -0
  65. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/azure.py +0 -0
  66. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/cerebras.py +0 -0
  67. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/chat.py +0 -0
  68. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/contracts.py +0 -0
  69. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/deepseek.py +0 -0
  70. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/fake.py +0 -0
  71. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/fireworks.py +0 -0
  72. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/gemini.py +0 -0
  73. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/github_models.py +0 -0
  74. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/groq.py +0 -0
  75. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/image.py +0 -0
  76. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/mistral.py +0 -0
  77. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/nvidia.py +0 -0
  78. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/ollama.py +0 -0
  79. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/openai.py +0 -0
  80. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/openrouter.py +0 -0
  81. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/perplexity.py +0 -0
  82. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/qwen.py +0 -0
  83. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/speech.py +0 -0
  84. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/together.py +0 -0
  85. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/video.py +0 -0
  86. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/xai.py +0 -0
  87. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/zai.py +0 -0
  88. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/py.typed +0 -0
  89. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/inputs.py +0 -0
  90. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/memory.py +0 -0
  91. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/resolve.py +0 -0
  92. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/rollback.py +0 -0
  93. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/search.py +0 -0
  94. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/skills.py +0 -0
  95. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/status.py +0 -0
  96. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/text.py +0 -0
  97. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/relations.py +0 -0
  98. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/replay.py +0 -0
  99. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/scheduler.py +0 -0
  100. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/session.py +0 -0
  101. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/sources.py +0 -0
  102. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/streaming.py +0 -0
  103. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/structured.py +0 -0
  104. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/__init__.py +0 -0
  105. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/assertions.py +0 -0
  106. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/exceptions.py +0 -0
  107. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/fault.py +0 -0
  108. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/lab.py +0 -0
  109. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/mock.py +0 -0
  110. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/record.py +0 -0
  111. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/registry.py +0 -0
  112. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tools.py +0 -0
  113. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/__init__.py +0 -0
  114. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/_otlp.py +0 -0
  115. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/langfuse.py +0 -0
  116. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/models.py +0 -0
  117. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/otlp.py +0 -0
  118. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/postgres.py +0 -0
  119. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/store.py +0 -0
  120. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/templates/ui.html +0 -0
  121. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/templates/ui_run.html +0 -0
  122. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/tracer.py +0 -0
  123. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/web.py +0 -0
  124. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/triggers.py +0 -0
  125. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/viz.py +0 -0
  126. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/web.py +0 -0
  127. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/dependency_links.txt +0 -0
  128. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/entry_points.txt +0 -0
  129. {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/top_level.txt +0 -0
  130. {reactifact-0.7.0 → reactifact-0.8.0}/setup.cfg +0 -0
  131. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_adaptive.py +0 -0
  132. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_anthropic_provider.py +0 -0
  133. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_artifacts.py +0 -0
  134. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_backbone.py +0 -0
  135. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_branching.py +0 -0
  136. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_budget.py +0 -0
  137. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_canonical_ports.py +0 -0
  138. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_chat_web.py +0 -0
  139. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_checkpoints_concurrency.py +0 -0
  140. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_cli.py +0 -0
  141. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_cli_scenario.py +0 -0
  142. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_commit_log.py +0 -0
  143. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_consumes_produces.py +0 -0
  144. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_devops.py +0 -0
  145. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_devops_web.py +0 -0
  146. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_effects.py +0 -0
  147. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_eval.py +0 -0
  148. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_forklab.py +0 -0
  149. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_forklab_web.py +0 -0
  150. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_friendly_api.py +0 -0
  151. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_gemini_provider.py +0 -0
  152. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_hitl.py +0 -0
  153. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_image_provider.py +0 -0
  154. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_invalidation.py +0 -0
  155. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_knowledge.py +0 -0
  156. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_knowledge_web.py +0 -0
  157. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_llm_ladder.py +0 -0
  158. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_mcp.py +0 -0
  159. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_medic_lab.py +0 -0
  160. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_medic_lab_web.py +0 -0
  161. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_multisource.py +0 -0
  162. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_openai_provider.py +0 -0
  163. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_patches.py +0 -0
  164. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_ports.py +0 -0
  165. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_produce_styles.py +0 -0
  166. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_prompts.py +0 -0
  167. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_provider_auth.py +0 -0
  168. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_provider_retry.py +0 -0
  169. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_providers_integration.py +0 -0
  170. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes.py +0 -0
  171. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_inputs.py +0 -0
  172. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_memory.py +0 -0
  173. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_skills.py +0 -0
  174. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_relation_graph.py +0 -0
  175. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_relations.py +0 -0
  176. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_repair.py +0 -0
  177. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_repair_web.py +0 -0
  178. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_replay.py +0 -0
  179. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_research.py +0 -0
  180. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_resources.py +0 -0
  181. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_retry.py +0 -0
  182. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_runtime.py +0 -0
  183. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_runtime_errors.py +0 -0
  184. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sessions.py +0 -0
  185. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources.py +0 -0
  186. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources_search.py +0 -0
  187. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources_vector.py +0 -0
  188. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_speech_provider.py +0 -0
  189. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_streaming.py +0 -0
  190. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_structured.py +0 -0
  191. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_assertions.py +0 -0
  192. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_lab.py +0 -0
  193. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_mock.py +0 -0
  194. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_registry.py +0 -0
  195. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_tracing.py +0 -0
  196. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_vendor_factories.py +0 -0
  197. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_vendor_multimodal_factories.py +0 -0
  198. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_video_provider.py +0 -0
  199. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_view.py +0 -0
  200. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_viz.py +0 -0
  201. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_web_source.py +0 -0
  202. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_workspace.py +0 -0
  203. {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_workspace_with_sources.py +0 -0
  204. {reactifact-0.7.0 → reactifact-0.8.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
7
7
  Project-URL: Repository, https://github.com/bzdvdn/reactifact
8
- Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
8
+ Project-URL: Documentation, https://bzdvdn.github.io/reactifact/
9
9
  Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
10
10
  Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
11
11
  Classifier: Development Status :: 4 - Beta
@@ -15,6 +15,8 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Typing :: Typed
19
21
  Requires-Python: >=3.11
20
22
  Description-Content-Type: text/markdown
@@ -34,6 +36,10 @@ Provides-Extra: pg
34
36
  Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
35
37
  Provides-Extra: mcp
36
38
  Requires-Dist: mcp>=2.2; extra == "mcp"
39
+ Provides-Extra: docs
40
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
41
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
42
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
37
43
  Dynamic: license-file
38
44
 
39
45
  <p align="center">
@@ -44,10 +50,11 @@ Dynamic: license-file
44
50
 
45
51
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
46
52
  [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
47
- [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
53
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/bzdvdn/reactifact)
48
54
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
49
55
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
50
56
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bzdvdn/reactifact)
57
+ [![Docs](https://img.shields.io/badge/docs-bzdvdn.github.io%2Freactifact-blue)](https://bzdvdn.github.io/reactifact/)
51
58
 
52
59
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
53
60
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -6,10 +6,11 @@
6
6
 
7
7
  [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
8
8
  [![codecov](https://codecov.io/gh/bzdvdn/reactifact/graph/badge.svg)](https://codecov.io/gh/bzdvdn/reactifact)
9
- [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
9
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/bzdvdn/reactifact)
10
10
  [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
12
12
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bzdvdn/reactifact)
13
+ [![Docs](https://img.shields.io/badge/docs-bzdvdn.github.io%2Freactifact-blue)](https://bzdvdn.github.io/reactifact/)
13
14
 
14
15
  Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
15
16
  declare control flow. But a knowledge question — *"why did infra costs jump in
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -19,6 +19,8 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3.11",
21
21
  "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
22
24
  "Typing :: Typed",
23
25
  ]
24
26
  dependencies = [
@@ -30,7 +32,7 @@ dependencies = [
30
32
  [project.urls]
31
33
  Homepage = "https://github.com/bzdvdn/reactifact"
32
34
  Repository = "https://github.com/bzdvdn/reactifact"
33
- Documentation = "https://github.com/bzdvdn/reactifact/tree/master/docs"
35
+ Documentation = "https://bzdvdn.github.io/reactifact/"
34
36
  Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
35
37
 
36
38
  [project.optional-dependencies]
@@ -50,6 +52,11 @@ pg = [
50
52
  mcp = [
51
53
  "mcp>=2.2",
52
54
  ]
55
+ docs = [
56
+ "mkdocs>=1.6",
57
+ "mkdocs-material>=9.5",
58
+ "mkdocstrings[python]>=0.27",
59
+ ]
53
60
 
54
61
  # uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
55
62
  # deps by default (no `--extra` flag needed to boot the examples). Extras stay
@@ -74,6 +81,11 @@ pg = [
74
81
  mcp = [
75
82
  "mcp>=2.2",
76
83
  ]
84
+ docs = [
85
+ "mkdocs>=1.6",
86
+ "mkdocs-material>=9.5",
87
+ "mkdocstrings[python]>=0.27",
88
+ ]
77
89
 
78
90
  [tool.uv]
79
91
  default-groups = ["dev", "web", "mcp"]
@@ -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.7.0"
51
+ __version__ = "0.8.0"
52
52
 
53
53
  __all__ = [
54
54
  "Agent",
@@ -0,0 +1,115 @@
1
+ """reactifact.agent_tool — a sub-agent as a `Tool` (delegation, §46).
2
+
3
+ `AgentAsTool` lets an `LLMAgent`/`HITLLMAgent` loop call another agent as an
4
+ ordinary tool: "delegate to X" runs a fresh, isolated nested `Runtime` to
5
+ completion and returns its final answer as the tool result. The delegating
6
+ loop sees only the final text — not the sub-agent's own tool calls, retries,
7
+ or internal reasoning.
8
+
9
+ Two deliberate constraints, not omissions:
10
+
11
+ - **Isolation is a fresh `Context`, not `context.branch()`.** The sub-agent
12
+ sees only the `query` it's given, nothing of the parent's history — no
13
+ prompt bloat from an unrelated conversation, and no accidental fork/merge
14
+ semantics (`branch()` exists for alternative-state exploration meant to be
15
+ merged back later, §39/§40; a one-shot delegate-and-discard call needs
16
+ neither). If a sub-agent genuinely needs to see the parent's state, build
17
+ its `Context` yourself and don't use this tool.
18
+ - **HITL sub-agents are rejected, not silently broken.** `Tool.execute` has
19
+ no channel back to a human — nothing would ever answer a `PendingQuestion`
20
+ the sub-agent raises inside its own isolated `Context`, and the nested run
21
+ would just finish with no `ToolAnswer`. `execute()` checks for this and
22
+ returns a `ToolOutput(error=...)` naming the unanswered question, instead
23
+ of returning empty text with no explanation.
24
+
25
+ Because `Tool.execute` receives only `args` (no `context`, see `tools.py`),
26
+ the sub-agent's `LLMProvider`/budget cannot be inherited implicitly from
27
+ whatever loop is calling this tool — pass them explicitly via `resources=`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections.abc import Callable
33
+ from typing import Any
34
+
35
+ from pydantic import BaseModel
36
+
37
+ from .agents import Agent
38
+ from .budget import Budget
39
+ from .context import Context
40
+ from .resources import RuntimeResources
41
+ from .runtime import Runtime
42
+ from .tool_use import ToolAnswer
43
+ from .tools import Tool, ToolOutput
44
+
45
+
46
+ class SubTask(BaseModel):
47
+ """Default input artifact for `AgentAsTool` — override via `input_type`
48
+ with any model that has a `text: str` field (the convention `ToolUse`'s
49
+ own `_goal` reads, `tool_use.py`)."""
50
+
51
+ text: str
52
+
53
+
54
+ class AgentAsTool(Tool):
55
+ """Wraps `agent_factory()` as a callable tool (see module docstring)."""
56
+
57
+ def __init__(
58
+ self,
59
+ *,
60
+ name: str,
61
+ description: str,
62
+ agent_factory: Callable[[], Agent],
63
+ resources: RuntimeResources,
64
+ input_type: type[BaseModel] = SubTask,
65
+ output_type: type[BaseModel] | None = None,
66
+ max_runs: int = 20,
67
+ destructive: bool = False,
68
+ ):
69
+ self.name = name
70
+ self.description = description
71
+ self.agent_factory = agent_factory
72
+ self.resources = resources
73
+ self.input_type = input_type
74
+ self.output_type = output_type or ToolAnswer
75
+ self.max_runs = max_runs
76
+ self.destructive = destructive
77
+ self.schema = {
78
+ "type": "object",
79
+ "properties": {
80
+ "query": {
81
+ "type": "string",
82
+ "description": "The task/question to delegate to the sub-agent.",
83
+ }
84
+ },
85
+ "required": ["query"],
86
+ }
87
+
88
+ async def execute(self, args: dict[str, Any]) -> ToolOutput:
89
+ query = str(args.get("query", ""))
90
+ sub_context = Context(resources=self.resources)
91
+ sub_agent = self.agent_factory()
92
+ sub_runtime = Runtime(
93
+ sub_context, agents=[sub_agent], budget=Budget(max_runs=self.max_runs)
94
+ )
95
+ sub_context.create(self.input_type(text=query))
96
+ await sub_runtime.arun()
97
+
98
+ if sub_context.has_pending_question():
99
+ pending = sub_context.latest_pending_question()
100
+ question = pending.data.question if pending is not None else ""
101
+ return ToolOutput(
102
+ error=(
103
+ f"sub-agent '{sub_agent.name}' needs clarification it wasn't "
104
+ f"given and can't ask for: {question!r}. Give '{self.name}' a "
105
+ "more complete query and try again."
106
+ )
107
+ )
108
+
109
+ outputs = sub_context.list_artifacts(self.output_type)
110
+ if not outputs:
111
+ return ToolOutput(
112
+ error=f"sub-agent '{sub_agent.name}' produced no {self.output_type.__name__}"
113
+ )
114
+ text = getattr(outputs[-1].data, "text", "") or ""
115
+ return ToolOutput(text=text)
@@ -90,7 +90,14 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
90
90
  return self._collect_inputs(context)
91
91
 
92
92
  def _collect_inputs(self, context: Context) -> list[Artifact[Any]]:
93
- """Collects all artifacts matching consumes and conditions."""
93
+ """Collects all artifacts matching consumes and conditions.
94
+
95
+ Ranking/truncation, if any, is a `Runtime`-level policy
96
+ (`context.resources.context_builder`, see `context_builder.py`), not
97
+ this agent's — applied here so both this and `collect_inputs()`
98
+ (used by the runtime for provenance) see the identical, already
99
+ built list.
100
+ """
94
101
  if not self.consumes:
95
102
  return []
96
103
  inputs: list[Artifact[Any]] = []
@@ -99,6 +106,9 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
99
106
  if c.condition:
100
107
  artifacts = [a for a in artifacts if c.condition(a)]
101
108
  inputs.extend(artifacts)
109
+ builder = context.resources.context_builder
110
+ if builder is not None:
111
+ inputs = builder.build(context, self, inputs)
102
112
  return inputs
103
113
 
104
114
  async def run(self, event: Event, context: Context) -> Patch | None:
@@ -63,6 +63,7 @@ def clone_context(source: Context) -> Context:
63
63
  new_ws._log = source._log.copy()
64
64
  new_ws._relations = source._relations.copy()
65
65
  new_ws._recompute_stale()
66
+ new_ws._reindex_by_type()
66
67
  return new_ws
67
68
 
68
69
 
@@ -209,6 +210,7 @@ def merge_contexts(
209
210
  # `delete()` — resync `_stale` from scratch rather than risk it
210
211
  # drifting from the post-merge state.
211
212
  target._recompute_stale()
213
+ target._reindex_by_type()
212
214
 
213
215
 
214
216
  class BranchStore:
@@ -112,11 +112,18 @@ class FileBackend(CheckpointBackend):
112
112
  """File backend: state is stored in a single JSON file."""
113
113
 
114
114
  def __init__(self, path: str):
115
- self.path = path
115
+ self.path = Path(path)
116
116
 
117
117
  def _save_sync(self, data: dict[str, Any]) -> None:
118
- with open(self.path, "w", encoding="utf-8") as f:
118
+ # Atomic write (tmp + rename), same reasoning as `FileKVBackend._set_sync`:
119
+ # a direct write left a truncated/corrupt file on a process kill mid-write
120
+ # (OOM, deploy, kill -9) — the next `load()` would raise JSONDecodeError
121
+ # and the session would be unrecoverable.
122
+ self.path.parent.mkdir(parents=True, exist_ok=True)
123
+ tmp = self.path.with_suffix(self.path.suffix + ".tmp")
124
+ with open(tmp, "w", encoding="utf-8") as f:
119
125
  json.dump(data, f, indent=2)
126
+ tmp.replace(self.path)
120
127
 
121
128
  async def save(self, data: dict[str, Any]) -> None:
122
129
  await asyncio.to_thread(self._save_sync, data)
@@ -81,6 +81,16 @@ class Context:
81
81
  # version. Kept up to date by `update()`/`log_commit()` so
82
82
  # `stale_artifacts()`/`has_stale()` never rescan the whole context.
83
83
  self._stale: set[str] = set()
84
+ # Incrementally maintained: exact `type(data)` -> ids of that exact
85
+ # type. Kept up to date by `create()`/`update()`/`delete()` so
86
+ # `list_artifacts(T)` doesn't call `isinstance()` per artifact — it
87
+ # unions the ids of every *distinct type ever created* that is an
88
+ # `issubclass` of `T` (a small set — bounded by distinct types, not
89
+ # artifact count) into an O(1) membership test, still walked in
90
+ # insertion order to preserve tie-break order in callers' own sorts.
91
+ # Any bulk rewrite that bypasses create/update/delete must call
92
+ # `_reindex_by_type()` (mirrors `_recompute_stale()`, same reasoning).
93
+ self._by_type: dict[type, set[str]] = {}
84
94
 
85
95
  # ---- announce: agent progress events streamed out ----
86
96
 
@@ -116,6 +126,7 @@ class Context:
116
126
  return self._artifacts[id]
117
127
  artifact = Artifact(data=data, id=id)
118
128
  self._artifacts[artifact.id] = artifact
129
+ self._by_type.setdefault(type(data), set()).add(artifact.id)
119
130
  self._events.append(
120
131
  Event(
121
132
  type=EventType.ARTIFACT_CREATED,
@@ -140,7 +151,12 @@ class Context:
140
151
  return None
141
152
  if artifact.data == new_data:
142
153
  return artifact
154
+ old_type = type(artifact.data)
143
155
  artifact.update(new_data)
156
+ new_type = type(new_data)
157
+ if new_type is not old_type:
158
+ self._by_type.get(old_type, set()).discard(artifact_id)
159
+ self._by_type.setdefault(new_type, set()).add(artifact_id)
144
160
  self._events.append(
145
161
  Event(
146
162
  type=EventType.ARTIFACT_UPDATED,
@@ -165,6 +181,7 @@ class Context:
165
181
  if artifact is None:
166
182
  return False
167
183
  self._stale.discard(artifact_id)
184
+ self._by_type.get(type(artifact.data), set()).discard(artifact_id)
168
185
  self._events.append(
169
186
  Event(
170
187
  type=EventType.ARTIFACT_DELETED,
@@ -185,13 +202,22 @@ class Context:
185
202
  def list_artifacts(
186
203
  self, artifact_type: type[TArtifact] | None = None
187
204
  ) -> list[Artifact[Any]]:
188
- """Returns a list of artifacts, optionally filtered by data type."""
205
+ """Returns a list of artifacts, optionally filtered by data type.
206
+
207
+ Preserves insertion order (matters: ties in a caller's own sort key,
208
+ e.g. `updated_at`, break in creation order, same as before this
209
+ method stopped `isinstance`-scanning every artifact).
210
+ """
189
211
  if artifact_type is None:
190
212
  return list(self._artifacts.values())
213
+ matching_ids: set[str] = set()
214
+ for t, ids in self._by_type.items():
215
+ if issubclass(t, artifact_type):
216
+ matching_ids |= ids
191
217
  return [
192
218
  cast(Artifact[TArtifact], a)
193
- for a in self._artifacts.values()
194
- if isinstance(a.data, artifact_type)
219
+ for aid, a in self._artifacts.items()
220
+ if aid in matching_ids
195
221
  ]
196
222
 
197
223
  def latest(self, artifact_type: type[TArtifact]) -> Artifact[TArtifact] | None:
@@ -493,6 +519,18 @@ class Context:
493
519
  break
494
520
  self._stale = stale
495
521
 
522
+ def _reindex_by_type(self) -> None:
523
+ """Full rebuild of `_by_type` from current artifacts.
524
+
525
+ Same reasoning as `_recompute_stale()`: only needed after a bulk
526
+ rewrite that bypasses `create`/`update`/`delete` — call it alongside
527
+ `_recompute_stale()` at every such site, never on its own.
528
+ """
529
+ by_type: dict[type, set[str]] = {}
530
+ for aid, artifact in self._artifacts.items():
531
+ by_type.setdefault(type(artifact.data), set()).add(aid)
532
+ self._by_type = by_type
533
+
496
534
  def _rebuild_artifacts_from_commits(
497
535
  self, upto_version: int
498
536
  ) -> dict[str, Artifact[Any]]:
@@ -534,6 +572,7 @@ class Context:
534
572
  self._events = []
535
573
  self._log.truncate(version)
536
574
  self._recompute_stale()
575
+ self._reindex_by_type()
537
576
 
538
577
  def to_dict(self) -> dict[str, Any]:
539
578
  return {
@@ -559,6 +598,7 @@ class Context:
559
598
  ws._fork_name = d.get("fork_name", "")
560
599
  ws._base = Context.from_dict(d["base"]) if d.get("base") is not None else None
561
600
  ws._recompute_stale()
601
+ ws._reindex_by_type()
562
602
  return ws
563
603
 
564
604
  async def save_checkpoint(self, backend_or_path: str | CheckpointBackend) -> None:
@@ -0,0 +1,157 @@
1
+ """reactifact.context_builder — Runtime-level policy for what goes into an
2
+ agent's inputs (ranking/truncation), separate from the agent itself.
3
+
4
+ An `Agent` only declares *which* artifact types/conditions it consumes
5
+ (`Consume`); it says nothing about *how many* or *which* matching artifacts
6
+ actually go into a produce call when there are more candidates than fit a
7
+ prompt. That decision is a `Runtime`-level concern — set it once via
8
+ `RuntimeResources(context_builder=...)` and every agent's
9
+ `collect_inputs()` (`Agent._collect_inputs`, `agents.py`) routes through it,
10
+ so a builder set on the `Context` a `Runtime` was constructed with applies
11
+ uniformly, without each `Agent` subclass reimplementing ranking/truncation.
12
+
13
+ Two implementations ship here: `DefaultContextBuilder` (no-op — today's
14
+ "every matching artifact, unranked" behavior) and `TokenBudgetContextBuilder`
15
+ (rank candidates, keep a prefix that fits a token budget).
16
+
17
+ Known limitation (not solved here): `build()` sees the *combined* candidate
18
+ list across every `Consume` an agent declares, not each `Consume` separately.
19
+ A high-volume type (e.g. many `Evidence`) can starve out a low-volume one
20
+ (e.g. the single triggering `Question`) under a tight budget if the
21
+ low-volume artifact ranks lower (e.g. it's older). If that bites, pass a
22
+ `rank_key` that accounts for it, or budget per `Consume` with several
23
+ smaller agents instead of one broad one.
24
+
25
+ One specific case of that limitation *is* solved: an agent often consumes
26
+ one real content type (`Evidence`) plus a marker type it only needs for
27
+ triggering (a completion/wake-up artifact with little or no meaningful
28
+ text) — see `TokenBudgetContextBuilder`'s `exempt_types`.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from abc import ABC, abstractmethod
34
+ from collections.abc import Callable
35
+ from typing import TYPE_CHECKING, Any, Protocol
36
+
37
+ from .artifacts import Artifact
38
+
39
+ if TYPE_CHECKING:
40
+ from .agents import Agent
41
+ from .context import Context
42
+
43
+
44
+ class TokenCounter(Protocol):
45
+ """Estimates how many tokens a piece of text costs.
46
+
47
+ Exactness is not the contract — see `HeuristicTokenCounter`. Plug in a
48
+ real tokenizer (tiktoken, a provider's count-tokens endpoint, ...) via
49
+ this protocol when you need precision for a specific provider.
50
+ """
51
+
52
+ def count(self, text: str) -> int: ...
53
+
54
+
55
+ class HeuristicTokenCounter:
56
+ """Provider-agnostic token estimate: zero dependencies, deliberately
57
+ conservative (overestimates rather than underestimates).
58
+
59
+ ~3.5 chars/token instead of the commonly quoted ~4: a budget built on
60
+ this errs toward trimming a bit more context, not toward overflowing a
61
+ provider's window. Good enough for a soft truncation threshold; not a
62
+ substitute for the real `usage` a provider returns after the call
63
+ (`LLMResponse.usage`, `providers/contracts.py`).
64
+ """
65
+
66
+ chars_per_token: float = 3.5
67
+
68
+ def count(self, text: str) -> int:
69
+ if not text:
70
+ return 0
71
+ return max(1, int(len(text) / self.chars_per_token) + 1)
72
+
73
+
74
+ def _default_render(artifact: Artifact[Any]) -> str:
75
+ return str(artifact.data.model_dump_json())
76
+
77
+
78
+ class ContextBuilder(ABC):
79
+ """Ranks/truncates the artifacts a single agent run would otherwise see.
80
+
81
+ Called once per agent run with every candidate matched by that agent's
82
+ `consumes` (combined across all of them). Returning the list unchanged
83
+ reproduces today's behavior.
84
+ """
85
+
86
+ @abstractmethod
87
+ def build(
88
+ self,
89
+ context: Context,
90
+ agent: Agent,
91
+ candidates: list[Artifact[Any]],
92
+ ) -> list[Artifact[Any]]: ...
93
+
94
+
95
+ class DefaultContextBuilder(ContextBuilder):
96
+ """No-op: every consumes-matched artifact, in collection order."""
97
+
98
+ def build(
99
+ self, context: Context, agent: Agent, candidates: list[Artifact[Any]]
100
+ ) -> list[Artifact[Any]]:
101
+ return candidates
102
+
103
+
104
+ class TokenBudgetContextBuilder(ContextBuilder):
105
+ """Ranks candidates (newest-first by default) and keeps a prefix that
106
+ fits `max_tokens`, estimated via `token_counter`.
107
+
108
+ At least one *costed* (non-exempt) artifact is always kept, even if it
109
+ alone exceeds the budget — an agent silently getting zero real content
110
+ is worse than one that gets an oversized item; `max_tokens` is a soft
111
+ cap, not a hard clip.
112
+
113
+ `exempt_types`: artifact types that cost nothing and never trigger that
114
+ clip — for a marker/trigger type an agent only consumes to wake up
115
+ (little or no meaningful text), not to reason over. Without this, a
116
+ freshly created marker can rank first (newest) and either eat the whole
117
+ budget itself or — worse — silently consume the "at least one" slot,
118
+ leaving zero real content if the budget is then too tight for the next
119
+ (real) candidate. Exempt artifacts are still ranked and still kept
120
+ (still wake the agent up); they just never count toward `max_tokens` or
121
+ toward what counts as "at least one" content item.
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ *,
127
+ max_tokens: int | None = None,
128
+ token_counter: TokenCounter | None = None,
129
+ rank_key: Callable[[Artifact[Any]], Any] | None = None,
130
+ render: Callable[[Artifact[Any]], str] | None = None,
131
+ exempt_types: tuple[type, ...] = (),
132
+ ):
133
+ self.max_tokens = max_tokens
134
+ self.token_counter = token_counter or HeuristicTokenCounter()
135
+ self.rank_key = rank_key or (lambda a: a.updated_at)
136
+ self.render = render or _default_render
137
+ self.exempt_types = exempt_types
138
+
139
+ def build(
140
+ self, context: Context, agent: Agent, candidates: list[Artifact[Any]]
141
+ ) -> list[Artifact[Any]]:
142
+ ranked = sorted(candidates, key=self.rank_key, reverse=True)
143
+ if self.max_tokens is None:
144
+ return ranked
145
+ kept: list[Artifact[Any]] = []
146
+ used = 0
147
+ kept_content = False
148
+ for artifact in ranked:
149
+ exempt = type(artifact.data) in self.exempt_types
150
+ cost = 0 if exempt else self.token_counter.count(self.render(artifact))
151
+ if kept_content and used + cost > self.max_tokens:
152
+ break
153
+ kept.append(artifact)
154
+ used += cost
155
+ if not exempt:
156
+ kept_content = True
157
+ return kept
@@ -137,6 +137,7 @@ class HITLLMAgent(Agent):
137
137
  tools: Sequence[Tool] | dict[str, Tool] = ()
138
138
  max_steps: int = 8
139
139
  max_asks: int = 2
140
+ max_approvals: int = 3
140
141
  resume_announce: Callable[[str], str] | None = None
141
142
  temperature: float | None = None
142
143
  max_tokens: int | None = None
@@ -162,6 +163,7 @@ class HITLLMAgent(Agent):
162
163
  tools=self.tools,
163
164
  max_steps=self.max_steps,
164
165
  max_asks=self.max_asks,
166
+ max_approvals=self.max_approvals,
165
167
  resume_announce=self.resume_announce,
166
168
  temperature=self.temperature,
167
169
  max_tokens=self.max_tokens,
@@ -13,6 +13,17 @@ keep reappearing across agent codebases and the bundled examples:
13
13
  - `StatusMachine` — a `Produce` that deterministically advances an artifact's
14
14
  `status` lifecycle driven by a pure `next_status(context, key)` (§67, §69) —
15
15
  see `recipes.status`;
16
+ - `PlanExecute` — sequential plan → execute → finish: ordering, gating each
17
+ step on its predecessor's result, and completion detection owned by the
18
+ recipe; domain supplies `plan`/`execute_step`/`finish` (§24, §42, §69) —
19
+ see `recipes.plan_execute`;
20
+ - `Router` / `ApprovalGate` — classify a request into a route with a
21
+ deterministic fallback, and ask a human to sign off on a report before
22
+ finalizing it (§60, §67) — see `recipes.supervisor`;
23
+ - `ReflectionLoop` — generate → critique → regenerate: round-capping, the
24
+ accept threshold, and completion detection owned by the recipe; domain
25
+ supplies `draft`/`critique`/`rewrite`/`finish` (§24, §42, §69) — see
26
+ `recipes.reflection`;
16
27
  - `WindowSummarizer` / `WindowPruner` / `llm_summarizer` — bounded
17
28
  conversation memory: periodic summarization + pruning as two plain
18
29
  `Produce`s, domain owns the summarizer callback and the summary artifact
@@ -34,15 +45,22 @@ from __future__ import annotations
34
45
 
35
46
  from .inputs import find, find_all
36
47
  from .memory import WindowPruner, WindowSummarizer, llm_summarizer
48
+ from .plan_execute import PlanExecute
49
+ from .reflection import ReflectionLoop
37
50
  from .resolve import materialize_doc
38
51
  from .rollback import changed_fields, downstream_fields, earliest_stage
39
52
  from .search import fan_out_sources
40
53
  from .skills import Skill, load_skills, match_skills
41
54
  from .status import StatusMachine
55
+ from .supervisor import ApprovalGate, Router
42
56
  from .text import EN_STOPWORDS, keyword_score, stem, stem_words
43
57
 
44
58
  __all__ = [
59
+ "ApprovalGate",
45
60
  "EN_STOPWORDS",
61
+ "PlanExecute",
62
+ "ReflectionLoop",
63
+ "Router",
46
64
  "Skill",
47
65
  "StatusMachine",
48
66
  "WindowPruner",