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

|
|
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
|
+

|
|
67
|
+
|
|
44
68
|
```bash
|
|
45
69
|
pip install reactifact
|
|
46
70
|
```
|
|
@@ -107,6 +131,12 @@ print(answer.data.text) # "Refunds are available within 14 d
|
|
|
107
131
|
print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
|
|
108
132
|
```
|
|
109
133
|
|
|
134
|
+
The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
|
|
135
|
+
harder, multi-source question (docs + a CSV) with a real computed number and
|
|
136
|
+
its sources, no LLM key required:
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
110
140
|
## How it works
|
|
111
141
|
|
|
112
142
|
```text
|
|
@@ -136,12 +166,33 @@ actual state.
|
|
|
136
166
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
137
167
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
138
168
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
169
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
170
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
171
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
139
172
|
|
|
140
173
|
Reactive. Deterministic. Accountable.
|
|
141
174
|
|
|
142
175
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
143
176
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
144
177
|
|
|
178
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
179
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
180
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
184
|
+
>>> LaborCost or Discount ever consumed.
|
|
185
|
+
|
|
186
|
+
LaborCost value=500.0 version=0 # untouched
|
|
187
|
+
Tax value=60.0 version=1 # recomputed
|
|
188
|
+
Discount value=25.0 version=0 # untouched
|
|
189
|
+
Total value=535.0 version=1 # recomputed
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
193
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
194
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
195
|
+
|
|
145
196
|
## Core primitives
|
|
146
197
|
|
|
147
198
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -149,7 +200,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
149
200
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
150
201
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
151
202
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
152
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
203
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
153
204
|
- **Provenance** — every derived artifact links to what produced it
|
|
154
205
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
155
206
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -160,6 +211,10 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
160
211
|
fallbacks instead of hallucinated answers; the model reasons, never "knows".
|
|
161
212
|
- **Observability** — every run traces agent spans, reads/writes, LLM calls,
|
|
162
213
|
tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
|
|
214
|
+
- **MCP, both ways** — call any MCP server's tools as a `Tool`
|
|
215
|
+
(`mcp_stdio_tools`/`mcp_http_tools`), or expose your own `Tool`s and a live
|
|
216
|
+
`Context` as an MCP server (`create_mcp_server`) for Claude Desktop, Claude
|
|
217
|
+
Code, or another agent to call into (`mcp` extra).
|
|
163
218
|
- **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
|
|
164
219
|
- **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
|
|
165
220
|
`ReplayLLM`, all for audit and safe alternative states.
|
|
@@ -186,7 +241,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
186
241
|
```
|
|
187
242
|
|
|
188
243
|
Classic-pattern ports run as one-liners too:
|
|
189
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
244
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
190
245
|
|
|
191
246
|
## Examples (in-repo, not shipped)
|
|
192
247
|
|
|
@@ -199,7 +254,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
199
254
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
200
255
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
201
256
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
202
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
257
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
203
258
|
|
|
204
259
|
## Documentation
|
|
205
260
|
|
|
@@ -211,6 +266,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
211
266
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
212
267
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
213
268
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
269
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
214
270
|
|
|
215
271
|
## Development
|
|
216
272
|
|
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
|
|
5
|
-
Project-URL: Homepage, https://github.com/bzdvdn/reactifact
|
|
6
|
-
Project-URL: Repository, https://github.com/bzdvdn/reactifact
|
|
7
|
-
Project-URL: Documentation, https://github.com/bzdvdn/reactifact/tree/master/docs
|
|
8
|
-
Requires-Python: >=3.11
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: pydantic>=2.13.4
|
|
12
|
-
Requires-Dist: httpx>=0.27
|
|
13
|
-
Requires-Dist: python-dotenv>=1.0
|
|
14
|
-
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
16
|
-
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
17
|
-
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
18
|
-
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
19
|
-
Provides-Extra: web
|
|
20
|
-
Requires-Dist: fastapi>=0.115; extra == "web"
|
|
21
|
-
Requires-Dist: uvicorn[standard]>=0.30; extra == "web"
|
|
22
|
-
Provides-Extra: pg
|
|
23
|
-
Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
|
|
24
|
-
Dynamic: license-file
|
|
25
|
-
|
|
26
|
-
# reactifact
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
3
|
+
</p>
|
|
27
4
|
|
|
28
5
|
**Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
|
|
29
6
|
|
|
30
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
8
|
+
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
31
9
|
[](https://github.com/bzdvdn/reactifact)
|
|
32
10
|
[](https://pypi.org/project/reactifact/)
|
|
33
11
|
[](LICENSE)
|
|
12
|
+
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
34
13
|
|
|
35
14
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
36
15
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -41,6 +20,13 @@ reactifact flips the model. You describe **what artifacts exist and what agents
|
|
|
41
20
|
do with them**; the runtime derives what runs next from **state changes**. Agents
|
|
42
21
|
react to events — there is no graph, no node pipeline.
|
|
43
22
|
|
|
23
|
+

|
|
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
|
+

|
|
29
|
+
|
|
44
30
|
```bash
|
|
45
31
|
pip install reactifact
|
|
46
32
|
```
|
|
@@ -107,6 +93,12 @@ print(answer.data.text) # "Refunds are available within 14 d
|
|
|
107
93
|
print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
|
|
108
94
|
```
|
|
109
95
|
|
|
96
|
+
The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
|
|
97
|
+
harder, multi-source question (docs + a CSV) with a real computed number and
|
|
98
|
+
its sources, no LLM key required:
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
110
102
|
## How it works
|
|
111
103
|
|
|
112
104
|
```text
|
|
@@ -136,12 +128,33 @@ actual state.
|
|
|
136
128
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
137
129
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
138
130
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
131
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
132
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
133
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
139
134
|
|
|
140
135
|
Reactive. Deterministic. Accountable.
|
|
141
136
|
|
|
142
137
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
143
138
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
144
139
|
|
|
140
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
141
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
142
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
146
|
+
>>> LaborCost or Discount ever consumed.
|
|
147
|
+
|
|
148
|
+
LaborCost value=500.0 version=0 # untouched
|
|
149
|
+
Tax value=60.0 version=1 # recomputed
|
|
150
|
+
Discount value=25.0 version=0 # untouched
|
|
151
|
+
Total value=535.0 version=1 # recomputed
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
155
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
156
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
157
|
+
|
|
145
158
|
## Core primitives
|
|
146
159
|
|
|
147
160
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -149,7 +162,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
149
162
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
150
163
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
151
164
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
152
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
165
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
153
166
|
- **Provenance** — every derived artifact links to what produced it
|
|
154
167
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
155
168
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -160,6 +173,10 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
160
173
|
fallbacks instead of hallucinated answers; the model reasons, never "knows".
|
|
161
174
|
- **Observability** — every run traces agent spans, reads/writes, LLM calls,
|
|
162
175
|
tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
|
|
176
|
+
- **MCP, both ways** — call any MCP server's tools as a `Tool`
|
|
177
|
+
(`mcp_stdio_tools`/`mcp_http_tools`), or expose your own `Tool`s and a live
|
|
178
|
+
`Context` as an MCP server (`create_mcp_server`) for Claude Desktop, Claude
|
|
179
|
+
Code, or another agent to call into (`mcp` extra).
|
|
163
180
|
- **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
|
|
164
181
|
- **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
|
|
165
182
|
`ReplayLLM`, all for audit and safe alternative states.
|
|
@@ -186,7 +203,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
186
203
|
```
|
|
187
204
|
|
|
188
205
|
Classic-pattern ports run as one-liners too:
|
|
189
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
206
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
190
207
|
|
|
191
208
|
## Examples (in-repo, not shipped)
|
|
192
209
|
|
|
@@ -199,7 +216,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
199
216
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
200
217
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
201
218
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
202
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
219
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
203
220
|
|
|
204
221
|
## Documentation
|
|
205
222
|
|
|
@@ -211,6 +228,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
211
228
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
212
229
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
213
230
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
231
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
214
232
|
|
|
215
233
|
## Development
|
|
216
234
|
|
|
@@ -4,10 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["agents", "llm", "ai-agents", "reactive", "provenance", "orchestration", "agentic"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
11
24
|
dependencies = [
|
|
12
25
|
"pydantic>=2.13.4",
|
|
13
26
|
"httpx>=0.27",
|
|
@@ -18,6 +31,7 @@ dependencies = [
|
|
|
18
31
|
Homepage = "https://github.com/bzdvdn/reactifact"
|
|
19
32
|
Repository = "https://github.com/bzdvdn/reactifact"
|
|
20
33
|
Documentation = "https://github.com/bzdvdn/reactifact/tree/master/docs"
|
|
34
|
+
Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
|
|
21
35
|
|
|
22
36
|
[project.optional-dependencies]
|
|
23
37
|
dev = [
|
|
@@ -33,10 +47,16 @@ web = [
|
|
|
33
47
|
pg = [
|
|
34
48
|
"psycopg[binary]>=3.2",
|
|
35
49
|
]
|
|
50
|
+
mcp = [
|
|
51
|
+
"mcp>=2.2",
|
|
52
|
+
]
|
|
36
53
|
|
|
37
54
|
# uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
|
|
38
55
|
# deps by default (no `--extra` flag needed to boot the examples). Extras stay
|
|
39
|
-
# for PyPI (`pip install reactifact[web]`).
|
|
56
|
+
# for PyPI (`pip install reactifact[web]`). `mcp` joins the default groups too —
|
|
57
|
+
# unlike `pg`, its tests need no external service (in-memory MCP transport), so
|
|
58
|
+
# there's no reason not to run them for real in CI. `pg` stays opt-in; its own
|
|
59
|
+
# test only checks the graceful ImportError path without psycopg installed.
|
|
40
60
|
[dependency-groups]
|
|
41
61
|
dev = [
|
|
42
62
|
"ruff>=0.8",
|
|
@@ -51,9 +71,12 @@ web = [
|
|
|
51
71
|
pg = [
|
|
52
72
|
"psycopg[binary]>=3.2",
|
|
53
73
|
]
|
|
74
|
+
mcp = [
|
|
75
|
+
"mcp>=2.2",
|
|
76
|
+
]
|
|
54
77
|
|
|
55
78
|
[tool.uv]
|
|
56
|
-
default-groups = ["dev", "web"]
|
|
79
|
+
default-groups = ["dev", "web", "mcp"]
|
|
57
80
|
|
|
58
81
|
# Публикация: в wheel едет только пакет `reactifact`; examples/tests остаются
|
|
59
82
|
# в репозитории для демо и разработки, но не попадают в дистрибутив.
|
|
@@ -70,6 +93,15 @@ reactifact = ["py.typed"]
|
|
|
70
93
|
[tool.pytest.ini_options]
|
|
71
94
|
testpaths = ["tests"]
|
|
72
95
|
|
|
96
|
+
# Enforced by CI's `pytest --cov=reactifact --cov-fail-under=85` (see
|
|
97
|
+
# .github/workflows/ci.yml). Current coverage is ~90%; 85% leaves headroom
|
|
98
|
+
# for `reactifact/tracing/postgres.py`, whose own test intentionally only
|
|
99
|
+
# checks the graceful-ImportError path without `psycopg` installed (`pg` is
|
|
100
|
+
# an opt-in extra with no external service in CI) rather than needing a
|
|
101
|
+
# per-file carve-out.
|
|
102
|
+
[tool.coverage.report]
|
|
103
|
+
fail_under = 85
|
|
104
|
+
|
|
73
105
|
[tool.ruff]
|
|
74
106
|
target-version = "py311"
|
|
75
107
|
line-length = 88
|
|
@@ -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")
|
|
@@ -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
|
-
|
|
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))
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Purely a call-site name: identical to `create(..., id=...)`, but says
|
|
132
|
+
at the call site that a refresh is an expected outcome, not a
|
|
133
|
+
surprise — reach for it when the artifact may already exist (e.g. a
|
|
134
|
+
re-derived id like `f"answer:{qid}"`).
|
|
134
135
|
"""
|
|
135
136
|
return self.create(data, id=id)
|
|
136
137
|
|
|
137
138
|
def update(self, artifact: Artifact[Any], **fields: Any) -> Effects:
|
|
138
|
-
"""Bumps fields of an *existing* artifact (a new version).
|
|
139
|
+
"""Bumps fields of an *existing* artifact (a new version).
|
|
140
|
+
|
|
141
|
+
Note the name means something different here than on `Patch.update`/
|
|
142
|
+
`Context.update`/`Artifact.update` (a full data replacement) — this is
|
|
143
|
+
the one intentional exception, matching `Patch.update_fields`
|
|
144
|
+
instead. `Effects` is the everyday authoring surface where "update
|
|
145
|
+
some fields" is the common case (§18 above), so it gets the short
|
|
146
|
+
name; the lower-level, less-used `Patch`/`Context`/`Artifact` surface
|
|
147
|
+
keeps `update` for the operation it's actually named after.
|
|
148
|
+
"""
|
|
139
149
|
new_data = artifact.data.model_copy(update=fields)
|
|
140
150
|
self.operations.append(Update(artifact.id, new_data))
|
|
141
151
|
return self
|
|
@@ -25,7 +25,7 @@ from .events import Event
|
|
|
25
25
|
from .interrupt import PendingQuestion
|
|
26
26
|
from .produce import Produce
|
|
27
27
|
from .structured import SYSTEM_STRUCTURED, structured_llm
|
|
28
|
-
from .tool_use import Observation, ToolAnswer, ToolUse, ToolUseHITL
|
|
28
|
+
from .tool_use import DeferredToolGroup, Observation, ToolAnswer, ToolUse, ToolUseHITL
|
|
29
29
|
from .tools import Tool
|
|
30
30
|
|
|
31
31
|
|
|
@@ -98,6 +98,7 @@ class LLMAgent(Agent):
|
|
|
98
98
|
max_steps: int = 8
|
|
99
99
|
temperature: float | None = None
|
|
100
100
|
max_tokens: int | None = None
|
|
101
|
+
deferred_tool_groups: Sequence[DeferredToolGroup] = ()
|
|
101
102
|
|
|
102
103
|
def __init__(self, *, name: str | None = None, **kwargs: Any):
|
|
103
104
|
llm_name = name or self.name or self.__class__.__name__.lower()
|
|
@@ -116,6 +117,7 @@ class LLMAgent(Agent):
|
|
|
116
117
|
max_steps=self.max_steps,
|
|
117
118
|
temperature=self.temperature,
|
|
118
119
|
max_tokens=self.max_tokens,
|
|
120
|
+
deferred_tool_groups=self.deferred_tool_groups,
|
|
119
121
|
),
|
|
120
122
|
Produce(ToolAnswer),
|
|
121
123
|
*user_produces,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP integration: talk to external MCP servers, or serve reactifact `Tool`s
|
|
2
|
+
and `Context` artifacts as one. Requires the `mcp` extra — nothing here is
|
|
3
|
+
imported by reactifact's core, so a bare `pip install reactifact` never pulls
|
|
4
|
+
in the `mcp` SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .client import MCPTool, mcp_http_tools, mcp_stdio_tools, mcp_tools
|
|
10
|
+
from .oauth import InMemoryTokenStorage, oauth_client_credentials
|
|
11
|
+
from .server import create_mcp_server
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"InMemoryTokenStorage",
|
|
15
|
+
"MCPTool",
|
|
16
|
+
"create_mcp_server",
|
|
17
|
+
"mcp_http_tools",
|
|
18
|
+
"mcp_stdio_tools",
|
|
19
|
+
"mcp_tools",
|
|
20
|
+
"oauth_client_credentials",
|
|
21
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""MCP client: turns tools exposed by an external MCP server into `Tool`s, so
|
|
2
|
+
`ToolUse`/`ToolUseHITL`/`LLMAgent` call remote MCP tools the same way they
|
|
3
|
+
call local ones — no separate code path for "MCP tool" vs. `@tool`. Requires
|
|
4
|
+
the `mcp` extra.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .._extras import require_extra
|
|
14
|
+
from ..tools import Tool, ToolOutput
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPTool(Tool):
|
|
18
|
+
"""One tool from a connected `mcp.ClientSession`, wrapped as a `Tool`."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
session: Any,
|
|
23
|
+
*,
|
|
24
|
+
name: str,
|
|
25
|
+
description: str,
|
|
26
|
+
schema: dict[str, Any],
|
|
27
|
+
destructive: bool = False,
|
|
28
|
+
):
|
|
29
|
+
self._session = session
|
|
30
|
+
self.name = name
|
|
31
|
+
self.description = description
|
|
32
|
+
self.schema = schema
|
|
33
|
+
self.destructive = destructive
|
|
34
|
+
|
|
35
|
+
async def execute(self, args: dict[str, Any]) -> ToolOutput:
|
|
36
|
+
result = await self._session.call_tool(self.name, args)
|
|
37
|
+
return _to_tool_output(result)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _to_tool_output(result: Any) -> ToolOutput:
|
|
41
|
+
texts = [block.text for block in result.content if getattr(block, "text", None)]
|
|
42
|
+
text = "\n".join(texts)
|
|
43
|
+
if result.is_error:
|
|
44
|
+
return ToolOutput(text=text, error=text or "MCP tool call failed")
|
|
45
|
+
data = dict(result.structured_content) if result.structured_content else {}
|
|
46
|
+
return ToolOutput(text=text, data=data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def mcp_tools(session: Any) -> list[Tool]:
|
|
50
|
+
"""Lists tools on a connected, initialized `mcp.ClientSession` and wraps each as a `Tool`."""
|
|
51
|
+
listed = await session.list_tools()
|
|
52
|
+
return [
|
|
53
|
+
MCPTool(
|
|
54
|
+
session,
|
|
55
|
+
name=t.name,
|
|
56
|
+
description=t.description or t.name,
|
|
57
|
+
schema=t.input_schema,
|
|
58
|
+
destructive=bool(t.annotations and t.annotations.destructive_hint),
|
|
59
|
+
)
|
|
60
|
+
for t in listed.tools
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@asynccontextmanager
|
|
65
|
+
async def mcp_stdio_tools(
|
|
66
|
+
command: str,
|
|
67
|
+
args: list[str] | None = None,
|
|
68
|
+
*,
|
|
69
|
+
env: dict[str, str] | None = None,
|
|
70
|
+
) -> AsyncGenerator[list[Tool], None]:
|
|
71
|
+
"""Spawns an MCP server over stdio and yields its tools as `Tool`s.
|
|
72
|
+
|
|
73
|
+
Example: `mcp_stdio_tools("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])`.
|
|
74
|
+
The connection stays open for the `async with` block; tools called after
|
|
75
|
+
it exits will fail.
|
|
76
|
+
"""
|
|
77
|
+
require_extra("mcp_stdio_tools", "mcp", "mcp")
|
|
78
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
79
|
+
|
|
80
|
+
from mcp import ClientSession
|
|
81
|
+
|
|
82
|
+
params = StdioServerParameters(command=command, args=args or [], env=env)
|
|
83
|
+
async with (
|
|
84
|
+
stdio_client(params) as (read, write),
|
|
85
|
+
ClientSession(read, write) as session,
|
|
86
|
+
):
|
|
87
|
+
await session.initialize()
|
|
88
|
+
yield await mcp_tools(session)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def mcp_http_tools(
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
headers: dict[str, str] | None = None,
|
|
96
|
+
auth: Any = None,
|
|
97
|
+
) -> AsyncGenerator[list[Tool], None]:
|
|
98
|
+
"""Connects to an MCP server over streamable HTTP and yields its tools as
|
|
99
|
+
`Tool`s.
|
|
100
|
+
|
|
101
|
+
Pass `headers` for servers that require a static credential (e.g.
|
|
102
|
+
`{"Authorization": "Bearer ..."}`), or `auth` (an `httpx2.Auth`) for a
|
|
103
|
+
server that requires OAuth — see `reactifact.mcp.oauth_client_credentials`
|
|
104
|
+
for the machine-to-machine case. The SDK's `streamable_http_client` has
|
|
105
|
+
no `headers=`/`auth=` kwarg of its own; the documented way is a
|
|
106
|
+
pre-configured client, which this builds (and owns/closes) with the same
|
|
107
|
+
recommended timeouts the SDK's own default client uses (30s
|
|
108
|
+
connect/write/pool, 300s read — a server may hold a response stream
|
|
109
|
+
open). Note this must be `httpx2.AsyncClient` (the MCP SDK's own httpx
|
|
110
|
+
fork/dependency, not plain `httpx` — `streamable_http_client` rejects
|
|
111
|
+
the wrong one at the type level), imported lazily here since it's only
|
|
112
|
+
guaranteed installed alongside the `mcp` extra.
|
|
113
|
+
"""
|
|
114
|
+
require_extra("mcp_http_tools", "mcp", "mcp")
|
|
115
|
+
import httpx2
|
|
116
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
117
|
+
|
|
118
|
+
from mcp import ClientSession
|
|
119
|
+
|
|
120
|
+
async with AsyncExitStack() as stack:
|
|
121
|
+
http_client = None
|
|
122
|
+
if headers is not None or auth is not None:
|
|
123
|
+
http_client = await stack.enter_async_context(
|
|
124
|
+
httpx2.AsyncClient(
|
|
125
|
+
headers=headers, auth=auth, timeout=httpx2.Timeout(30.0, read=300.0)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
read, write = await stack.enter_async_context(
|
|
129
|
+
streamable_http_client(url, http_client=http_client)
|
|
130
|
+
)
|
|
131
|
+
session = await stack.enter_async_context(ClientSession(read, write))
|
|
132
|
+
await session.initialize()
|
|
133
|
+
yield await mcp_tools(session)
|