reactifact 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. reactifact-0.6.0/LICENSE +21 -0
  2. reactifact-0.6.0/PKG-INFO +226 -0
  3. reactifact-0.6.0/README.md +201 -0
  4. reactifact-0.6.0/pyproject.toml +107 -0
  5. reactifact-0.6.0/reactifact/__init__.py +96 -0
  6. reactifact-0.6.0/reactifact/__main__.py +10 -0
  7. reactifact-0.6.0/reactifact/_extras.py +36 -0
  8. reactifact-0.6.0/reactifact/agents.py +173 -0
  9. reactifact-0.6.0/reactifact/artifacts.py +130 -0
  10. reactifact-0.6.0/reactifact/branching.py +255 -0
  11. reactifact-0.6.0/reactifact/budget.py +41 -0
  12. reactifact-0.6.0/reactifact/chat.py +373 -0
  13. reactifact-0.6.0/reactifact/checkpoints.py +329 -0
  14. reactifact-0.6.0/reactifact/cli/__init__.py +73 -0
  15. reactifact-0.6.0/reactifact/cli/branch.py +77 -0
  16. reactifact-0.6.0/reactifact/cli/common.py +67 -0
  17. reactifact-0.6.0/reactifact/cli/context.py +53 -0
  18. reactifact-0.6.0/reactifact/cli/graph.py +21 -0
  19. reactifact-0.6.0/reactifact/cli/replay.py +69 -0
  20. reactifact-0.6.0/reactifact/cli/scenario.py +94 -0
  21. reactifact-0.6.0/reactifact/cli/trace.py +45 -0
  22. reactifact-0.6.0/reactifact/commit.py +97 -0
  23. reactifact-0.6.0/reactifact/commit_log.py +235 -0
  24. reactifact-0.6.0/reactifact/consume.py +96 -0
  25. reactifact-0.6.0/reactifact/context.py +599 -0
  26. reactifact-0.6.0/reactifact/effects.py +232 -0
  27. reactifact-0.6.0/reactifact/eval.py +319 -0
  28. reactifact-0.6.0/reactifact/events.py +34 -0
  29. reactifact-0.6.0/reactifact/interrupt.py +22 -0
  30. reactifact-0.6.0/reactifact/llm_agent.py +172 -0
  31. reactifact-0.6.0/reactifact/operations.py +192 -0
  32. reactifact-0.6.0/reactifact/patches.py +112 -0
  33. reactifact-0.6.0/reactifact/produce.py +226 -0
  34. reactifact-0.6.0/reactifact/prompts.py +111 -0
  35. reactifact-0.6.0/reactifact/providers/__init__.py +153 -0
  36. reactifact-0.6.0/reactifact/providers/_retry.py +61 -0
  37. reactifact-0.6.0/reactifact/providers/anthropic.py +182 -0
  38. reactifact-0.6.0/reactifact/providers/azure.py +31 -0
  39. reactifact-0.6.0/reactifact/providers/cerebras.py +11 -0
  40. reactifact-0.6.0/reactifact/providers/chat.py +417 -0
  41. reactifact-0.6.0/reactifact/providers/contracts.py +105 -0
  42. reactifact-0.6.0/reactifact/providers/deepseek.py +11 -0
  43. reactifact-0.6.0/reactifact/providers/fake.py +40 -0
  44. reactifact-0.6.0/reactifact/providers/fireworks.py +17 -0
  45. reactifact-0.6.0/reactifact/providers/gemini.py +284 -0
  46. reactifact-0.6.0/reactifact/providers/github_models.py +13 -0
  47. reactifact-0.6.0/reactifact/providers/groq.py +18 -0
  48. reactifact-0.6.0/reactifact/providers/image.py +157 -0
  49. reactifact-0.6.0/reactifact/providers/mistral.py +17 -0
  50. reactifact-0.6.0/reactifact/providers/nvidia.py +18 -0
  51. reactifact-0.6.0/reactifact/providers/ollama.py +18 -0
  52. reactifact-0.6.0/reactifact/providers/openai.py +44 -0
  53. reactifact-0.6.0/reactifact/providers/openrouter.py +70 -0
  54. reactifact-0.6.0/reactifact/providers/perplexity.py +11 -0
  55. reactifact-0.6.0/reactifact/providers/qwen.py +17 -0
  56. reactifact-0.6.0/reactifact/providers/speech.py +347 -0
  57. reactifact-0.6.0/reactifact/providers/together.py +17 -0
  58. reactifact-0.6.0/reactifact/providers/video.py +407 -0
  59. reactifact-0.6.0/reactifact/providers/xai.py +11 -0
  60. reactifact-0.6.0/reactifact/providers/zai.py +11 -0
  61. reactifact-0.6.0/reactifact/py.typed +0 -0
  62. reactifact-0.6.0/reactifact/recipes/__init__.py +63 -0
  63. reactifact-0.6.0/reactifact/recipes/inputs.py +34 -0
  64. reactifact-0.6.0/reactifact/recipes/memory.py +166 -0
  65. reactifact-0.6.0/reactifact/recipes/resolve.py +51 -0
  66. reactifact-0.6.0/reactifact/recipes/rollback.py +87 -0
  67. reactifact-0.6.0/reactifact/recipes/search.py +81 -0
  68. reactifact-0.6.0/reactifact/recipes/skills.py +108 -0
  69. reactifact-0.6.0/reactifact/recipes/status.py +79 -0
  70. reactifact-0.6.0/reactifact/recipes/text.py +202 -0
  71. reactifact-0.6.0/reactifact/relations.py +104 -0
  72. reactifact-0.6.0/reactifact/replay.py +187 -0
  73. reactifact-0.6.0/reactifact/resources.py +45 -0
  74. reactifact-0.6.0/reactifact/runtime.py +498 -0
  75. reactifact-0.6.0/reactifact/scheduler.py +188 -0
  76. reactifact-0.6.0/reactifact/session.py +75 -0
  77. reactifact-0.6.0/reactifact/sources.py +498 -0
  78. reactifact-0.6.0/reactifact/streaming.py +58 -0
  79. reactifact-0.6.0/reactifact/structured.py +245 -0
  80. reactifact-0.6.0/reactifact/testing/__init__.py +48 -0
  81. reactifact-0.6.0/reactifact/testing/assertions.py +326 -0
  82. reactifact-0.6.0/reactifact/testing/exceptions.py +27 -0
  83. reactifact-0.6.0/reactifact/testing/fault.py +164 -0
  84. reactifact-0.6.0/reactifact/testing/lab.py +350 -0
  85. reactifact-0.6.0/reactifact/testing/mock.py +166 -0
  86. reactifact-0.6.0/reactifact/testing/record.py +50 -0
  87. reactifact-0.6.0/reactifact/testing/registry.py +87 -0
  88. reactifact-0.6.0/reactifact/tool_use.py +528 -0
  89. reactifact-0.6.0/reactifact/tools.py +111 -0
  90. reactifact-0.6.0/reactifact/tracing/__init__.py +29 -0
  91. reactifact-0.6.0/reactifact/tracing/langfuse.py +125 -0
  92. reactifact-0.6.0/reactifact/tracing/models.py +93 -0
  93. reactifact-0.6.0/reactifact/tracing/postgres.py +220 -0
  94. reactifact-0.6.0/reactifact/tracing/store.py +254 -0
  95. reactifact-0.6.0/reactifact/tracing/templates/ui.html +196 -0
  96. reactifact-0.6.0/reactifact/tracing/templates/ui_run.html +264 -0
  97. reactifact-0.6.0/reactifact/tracing/tracer.py +370 -0
  98. reactifact-0.6.0/reactifact/tracing/web.py +117 -0
  99. reactifact-0.6.0/reactifact/triggers.py +41 -0
  100. reactifact-0.6.0/reactifact/viz.py +248 -0
  101. reactifact-0.6.0/reactifact/web.py +117 -0
  102. reactifact-0.6.0/reactifact.egg-info/PKG-INFO +226 -0
  103. reactifact-0.6.0/reactifact.egg-info/SOURCES.txt +181 -0
  104. reactifact-0.6.0/reactifact.egg-info/dependency_links.txt +1 -0
  105. reactifact-0.6.0/reactifact.egg-info/entry_points.txt +2 -0
  106. reactifact-0.6.0/reactifact.egg-info/requires.txt +16 -0
  107. reactifact-0.6.0/reactifact.egg-info/top_level.txt +1 -0
  108. reactifact-0.6.0/setup.cfg +4 -0
  109. reactifact-0.6.0/tests/test_adaptive.py +168 -0
  110. reactifact-0.6.0/tests/test_anthropic_provider.py +110 -0
  111. reactifact-0.6.0/tests/test_artifacts.py +63 -0
  112. reactifact-0.6.0/tests/test_backbone.py +160 -0
  113. reactifact-0.6.0/tests/test_branching.py +155 -0
  114. reactifact-0.6.0/tests/test_budget.py +118 -0
  115. reactifact-0.6.0/tests/test_chat_web.py +294 -0
  116. reactifact-0.6.0/tests/test_checkpoint.py +107 -0
  117. reactifact-0.6.0/tests/test_checkpoints_concurrency.py +79 -0
  118. reactifact-0.6.0/tests/test_cli.py +159 -0
  119. reactifact-0.6.0/tests/test_cli_scenario.py +90 -0
  120. reactifact-0.6.0/tests/test_commit_log.py +157 -0
  121. reactifact-0.6.0/tests/test_concurrency.py +148 -0
  122. reactifact-0.6.0/tests/test_consumes_produces.py +69 -0
  123. reactifact-0.6.0/tests/test_devops.py +168 -0
  124. reactifact-0.6.0/tests/test_devops_web.py +149 -0
  125. reactifact-0.6.0/tests/test_effects.py +191 -0
  126. reactifact-0.6.0/tests/test_eval.py +234 -0
  127. reactifact-0.6.0/tests/test_forklab.py +125 -0
  128. reactifact-0.6.0/tests/test_forklab_web.py +74 -0
  129. reactifact-0.6.0/tests/test_friendly_api.py +82 -0
  130. reactifact-0.6.0/tests/test_gemini_provider.py +191 -0
  131. reactifact-0.6.0/tests/test_hitl.py +157 -0
  132. reactifact-0.6.0/tests/test_image_provider.py +56 -0
  133. reactifact-0.6.0/tests/test_invalidation.py +132 -0
  134. reactifact-0.6.0/tests/test_knowledge.py +341 -0
  135. reactifact-0.6.0/tests/test_knowledge_web.py +123 -0
  136. reactifact-0.6.0/tests/test_llm_ladder.py +108 -0
  137. reactifact-0.6.0/tests/test_medic_lab.py +220 -0
  138. reactifact-0.6.0/tests/test_medic_lab_web.py +54 -0
  139. reactifact-0.6.0/tests/test_multisource.py +174 -0
  140. reactifact-0.6.0/tests/test_openai_provider.py +123 -0
  141. reactifact-0.6.0/tests/test_patches.py +45 -0
  142. reactifact-0.6.0/tests/test_ports.py +110 -0
  143. reactifact-0.6.0/tests/test_produce_styles.py +216 -0
  144. reactifact-0.6.0/tests/test_prompts.py +102 -0
  145. reactifact-0.6.0/tests/test_provider_auth.py +302 -0
  146. reactifact-0.6.0/tests/test_provider_retry.py +185 -0
  147. reactifact-0.6.0/tests/test_providers_integration.py +51 -0
  148. reactifact-0.6.0/tests/test_recipes.py +231 -0
  149. reactifact-0.6.0/tests/test_recipes_inputs.py +52 -0
  150. reactifact-0.6.0/tests/test_recipes_memory.py +105 -0
  151. reactifact-0.6.0/tests/test_recipes_skills.py +69 -0
  152. reactifact-0.6.0/tests/test_relation_graph.py +70 -0
  153. reactifact-0.6.0/tests/test_relations.py +163 -0
  154. reactifact-0.6.0/tests/test_repair.py +555 -0
  155. reactifact-0.6.0/tests/test_repair_web.py +154 -0
  156. reactifact-0.6.0/tests/test_replay.py +159 -0
  157. reactifact-0.6.0/tests/test_research.py +81 -0
  158. reactifact-0.6.0/tests/test_resources.py +69 -0
  159. reactifact-0.6.0/tests/test_retry.py +121 -0
  160. reactifact-0.6.0/tests/test_runtime.py +144 -0
  161. reactifact-0.6.0/tests/test_runtime_errors.py +108 -0
  162. reactifact-0.6.0/tests/test_sessions.py +153 -0
  163. reactifact-0.6.0/tests/test_sources.py +18 -0
  164. reactifact-0.6.0/tests/test_sources_search.py +49 -0
  165. reactifact-0.6.0/tests/test_sources_vector.py +87 -0
  166. reactifact-0.6.0/tests/test_speech_provider.py +95 -0
  167. reactifact-0.6.0/tests/test_streaming.py +68 -0
  168. reactifact-0.6.0/tests/test_structured.py +235 -0
  169. reactifact-0.6.0/tests/test_testing_assertions.py +102 -0
  170. reactifact-0.6.0/tests/test_testing_lab.py +185 -0
  171. reactifact-0.6.0/tests/test_testing_mock.py +272 -0
  172. reactifact-0.6.0/tests/test_testing_registry.py +39 -0
  173. reactifact-0.6.0/tests/test_tools.py +485 -0
  174. reactifact-0.6.0/tests/test_tracing.py +553 -0
  175. reactifact-0.6.0/tests/test_vendor_factories.py +57 -0
  176. reactifact-0.6.0/tests/test_vendor_multimodal_factories.py +81 -0
  177. reactifact-0.6.0/tests/test_video_provider.py +222 -0
  178. reactifact-0.6.0/tests/test_view.py +69 -0
  179. reactifact-0.6.0/tests/test_viz.py +183 -0
  180. reactifact-0.6.0/tests/test_web_source.py +93 -0
  181. reactifact-0.6.0/tests/test_workspace.py +30 -0
  182. reactifact-0.6.0/tests/test_workspace_with_sources.py +53 -0
  183. reactifact-0.6.0/tests/tests_checkpoints_sqlite.py +40 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bzdv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: reactifact
