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

|
|
58
62
|
|
|
63
|
+
Two agents explore independently on their own forks and merge back automatically
|
|
64
|
+
— and when they disagree, reactifact refuses to merge silently:
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
59
68
|
```bash
|
|
60
69
|
pip install reactifact
|
|
61
70
|
```
|
|
@@ -157,12 +166,33 @@ actual state.
|
|
|
157
166
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
158
167
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
159
168
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
169
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
170
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
171
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
160
172
|
|
|
161
173
|
Reactive. Deterministic. Accountable.
|
|
162
174
|
|
|
163
175
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
164
176
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
165
177
|
|
|
178
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
179
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
180
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
184
|
+
>>> LaborCost or Discount ever consumed.
|
|
185
|
+
|
|
186
|
+
LaborCost value=500.0 version=0 # untouched
|
|
187
|
+
Tax value=60.0 version=1 # recomputed
|
|
188
|
+
Discount value=25.0 version=0 # untouched
|
|
189
|
+
Total value=535.0 version=1 # recomputed
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
193
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
194
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
195
|
+
|
|
166
196
|
## Core primitives
|
|
167
197
|
|
|
168
198
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -170,7 +200,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
170
200
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
171
201
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
172
202
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
173
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
203
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
174
204
|
- **Provenance** — every derived artifact links to what produced it
|
|
175
205
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
176
206
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -211,7 +241,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
211
241
|
```
|
|
212
242
|
|
|
213
243
|
Classic-pattern ports run as one-liners too:
|
|
214
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
244
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
215
245
|
|
|
216
246
|
## Examples (in-repo, not shipped)
|
|
217
247
|
|
|
@@ -224,7 +254,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
224
254
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
225
255
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
226
256
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
227
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
257
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
228
258
|
|
|
229
259
|
## Documentation
|
|
230
260
|
|
|
@@ -236,6 +266,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
236
266
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
237
267
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
238
268
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
269
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
239
270
|
|
|
240
271
|
## Development
|
|
241
272
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
5
|
**Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
|
|
4
6
|
|
|
5
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
8
|
+
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
6
9
|
[](https://github.com/bzdvdn/reactifact)
|
|
7
10
|
[](https://pypi.org/project/reactifact/)
|
|
8
11
|
[](LICENSE)
|
|
12
|
+
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
9
13
|
|
|
10
14
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
11
15
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -18,6 +22,11 @@ react to events — there is no graph, no node pipeline.
|
|
|
18
22
|
|
|
19
23
|

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

|
|
29
|
+
|
|
21
30
|
```bash
|
|
22
31
|
pip install reactifact
|
|
23
32
|
```
|
|
@@ -119,12 +128,33 @@ actual state.
|
|
|
119
128
|
| Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
|
|
120
129
|
| "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
|
|
121
130
|
| The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
|
|
131
|
+
| Tracing needs a SaaS add-on | **Native trace store** (SQLite + dashboard), exportable to Langfuse/Postgres |
|
|
132
|
+
| MCP via a framework adapter | **MCP both ways** built in — call any server, or expose your own `Context` as one |
|
|
133
|
+
| Pulls in a framework's dependency tree | **3 core deps**: `pydantic`, `httpx`, `python-dotenv` |
|
|
122
134
|
|
|
123
135
|
Reactive. Deterministic. Accountable.
|
|
124
136
|
|
|
125
137
|
Full breakdown, including where reactifact is *not* the right choice:
|
|
126
138
|
[docs/en/comparison.md](docs/en/comparison.md).
|
|
127
139
|
|
|
140
|
+
**Proof, not a claim** — [`examples/ledger`](examples/ledger) is a 4-artifact
|
|
141
|
+
billing calc (`LaborCost`, `Tax`, `Discount`, `Total`) with no LLM, fully
|
|
142
|
+
offline. Edit *one* fact and see what actually reruns:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
>>> editing ONLY TaxRate (0.08 -> 0.12) — a fact nothing about
|
|
146
|
+
>>> LaborCost or Discount ever consumed.
|
|
147
|
+
|
|
148
|
+
LaborCost value=500.0 version=0 # untouched
|
|
149
|
+
Tax value=60.0 version=1 # recomputed
|
|
150
|
+
Discount value=25.0 version=0 # untouched
|
|
151
|
+
Total value=535.0 version=1 # recomputed
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
2 of 4 artifacts recompute — the 2 that actually depend on `TaxRate` —
|
|
155
|
+
because `Artifact.version` tracks real consumption, not a graph edge you drew
|
|
156
|
+
by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
157
|
+
|
|
128
158
|
## Core primitives
|
|
129
159
|
|
|
130
160
|
- **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
|
|
@@ -132,7 +162,7 @@ Full breakdown, including where reactifact is *not* the right choice:
|
|
|
132
162
|
- **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
|
|
133
163
|
- **Patch** — the compiled, validated change-set applied as one atomic commit.
|
|
134
164
|
- **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
|
|
135
|
-
- **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL
|
|
165
|
+
- **Source** — retrieval is a capability: vector search is *one* strategy, not the only one; filesystem, CSV, and the web are equally first-class today (direct API, keyword, and SQL sources are on the [roadmap](docs/roadmap.md#next), not yet shipped).
|
|
136
166
|
- **Provenance** — every derived artifact links to what produced it
|
|
137
167
|
(`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
|
|
138
168
|
- **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
|
|
@@ -173,7 +203,7 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
173
203
|
```
|
|
174
204
|
|
|
175
205
|
Classic-pattern ports run as one-liners too:
|
|
176
|
-
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
|
|
206
|
+
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
177
207
|
|
|
178
208
|
## Examples (in-repo, not shipped)
|
|
179
209
|
|
|
@@ -186,7 +216,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
186
216
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
187
217
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
188
218
|
- `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
|
|
189
|
-
- `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
219
|
+
- `{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
|
|
190
220
|
|
|
191
221
|
## Documentation
|
|
192
222
|
|
|
@@ -198,6 +228,7 @@ Classic-pattern ports run as one-liners too:
|
|
|
198
228
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
199
229
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
200
230
|
- [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
|
|
231
|
+
- [Roadmap](docs/roadmap.md) — what's next, and what's deliberately out of scope.
|
|
201
232
|
|
|
202
233
|
## Development
|
|
203
234
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ license = "MIT"
|
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
keywords = ["agents", "llm", "ai-agents", "reactive", "provenance", "orchestration", "agentic"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
18
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
@@ -93,6 +93,15 @@ reactifact = ["py.typed"]
|
|
|
93
93
|
[tool.pytest.ini_options]
|
|
94
94
|
testpaths = ["tests"]
|
|
95
95
|
|
|
96
|
+
# Enforced by CI's `pytest --cov=reactifact --cov-fail-under=85` (see
|
|
97
|
+
# .github/workflows/ci.yml). Current coverage is ~90%; 85% leaves headroom
|
|
98
|
+
# for `reactifact/tracing/postgres.py`, whose own test intentionally only
|
|
99
|
+
# checks the graceful-ImportError path without `psycopg` installed (`pg` is
|
|
100
|
+
# an opt-in extra with no external service in CI) rather than needing a
|
|
101
|
+
# per-file carve-out.
|
|
102
|
+
[tool.coverage.report]
|
|
103
|
+
fail_under = 85
|
|
104
|
+
|
|
96
105
|
[tool.ruff]
|
|
97
106
|
target-version = "py311"
|
|
98
107
|
line-length = 88
|
|
@@ -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,
|
|
@@ -7,12 +7,15 @@ in the `mcp` SDK.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from .client import MCPTool, mcp_http_tools, mcp_stdio_tools, mcp_tools
|
|
10
|
+
from .oauth import InMemoryTokenStorage, oauth_client_credentials
|
|
10
11
|
from .server import create_mcp_server
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
14
|
+
"InMemoryTokenStorage",
|
|
13
15
|
"MCPTool",
|
|
14
16
|
"create_mcp_server",
|
|
15
17
|
"mcp_http_tools",
|
|
16
18
|
"mcp_stdio_tools",
|
|
17
19
|
"mcp_tools",
|
|
20
|
+
"oauth_client_credentials",
|
|
18
21
|
]
|
|
@@ -7,7 +7,7 @@ the `mcp` extra.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from collections.abc import AsyncGenerator
|
|
10
|
-
from contextlib import asynccontextmanager
|
|
10
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from .._extras import require_extra
|
|
@@ -89,16 +89,45 @@ async def mcp_stdio_tools(
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
@asynccontextmanager
|
|
92
|
-
async def mcp_http_tools(
|
|
93
|
-
|
|
92
|
+
async def mcp_http_tools(
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
headers: dict[str, str] | None = None,
|
|
96
|
+
auth: Any = None,
|
|
97
|
+
) -> AsyncGenerator[list[Tool], None]:
|
|
98
|
+
"""Connects to an MCP server over streamable HTTP and yields its tools as
|
|
99
|
+
`Tool`s.
|
|
100
|
+
|
|
101
|
+
Pass `headers` for servers that require a static credential (e.g.
|
|
102
|
+
`{"Authorization": "Bearer ..."}`), or `auth` (an `httpx2.Auth`) for a
|
|
103
|
+
server that requires OAuth — see `reactifact.mcp.oauth_client_credentials`
|
|
104
|
+
for the machine-to-machine case. The SDK's `streamable_http_client` has
|
|
105
|
+
no `headers=`/`auth=` kwarg of its own; the documented way is a
|
|
106
|
+
pre-configured client, which this builds (and owns/closes) with the same
|
|
107
|
+
recommended timeouts the SDK's own default client uses (30s
|
|
108
|
+
connect/write/pool, 300s read — a server may hold a response stream
|
|
109
|
+
open). Note this must be `httpx2.AsyncClient` (the MCP SDK's own httpx
|
|
110
|
+
fork/dependency, not plain `httpx` — `streamable_http_client` rejects
|
|
111
|
+
the wrong one at the type level), imported lazily here since it's only
|
|
112
|
+
guaranteed installed alongside the `mcp` extra.
|
|
113
|
+
"""
|
|
94
114
|
require_extra("mcp_http_tools", "mcp", "mcp")
|
|
115
|
+
import httpx2
|
|
95
116
|
from mcp.client.streamable_http import streamable_http_client
|
|
96
117
|
|
|
97
118
|
from mcp import ClientSession
|
|
98
119
|
|
|
99
|
-
async with (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
async with AsyncExitStack() as stack:
|
|
121
|
+
http_client = None
|
|
122
|
+
if headers is not None or auth is not None:
|
|
123
|
+
http_client = await stack.enter_async_context(
|
|
124
|
+
httpx2.AsyncClient(
|
|
125
|
+
headers=headers, auth=auth, timeout=httpx2.Timeout(30.0, read=300.0)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
read, write = await stack.enter_async_context(
|
|
129
|
+
streamable_http_client(url, http_client=http_client)
|
|
130
|
+
)
|
|
131
|
+
session = await stack.enter_async_context(ClientSession(read, write))
|
|
103
132
|
await session.initialize()
|
|
104
133
|
yield await mcp_tools(session)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""OAuth 2.1 for the MCP client: machine-to-machine auth (client_credentials)
|
|
2
|
+
for MCP servers that require it.
|
|
3
|
+
|
|
4
|
+
Scoped to `client_credentials` only — the flow that needs no browser or
|
|
5
|
+
human consent, which covers an agent authenticating to a server on its own
|
|
6
|
+
behalf. The authorization-code flow (a person granting consent through a
|
|
7
|
+
browser redirect) needs a `redirect_handler`/`callback_handler` wired to
|
|
8
|
+
whatever hosts the app (CLI, a script, a web app), which is host-specific
|
|
9
|
+
plumbing outside a headless library's scope; use
|
|
10
|
+
`mcp.client.auth.OAuthClientProvider` directly if you need it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
from .._extras import require_extra
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InMemoryTokenStorage:
|
|
21
|
+
"""The MCP SDK's `TokenStorage` protocol, held in process memory only.
|
|
22
|
+
|
|
23
|
+
Tokens (and refreshes) live only as long as this object — nothing is
|
|
24
|
+
written to disk. Fine for a long-running process reusing one provider
|
|
25
|
+
across calls; pass your own `TokenStorage` implementation instead if you
|
|
26
|
+
need tokens to survive a restart.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._tokens: Any = None
|
|
31
|
+
self._client_info: Any = None
|
|
32
|
+
|
|
33
|
+
async def get_tokens(self) -> Any:
|
|
34
|
+
return self._tokens
|
|
35
|
+
|
|
36
|
+
async def set_tokens(self, tokens: Any) -> None:
|
|
37
|
+
self._tokens = tokens
|
|
38
|
+
|
|
39
|
+
async def get_client_info(self) -> Any:
|
|
40
|
+
return self._client_info
|
|
41
|
+
|
|
42
|
+
async def set_client_info(self, client_info: Any) -> None:
|
|
43
|
+
self._client_info = client_info
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def oauth_client_credentials(
|
|
47
|
+
server_url: str,
|
|
48
|
+
*,
|
|
49
|
+
client_id: str,
|
|
50
|
+
client_secret: str,
|
|
51
|
+
issuer: str,
|
|
52
|
+
scope: str | None = None,
|
|
53
|
+
token_endpoint_auth_method: Literal[
|
|
54
|
+
"client_secret_basic", "client_secret_post"
|
|
55
|
+
] = "client_secret_basic",
|
|
56
|
+
storage: Any = None,
|
|
57
|
+
) -> Any:
|
|
58
|
+
"""Builds an `httpx2.Auth` that authenticates to `server_url` via OAuth's
|
|
59
|
+
`client_credentials` grant — pass it as `auth=` to `mcp_http_tools`.
|
|
60
|
+
|
|
61
|
+
`issuer` names the authorization server `client_id`/`client_secret`
|
|
62
|
+
belong to: the token request is only ever built from that server's
|
|
63
|
+
discovered metadata, so a compromised or misconfigured MCP server can't
|
|
64
|
+
redirect the credential exchange elsewhere. Requires the `mcp` extra.
|
|
65
|
+
|
|
66
|
+
from reactifact.mcp import mcp_http_tools, oauth_client_credentials
|
|
67
|
+
|
|
68
|
+
auth = oauth_client_credentials(
|
|
69
|
+
"https://mcp.example.com",
|
|
70
|
+
client_id="...",
|
|
71
|
+
client_secret="...",
|
|
72
|
+
issuer="https://auth.example.com",
|
|
73
|
+
)
|
|
74
|
+
async with mcp_http_tools("https://mcp.example.com", auth=auth) as tools:
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
`storage` defaults to a fresh `InMemoryTokenStorage()` (tokens live only
|
|
78
|
+
for this process) — pass your own `TokenStorage` to persist/reuse tokens
|
|
79
|
+
across runs.
|
|
80
|
+
"""
|
|
81
|
+
require_extra("oauth_client_credentials", "mcp", "mcp")
|
|
82
|
+
from mcp.client.auth.extensions.client_credentials import (
|
|
83
|
+
ClientCredentialsOAuthProvider,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ClientCredentialsOAuthProvider(
|
|
87
|
+
server_url,
|
|
88
|
+
storage=storage if storage is not None else InMemoryTokenStorage(),
|
|
89
|
+
client_id=client_id,
|
|
90
|
+
client_secret=client_secret,
|
|
91
|
+
token_endpoint_auth_method=token_endpoint_auth_method,
|
|
92
|
+
scope=scope,
|
|
93
|
+
issuer=issuer,
|
|
94
|
+
)
|
|
@@ -13,14 +13,8 @@ for one of these first:
|
|
|
13
13
|
decorator, both signatures recognized by parameter name — pick this for a
|
|
14
14
|
short, one-off produce with no class ceremony.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `Produce(Model, factory=fn)` — a bare two-argument-factory constructor
|
|
19
|
-
kwarg, **deprecated** (raises `DeprecationWarning`). It predates the
|
|
20
|
-
`@produce` decorator, only supports `(context, inputs[, event]) -> Model |
|
|
21
|
-
list | Patch | None`, and can't see the effects slot at all — strictly
|
|
22
|
-
weaker than `@produce(Model)`, which covers the same signature plus
|
|
23
|
-
`effects`. Kept only so old code doesn't break; port it to `@produce`.
|
|
16
|
+
One other thing `Produce`/`Agent` accepts is *not* on that list on purpose:
|
|
17
|
+
|
|
24
18
|
- Overriding `Agent.run(self, event, context) -> Patch | None` directly,
|
|
25
19
|
bypassing `Produce` entirely (though not necessarily `effects` — the
|
|
26
20
|
runtime still merges whatever `current_effects()` collected during the
|
|
@@ -36,7 +30,6 @@ from __future__ import annotations
|
|
|
36
30
|
|
|
37
31
|
import asyncio
|
|
38
32
|
import inspect
|
|
39
|
-
import warnings
|
|
40
33
|
from collections.abc import Callable
|
|
41
34
|
from typing import Any, Generic, TypeVar, get_args, get_origin
|
|
42
35
|
|
|
@@ -71,43 +64,13 @@ class Produce(Generic[TOut]):
|
|
|
71
64
|
return
|
|
72
65
|
|
|
73
66
|
artifact_type: ArtifactType | None = None
|
|
74
|
-
factory: Callable[..., Any] | None = None
|
|
75
67
|
|
|
76
|
-
def __init__(
|
|
77
|
-
self,
|
|
78
|
-
artifact_type: ArtifactType | None = None,
|
|
79
|
-
factory: Callable[..., Any] | None = None,
|
|
80
|
-
):
|
|
68
|
+
def __init__(self, artifact_type: ArtifactType | None = None):
|
|
81
69
|
self.artifact_type = artifact_type or self.__class__.artifact_type
|
|
82
70
|
if self.artifact_type is None:
|
|
83
71
|
raise ValueError(
|
|
84
72
|
"artifact_type must be provided either as class attribute or constructor argument"
|
|
85
73
|
)
|
|
86
|
-
self.factory = factory if factory is not None else self.__class__.factory
|
|
87
|
-
if self.factory is not None:
|
|
88
|
-
warnings.warn(
|
|
89
|
-
"Produce(..., factory=...) is deprecated: it only supports "
|
|
90
|
-
"(context, inputs[, event]) -> Model | list | Patch | None and "
|
|
91
|
-
"can't see the effects slot (no `effects` param support). Use "
|
|
92
|
-
"the @produce(Type) decorator instead — same return-style "
|
|
93
|
-
"signature, plus optional effects/event params when you need "
|
|
94
|
-
"them. See the reactifact.produce module docstring.",
|
|
95
|
-
DeprecationWarning,
|
|
96
|
-
stacklevel=2,
|
|
97
|
-
)
|
|
98
|
-
self._accepts_event = False
|
|
99
|
-
if self.factory is not None:
|
|
100
|
-
try:
|
|
101
|
-
signature = inspect.signature(self.factory)
|
|
102
|
-
except (TypeError, ValueError):
|
|
103
|
-
signature = None
|
|
104
|
-
if signature is not None:
|
|
105
|
-
positional = [
|
|
106
|
-
p
|
|
107
|
-
for p in signature.parameters.values()
|
|
108
|
-
if not p.kind & inspect.Parameter.VAR_POSITIONAL
|
|
109
|
-
]
|
|
110
|
-
self._accepts_event = len(positional) >= 3
|
|
111
74
|
|
|
112
75
|
@property
|
|
113
76
|
def effects(self) -> Effects:
|
|
@@ -132,29 +95,22 @@ class Produce(Generic[TOut]):
|
|
|
132
95
|
inputs: list[Artifact[Any]],
|
|
133
96
|
event: Event | None = None,
|
|
134
97
|
) -> None:
|
|
135
|
-
"""
|
|
98
|
+
"""No-op by default (§24).
|
|
136
99
|
|
|
137
|
-
Subclass-style overrides write `self.effects.*` and return None;
|
|
138
|
-
|
|
139
|
-
|
|
100
|
+
Subclass-style overrides write `self.effects.*` and return None; a
|
|
101
|
+
`None` return means "no work". A bare `Produce(Model)` (no override)
|
|
102
|
+
is a valid, deliberate no-op — used e.g. to widen an agent's allowed
|
|
103
|
+
`Create` types when the actual write happens via `self.effects.ask(...)`
|
|
104
|
+
in another produce (see `examples/supervisor`).
|
|
140
105
|
"""
|
|
141
|
-
|
|
142
|
-
return None
|
|
143
|
-
|
|
144
|
-
if self._accepts_event:
|
|
145
|
-
result = self.factory(context, inputs, event)
|
|
146
|
-
else:
|
|
147
|
-
result = self.factory(context, inputs)
|
|
148
|
-
if asyncio.iscoroutine(result):
|
|
149
|
-
result = await result
|
|
150
|
-
|
|
151
|
-
self._apply_result(result)
|
|
106
|
+
return None
|
|
152
107
|
|
|
153
108
|
def _apply_result(self, result: Any) -> None:
|
|
154
|
-
"""Writes a
|
|
109
|
+
"""Writes a `@produce`-decorated function's return-style result into
|
|
110
|
+
the effect slot (§24).
|
|
155
111
|
|
|
156
|
-
`None` — nothing; a model or a list of models — creates; a Patch —
|
|
157
|
-
|
|
112
|
+
`None` — nothing; a model or a list of models — creates; a Patch — a
|
|
113
|
+
legacy escape (its operations are appended to the effects).
|
|
158
114
|
"""
|
|
159
115
|
if result is None:
|
|
160
116
|
return
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from .providers import EmbeddingProvider, LLMProvider
|
|
6
6
|
from .sources import Source
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .budget import Budget
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class RuntimeResources:
|
|
10
13
|
def __init__(
|
|
@@ -18,6 +21,13 @@ class RuntimeResources:
|
|
|
18
21
|
self.embedder = embedder
|
|
19
22
|
self.sources = sources or {}
|
|
20
23
|
self.additional = additional
|
|
24
|
+
# Set by Runtime per turn (not a constructor param — the runtime, not
|
|
25
|
+
# the caller, owns these): the active Budget and its wall-clock
|
|
26
|
+
# deadline, read back by ToolUse's own inner loop (§ tool_use.py) to
|
|
27
|
+
# enforce the tool-call/time budget between its own round-trips, not
|
|
28
|
+
# just at the top-level Runtime._budget_exhausted check.
|
|
29
|
+
self.budget: Budget | None = None
|
|
30
|
+
self.budget_deadline: float | None = None
|
|
21
31
|
|
|
22
32
|
def get_source(self, source_id: str) -> Source | None:
|
|
23
33
|
return self.sources.get(source_id)
|