reactifact 0.12.0__tar.gz → 0.13.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.12.0/reactifact.egg-info → reactifact-0.13.0}/PKG-INFO +51 -29
- {reactifact-0.12.0 → reactifact-0.13.0}/README.md +50 -28
- {reactifact-0.12.0 → reactifact-0.13.0}/pyproject.toml +8 -2
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/__init__.py +5 -2
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/artifacts.py +37 -12
- reactifact-0.13.0/reactifact/authz.py +112 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/branching.py +1 -0
- reactifact-0.13.0/reactifact/budget.py +162 -0
- reactifact-0.13.0/reactifact/cache.py +167 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/chat.py +108 -7
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/__init__.py +7 -2
- reactifact-0.13.0/reactifact/cli/eval.py +126 -0
- reactifact-0.13.0/reactifact/cli/scenario.py +192 -0
- reactifact-0.13.0/reactifact/cli/skills.py +173 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/commit_log.py +142 -16
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/context.py +76 -1
- reactifact-0.13.0/reactifact/eval/__init__.py +165 -0
- reactifact-0.13.0/reactifact/eval/dataset.py +144 -0
- reactifact-0.13.0/reactifact/eval/evaluators.py +208 -0
- reactifact-0.13.0/reactifact/eval/judge.py +223 -0
- reactifact-0.13.0/reactifact/eval/online.py +435 -0
- reactifact-0.13.0/reactifact/eval/runner.py +322 -0
- reactifact-0.12.0/reactifact/eval.py → reactifact-0.13.0/reactifact/eval/scoring.py +88 -5
- reactifact-0.13.0/reactifact/events.py +77 -0
- reactifact-0.13.0/reactifact/guardrails.py +272 -0
- reactifact-0.13.0/reactifact/metrics.py +294 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/operations.py +18 -16
- reactifact-0.13.0/reactifact/pricing.py +33 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/anthropic.py +18 -1
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/chat.py +15 -4
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/contracts.py +4 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/fake.py +10 -2
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/gemini.py +9 -0
- reactifact-0.13.0/reactifact/quota.py +215 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/__init__.py +27 -0
- reactifact-0.13.0/reactifact/recipes/conversation.py +302 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/resources.py +92 -4
- reactifact-0.13.0/reactifact/routing.py +94 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/runtime.py +175 -22
- reactifact-0.13.0/reactifact/skills/reactifact/SKILL.md +164 -0
- reactifact-0.13.0/reactifact/skills/reactifact/references/effects-and-state.md +83 -0
- reactifact-0.13.0/reactifact/skills/reactifact/references/trust-and-safety.md +96 -0
- reactifact-0.13.0/reactifact/skills/reactifact-agents/SKILL.md +156 -0
- reactifact-0.13.0/reactifact/skills/reactifact-agents/references/consume.md +109 -0
- reactifact-0.13.0/reactifact/skills/reactifact-agents/references/effects.md +65 -0
- reactifact-0.13.0/reactifact/skills/reactifact-eval/SKILL.md +160 -0
- reactifact-0.13.0/reactifact/skills/reactifact-eval/references/online.md +72 -0
- reactifact-0.13.0/reactifact/skills/reactifact-from-langchain/SKILL.md +102 -0
- reactifact-0.13.0/reactifact/skills/reactifact-llm/SKILL.md +159 -0
- reactifact-0.13.0/reactifact/skills/reactifact-llm/references/providers.md +77 -0
- reactifact-0.13.0/reactifact/skills/reactifact-llm/references/tools.md +83 -0
- reactifact-0.13.0/reactifact/skills/reactifact-observability/SKILL.md +134 -0
- reactifact-0.13.0/reactifact/skills/reactifact-rag/SKILL.md +150 -0
- reactifact-0.13.0/reactifact/skills/reactifact-rag/references/sources.md +103 -0
- reactifact-0.13.0/reactifact/skills/reactifact-testing/SKILL.md +114 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/__init__.py +13 -2
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/assertions.py +116 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/fault.py +26 -7
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/golden.py +71 -1
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/lab.py +250 -12
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/mock.py +80 -25
- reactifact-0.13.0/reactifact/testing/pytest_plugin.py +69 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tool_use.py +29 -3
- reactifact-0.13.0/reactifact/types.py +168 -0
- {reactifact-0.12.0 → reactifact-0.13.0/reactifact.egg-info}/PKG-INFO +51 -29
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact.egg-info/SOURCES.txt +57 -1
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_anthropic_provider.py +31 -0
- reactifact-0.13.0/tests/test_authz.py +120 -0
- reactifact-0.13.0/tests/test_cache.py +171 -0
- reactifact-0.13.0/tests/test_chat_example.py +18 -0
- reactifact-0.13.0/tests/test_chat_memory.py +142 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_cli.py +2 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_cli_scenario.py +24 -6
- reactifact-0.13.0/tests/test_compaction.py +153 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_devops.py +81 -1
- reactifact-0.13.0/tests/test_devops_online.py +114 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_devops_web.py +15 -0
- reactifact-0.13.0/tests/test_eval_dataset.py +293 -0
- reactifact-0.13.0/tests/test_eval_judge.py +131 -0
- reactifact-0.13.0/tests/test_eval_online.py +319 -0
- reactifact-0.13.0/tests/test_eval_trajectory.py +140 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_gemini_provider.py +27 -0
- reactifact-0.13.0/tests/test_guardrails.py +138 -0
- reactifact-0.13.0/tests/test_metrics.py +207 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_openai_provider.py +40 -0
- reactifact-0.13.0/tests/test_public_api.py +281 -0
- reactifact-0.13.0/tests/test_quota.py +214 -0
- reactifact-0.13.0/tests/test_recipe_conversation.py +154 -0
- reactifact-0.13.0/tests/test_routing.py +160 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_runtime_errors.py +86 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_sessions.py +68 -3
- reactifact-0.13.0/tests/test_skills.py +106 -0
- reactifact-0.13.0/tests/test_testing_golden.py +135 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_testing_lab.py +98 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_testing_mock.py +74 -0
- reactifact-0.13.0/tests/test_testing_provenance.py +91 -0
- reactifact-0.13.0/tests/test_testing_pytest_plugin.py +48 -0
- reactifact-0.13.0/tests/test_token_budget.py +182 -0
- reactifact-0.13.0/tests/test_type_registry.py +178 -0
- reactifact-0.12.0/reactifact/budget.py +0 -43
- reactifact-0.12.0/reactifact/cli/scenario.py +0 -94
- reactifact-0.12.0/reactifact/events.py +0 -34
- {reactifact-0.12.0 → reactifact-0.13.0}/LICENSE +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/__main__.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/_extras.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/_httpx.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/agent_tool.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/agents.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/audit.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/checkpoints.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/commit.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/consume.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/context_builder.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/effects.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/llm_agent.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/patches.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/produce.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/prompts.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/py.typed +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/agent.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/chat.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/models.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/rag.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/recipes/tool_loop.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/redaction.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/relations.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/replay.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/request.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/session.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/sources.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/streaming.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/structured.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tools.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/__init__.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/columns.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/models.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/store.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/templates/app.css +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/templates/sessions.html +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/templates/ui_run.html +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/tracer.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/tracing/web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/triggers.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/verify.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/viz.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact/web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/setup.cfg +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_absent_consume.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_agent_tool.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_audit.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_backbone.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_branching.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_budget.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_chat_web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_checkpoint.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_concurrency.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_context_builder.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_debounce.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_determinism.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_effects.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_eval.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_forklab.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_golden.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_hitl.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_incident_commander.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_join_consume.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_mcp.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_multisource.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_patches.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_ports.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_produce_styles.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_prompts.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_quick.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_reacts_to.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipe_tool_loop.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipes.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_redaction.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_relations.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_repair.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_replay.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_repo_agent.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_request.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_research.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_resources.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_retry.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_runtime.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_sources.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_starter_app.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_streaming.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_structured.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_support_copilot.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_tools.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_trace_columns.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_trace_tags.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_tracing.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_verify.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_view.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_viz.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_web_source.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_workspace.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.12.0 → reactifact-0.13.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.13.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
|
|
@@ -46,7 +46,7 @@ Dynamic: license-file
|
|
|
46
46
|
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**Auditable agents for Python.** Build **knowledge assistants** over docs, spreadsheets and the web — where the path per question isn't a graph you can draw. Every answer is computed, versioned and reproducible — not a string you have to trust.
|
|
50
50
|
|
|
51
51
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
52
52
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
@@ -56,29 +56,15 @@ Dynamic: license-file
|
|
|
56
56
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
57
57
|
[](https://bzdvdn.github.io/reactifact/)
|
|
58
58
|
|
|
59
|
-
Python
|
|
60
|
-
|
|
61
|
-
reacts to a **typed, versioned artifact** appearing in the context, not to a
|
|
62
|
-
queue message you push or a graph edge you draw. The runtime derives what runs
|
|
63
|
-
next from state.
|
|
59
|
+
> **Is:** a Python library (3 core deps) · single-process · typed, versioned artifacts with provenance · deterministic replay · runs offline, no API key
|
|
60
|
+
> **Isn't:** a managed platform · a distributed task queue · a pre-built agent/tool marketplace
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
| --- | --- |
|
|
67
|
-
| a task | `@produce(Model)` — a unit of work that writes an artifact |
|
|
68
|
-
| `delay()` / `apply_async()` | you don't call it: creating the input artifact **is** the trigger |
|
|
69
|
-
| routing key / queue | `Consume(Type)` — which artifact type wakes the task |
|
|
70
|
-
| chain / group / chord | several `consumes` / `produces`; the runtime derives the order |
|
|
71
|
-
| retries, `acks_late` | guards + `Budget`, an honest `None` instead of a wrong result |
|
|
72
|
-
| result backend | the `Context` — typed, versioned artifacts |
|
|
73
|
-
| worker | `Runtime` |
|
|
62
|
+
A question like *"why did infra costs jump in Q2?"* needs a spreadsheet, a policy doc and a page off the web — and the *next* question needs a different subset. That is not one graph you can draw up front. You declare what artifacts exist and what agents can do with them; the runtime derives the order from state.
|
|
74
63
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
**versioned with provenance**, so a run is reproducible (`context_hash`) and
|
|
80
|
-
auditable (`audit.report`) for free. The model reasons; the arithmetic stays
|
|
81
|
-
deterministic; every claim carries provenance.
|
|
64
|
+
Every derived artifact then links to what produced it
|
|
65
|
+
(`Answer —supported_by→ Evidence —extracted_from→ Doc`), so *"why did it say
|
|
66
|
+
that?"* is a query, not a guess — and the run is reproducible (`context_hash`)
|
|
67
|
+
and verifiable (`reactifact replay --verify`).
|
|
82
68
|
|
|
83
69
|
## On top: a provable answer
|
|
84
70
|
|
|
@@ -130,6 +116,14 @@ print(report_to_markdown(build_report(ctx, answer))) # hash per artifact + edge
|
|
|
130
116
|
print(context_hash(ctx)) # reproducible fingerprint
|
|
131
117
|
```
|
|
132
118
|
|
|
119
|
+
## Knowledge assistants, live
|
|
120
|
+
|
|
121
|
+
The same shape over many sources, not a fixed path — the
|
|
122
|
+
[`knowledge`](examples/knowledge) CLI answers a harder, multi-source question
|
|
123
|
+
(docs + a CSV) with a real computed number and its sources, no LLM key required:
|
|
124
|
+
|
|
125
|
+

|
|
126
|
+
|
|
133
127
|

|
|
134
128
|
|
|
135
129
|
Two agents explore independently on their own forks and merge back automatically
|
|
@@ -137,6 +131,31 @@ Two agents explore independently on their own forks and merge back automatically
|
|
|
137
131
|
|
|
138
132
|

|
|
139
133
|
|
|
134
|
+
<details>
|
|
135
|
+
<summary><strong>If you know Celery…</strong> — the programming model, not its distributed runtime</summary>
|
|
136
|
+
|
|
137
|
+
Python developers already know this model from Celery: define a **task**,
|
|
138
|
+
declare what triggers it, let the runtime run it. reactifact applies it to
|
|
139
|
+
agents — a task reacts to a **typed, versioned artifact** appearing in the
|
|
140
|
+
context, not to a queue message you push or a graph edge you draw. The runtime
|
|
141
|
+
derives what runs next from state.
|
|
142
|
+
|
|
143
|
+
| Celery | reactifact |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| a task | `@produce(Model)` — a unit of work that writes an artifact |
|
|
146
|
+
| `delay()` / `apply_async()` | you don't call it: creating the input artifact **is** the trigger |
|
|
147
|
+
| routing key / queue | `Consume(Type)` — which artifact type wakes the task |
|
|
148
|
+
| chain / group / chord | several `consumes` / `produces`; the runtime derives the order |
|
|
149
|
+
| result backend | the `Context` — typed, versioned artifacts |
|
|
150
|
+
| worker | `Runtime` |
|
|
151
|
+
|
|
152
|
+
**Not inherited:** a broker, a worker pool, `acks_late`/redelivery, cross-process
|
|
153
|
+
exactly-once. reactifact is a library with an in-process runtime — the *model* is
|
|
154
|
+
Celery-shaped, the deployment is not. (Session-backed runs do survive a restart;
|
|
155
|
+
see [Durability & resume](docs/en/durability.md).)
|
|
156
|
+
|
|
157
|
+
</details>
|
|
158
|
+
|
|
140
159
|
```bash
|
|
141
160
|
pip install reactifact
|
|
142
161
|
```
|
|
@@ -203,12 +222,6 @@ print(answer.data.text) # "Refunds are available within 14 d
|
|
|
203
222
|
print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
|
|
204
223
|
```
|
|
205
224
|
|
|
206
|
-
The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
|
|
207
|
-
harder, multi-source question (docs + a CSV) with a real computed number and
|
|
208
|
-
its sources, no LLM key required:
|
|
209
|
-
|
|
210
|
-

|
|
211
|
-
|
|
212
225
|
## How it works
|
|
213
226
|
|
|
214
227
|
```text
|
|
@@ -325,6 +338,7 @@ Prefer a notebook? [.
|
|
341
|
+
- `chat` — minimal offline single-model multi-turn conversation (`Conversation` + bounded `ChatMemory`).
|
|
328
342
|
- `forklab` — deterministic branch & merge: two strategies on their own forks, three-way merge.
|
|
329
343
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
330
344
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
@@ -355,6 +369,14 @@ uv sync --extra dev --extra web
|
|
|
355
369
|
.venv/bin/ruff check
|
|
356
370
|
```
|
|
357
371
|
|
|
372
|
+
## Status & contributing
|
|
373
|
+
|
|
374
|
+
Active, `0.13.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
375
|
+
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
376
|
+
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
377
|
+
[GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
|
|
378
|
+
or a `good first issue`, helps more than you'd think.
|
|
379
|
+
|
|
358
380
|
## License
|
|
359
381
|
|
|
360
382
|
MIT — see [LICENSE](LICENSE).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="docs/img/reactifact-hero.png" alt="reactifact — Agents that react to artifacts, not graphs" width="800">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Auditable agents for Python.** Build **knowledge assistants** over docs, spreadsheets and the web — where the path per question isn't a graph you can draw. Every answer is computed, versioned and reproducible — not a string you have to trust.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
@@ -12,29 +12,15 @@
|
|
|
12
12
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
13
13
|
[](https://bzdvdn.github.io/reactifact/)
|
|
14
14
|
|
|
15
|
-
Python
|
|
16
|
-
|
|
17
|
-
reacts to a **typed, versioned artifact** appearing in the context, not to a
|
|
18
|
-
queue message you push or a graph edge you draw. The runtime derives what runs
|
|
19
|
-
next from state.
|
|
15
|
+
> **Is:** a Python library (3 core deps) · single-process · typed, versioned artifacts with provenance · deterministic replay · runs offline, no API key
|
|
16
|
+
> **Isn't:** a managed platform · a distributed task queue · a pre-built agent/tool marketplace
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
| --- | --- |
|
|
23
|
-
| a task | `@produce(Model)` — a unit of work that writes an artifact |
|
|
24
|
-
| `delay()` / `apply_async()` | you don't call it: creating the input artifact **is** the trigger |
|
|
25
|
-
| routing key / queue | `Consume(Type)` — which artifact type wakes the task |
|
|
26
|
-
| chain / group / chord | several `consumes` / `produces`; the runtime derives the order |
|
|
27
|
-
| retries, `acks_late` | guards + `Budget`, an honest `None` instead of a wrong result |
|
|
28
|
-
| result backend | the `Context` — typed, versioned artifacts |
|
|
29
|
-
| worker | `Runtime` |
|
|
18
|
+
A question like *"why did infra costs jump in Q2?"* needs a spreadsheet, a policy doc and a page off the web — and the *next* question needs a different subset. That is not one graph you can draw up front. You declare what artifacts exist and what agents can do with them; the runtime derives the order from state.
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
**versioned with provenance**, so a run is reproducible (`context_hash`) and
|
|
36
|
-
auditable (`audit.report`) for free. The model reasons; the arithmetic stays
|
|
37
|
-
deterministic; every claim carries provenance.
|
|
20
|
+
Every derived artifact then links to what produced it
|
|
21
|
+
(`Answer —supported_by→ Evidence —extracted_from→ Doc`), so *"why did it say
|
|
22
|
+
that?"* is a query, not a guess — and the run is reproducible (`context_hash`)
|
|
23
|
+
and verifiable (`reactifact replay --verify`).
|
|
38
24
|
|
|
39
25
|
## On top: a provable answer
|
|
40
26
|
|
|
@@ -86,6 +72,14 @@ print(report_to_markdown(build_report(ctx, answer))) # hash per artifact + edge
|
|
|
86
72
|
print(context_hash(ctx)) # reproducible fingerprint
|
|
87
73
|
```
|
|
88
74
|
|
|
75
|
+
## Knowledge assistants, live
|
|
76
|
+
|
|
77
|
+
The same shape over many sources, not a fixed path — the
|
|
78
|
+
[`knowledge`](examples/knowledge) CLI answers a harder, multi-source question
|
|
79
|
+
(docs + a CSV) with a real computed number and its sources, no LLM key required:
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
89
83
|

|
|
90
84
|
|
|
91
85
|
Two agents explore independently on their own forks and merge back automatically
|
|
@@ -93,6 +87,31 @@ Two agents explore independently on their own forks and merge back automatically
|
|
|
93
87
|
|
|
94
88
|

|
|
95
89
|
|
|
90
|
+
<details>
|
|
91
|
+
<summary><strong>If you know Celery…</strong> — the programming model, not its distributed runtime</summary>
|
|
92
|
+
|
|
93
|
+
Python developers already know this model from Celery: define a **task**,
|
|
94
|
+
declare what triggers it, let the runtime run it. reactifact applies it to
|
|
95
|
+
agents — a task reacts to a **typed, versioned artifact** appearing in the
|
|
96
|
+
context, not to a queue message you push or a graph edge you draw. The runtime
|
|
97
|
+
derives what runs next from state.
|
|
98
|
+
|
|
99
|
+
| Celery | reactifact |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| a task | `@produce(Model)` — a unit of work that writes an artifact |
|
|
102
|
+
| `delay()` / `apply_async()` | you don't call it: creating the input artifact **is** the trigger |
|
|
103
|
+
| routing key / queue | `Consume(Type)` — which artifact type wakes the task |
|
|
104
|
+
| chain / group / chord | several `consumes` / `produces`; the runtime derives the order |
|
|
105
|
+
| result backend | the `Context` — typed, versioned artifacts |
|
|
106
|
+
| worker | `Runtime` |
|
|
107
|
+
|
|
108
|
+
**Not inherited:** a broker, a worker pool, `acks_late`/redelivery, cross-process
|
|
109
|
+
exactly-once. reactifact is a library with an in-process runtime — the *model* is
|
|
110
|
+
Celery-shaped, the deployment is not. (Session-backed runs do survive a restart;
|
|
111
|
+
see [Durability & resume](docs/en/durability.md).)
|
|
112
|
+
|
|
113
|
+
</details>
|
|
114
|
+
|
|
96
115
|
```bash
|
|
97
116
|
pip install reactifact
|
|
98
117
|
```
|
|
@@ -159,12 +178,6 @@ print(answer.data.text) # "Refunds are available within 14 d
|
|
|
159
178
|
print("supported_by:", evidence.data.text) # provenance you can trace, not just a string in a log
|
|
160
179
|
```
|
|
161
180
|
|
|
162
|
-
The same idea, live — the [`knowledge`](examples/knowledge) example's CLI answering a
|
|
163
|
-
harder, multi-source question (docs + a CSV) with a real computed number and
|
|
164
|
-
its sources, no LLM key required:
|
|
165
|
-
|
|
166
|
-

|
|
167
|
-
|
|
168
181
|
## How it works
|
|
169
182
|
|
|
170
183
|
```text
|
|
@@ -281,6 +294,7 @@ Prefer a notebook? [.
|
|
297
|
+
- `chat` — minimal offline single-model multi-turn conversation (`Conversation` + bounded `ChatMemory`).
|
|
284
298
|
- `forklab` — deterministic branch & merge: two strategies on their own forks, three-way merge.
|
|
285
299
|
- `ledger` — offline proof of reactive recompute: edit one fact, only its real `Consume`rs re-run.
|
|
286
300
|
- `llm_ladder` — the workflow from one LLM call to state-changing patches (3 levels).
|
|
@@ -311,6 +325,14 @@ uv sync --extra dev --extra web
|
|
|
311
325
|
.venv/bin/ruff check
|
|
312
326
|
```
|
|
313
327
|
|
|
328
|
+
## Status & contributing
|
|
329
|
+
|
|
330
|
+
Active, `0.13.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
331
|
+
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
332
|
+
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
333
|
+
[GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
|
|
334
|
+
or a `good first issue`, helps more than you'd think.
|
|
335
|
+
|
|
314
336
|
## License
|
|
315
337
|
|
|
316
338
|
MIT — see [LICENSE](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.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"
|
|
@@ -99,7 +99,7 @@ reactifact = "reactifact.__main__:main"
|
|
|
99
99
|
packages = { find = { where = ["."], include = ["reactifact*"], exclude = ["examples*", "tests*"] } }
|
|
100
100
|
|
|
101
101
|
[tool.setuptools.package-data]
|
|
102
|
-
reactifact = ["py.typed"]
|
|
102
|
+
reactifact = ["py.typed", "skills/*/SKILL.md", "skills/*/references/*.md"]
|
|
103
103
|
"reactifact.tracing" = ["templates/*.html", "templates/*.css"]
|
|
104
104
|
|
|
105
105
|
[tool.pytest.ini_options]
|
|
@@ -152,3 +152,9 @@ python_version = "3.11"
|
|
|
152
152
|
packages = ["reactifact", "examples"]
|
|
153
153
|
strict = true
|
|
154
154
|
warn_unused_ignores = true
|
|
155
|
+
|
|
156
|
+
# pyright defaults its target to the interpreter it finds, which can resolve
|
|
157
|
+
# below the 3.11 floor this package requires — making `StrEnum`/`datetime.UTC`
|
|
158
|
+
# "unknown import symbol" and cascading into false positives on `RunOutcome`.
|
|
159
|
+
[tool.pyright]
|
|
160
|
+
pythonVersion = "3.11"
|
|
@@ -24,7 +24,7 @@ of the names you see by default even though they were always cheap to import
|
|
|
24
24
|
from .agents import Agent, create_agent
|
|
25
25
|
from .artifacts import Artifact
|
|
26
26
|
from .branching import MergeConflict
|
|
27
|
-
from .budget import Budget, RunOutcome, RunStats
|
|
27
|
+
from .budget import Budget, BudgetTracker, RunOutcome, RunStats
|
|
28
28
|
from .consume import Consume, consume
|
|
29
29
|
from .context import Context, View
|
|
30
30
|
from .effects import Effects, Handle
|
|
@@ -48,13 +48,15 @@ from .runtime import Runtime
|
|
|
48
48
|
from .session import Session, SessionStore
|
|
49
49
|
from .tools import FunctionTool, Tool, ToolOutput, tool
|
|
50
50
|
from .triggers import Trigger
|
|
51
|
+
from .types import register as register_type
|
|
51
52
|
|
|
52
|
-
__version__ = "0.
|
|
53
|
+
__version__ = "0.13.0"
|
|
53
54
|
|
|
54
55
|
__all__ = [
|
|
55
56
|
"Agent",
|
|
56
57
|
"Artifact",
|
|
57
58
|
"Budget",
|
|
59
|
+
"BudgetTracker",
|
|
58
60
|
"Consume",
|
|
59
61
|
"Context",
|
|
60
62
|
"Create",
|
|
@@ -97,5 +99,6 @@ __all__ = [
|
|
|
97
99
|
"create_agent",
|
|
98
100
|
"current_request",
|
|
99
101
|
"produce",
|
|
102
|
+
"register_type",
|
|
100
103
|
"tool",
|
|
101
104
|
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import importlib
|
|
4
3
|
import uuid
|
|
5
4
|
from datetime import UTC, datetime
|
|
6
5
|
from typing import Any, Generic, TypeVar
|
|
7
6
|
|
|
8
7
|
from pydantic import BaseModel
|
|
9
8
|
|
|
9
|
+
from .types import migrate_payload, resolve, type_id_of
|
|
10
|
+
|
|
10
11
|
TData = TypeVar("TData", bound=BaseModel)
|
|
11
12
|
ArtifactType = type[BaseModel]
|
|
12
13
|
|
|
@@ -31,12 +32,6 @@ def compute_dict_diff(old: dict[str, Any], new: dict[str, Any]) -> dict[str, Any
|
|
|
31
32
|
return diff
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
def _import_class(full_name: str) -> Any:
|
|
35
|
-
module_name, class_name = full_name.rsplit(".", 1)
|
|
36
|
-
module = importlib.import_module(module_name)
|
|
37
|
-
return getattr(module, class_name)
|
|
38
|
-
|
|
39
|
-
|
|
40
35
|
class Artifact(Generic[TData]):
|
|
41
36
|
"""Wrapper around a Pydantic model with versioning."""
|
|
42
37
|
|
|
@@ -48,13 +43,21 @@ class Artifact(Generic[TData]):
|
|
|
48
43
|
):
|
|
49
44
|
self.id = id or str(uuid.uuid4())
|
|
50
45
|
self.data = data
|
|
51
|
-
|
|
46
|
+
# Stable type id (reactifact.types): a registered `type_id`/`TYPE_ID`
|
|
47
|
+
# classvar, else the qualified name — so a rename/move can be tolerated
|
|
48
|
+
# via aliases instead of breaking a persisted session/replay.
|
|
49
|
+
self.data_type = type_id_of(type(data))
|
|
52
50
|
self.created_at = datetime.now(UTC)
|
|
53
51
|
self.updated_at = self.created_at
|
|
54
52
|
self.created_by_commit = created_by_commit
|
|
55
53
|
self._history: list[
|
|
56
54
|
TData
|
|
57
55
|
] = [] # previous data versions (excluding the current one)
|
|
56
|
+
#: Absolute version counter — bumped by `update()`, independent of
|
|
57
|
+
#: `len(_history)`. `compact(keep_versions=…)` trims `_history` but must
|
|
58
|
+
#: not change `version` (it is part of `context_hash` and of commit
|
|
59
|
+
#: `Write`s), so it is tracked separately.
|
|
60
|
+
self._version = 0
|
|
58
61
|
# Memoized to_dict(), keyed by version: re-derived on every save()
|
|
59
62
|
# otherwise (session persistence saves after every commit, §reactifact.session)
|
|
60
63
|
# even though most artifacts in a large context are unchanged since the
|
|
@@ -65,8 +68,22 @@ class Artifact(Generic[TData]):
|
|
|
65
68
|
"""Saves the current version to history and replaces the data."""
|
|
66
69
|
self._history.append(self.data)
|
|
67
70
|
self.data = new_data
|
|
71
|
+
self._version += 1
|
|
68
72
|
self.updated_at = datetime.now(UTC)
|
|
69
73
|
|
|
74
|
+
def trim_history(self, keep: int) -> None:
|
|
75
|
+
"""Keeps only the last `keep` previous versions (0 drops them all).
|
|
76
|
+
|
|
77
|
+
`version` (and so `context_hash`) is unaffected — only the ability to
|
|
78
|
+
inspect older versions via `history`/`get_all_versions`/`diff` is
|
|
79
|
+
bounded. Used by `Context.compact`.
|
|
80
|
+
"""
|
|
81
|
+
if keep < 0:
|
|
82
|
+
raise ValueError("keep must be >= 0")
|
|
83
|
+
if len(self._history) > keep:
|
|
84
|
+
self._history = self._history[-keep:] if keep else []
|
|
85
|
+
self._dict_cache = None
|
|
86
|
+
|
|
70
87
|
@property
|
|
71
88
|
def history(self) -> list[TData]:
|
|
72
89
|
"""Returns a copy of the list of previous versions (excluding the current one)."""
|
|
@@ -75,7 +92,7 @@ class Artifact(Generic[TData]):
|
|
|
75
92
|
@property
|
|
76
93
|
def version(self) -> int:
|
|
77
94
|
"""Current version (0 – original, 1 – after the first update, etc.)"""
|
|
78
|
-
return
|
|
95
|
+
return self._version
|
|
79
96
|
|
|
80
97
|
def get_all_versions(self) -> list[TData]:
|
|
81
98
|
"""Returns all data versions, including the current one, from oldest to newest."""
|
|
@@ -106,6 +123,7 @@ class Artifact(Generic[TData]):
|
|
|
106
123
|
"id": self.id,
|
|
107
124
|
"data_type": self.data_type,
|
|
108
125
|
"data": self.data.model_dump(mode="json"),
|
|
126
|
+
"version": self._version,
|
|
109
127
|
"created_at": self.created_at.isoformat(),
|
|
110
128
|
"updated_at": self.updated_at.isoformat(),
|
|
111
129
|
"history": [v.model_dump(mode="json") for v in self._history],
|
|
@@ -116,14 +134,21 @@ class Artifact(Generic[TData]):
|
|
|
116
134
|
|
|
117
135
|
@classmethod
|
|
118
136
|
def from_dict(cls, d: dict[str, Any]) -> Artifact[Any]:
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
data_type = d["data_type"]
|
|
138
|
+
model_class = resolve(data_type)
|
|
139
|
+
data: Any = model_class.model_validate(migrate_payload(data_type, d["data"]))
|
|
121
140
|
artifact = cls(
|
|
122
141
|
data=data, id=d["id"], created_by_commit=d.get("created_by_commit")
|
|
123
142
|
)
|
|
124
143
|
artifact.created_at = datetime.fromisoformat(d["created_at"])
|
|
125
144
|
artifact.updated_at = datetime.fromisoformat(d["updated_at"])
|
|
126
|
-
|
|
145
|
+
history: list[Any] = [
|
|
146
|
+
model_class.model_validate(migrate_payload(data_type, h))
|
|
147
|
+
for h in d["history"]
|
|
148
|
+
]
|
|
149
|
+
artifact._history = history
|
|
150
|
+
# Older payloads predate the explicit counter — fall back to history len.
|
|
151
|
+
artifact._version = int(d.get("version", len(artifact._history)))
|
|
127
152
|
return artifact
|
|
128
153
|
|
|
129
154
|
def __repr__(self) -> str:
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Authorization — a principal and a permission policy for a run (§57).
|
|
2
|
+
|
|
3
|
+
reactifact has no server, so unlike a hosted platform it can enforce access in
|
|
4
|
+
the library itself, at the points that actually matter: which artifact types an
|
|
5
|
+
agent may create/update/delete, which agent may run, and which tool may
|
|
6
|
+
execute. `RuntimeResources(principal=..., authorizer=...)` turns it on.
|
|
7
|
+
|
|
8
|
+
from reactifact.authz import PermissionPolicy, Principal
|
|
9
|
+
|
|
10
|
+
resources = RuntimeResources(
|
|
11
|
+
principal=Principal("alice", capabilities=("analyst",)),
|
|
12
|
+
authorizer=PermissionPolicy(grants={"analyst": {"run:*", "create:Answer"}}),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
A principal is whoever the run acts for (an end user, a tenant, a service).
|
|
16
|
+
The default is `None`, and with no authorizer every action is allowed — authz
|
|
17
|
+
is opt-in and does not change existing behavior. Denials raise
|
|
18
|
+
`AuthorizationError` from `Runtime.arun()` (or that agent's generation is
|
|
19
|
+
dropped when `isolate_errors=True`).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Iterable, Mapping
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Principal(BaseModel):
|
|
32
|
+
"""Who a run acts for: an id plus the capabilities it holds.
|
|
33
|
+
|
|
34
|
+
Kept a plain model so it composes with artifacts and sessions (it can be
|
|
35
|
+
seeded as an artifact, loaded from a session, or restored by a `SeedIdentity`
|
|
36
|
+
recipe) rather than living only in `RuntimeResources`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
capabilities: tuple[str, ...] = ()
|
|
41
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthorizationError(PermissionError):
|
|
45
|
+
"""Raised when a principal may not perform an action on a resource."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, action: str, resource: str, principal: Principal | None = None
|
|
49
|
+
) -> None:
|
|
50
|
+
self.action = action
|
|
51
|
+
self.resource = resource
|
|
52
|
+
self.principal = principal
|
|
53
|
+
who = principal.id if principal is not None else "anonymous"
|
|
54
|
+
super().__init__(f"principal {who!r} may not {action} {resource!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@runtime_checkable
|
|
58
|
+
class Authorizer(Protocol):
|
|
59
|
+
"""Structural interface: `authorize(principal, action, resource) -> bool`."""
|
|
60
|
+
|
|
61
|
+
def authorize(
|
|
62
|
+
self, principal: Principal | None, action: str, resource: str
|
|
63
|
+
) -> bool: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _permission_matches(pattern: str, action: str, resource: str) -> bool:
|
|
67
|
+
"""Matches `"action:resource"`, `"action:*"`, `"*:resource"` or `"*"`."""
|
|
68
|
+
if pattern == "*":
|
|
69
|
+
return True
|
|
70
|
+
if ":" not in pattern:
|
|
71
|
+
return pattern == action
|
|
72
|
+
allowed_action, _, allowed_resource = pattern.partition(":")
|
|
73
|
+
return (allowed_action in ("*", action)) and (allowed_resource in ("*", resource))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class PermissionPolicy:
|
|
78
|
+
"""Capability-based permissions: a capability grants `action:resource` patterns.
|
|
79
|
+
|
|
80
|
+
`grants` maps a capability name to the patterns it allows, e.g.
|
|
81
|
+
`{"analyst": {"run:*", "create:Answer"}, "admin": {"*"}}`. A principal is
|
|
82
|
+
allowed an action when any of its capabilities grants a matching pattern.
|
|
83
|
+
With `default_allow=False` (the default) an anonymous/unknown principal is
|
|
84
|
+
denied; set it `True` to allow by default and use the grants as the
|
|
85
|
+
allowlist an audit needs.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
grants: Mapping[str, Iterable[str]] = field(default_factory=dict)
|
|
89
|
+
default_allow: bool = False
|
|
90
|
+
|
|
91
|
+
def authorize(
|
|
92
|
+
self, principal: Principal | None, action: str, resource: str
|
|
93
|
+
) -> bool:
|
|
94
|
+
if principal is None:
|
|
95
|
+
return self.default_allow
|
|
96
|
+
for capability in principal.capabilities:
|
|
97
|
+
for pattern in self.grants.get(capability, ()):
|
|
98
|
+
if _permission_matches(pattern, action, resource):
|
|
99
|
+
return True
|
|
100
|
+
return self.default_allow
|
|
101
|
+
|
|
102
|
+
def require(self, principal: Principal | None, action: str, resource: str) -> None:
|
|
103
|
+
if not self.authorize(principal, action, resource):
|
|
104
|
+
raise AuthorizationError(action, resource, principal)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"AuthorizationError",
|
|
109
|
+
"Authorizer",
|
|
110
|
+
"PermissionPolicy",
|
|
111
|
+
"Principal",
|
|
112
|
+
]
|
|
@@ -57,6 +57,7 @@ def clone_context(source: Context) -> Context:
|
|
|
57
57
|
created_by_commit=artifact.created_by_commit,
|
|
58
58
|
)
|
|
59
59
|
new_artifact._history = [v.model_copy(deep=True) for v in artifact._history]
|
|
60
|
+
new_artifact._version = artifact._version
|
|
60
61
|
new_artifact.created_at = artifact.created_at
|
|
61
62
|
new_artifact.updated_at = artifact.updated_at
|
|
62
63
|
new_ws._artifacts[artifact.id] = new_artifact
|