3
+ Version: 0.6.0
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
27
+
28
+ **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
29
+
30
+ [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
31
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
32
+ [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
33
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
34
+
35
+ Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
36
+ declare control flow. But a knowledge question — *"why did infra costs jump in
37
+ Q2?"* — needs Confluence + GitLab + CSV + calculations + verification, and the
38
+ *next* question needs a different path. There is no universal graph to draw.
39
+
40
+ reactifact flips the model. You describe **what artifacts exist and what agents can
41
+ do with them**; the runtime derives what runs next from **state changes**. Agents
42
+ react to events — there is no graph, no node pipeline.
43
+
44
+ ```bash
45
+ pip install reactifact
46
+ ```
47
+
48
+ Runs offline, no API key needed — paste this straight into a `.py` file. Two
49
+ agents, no graph edge declared between them — the second reacts because the
50
+ first one's output exists, and the answer carries *proof* of where it came
51
+ from:
52
+
53
+ ```python
54
+ from pydantic import BaseModel
55
+
56
+ from reactifact import Budget, Consume, Context, Runtime, RuntimeResources, create_agent, produce
57
+
58
+
59
+ class Question(BaseModel):
60
+ text: str
61
+
62
+
63
+ class Evidence(BaseModel):
64
+ text: str
65
+
66
+
67
+ class Answer(BaseModel):
68
+ text: str
69
+
70
+
71
+ DOCS = {
72
+ "refund": "Refunds are available within 14 days of purchase.",
73
+ "pricing": "The Pro plan is $49/month, billed annually.",
74
+ }
75
+
76
+
77
+ @produce(Evidence)
78
+ async def find_evidence(context, inputs, event, effects):
79
+ question = next((a for a in inputs if isinstance(a.data, Question)), None)
80
+ if question is None:
81
+ return None
82
+ hit = next((v for k, v in DOCS.items() if k in question.data.text.lower()), None)
83
+ if hit is not None:
84
+ effects.create(Evidence(text=hit))
85
+
86
+
87
+ @produce(Answer)
88
+ async def answer_from_evidence(context, inputs, event, effects):
89
+ evidence = next((a for a in inputs if isinstance(a.data, Evidence)), None)
90
+ if evidence is None:
91
+ return None
92
+ effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
93
+
94
+
95
+ search_agent = create_agent("search", consumes=[Consume(Question)], produces=[find_evidence])
96
+ answer_agent = create_agent("answer", consumes=[Consume(Evidence)], produces=[answer_from_evidence])
97
+
98
+ ctx = Context(resources=RuntimeResources())
99
+ runtime = Runtime(ctx, agents=[search_agent, answer_agent], budget=Budget(max_runs=10))
100
+
101
+ ctx.create(Question(text="what's your refund policy?"))
102
+ runtime.run() # search_agent and answer_agent both react — nobody wired them together
103
+
104
+ answer = ctx.latest(Answer)
105
+ evidence = ctx.related(answer.id, "supported_by")[0]
106
+ print(answer.data.text) # "Refunds are available within 14 days of purchase."
107
+ print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
108
+ ```
109
+
110
+ ## How it works
111
+
112
+ ```text
113
+ ARTIFACT CREATED / UPDATED
114
+
115
+
116
+ AGENTS REACT ──self.effects──► Effects ──compile──► Patch
117
+ ▲ │
118
+ └──────────────────────────────────────────────────────┘
119
+ Context v+1
120
+ ```
121
+
122
+ A produce writes what should change (`self.effects.create/update/link/ask`) and
123
+ returns `None`; the runtime compiles the effect set into one **atomic** `Patch`
124
+ and moves the context to the next version. The `event` that wakes an agent is
125
+ *derived* from that same change — the causal chain can never drift from the
126
+ actual state.
127
+
128
+ ## What makes it different
129
+
130
+ | Traditional agent (LangGraph / CrewAI / LangChain) | reactifact |
131
+ | --- | --- |
132
+ | A program follows a graph / plan | Agents **react** to state changes |
133
+ | Messages are strings | **Typed, versioned artifacts** (`Claim`, `Evidence`, `Answer`) |
134
+ | Orchestration is explicit wiring | Orchestration **falls out of the state** |
135
+ | A unit of work *returns* a change | An agent **writes effects**; the runtime compiles them |
136
+ | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
137
+ | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
138
+ | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
139
+
140
+ Reactive. Deterministic. Accountable.
141
+
142
+ Full breakdown, including where reactifact is *not* the right choice:
143
+ [docs/en/comparison.md](docs/en/comparison.md).
144
+
145
+ ## Core primitives
146
+
147
+ - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
148
+ - **Artifact** — a first-class typed object (`Claim`, `Evidence`, `Answer`), not a string blob.
149
+ - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
150
+ - **Patch** — the compiled, validated change-set applied as one atomic commit.
151
+ - **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, filesystem are equally first-class.
153
+ - **Provenance** — every derived artifact links to what produced it
154
+ (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
155
+ - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
156
+
157
+ ## In the box
158
+
159
+ - **Deterministic by design** — calculations over structured data, honest `None`
160
+ fallbacks instead of hallucinated answers; the model reasons, never "knows".
161
+ - **Observability** — every run traces agent spans, reads/writes, LLM calls,
162
+ tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
163
+ - **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
164
+ - **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
165
+ `ReplayLLM`, all for audit and safe alternative states.
166
+ - **Sessions** — `SessionStore` over `FileKVBackend`/`SQLiteKVBackend`
167
+ (and `PostgreSQLKVBackend`) for durable chat memory.
168
+ - **Web layer** — `ChatAssistant` + `create_chat_router` mount a canonical SSE
169
+ chat on *your* FastAPI app; errors degrade to a logged fallback, never a 500.
170
+ - **Recipes** — `find`/`find_all` (typed lookup in `inputs`), `fan_out_sources`,
171
+ `materialize_doc`, `StatusMachine`, `WindowSummarizer`/`WindowPruner`
172
+ (bounded conversation memory), change→rebuild rollback helpers,
173
+ `Skill`/`match_skills` (Claude-Skills-shaped instructions, keyword-triggered)
174
+ — pure and LLM-free, except the summarizer, which takes your callback.
175
+ - **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
176
+ `reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
177
+
178
+ ## Run a demo
179
+
180
+ Offline-capable, no API keys required (deterministic fallbacks):
181
+
182
+ ```bash
183
+ uv run python ./examples/llm_ladder/level1.py # the simplest LLM turn (offline too)
184
+ uv run python ./examples/repair/web.py # room renovation: plan, estimate, CSV export
185
+ uv run python ./examples/devops/web.py # HITL ops assistant + trace dashboard
186
+ ```
187
+
188
+ Classic-pattern ports run as one-liners too:
189
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
190
+
191
+ ## Examples (in-repo, not shipped)
192
+
193
+ - `knowledge` — multi-source chat: search → evidence → claim verification → answer, with CSV calculation.
194
+ - `research` — goes to the web (`WebSource`): lazy page fetch → evidence → verified claims → answer with URL provenance.
195
+ - `medic-lab` — hypothesis laboratory: competing hypotheses scored, HITL steering, honest report.
196
+ - `devops` — HITL tool agents + LLM tool router + trace dashboard.
197
+ - `repair` — budget-aware replanning (chat/data in Russian by design).
198
+ - `forklab` — deterministic branch & merge: two strategies on their own forks, three-way merge.
199
+ - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
200
+ - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
201
+ - `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)).
203
+
204
+ ## Documentation
205
+
206
+ - [English](docs/en/index.md) · [Русский](docs/ru/index.md) — concepts, sources,
207
+ providers, recipes, patterns, observability, eval, branching, replay, viz/CLI, API.
208
+ - [Quickstart](docs/en/quickstart.md) — three runnable snippets: tool-calling
209
+ agent, retrieval over your docs, session-persisted chat bot.
210
+ - [Why reactifact](docs/en/why-reactifact.md) — the *design argument*: why effects, why no graph, why determinism.
211
+ - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
212
+ - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
213
+ - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ uv sync --extra dev --extra web
219
+ .venv/bin/python -m pytest
220
+ .venv/bin/mypy
221
+ .venv/bin/ruff check
222
+ ```
223
+
224
+ ## License
225
+
226
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,201 @@
1
+ # reactifact
2
+
3
+ **Stop drawing the graph. Build agents as reactions to versioned, provable artifacts.**
4
+
5
+ [![CI](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml/badge.svg)](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
6
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://github.com/bzdvdn/reactifact)
7
+ [![PyPI version](https://img.shields.io/pypi/v/reactifact)](https://pypi.org/project/reactifact/)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
9
+
10
+ Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
11
+ declare control flow. But a knowledge question — *"why did infra costs jump in
12
+ Q2?"* — needs Confluence + GitLab + CSV + calculations + verification, and the
13
+ *next* question needs a different path. There is no universal graph to draw.
14
+
15
+ reactifact flips the model. You describe **what artifacts exist and what agents can
16
+ do with them**; the runtime derives what runs next from **state changes**. Agents
17
+ react to events — there is no graph, no node pipeline.
18
+
19
+ ```bash
20
+ pip install reactifact
21
+ ```
22
+
23
+ Runs offline, no API key needed — paste this straight into a `.py` file. Two
24
+ agents, no graph edge declared between them — the second reacts because the
25
+ first one's output exists, and the answer carries *proof* of where it came
26
+ from:
27
+
28
+ ```python
29
+ from pydantic import BaseModel
30
+
31
+ from reactifact import Budget, Consume, Context, Runtime, RuntimeResources, create_agent, produce
32
+
33
+
34
+ class Question(BaseModel):
35
+ text: str
36
+
37
+
38
+ class Evidence(BaseModel):
39
+ text: str
40
+
41
+
42
+ class Answer(BaseModel):
43
+ text: str
44
+
45
+
46
+ DOCS = {
47
+ "refund": "Refunds are available within 14 days of purchase.",
48
+ "pricing": "The Pro plan is $49/month, billed annually.",
49
+ }
50
+
51
+
52
+ @produce(Evidence)
53
+ async def find_evidence(context, inputs, event, effects):
54
+ question = next((a for a in inputs if isinstance(a.data, Question)), None)
55
+ if question is None:
56
+ return None
57
+ hit = next((v for k, v in DOCS.items() if k in question.data.text.lower()), None)
58
+ if hit is not None:
59
+ effects.create(Evidence(text=hit))
60
+
61
+
62
+ @produce(Answer)
63
+ async def answer_from_evidence(context, inputs, event, effects):
64
+ evidence = next((a for a in inputs if isinstance(a.data, Evidence)), None)
65
+ if evidence is None:
66
+ return None
67
+ effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
68
+
69
+
70
+ search_agent = create_agent("search", consumes=[Consume(Question)], produces=[find_evidence])
71
+ answer_agent = create_agent("answer", consumes=[Consume(Evidence)], produces=[answer_from_evidence])
72
+
73
+ ctx = Context(resources=RuntimeResources())
74
+ runtime = Runtime(ctx, agents=[search_agent, answer_agent], budget=Budget(max_runs=10))
75
+
76
+ ctx.create(Question(text="what's your refund policy?"))
77
+ runtime.run() # search_agent and answer_agent both react — nobody wired them together
78
+
79
+ answer = ctx.latest(Answer)
80
+ evidence = ctx.related(answer.id, "supported_by")[0]
81
+ print(answer.data.text) # "Refunds are available within 14 days of purchase."
82
+ print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
83
+ ```
84
+
85
+ ## How it works
86
+
87
+ ```text
88
+ ARTIFACT CREATED / UPDATED
89
+
90
+
91
+ AGENTS REACT ──self.effects──► Effects ──compile──► Patch
92
+ ▲ │
93
+ └──────────────────────────────────────────────────────┘
94
+ Context v+1
95
+ ```
96
+
97
+ A produce writes what should change (`self.effects.create/update/link/ask`) and
98
+ returns `None`; the runtime compiles the effect set into one **atomic** `Patch`
99
+ and moves the context to the next version. The `event` that wakes an agent is
100
+ *derived* from that same change — the causal chain can never drift from the
101
+ actual state.
102
+
103
+ ## What makes it different
104
+
105
+ | Traditional agent (LangGraph / CrewAI / LangChain) | reactifact |
106
+ | --- | --- |
107
+ | A program follows a graph / plan | Agents **react** to state changes |
108
+ | Messages are strings | **Typed, versioned artifacts** (`Claim`, `Evidence`, `Answer`) |
109
+ | Orchestration is explicit wiring | Orchestration **falls out of the state** |
110
+ | A unit of work *returns* a change | An agent **writes effects**; the runtime compiles them |
111
+ | Retries/rollback are manual | Context is **git-like versioned** (diff, rollback, branch, merge) |
112
+ | "Who produced this?" is lost | **Provenance** links every derived artifact to its inputs |
113
+ | The model guesses the numbers | **Calculations are calculated** — the LLM is a reasoning component, not the source of truth |
114
+
115
+ Reactive. Deterministic. Accountable.
116
+
117
+ Full breakdown, including where reactifact is *not* the right choice:
118
+ [docs/en/comparison.md](docs/en/comparison.md).
119
+
120
+ ## Core primitives
121
+
122
+ - **Context** — versioned working state, git-like commits, `diff`/`rollback`/`merge`.
123
+ - **Artifact** — a first-class typed object (`Claim`, `Evidence`, `Answer`), not a string blob.
124
+ - **Effects** — an agent states its change via `self.effects.create/update/link/ask`; the runtime compiles it.
125
+ - **Patch** — the compiled, validated change-set applied as one atomic commit.
126
+ - **Agent** — a thin container declaring `consumes`/`produces`; logic lives in a `Produce`.
127
+ - **Source** — retrieval is a capability: vector search is *one* strategy; direct API, keyword, SQL, filesystem are equally first-class.
128
+ - **Provenance** — every derived artifact links to what produced it
129
+ (`Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc`).
130
+ - **HITL** — humans as `effects.ask(...)` → `PendingQuestion`, answered via `effects.resume(...)` like any agent.
131
+
132
+ ## In the box
133
+
134
+ - **Deterministic by design** — calculations over structured data, honest `None`
135
+ fallbacks instead of hallucinated answers; the model reasons, never "knows".
136
+ - **Observability** — every run traces agent spans, reads/writes, LLM calls,
137
+ tokens: SQLite store + web dashboard, exportable to Langfuse/Postgres (async sinks).
138
+ - **Budgets & replanning** — cap by runs/time/iterations/tool-calls, replan on decline.
139
+ - **Branching & replay** — `context.branch()`, three-way `merge()`, deterministic
140
+ `ReplayLLM`, all for audit and safe alternative states.
141
+ - **Sessions** — `SessionStore` over `FileKVBackend`/`SQLiteKVBackend`
142
+ (and `PostgreSQLKVBackend`) for durable chat memory.
143
+ - **Web layer** — `ChatAssistant` + `create_chat_router` mount a canonical SSE
144
+ chat on *your* FastAPI app; errors degrade to a logged fallback, never a 500.
145
+ - **Recipes** — `find`/`find_all` (typed lookup in `inputs`), `fan_out_sources`,
146
+ `materialize_doc`, `StatusMachine`, `WindowSummarizer`/`WindowPruner`
147
+ (bounded conversation memory), change→rebuild rollback helpers,
148
+ `Skill`/`match_skills` (Claude-Skills-shaped instructions, keyword-triggered)
149
+ — pure and LLM-free, except the summarizer, which takes your callback.
150
+ - **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
151
+ `reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
152
+
153
+ ## Run a demo
154
+
155
+ Offline-capable, no API keys required (deterministic fallbacks):
156
+
157
+ ```bash
158
+ uv run python ./examples/llm_ladder/level1.py # the simplest LLM turn (offline too)
159
+ uv run python ./examples/repair/web.py # room renovation: plan, estimate, CSV export
160
+ uv run python ./examples/devops/web.py # HITL ops assistant + trace dashboard
161
+ ```
162
+
163
+ Classic-pattern ports run as one-liners too:
164
+ `python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,adaptive,ledger}.main`.
165
+
166
+ ## Examples (in-repo, not shipped)
167
+
168
+ - `knowledge` — multi-source chat: search → evidence → claim verification → answer, with CSV calculation.
169
+ - `research` — goes to the web (`WebSource`): lazy page fetch → evidence → verified claims → answer with URL provenance.
170
+ - `medic-lab` — hypothesis laboratory: competing hypotheses scored, HITL steering, honest report.
171
+ - `devops` — HITL tool agents + LLM tool router + trace dashboard.
172
+ - `repair` — budget-aware replanning (chat/data in Russian by design).
173
+ - `forklab` — deterministic branch & merge: two strategies on their own forks, three-way merge.
174
+ - `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
175
+ - `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
176
+ - `adaptive` — hybrid scheduler: rule filters + deterministic rank + LLM tie-break + `rank_limit`.
177
+ - `{reflection,map_reduce,supervisor,summarize,time_travel}` — canonical ports (see [port-matrix](docs/en/port-matrix.md)).
178
+
179
+ ## Documentation
180
+
181
+ - [English](docs/en/index.md) · [Русский](docs/ru/index.md) — concepts, sources,
182
+ providers, recipes, patterns, observability, eval, branching, replay, viz/CLI, API.
183
+ - [Quickstart](docs/en/quickstart.md) — three runnable snippets: tool-calling
184
+ agent, retrieval over your docs, session-persisted chat bot.
185
+ - [Why reactifact](docs/en/why-reactifact.md) — the *design argument*: why effects, why no graph, why determinism.
186
+ - [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
187
+ - [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
188
+ - [docs/constitution.md](docs/constitution.md) — the full design rationale and invariants.
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ uv sync --extra dev --extra web
194
+ .venv/bin/python -m pytest
195
+ .venv/bin/mypy
196
+ .venv/bin/ruff check
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "reactifact"
7
+ version = "0.6.0"
8
+ description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "pydantic>=2.13.4",
13
+ "httpx>=0.27",
14
+ "python-dotenv>=1.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/bzdvdn/reactifact"
19
+ Repository = "https://github.com/bzdvdn/reactifact"
20
+ Documentation = "https://github.com/bzdvdn/reactifact/tree/master/docs"
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "ruff>=0.8",
25
+ "mypy>=1.11",
26
+ "pytest>=9.1.1",
27
+ "pytest-cov>=7.1.0",
28
+ ]
29
+ web = [
30
+ "fastapi>=0.115",
31
+ "uvicorn[standard]>=0.30",
32
+ ]
33
+ pg = [
34
+ "psycopg[binary]>=3.2",
35
+ ]
36
+
37
+ # uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
38
+ # deps by default (no `--extra` flag needed to boot the examples). Extras stay
39
+ # for PyPI (`pip install reactifact[web]`).
40
+ [dependency-groups]
41
+ dev = [
42
+ "ruff>=0.8",
43
+ "mypy>=1.11",
44
+ "pytest>=9.1.1",
45
+ "pytest-cov>=7.1.0",
46
+ ]
47
+ web = [
48
+ "fastapi>=0.115",
49
+ "uvicorn[standard]>=0.30",
50
+ ]
51
+ pg = [
52
+ "psycopg[binary]>=3.2",
53
+ ]
54
+
55
+ [tool.uv]
56
+ default-groups = ["dev", "web"]
57
+
58
+ # Публикация: в wheel едет только пакет `reactifact`; examples/tests остаются
59
+ # в репозитории для демо и разработки, но не попадают в дистрибутив.
60
+ [project.scripts]
61
+ reactifact = "reactifact.__main__:main"
62
+
63
+ [tool.setuptools]
64
+ packages = { find = { where = ["."], include = ["reactifact*"], exclude = ["examples*", "tests*"] } }
65
+
66
+ [tool.setuptools.package-data]
67
+ reactifact = ["py.typed"]
68
+ "reactifact.tracing" = ["templates/*.html"]
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+
73
+ [tool.ruff]
74
+ target-version = "py311"
75
+ line-length = 88
76
+ src = ["reactifact", "examples"]
77
+
78
+ [tool.ruff.lint]
79
+ select = [
80
+ "E", # pycodestyle errors
81
+ "F", # pyflakes
82
+ "I", # isort
83
+ "B", # bugbear
84
+ "UP", # pyupgrade
85
+ "SIM",# simplify
86
+ ]
87
+ ignore = [
88
+ "E501", # line length handled by formatter; long prompts allowed
89
+ ]
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "__init__.py" = ["F401"]
93
+ # entry-скрипты: bootstrap sys.path перед импортом пакета src
94
+ "web.py" = ["E402"]
95
+ "repair/chat.py" = ["E402"]
96
+ "assistant_chat.py" = ["E402"]
97
+ "run_example.py" = ["E402"] # пакет-реэксорт: импорты — публичный API
98
+
99
+ [tool.ruff.format]
100
+ # Документы (docs/constitution.md и пр.) не трогаем форматером
101
+ exclude = ["*.md"]
102
+
103
+ [tool.mypy]
104
+ python_version = "3.11"
105
+ packages = ["reactifact", "examples"]
106
+ strict = true
107
+ warn_unused_ignores = true
@@ -0,0 +1,96 @@
1
+ """reactifact's core public API.
2
+
3
+ Deliberately small: the primitives from the README's "Core primitives"
4
+ section, plus the everyday building blocks (tool calling, sessions, the LLM
5
+ provider protocol) most agents need regardless of what else they use.
6
+
7
+ Everything else — eval, tracing, checkpoint/branch backends beyond the
8
+ in-memory default, the chat/web layer, the adaptive scheduler, replay,
9
+ structured-LLM helpers, viz, prompts — is one level down, in its own
10
+ submodule (`reactifact.eval`, `reactifact.tracing`, `reactifact.chat`, ...). Import it
11
+ from there:
12
+
13
+ from reactifact.structured import structured_llm
14
+ from reactifact.tracing import TraceStore
15
+ from reactifact.chat import ChatAssistant
16
+
17
+ This keeps `dir(reactifact)` / editor autocomplete to what you need to build a
18
+ first agent, and keeps optional-dependency features (Postgres, FastAPI) out
19
+ of the names you see by default even though they were always cheap to import
20
+ (the driver itself is still lazily imported inside the class, see
21
+ `reactifact._extras`).
22
+ """
23
+
24
+ from .agents import Agent, create_agent
25
+ from .artifacts import Artifact
26
+ from .branching import MergeConflict
27
+ from .budget import Budget, RunOutcome, RunStats
28
+ from .consume import Consume, consume
29
+ from .context import Context, View
30
+ from .effects import Effects, Handle
31
+ from .events import Event, EventType
32
+ from .interrupt import PendingQuestion
33
+ from .patches import Create, Delete, Link, Patch, Relation, Unlink, Update
34
+ from .produce import Produce, produce
35
+ from .providers import (
36
+ EmbeddingProvider,
37
+ FakeEmbedder,
38
+ FakeLLM,
39
+ LLMProvider,
40
+ LLMRequest,
41
+ LLMResponse,
42
+ LLMResponseChunk,
43
+ Message,
44
+ )
45
+ from .resources import RuntimeResources
46
+ from .runtime import Runtime
47
+ from .session import Session, SessionStore
48
+ from .tools import FunctionTool, Tool, ToolOutput, tool
49
+ from .triggers import Trigger
50
+
51
+ __version__ = "0.6.0"
52
+
53
+ __all__ = [
54
+ "Agent",
55
+ "Artifact",
56
+ "Budget",
57
+ "Consume",
58
+ "Context",
59
+ "Create",
60
+ "Delete",
61
+ "EmbeddingProvider",
62
+ "Effects",
63
+ "Event",
64
+ "EventType",
65
+ "FakeEmbedder",
66
+ "FakeLLM",
67
+ "FunctionTool",
68
+ "Handle",
69
+ "Link",
70
+ "Message",
71
+ "MergeConflict",
72
+ "Patch",
73
+ "PendingQuestion",
74
+ "Produce",
75
+ "Relation",
76
+ "RunOutcome",
77
+ "RunStats",
78
+ "Runtime",
79
+ "RuntimeResources",
80
+ "LLMProvider",
81
+ "LLMRequest",
82
+ "LLMResponse",
83
+ "LLMResponseChunk",
84
+ "Session",
85
+ "SessionStore",
86
+ "Tool",
87
+ "ToolOutput",
88
+ "Trigger",
89
+ "Unlink",
90
+ "Update",
91
+ "View",
92
+ "consume",
93
+ "create_agent",
94
+ "produce",
95
+ "tool",
96
+ ]
@@ -0,0 +1,10 @@
1
+ """Console-script / `python -m reactifact` entry point — see `reactifact.cli`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())