reactifact 0.6.1__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reactifact-0.6.1 → reactifact-0.8.0}/PKG-INFO +46 -8
- reactifact-0.6.1/reactifact.egg-info/PKG-INFO → reactifact-0.8.0/README.md +37 -43
- {reactifact-0.6.1 → reactifact-0.8.0}/pyproject.toml +24 -3
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/__init__.py +1 -1
- reactifact-0.8.0/reactifact/agent_tool.py +115 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/agents.py +11 -1
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/branching.py +10 -9
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/checkpoints.py +9 -2
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/context.py +43 -3
- reactifact-0.8.0/reactifact/context_builder.py +157 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/effects.py +14 -4
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/llm_agent.py +5 -1
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/__init__.py +3 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/client.py +36 -7
- reactifact-0.8.0/reactifact/mcp/oauth.py +94 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/produce.py +14 -58
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/__init__.py +18 -0
- reactifact-0.8.0/reactifact/recipes/plan_execute.py +207 -0
- reactifact-0.8.0/reactifact/recipes/reflection.py +262 -0
- reactifact-0.8.0/reactifact/recipes/supervisor.py +224 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/resources.py +25 -1
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/runtime.py +60 -4
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/scheduler.py +31 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tool_use.py +254 -17
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/__init__.py +2 -0
- reactifact-0.8.0/reactifact/tracing/_otlp.py +59 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/langfuse.py +5 -38
- reactifact-0.8.0/reactifact/tracing/otlp.py +169 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/triggers.py +5 -5
- reactifact-0.8.0/reactifact/verify.py +161 -0
- reactifact-0.6.1/README.md → reactifact-0.8.0/reactifact.egg-info/PKG-INFO +81 -5
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/SOURCES.txt +17 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/requires.txt +5 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_adaptive.py +88 -1
- reactifact-0.8.0/tests/test_agent_tool.py +190 -0
- reactifact-0.8.0/tests/test_canonical_ports.py +65 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_checkpoint.py +22 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_concurrency.py +56 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_consumes_produces.py +5 -20
- reactifact-0.8.0/tests/test_context_builder.py +165 -0
- reactifact-0.8.0/tests/test_incident_commander.py +114 -0
- reactifact-0.8.0/tests/test_mcp.py +348 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_medic_lab.py +31 -1
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_produce_styles.py +4 -14
- reactifact-0.8.0/tests/test_recipe_plan_execute.py +106 -0
- reactifact-0.8.0/tests/test_recipe_reflection.py +124 -0
- reactifact-0.8.0/tests/test_recipe_supervisor.py +162 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_relations.py +10 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_tools.py +206 -1
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_tracing.py +79 -0
- reactifact-0.8.0/tests/test_verify.py +129 -0
- reactifact-0.6.1/tests/test_mcp.py +0 -169
- {reactifact-0.6.1 → reactifact-0.8.0}/LICENSE +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/__main__.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/_extras.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/budget.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/chat.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/commit.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/consume.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/eval.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/events.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/operations.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/patches.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/prompts.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/chat.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/py.typed +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/relations.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/replay.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/session.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/sources.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/streaming.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/structured.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/fault.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/lab.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/mock.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tools.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/models.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/store.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/templates/ui_run.html +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/tracer.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/tracing/web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/viz.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact/web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/setup.cfg +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_backbone.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_branching.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_budget.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_chat_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_cli.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_devops.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_devops_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_effects.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_eval.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_forklab.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_hitl.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_multisource.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_patches.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_ports.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_prompts.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_repair.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_replay.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_research.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_resources.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_retry.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_runtime.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sessions.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_streaming.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_structured.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_lab.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_mock.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_view.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_viz.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_web_source.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_workspace.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.6.1 → reactifact-0.8.0}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.
|
|
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.
|
|
8
|
+
Project-URL: Documentation, https://bzdvdn.github.io/reactifact/
|
|
9
9
|
Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
|
|
10
10
|
Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
20
|
Classifier: Typing :: Typed
|
|
19
21
|
Requires-Python: >=3.11
|
|
20
22
|
Description-Content-Type: text/markdown
|
|
@@ -34,16 +36,25 @@ Provides-Extra: pg
|
|
|
34
36
|
Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
|
|
35
37
|
Provides-Extra: mcp
|
|
36
38
|
Requires-Dist: mcp>=2.2; extra == "mcp"
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: mkdocs>=1.6; extra == "docs"
|
|
41
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
42
|
+
Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
|
|
37
43
|
Dynamic: license-file
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
47
|
+
</p>
|
|
40
48
|
|
|
41
49
|
**Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
|
|
42
50
|
|
|
43
51
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
44
|
-
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
53
|
+
[](https://github.com/bzdvdn/reactifact)
|
|
45
54
|
[](https://pypi.org/project/reactifact/)
|
|
46
55
|
[](LICENSE)
|
|
56
|
+
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
57
|
+
[](https://bzdvdn.github.io/reactifact/)
|
|
47
58
|
|
|
48
59
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
49
60
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -56,6 +67,11 @@ react to events — there is no graph, no node pipeline.
|
|
|
56
67
|
|
|
57
68
|

|
|
58
69
|
|
|
70
|
+
Two agents explore independently on their own forks and merge back automatically
|
|
71
|
+
— and when they disagree, reactifact refuses to merge silently:
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
59
75
|
```bash
|
|
60
76
|
pip install reactifact
|
|
61
77
|
```
|
|
@@ -157,12 +173,33 @@ actual state.
|
|
|
157
173
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
158
174
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
159
175
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
176
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
177
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
178
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
160
179
|
|
|
161
180
|
Reactive. Deterministic. Accountable.
|
|
162
181
|
|
|
163
182
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
164
183
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
165
184
|
|
|
185
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
186
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
187
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
191
|
+
>>> LaborCost or Discount ever consumed.
|
|
192
|
+
|
|
193
|
+
LaborCost value=500.0 version=0 # untouched
|
|
194
|
+
Tax value=60.0 version=1 # recomputed
|
|
195
|
+
Discount value=25.0 version=0 # untouched
|
|
196
|
+
Total value=535.0 version=1 # recomputed
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
200
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
201
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
202
|
+
|
|
166
203
|
## Core primitives
|
|
167
204
|
|
|
168
205
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -170,7 +207,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
170
207
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
171
208
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
172
209
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
173
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
210
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
174
211
|
- **Provenance** — every derived artifact links to what produced it
|
|
175
212
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
176
213
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -211,7 +248,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
211
248
|
```
|
|
212
249
|
|
|
213
250
|
Classic-pattern ports run as one-liners too:
|
|
214
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
251
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
215
252
|
|
|
216
253
|
## Examples (in-repo, not shipped)
|
|
217
254
|
|
|
@@ -224,7 +261,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
224
261
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
225
262
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
226
263
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
227
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
264
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
228
265
|
|
|
229
266
|
## Documentation
|
|
230
267
|
|
|
@@ -236,6 +273,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
236
273
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
237
274
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
238
275
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
276
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
239
277
|
|
|
240
278
|
## Development
|
|
241
279
|
|
|
@@ -1,49 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://github.com/bzdvdn/reactifact
|
|
7
|
-
Project-URL: Repository, https://github.com/bzdvdn/reactifact
|
|
8
|
-
Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
|
|
9
|
-
Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
|
|
10
|
-
Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Python: >=3.11
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
|
-
Requires-Dist: pydantic>=2.13.4
|
|
23
|
-
Requires-Dist: httpx>=0.27
|
|
24
|
-
Requires-Dist: python-dotenv>=1.0
|
|
25
|
-
Provides-Extra: dev
|
|
26
|
-
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
27
|
-
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
29
|
-
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
30
|
-
Provides-Extra: web
|
|
31
|
-
Requires-Dist: fastapi>=0.115; extra == "web"
|
|
32
|
-
Requires-Dist: uvicorn[standard]>=0.30; extra == "web"
|
|
33
|
-
Provides-Extra: pg
|
|
34
|
-
Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
|
|
35
|
-
Provides-Extra: mcp
|
|
36
|
-
Requires-Dist: mcp>=2.2; extra == "mcp"
|
|
37
|
-
Dynamic: license-file
|
|
38
|
-
|
|
39
|
-
# reactifact
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
3
|
+
</p>
|
|
40
4
|
|
|
41
5
|
**Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
|
|
42
6
|
|
|
43
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
44
|
-
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
9
|
+
[](https://github.com/bzdvdn/reactifact)
|
|
45
10
|
[](https://pypi.org/project/reactifact/)
|
|
46
11
|
[](LICENSE)
|
|
12
|
+
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
13
|
+
[](https://bzdvdn.github.io/reactifact/)
|
|
47
14
|
|
|
48
15
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
49
16
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -56,6 +23,11 @@ react to events — there is no graph, no node pipeline.
|
|
|
56
23
|
|
|
57
24
|

|
|
58
25
|
|
|
26
|
+
Two agents explore independently on their own forks and merge back automatically
|
|
27
|
+
— and when they disagree, reactifact refuses to merge silently:
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
59
31
|
```bash
|
|
60
32
|
pip install reactifact
|
|
61
33
|
```
|
|
@@ -157,12 +129,33 @@ actual state.
|
|
|
157
129
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
158
130
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
159
131
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
132
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
133
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
134
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
160
135
|
|
|
161
136
|
Reactive. Deterministic. Accountable.
|
|
162
137
|
|
|
163
138
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
164
139
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
165
140
|
|
|
141
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
142
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
143
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
147
|
+
>>> LaborCost or Discount ever consumed.
|
|
148
|
+
|
|
149
|
+
LaborCost value=500.0 version=0 # untouched
|
|
150
|
+
Tax value=60.0 version=1 # recomputed
|
|
151
|
+
Discount value=25.0 version=0 # untouched
|
|
152
|
+
Total value=535.0 version=1 # recomputed
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
156
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
157
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
158
|
+
|
|
166
159
|
## Core primitives
|
|
167
160
|
|
|
168
161
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -170,7 +163,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
170
163
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
171
164
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
172
165
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
173
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
166
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
174
167
|
- **Provenance** — every derived artifact links to what produced it
|
|
175
168
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
176
169
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -211,7 +204,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
211
204
|
```
|
|
212
205
|
|
|
213
206
|
Classic-pattern ports run as one-liners too:
|
|
214
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
207
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
215
208
|
|
|
216
209
|
## Examples (in-repo, not shipped)
|
|
217
210
|
|
|
@@ -224,7 +217,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
224
217
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
225
218
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
226
219
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
227
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
220
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
228
221
|
|
|
229
222
|
## Documentation
|
|
230
223
|
|
|
@@ -236,6 +229,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
236
229
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
237
230
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
238
231
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
232
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
239
233
|
|
|
240
234
|
## Development
|
|
241
235
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -12,13 +12,15 @@ license = "MIT"
|
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
keywords = ["agents", "llm", "ai-agents", "reactive", "provenance", "orchestration", "agentic"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
18
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
22
24
|
"Typing :: Typed",
|
|
23
25
|
]
|
|
24
26
|
dependencies = [
|
|
@@ -30,7 +32,7 @@ dependencies = [
|
|
|
30
32
|
[project.urls]
|
|
31
33
|
Homepage = "https://github.com/bzdvdn/reactifact"
|
|
32
34
|
Repository = "https://github.com/bzdvdn/reactifact"
|
|
33
|
-
Documentation = "https://github.
|
|
35
|
+
Documentation = "https://bzdvdn.github.io/reactifact/"
|
|
34
36
|
Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
|
@@ -50,6 +52,11 @@ pg = [
|
|
|
50
52
|
mcp = [
|
|
51
53
|
"mcp>=2.2",
|
|
52
54
|
]
|
|
55
|
+
docs = [
|
|
56
|
+
"mkdocs>=1.6",
|
|
57
|
+
"mkdocs-material>=9.5",
|
|
58
|
+
"mkdocstrings[python]>=0.27",
|
|
59
|
+
]
|
|
53
60
|
|
|
54
61
|
# uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
|
|
55
62
|
# deps by default (no `--extra` flag needed to boot the examples). Extras stay
|
|
@@ -74,6 +81,11 @@ pg = [
|
|
|
74
81
|
mcp = [
|
|
75
82
|
"mcp>=2.2",
|
|
76
83
|
]
|
|
84
|
+
docs = [
|
|
85
|
+
"mkdocs>=1.6",
|
|
86
|
+
"mkdocs-material>=9.5",
|
|
87
|
+
"mkdocstrings[python]>=0.27",
|
|
88
|
+
]
|
|
77
89
|
|
|
78
90
|
[tool.uv]
|
|
79
91
|
default-groups = ["dev", "web", "mcp"]
|
|
@@ -93,6 +105,15 @@ reactifact = ["py.typed"]
|
|
|
93
105
|
[tool.pytest.ini_options]
|
|
94
106
|
testpaths = ["tests"]
|
|
95
107
|
|
|
108
|
+
# Enforced by CI's `pytest --cov=reactifact --cov-fail-under=85` (see
|
|
109
|
+
# .github/workflows/ci.yml). Current coverage is ~90%; 85% leaves headroom
|
|
110
|
+
# for `reactifact/tracing/postgres.py`, whose own test intentionally only
|
|
111
|
+
# checks the graceful-ImportError path without `psycopg` installed (`pg` is
|
|
112
|
+
# an opt-in extra with no external service in CI) rather than needing a
|
|
113
|
+
# per-file carve-out.
|
|
114
|
+
[tool.coverage.report]
|
|
115
|
+
fail_under = 85
|
|
116
|
+
|
|
96
117
|
[tool.ruff]
|
|
97
118
|
target-version = "py311"
|
|
98
119
|
line-length = 88
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""reactifact.agent_tool — a sub-agent as a `Tool` (delegation, §46).
|
|
2
|
+
|
|
3
|
+
`AgentAsTool` lets an `LLMAgent`/`HITLLMAgent` loop call another agent as an
|
|
4
|
+
ordinary tool: "delegate to X" runs a fresh, isolated nested `Runtime` to
|
|
5
|
+
completion and returns its final answer as the tool result. The delegating
|
|
6
|
+
loop sees only the final text — not the sub-agent's own tool calls, retries,
|
|
7
|
+
or internal reasoning.
|
|
8
|
+
|
|
9
|
+
Two deliberate constraints, not omissions:
|
|
10
|
+
|
|
11
|
+
- **Isolation is a fresh `Context`, not `context.branch()`.** The sub-agent
|
|
12
|
+
sees only the `query` it's given, nothing of the parent's history — no
|
|
13
|
+
prompt bloat from an unrelated conversation, and no accidental fork/merge
|
|
14
|
+
semantics (`branch()` exists for alternative-state exploration meant to be
|
|
15
|
+
merged back later, §39/§40; a one-shot delegate-and-discard call needs
|
|
16
|
+
neither). If a sub-agent genuinely needs to see the parent's state, build
|
|
17
|
+
its `Context` yourself and don't use this tool.
|
|
18
|
+
- **HITL sub-agents are rejected, not silently broken.** `Tool.execute` has
|
|
19
|
+
no channel back to a human — nothing would ever answer a `PendingQuestion`
|
|
20
|
+
the sub-agent raises inside its own isolated `Context`, and the nested run
|
|
21
|
+
would just finish with no `ToolAnswer`. `execute()` checks for this and
|
|
22
|
+
returns a `ToolOutput(error=...)` naming the unanswered question, instead
|
|
23
|
+
of returning empty text with no explanation.
|
|
24
|
+
|
|
25
|
+
Because `Tool.execute` receives only `args` (no `context`, see `tools.py`),
|
|
26
|
+
the sub-agent's `LLMProvider`/budget cannot be inherited implicitly from
|
|
27
|
+
whatever loop is calling this tool — pass them explicitly via `resources=`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel
|
|
36
|
+
|
|
37
|
+
from .agents import Agent
|
|
38
|
+
from .budget import Budget
|
|
39
|
+
from .context import Context
|
|
40
|
+
from .resources import RuntimeResources
|
|
41
|
+
from .runtime import Runtime
|
|
42
|
+
from .tool_use import ToolAnswer
|
|
43
|
+
from .tools import Tool, ToolOutput
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SubTask(BaseModel):
|
|
47
|
+
"""Default input artifact for `AgentAsTool` — override via `input_type`
|
|
48
|
+
with any model that has a `text: str` field (the convention `ToolUse`'s
|
|
49
|
+
own `_goal` reads, `tool_use.py`)."""
|
|
50
|
+
|
|
51
|
+
text: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentAsTool(Tool):
|
|
55
|
+
"""Wraps `agent_factory()` as a callable tool (see module docstring)."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
name: str,
|
|
61
|
+
description: str,
|
|
62
|
+
agent_factory: Callable[[], Agent],
|
|
63
|
+
resources: RuntimeResources,
|
|
64
|
+
input_type: type[BaseModel] = SubTask,
|
|
65
|
+
output_type: type[BaseModel] | None = None,
|
|
66
|
+
max_runs: int = 20,
|
|
67
|
+
destructive: bool = False,
|
|
68
|
+
):
|
|
69
|
+
self.name = name
|
|
70
|
+
self.description = description
|
|
71
|
+
self.agent_factory = agent_factory
|
|
72
|
+
self.resources = resources
|
|
73
|
+
self.input_type = input_type
|
|
74
|
+
self.output_type = output_type or ToolAnswer
|
|
75
|
+
self.max_runs = max_runs
|
|
76
|
+
self.destructive = destructive
|
|
77
|
+
self.schema = {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"query": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "The task/question to delegate to the sub-agent.",
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"required": ["query"],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async def execute(self, args: dict[str, Any]) -> ToolOutput:
|
|
89
|
+
query = str(args.get("query", ""))
|
|
90
|
+
sub_context = Context(resources=self.resources)
|
|
91
|
+
sub_agent = self.agent_factory()
|
|
92
|
+
sub_runtime = Runtime(
|
|
93
|
+
sub_context, agents=[sub_agent], budget=Budget(max_runs=self.max_runs)
|
|
94
|
+
)
|
|
95
|
+
sub_context.create(self.input_type(text=query))
|
|
96
|
+
await sub_runtime.arun()
|
|
97
|
+
|
|
98
|
+
if sub_context.has_pending_question():
|
|
99
|
+
pending = sub_context.latest_pending_question()
|
|
100
|
+
question = pending.data.question if pending is not None else ""
|
|
101
|
+
return ToolOutput(
|
|
102
|
+
error=(
|
|
103
|
+
f"sub-agent '{sub_agent.name}' needs clarification it wasn't "
|
|
104
|
+
f"given and can't ask for: {question!r}. Give '{self.name}' a "
|
|
105
|
+
"more complete query and try again."
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
outputs = sub_context.list_artifacts(self.output_type)
|
|
110
|
+
if not outputs:
|
|
111
|
+
return ToolOutput(
|
|
112
|
+
error=f"sub-agent '{sub_agent.name}' produced no {self.output_type.__name__}"
|
|
113
|
+
)
|
|
114
|
+
text = getattr(outputs[-1].data, "text", "") or ""
|
|
115
|
+
return ToolOutput(text=text)
|
|
@@ -90,7 +90,14 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
90
90
|
return self._collect_inputs(context)
|
|
91
91
|
|
|
92
92
|
def _collect_inputs(self, context: Context) -> list[Artifact[Any]]:
|
|
93
|
-
"""Collects all artifacts matching consumes and conditions.
|
|
93
|
+
"""Collects all artifacts matching consumes and conditions.
|
|
94
|
+
|
|
95
|
+
Ranking/truncation, if any, is a `Runtime`-level policy
|
|
96
|
+
(`context.resources.context_builder`, see `context_builder.py`), not
|
|
97
|
+
this agent's — applied here so both this and `collect_inputs()`
|
|
98
|
+
(used by the runtime for provenance) see the identical, already
|
|
99
|
+
built list.
|
|
100
|
+
"""
|
|
94
101
|
if not self.consumes:
|
|
95
102
|
return []
|
|
96
103
|
inputs: list[Artifact[Any]] = []
|
|
@@ -99,6 +106,9 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
99
106
|
if c.condition:
|
|
100
107
|
artifacts = [a for a in artifacts if c.condition(a)]
|
|
101
108
|
inputs.extend(artifacts)
|
|
109
|
+
builder = context.resources.context_builder
|
|
110
|
+
if builder is not None:
|
|
111
|
+
inputs = builder.build(context, self, inputs)
|
|
102
112
|
return inputs
|
|
103
113
|
|
|
104
114
|
async def run(self, event: Event, context: Context) -> Patch | None:
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
git-like state operations `Context.clone()`/`.branch()`/`.merge_from()`/
|
|
6
6
|
`.merge()` delegate to — moved here so the git-like algorithm lives next to
|
|
7
7
|
the concept it implements, not folded into `Context`'s general CRUD/relations/
|
|
8
|
-
HITL surface.
|
|
9
|
-
|
|
8
|
+
HITL surface. Application code should call the `Context` methods (see
|
|
9
|
+
[docs/en/branching.md](../docs/en/branching.md)); these module-level
|
|
10
|
+
functions are exported for building on `Context` without one already in
|
|
11
|
+
hand (e.g. `BranchStore`'s own load/merge path below), not as an equally
|
|
12
|
+
first-class alternative entry point. `BranchStore` only *persists* named
|
|
13
|
+
forks so they survive a restart:
|
|
10
14
|
|
|
11
15
|
store = BranchStore(SQLiteKVBackend("sessions.sqlite3"))
|
|
12
16
|
await store.save_branch(ctx_branch, session_id="demo", name="hypothesis-a")
|
|
@@ -59,6 +63,7 @@ def clone_context(source: Context) -> Context:
|
|
|
59
63
|
new_ws._log = source._log.copy()
|
|
60
64
|
new_ws._relations = source._relations.copy()
|
|
61
65
|
new_ws._recompute_stale()
|
|
66
|
+
new_ws._reindex_by_type()
|
|
62
67
|
return new_ws
|
|
63
68
|
|
|
64
69
|
|
|
@@ -91,13 +96,8 @@ def merge_context_from(target: Context, other: Context) -> None:
|
|
|
91
96
|
operations.append(Update(other_id, new_data))
|
|
92
97
|
else:
|
|
93
98
|
new_data = other_artifact.data.model_copy(deep=True)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# `target` gets a freshly minted id, not `other_id`). Not fixed
|
|
97
|
-
# here — a real behavior change belongs in its own change, not a
|
|
98
|
-
# pure extraction.
|
|
99
|
-
target.create(new_data)
|
|
100
|
-
operations.append(Create(new_data))
|
|
99
|
+
target.create(new_data, id=other_id)
|
|
100
|
+
operations.append(Create(new_data, id=other_id))
|
|
101
101
|
for rel in other.relations():
|
|
102
102
|
if (rel.source_id, rel.relation, rel.target_id) not in target._relations:
|
|
103
103
|
target.link(rel.source_id, rel.relation, rel.target_id)
|
|
@@ -210,6 +210,7 @@ def merge_contexts(
|
|
|
210
210
|
# `delete()` — resync `_stale` from scratch rather than risk it
|
|
211
211
|
# drifting from the post-merge state.
|
|
212
212
|
target._recompute_stale()
|
|
213
|
+
target._reindex_by_type()
|
|
213
214
|
|
|
214
215
|
|
|
215
216
|
class BranchStore:
|
|
@@ -112,11 +112,18 @@ class FileBackend(CheckpointBackend):
|
|
|
112
112
|
"""File backend: state is stored in a single JSON file."""
|
|
113
113
|
|
|
114
114
|
def __init__(self, path: str):
|
|
115
|
-
self.path = path
|
|
115
|
+
self.path = Path(path)
|
|
116
116
|
|
|
117
117
|
def _save_sync(self, data: dict[str, Any]) -> None:
|
|
118
|
-
|
|
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)
|