reactifact 0.9.1__tar.gz → 0.11.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.9.1/reactifact.egg-info → reactifact-0.11.0}/PKG-INFO +82 -11
- {reactifact-0.9.1 → reactifact-0.11.0}/README.md +81 -10
- {reactifact-0.9.1 → reactifact-0.11.0}/pyproject.toml +5 -2
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/__init__.py +6 -2
- reactifact-0.11.0/reactifact/_httpx.py +87 -0
- reactifact-0.11.0/reactifact/audit.py +255 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/budget.py +2 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/chat.py +44 -7
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/checkpoints.py +29 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/replay.py +20 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/context.py +2 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/effects.py +36 -3
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/produce.py +13 -1
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/prompts.py +70 -2
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/anthropic.py +8 -9
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/chat.py +15 -18
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/contracts.py +4 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/gemini.py +8 -9
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/image.py +15 -15
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/speech.py +27 -28
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/video.py +10 -11
- reactifact-0.11.0/reactifact/quick/__init__.py +66 -0
- reactifact-0.11.0/reactifact/quick/_shared.py +72 -0
- reactifact-0.11.0/reactifact/quick/agent.py +147 -0
- reactifact-0.11.0/reactifact/quick/chat.py +85 -0
- reactifact-0.11.0/reactifact/quick/models.py +47 -0
- reactifact-0.11.0/reactifact/quick/rag.py +407 -0
- reactifact-0.11.0/reactifact/quick/tools_agent.py +167 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/__init__.py +4 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/text.py +22 -1
- reactifact-0.11.0/reactifact/recipes/tool_loop.py +190 -0
- reactifact-0.11.0/reactifact/redaction.py +94 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/replay.py +92 -1
- reactifact-0.11.0/reactifact/request.py +56 -0
- reactifact-0.11.0/reactifact/resources.py +213 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/runtime.py +37 -9
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/sources.py +14 -7
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/structured.py +79 -20
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/__init__.py +6 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/fault.py +15 -8
- reactifact-0.11.0/reactifact/testing/golden.py +110 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/lab.py +11 -8
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/mock.py +24 -10
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tools.py +21 -1
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/__init__.py +20 -2
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/langfuse.py +7 -7
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/models.py +36 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/otlp.py +7 -7
- reactifact-0.11.0/reactifact/tracing/postgres.py +567 -0
- reactifact-0.11.0/reactifact/tracing/store.py +647 -0
- reactifact-0.11.0/reactifact/tracing/templates/app.css +326 -0
- reactifact-0.11.0/reactifact/tracing/templates/sessions.html +145 -0
- reactifact-0.11.0/reactifact/tracing/templates/ui.html +487 -0
- reactifact-0.11.0/reactifact/tracing/templates/ui_run.html +386 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/tracer.py +54 -11
- reactifact-0.11.0/reactifact/tracing/web.py +291 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/web.py +64 -11
- {reactifact-0.9.1 → reactifact-0.11.0/reactifact.egg-info}/PKG-INFO +82 -11
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact.egg-info/SOURCES.txt +29 -0
- reactifact-0.11.0/tests/test_audit.py +134 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_chat_web.py +108 -0
- reactifact-0.11.0/tests/test_determinism.py +155 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_devops_web.py +2 -1
- reactifact-0.11.0/tests/test_fintech_audit.py +40 -0
- reactifact-0.11.0/tests/test_golden.py +87 -0
- reactifact-0.11.0/tests/test_loop_bound_client.py +72 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_prompts.py +36 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_provider_auth.py +7 -2
- reactifact-0.11.0/tests/test_quick.py +325 -0
- reactifact-0.11.0/tests/test_recipe_tool_loop.py +160 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipes.py +12 -0
- reactifact-0.11.0/tests/test_redaction.py +148 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_repair.py +47 -0
- reactifact-0.11.0/tests/test_repo_agent.py +60 -0
- reactifact-0.11.0/tests/test_request.py +88 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_resources.py +66 -1
- reactifact-0.11.0/tests/test_scheduler_semantics.py +120 -0
- reactifact-0.11.0/tests/test_starter_app.py +87 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_structured.py +105 -0
- reactifact-0.11.0/tests/test_support_copilot.py +52 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_testing_lab.py +46 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_testing_mock.py +64 -1
- reactifact-0.11.0/tests/test_trace_tags.py +321 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_tracing.py +122 -0
- reactifact-0.9.1/reactifact/resources.py +0 -69
- reactifact-0.9.1/reactifact/tracing/postgres.py +0 -220
- reactifact-0.9.1/reactifact/tracing/store.py +0 -254
- reactifact-0.9.1/reactifact/tracing/templates/ui.html +0 -196
- reactifact-0.9.1/reactifact/tracing/templates/ui_run.html +0 -264
- reactifact-0.9.1/reactifact/tracing/web.py +0 -117
- {reactifact-0.9.1 → reactifact-0.11.0}/LICENSE +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/__main__.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/_extras.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/agent_tool.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/agents.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/branching.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/commit.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/consume.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/context_builder.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/eval.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/events.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/llm_agent.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/operations.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/patches.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/py.typed +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/relations.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/session.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/streaming.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tool_use.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/triggers.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/verify.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact/viz.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/setup.cfg +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_absent_consume.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_agent_tool.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_backbone.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_branching.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_budget.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_checkpoint.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_cli.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_concurrency.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_context_builder.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_debounce.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_devops.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_effects.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_eval.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_forklab.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_hitl.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_incident_commander.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_join_consume.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_mcp.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_multisource.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_patches.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_ports.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_produce_styles.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_reacts_to.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_relations.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_replay.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_research.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_retry.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_runtime.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_sessions.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_sources.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_streaming.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_tools.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_verify.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_view.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_viz.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_web_source.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_workspace.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.9.1 → reactifact-0.11.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.11.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
|
+
**Event-driven agents for Python developers — tasks wake on typed artifacts, like Celery tasks wake on messages. No graph to draw.**
|
|
50
50
|
|
|
51
51
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
52
52
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
@@ -56,14 +56,79 @@ Dynamic: license-file
|
|
|
56
56
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
57
57
|
[](https://bzdvdn.github.io/reactifact/)
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
declare
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
Python developers already know this model from Celery: define a **task**,
|
|
60
|
+
declare what triggers it, let the runtime run it. reactifact applies it to agents — a task
|
|
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.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
| Celery | reactifact |
|
|
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` |
|
|
74
|
+
|
|
75
|
+
Single process today (no broker, no worker pool) — the *model* is Celery-shaped,
|
|
76
|
+
not its distributed runtime.
|
|
77
|
+
|
|
78
|
+
On top of that model you get something a task queue doesn't: every artifact is
|
|
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.
|
|
82
|
+
|
|
83
|
+
## On top: a provable answer
|
|
84
|
+
|
|
85
|
+
[`examples/fintech_audit`](examples/fintech_audit) — a transactions CSV, a budget
|
|
86
|
+
CSV and a policy doc, **no API key**. The model never produces the number; plain
|
|
87
|
+
Python does, and the answer is linked to its evidence:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
$ .venv/bin/python -m examples.fintech_audit.main
|
|
91
|
+
|
|
92
|
+
cloud spend: $45,000 (2026-04 $12k, 2026-05 $15k, 2026-06 $18k)
|
|
93
|
+
variance vs budget: +12.5% budget $40,000, policy threshold 10% → over
|
|
94
|
+
|
|
95
|
+
answer: Q2 cloud spend was $45,000 against a $40,000 budget (+12.5%) —
|
|
96
|
+
exceeds the 10% policy threshold. CFO approval is required.
|
|
97
|
+
citations: budget.csv, transactions.csv, policy.md
|
|
98
|
+
audit: Answer —supported_by→ {Variance, Spend, Table, Policy}
|
|
99
|
+
answer sha256 5461290d… · context sha256 24449f6f…
|
|
100
|
+
|
|
101
|
+
>>> re-running the pipeline hashes identically — or verify a saved run:
|
|
102
|
+
>>> reactifact replay <store> --session <id> --verify 24449f6f…
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That hash is the whole point: the answer is *reproducible* and its provenance is
|
|
106
|
+
a queryable graph (`context.related(answer.id, "supported_by")`), not a log line.
|
|
107
|
+
|
|
108
|
+

|
|
109
|
+
|
|
110
|
+
The number is computed, and its provenance recorded, in plain Python — the model
|
|
111
|
+
is never the source of truth (trimmed from
|
|
112
|
+
[`examples/fintech_audit/produce.py`](examples/fintech_audit/produce.py)):
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
@produce(Variance, reacts_to=Spend) # wakes when a Spend artifact exists
|
|
116
|
+
async def compute_variance(call: ProduceCall) -> None:
|
|
117
|
+
spend = call.trigger # the artifact that triggered this run
|
|
118
|
+
budget = cloud_budget(call.context) # read from budget.csv
|
|
119
|
+
pct = (spend.data.total - budget) / budget # deterministic — never the model
|
|
120
|
+
variance = call.effects.create_once_from(
|
|
121
|
+
spend, # stable id → idempotent re-runs (§42)
|
|
122
|
+
Variance(actual=spend.data.total, budget=budget, pct=pct,
|
|
123
|
+
threshold=0.10, within_policy=abs(pct) <= 0.10),
|
|
124
|
+
)
|
|
125
|
+
variance.link("calculated_from", spend) # provenance edge, queryable
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
answer = ctx.latest(AuditAnswer)
|
|
129
|
+
print(report_to_markdown(build_report(ctx, answer))) # hash per artifact + edges
|
|
130
|
+
print(context_hash(ctx)) # reproducible fingerprint
|
|
131
|
+
```
|
|
67
132
|
|
|
68
133
|

|
|
69
134
|
|
|
@@ -250,6 +315,9 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
250
315
|
Classic-pattern ports run as one-liners too:
|
|
251
316
|
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
252
317
|
|
|
318
|
+
Prefer a notebook? [](https://colab.research.google.com/github/bzdvdn/reactifact/blob/master/examples/quickstart.ipynb)
|
|
319
|
+
— `examples/quickstart.ipynb` covers the four `quick` cases (`agent`/`rag`/`tools_agent`/`chat_agent`), offline by default.
|
|
320
|
+
|
|
253
321
|
## Examples (in-repo, not shipped)
|
|
254
322
|
|
|
255
323
|
- `knowledge` — multi-source chat: search → evidence → claim verification → answer, with CSV calculation.
|
|
@@ -267,8 +335,11 @@ Classic-pattern ports run as one-liners too:
|
|
|
267
335
|
|
|
268
336
|
- [English](docs/en/index.md) · [Русский](docs/ru/index.md) — concepts, sources,
|
|
269
337
|
providers, recipes, patterns, observability, eval, branching, replay, viz/CLI, API.
|
|
270
|
-
- [Quickstart](docs/en/quickstart.md) —
|
|
271
|
-
|
|
338
|
+
- [Quickstart](docs/en/quickstart.md) — the four `quick` cases (structured call,
|
|
339
|
+
RAG, tools, chat) in a few lines; also as a
|
|
340
|
+
[Colab notebook](examples/quickstart.ipynb).
|
|
341
|
+
- [Migrating](docs/en/migrating.md) — from LangGraph / CrewAI / LlamaIndex /
|
|
342
|
+
plain Python: concept map, port-one-node, interop, checklist.
|
|
272
343
|
- [Why reactifact](docs/en/why-reactifact.md) — the *design argument*: why effects, why no graph, why determinism.
|
|
273
344
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
274
345
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
@@ -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
|
+
**Event-driven agents for Python developers — tasks wake on typed artifacts, like Celery tasks wake on messages. No graph to draw.**
|
|
6
6
|
|
|
7
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
@@ -12,14 +12,79 @@
|
|
|
12
12
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
13
13
|
[](https://bzdvdn.github.io/reactifact/)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
declare
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
Python developers already know this model from Celery: define a **task**,
|
|
16
|
+
declare what triggers it, let the runtime run it. reactifact applies it to agents — a task
|
|
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.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
| Celery | reactifact |
|
|
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` |
|
|
30
|
+
|
|
31
|
+
Single process today (no broker, no worker pool) — the *model* is Celery-shaped,
|
|
32
|
+
not its distributed runtime.
|
|
33
|
+
|
|
34
|
+
On top of that model you get something a task queue doesn't: every artifact is
|
|
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.
|
|
38
|
+
|
|
39
|
+
## On top: a provable answer
|
|
40
|
+
|
|
41
|
+
[`examples/fintech_audit`](examples/fintech_audit) — a transactions CSV, a budget
|
|
42
|
+
CSV and a policy doc, **no API key**. The model never produces the number; plain
|
|
43
|
+
Python does, and the answer is linked to its evidence:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
$ .venv/bin/python -m examples.fintech_audit.main
|
|
47
|
+
|
|
48
|
+
cloud spend: $45,000 (2026-04 $12k, 2026-05 $15k, 2026-06 $18k)
|
|
49
|
+
variance vs budget: +12.5% budget $40,000, policy threshold 10% → over
|
|
50
|
+
|
|
51
|
+
answer: Q2 cloud spend was $45,000 against a $40,000 budget (+12.5%) —
|
|
52
|
+
exceeds the 10% policy threshold. CFO approval is required.
|
|
53
|
+
citations: budget.csv, transactions.csv, policy.md
|
|
54
|
+
audit: Answer —supported_by→ {Variance, Spend, Table, Policy}
|
|
55
|
+
answer sha256 5461290d… · context sha256 24449f6f…
|
|
56
|
+
|
|
57
|
+
>>> re-running the pipeline hashes identically — or verify a saved run:
|
|
58
|
+
>>> reactifact replay <store> --session <id> --verify 24449f6f…
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That hash is the whole point: the answer is *reproducible* and its provenance is
|
|
62
|
+
a queryable graph (`context.related(answer.id, "supported_by")`), not a log line.
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
The number is computed, and its provenance recorded, in plain Python — the model
|
|
67
|
+
is never the source of truth (trimmed from
|
|
68
|
+
[`examples/fintech_audit/produce.py`](examples/fintech_audit/produce.py)):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
@produce(Variance, reacts_to=Spend) # wakes when a Spend artifact exists
|
|
72
|
+
async def compute_variance(call: ProduceCall) -> None:
|
|
73
|
+
spend = call.trigger # the artifact that triggered this run
|
|
74
|
+
budget = cloud_budget(call.context) # read from budget.csv
|
|
75
|
+
pct = (spend.data.total - budget) / budget # deterministic — never the model
|
|
76
|
+
variance = call.effects.create_once_from(
|
|
77
|
+
spend, # stable id → idempotent re-runs (§42)
|
|
78
|
+
Variance(actual=spend.data.total, budget=budget, pct=pct,
|
|
79
|
+
threshold=0.10, within_policy=abs(pct) <= 0.10),
|
|
80
|
+
)
|
|
81
|
+
variance.link("calculated_from", spend) # provenance edge, queryable
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
answer = ctx.latest(AuditAnswer)
|
|
85
|
+
print(report_to_markdown(build_report(ctx, answer))) # hash per artifact + edges
|
|
86
|
+
print(context_hash(ctx)) # reproducible fingerprint
|
|
87
|
+
```
|
|
23
88
|
|
|
24
89
|

|
|
25
90
|
|
|
@@ -206,6 +271,9 @@ uv run python ./examples/devops/web.py # HITL ops assistant + trace dash
|
|
|
206
271
|
Classic-pattern ports run as one-liners too:
|
|
207
272
|
`python -m examples.{reflection,map_reduce,supervisor,summarize,time_travel,plan_execute,adaptive,ledger}.main`.
|
|
208
273
|
|
|
274
|
+
Prefer a notebook? [](https://colab.research.google.com/github/bzdvdn/reactifact/blob/master/examples/quickstart.ipynb)
|
|
275
|
+
— `examples/quickstart.ipynb` covers the four `quick` cases (`agent`/`rag`/`tools_agent`/`chat_agent`), offline by default.
|
|
276
|
+
|
|
209
277
|
## Examples (in-repo, not shipped)
|
|
210
278
|
|
|
211
279
|
- `knowledge` — multi-source chat: search → evidence → claim verification → answer, with CSV calculation.
|
|
@@ -223,8 +291,11 @@ Classic-pattern ports run as one-liners too:
|
|
|
223
291
|
|
|
224
292
|
- [English](docs/en/index.md) · [Русский](docs/ru/index.md) — concepts, sources,
|
|
225
293
|
providers, recipes, patterns, observability, eval, branching, replay, viz/CLI, API.
|
|
226
|
-
- [Quickstart](docs/en/quickstart.md) —
|
|
227
|
-
|
|
294
|
+
- [Quickstart](docs/en/quickstart.md) — the four `quick` cases (structured call,
|
|
295
|
+
RAG, tools, chat) in a few lines; also as a
|
|
296
|
+
[Colab notebook](examples/quickstart.ipynb).
|
|
297
|
+
- [Migrating](docs/en/migrating.md) — from LangGraph / CrewAI / LlamaIndex /
|
|
298
|
+
plain Python: concept map, port-one-node, interop, checklist.
|
|
228
299
|
- [Why reactifact](docs/en/why-reactifact.md) — the *design argument*: why effects, why no graph, why determinism.
|
|
229
300
|
- [Comparison](docs/en/comparison.md) — reactifact vs LangGraph/CrewAI, feature by feature, and when *not* to use reactifact.
|
|
230
301
|
- [Tutorial · llm-ladder](docs/en/examples.md#tutorial-ladder) — learn the workflow.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.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"
|
|
@@ -100,7 +100,7 @@ packages = { find = { where = ["."], include = ["reactifact*"], exclude = ["exam
|
|
|
100
100
|
|
|
101
101
|
[tool.setuptools.package-data]
|
|
102
102
|
reactifact = ["py.typed"]
|
|
103
|
-
"reactifact.tracing" = ["templates/*.html"]
|
|
103
|
+
"reactifact.tracing" = ["templates/*.html", "templates/*.css"]
|
|
104
104
|
|
|
105
105
|
[tool.pytest.ini_options]
|
|
106
106
|
testpaths = ["tests"]
|
|
@@ -118,6 +118,9 @@ fail_under = 85
|
|
|
118
118
|
target-version = "py311"
|
|
119
119
|
line-length = 88
|
|
120
120
|
src = ["reactifact", "examples"]
|
|
121
|
+
# Notebooks are Colab artifacts (IPython magics, top-level await) — not package
|
|
122
|
+
# code; don't lint/format them.
|
|
123
|
+
extend-exclude = ["*.ipynb"]
|
|
121
124
|
|
|
122
125
|
[tool.ruff.lint]
|
|
123
126
|
select = [
|
|
@@ -42,13 +42,14 @@ from .providers import (
|
|
|
42
42
|
LLMResponseChunk,
|
|
43
43
|
Message,
|
|
44
44
|
)
|
|
45
|
-
from .
|
|
45
|
+
from .request import current_request
|
|
46
|
+
from .resources import ResourceKey, ResourceScope, RuntimeResources
|
|
46
47
|
from .runtime import Runtime
|
|
47
48
|
from .session import Session, SessionStore
|
|
48
49
|
from .tools import FunctionTool, Tool, ToolOutput, tool
|
|
49
50
|
from .triggers import Trigger
|
|
50
51
|
|
|
51
|
-
__version__ = "0.
|
|
52
|
+
__version__ = "0.11.0"
|
|
52
53
|
|
|
53
54
|
__all__ = [
|
|
54
55
|
"Agent",
|
|
@@ -74,6 +75,8 @@ __all__ = [
|
|
|
74
75
|
"Produce",
|
|
75
76
|
"ProduceCall",
|
|
76
77
|
"Relation",
|
|
78
|
+
"ResourceKey",
|
|
79
|
+
"ResourceScope",
|
|
77
80
|
"RunOutcome",
|
|
78
81
|
"RunStats",
|
|
79
82
|
"Runtime",
|
|
@@ -92,6 +95,7 @@ __all__ = [
|
|
|
92
95
|
"View",
|
|
93
96
|
"consume",
|
|
94
97
|
"create_agent",
|
|
98
|
+
"current_request",
|
|
95
99
|
"produce",
|
|
96
100
|
"tool",
|
|
97
101
|
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Loop-aware `httpx.AsyncClient` ownership for providers and sinks.
|
|
2
|
+
|
|
3
|
+
`httpx.AsyncClient` binds its connection pool (and internal locks) to the event
|
|
4
|
+
loop it was created on. A provider/sink instance that caches the client forever
|
|
5
|
+
and is then reused on a *different* loop — pytest gives each test its own loop,
|
|
6
|
+
a CLI calls `asyncio.run(...)` more than once, uvicorn reloads — raises
|
|
7
|
+
`RuntimeError: Event loop is closed` (or "bound to a different event loop") on
|
|
8
|
+
the second use.
|
|
9
|
+
|
|
10
|
+
`LoopBoundClient` keeps the client **lazy** (created on first use, as before)
|
|
11
|
+
but **rebinds it to the current running loop**: when the loop changes, a fresh
|
|
12
|
+
client is created. The previous client's loop is already gone, so it is dropped
|
|
13
|
+
rather than awaited (its pool died with the loop). An injected `client=` (tests,
|
|
14
|
+
a custom transport) is returned as-is and never recreated.
|
|
15
|
+
|
|
16
|
+
Typical use inside a provider:
|
|
17
|
+
|
|
18
|
+
self._http = LoopBoundClient(
|
|
19
|
+
lambda: httpx.AsyncClient(timeout=self._timeout, headers=self._headers)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def complete(self, ...):
|
|
23
|
+
response = await self._http.get().post(url, json=payload)
|
|
24
|
+
|
|
25
|
+
async def aclose(self) -> None:
|
|
26
|
+
await self._http.aclose()
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from typing import Any, cast
|
|
34
|
+
|
|
35
|
+
import httpx
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoopBoundClient:
|
|
39
|
+
"""Owns an `httpx.AsyncClient` bound to the current event loop."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
factory: Callable[[], httpx.AsyncClient],
|
|
44
|
+
*,
|
|
45
|
+
client: Any | None = None,
|
|
46
|
+
):
|
|
47
|
+
self._factory = factory
|
|
48
|
+
self._fixed = client
|
|
49
|
+
self._client: httpx.AsyncClient | None = None
|
|
50
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
51
|
+
|
|
52
|
+
def get(self) -> httpx.AsyncClient:
|
|
53
|
+
"""The client for the running loop (created/recreated as needed)."""
|
|
54
|
+
if self._fixed is not None:
|
|
55
|
+
return cast("httpx.AsyncClient", self._fixed)
|
|
56
|
+
loop = asyncio.get_running_loop()
|
|
57
|
+
if self._client is None or self._loop is not loop:
|
|
58
|
+
self._client = self._factory()
|
|
59
|
+
self._loop = loop
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
async def aclose(self) -> None:
|
|
63
|
+
"""Closes the current client, if it belongs to the running loop.
|
|
64
|
+
|
|
65
|
+
A client whose owner loop is closed (or is a different, still-running
|
|
66
|
+
loop) cannot be awaited here; it is dropped — nothing can safely close
|
|
67
|
+
it from this loop. The injected `client=`, if any, is left to its owner.
|
|
68
|
+
"""
|
|
69
|
+
client, loop = self._client, self._loop
|
|
70
|
+
self._client = None
|
|
71
|
+
self._loop = None
|
|
72
|
+
if client is None:
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
current = asyncio.get_running_loop()
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
return
|
|
78
|
+
if loop is not None and loop is not current:
|
|
79
|
+
return
|
|
80
|
+
await client.aclose()
|
|
81
|
+
|
|
82
|
+
def raw(self) -> Any | None:
|
|
83
|
+
"""The injected client if there is one, else the current one (or None)."""
|
|
84
|
+
return self._fixed if self._fixed is not None else self._client
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["LoopBoundClient"]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""reactifact.audit — answer provenance and reproducibility (§34, §55, §56).
|
|
2
|
+
|
|
3
|
+
Two things an auditable agent needs, both offline and dependency-free:
|
|
4
|
+
|
|
5
|
+
1. `build_report(context, answer)` — a self-contained record of *why* an answer
|
|
6
|
+
exists: the answer's content hash, the provenance chain behind it
|
|
7
|
+
(`supported_by` / `derived_from` / `materialized_from` / …), each artifact's
|
|
8
|
+
version, author and content hash, and the source locators it rests on.
|
|
9
|
+
Exportable as JSON (machine-checkable) or Markdown (human/regulator-readable).
|
|
10
|
+
|
|
11
|
+
2. `context_hash(context)` — a sha256 over the run's canonical state
|
|
12
|
+
(artifacts + relations + version). Two runs that produce the same state hash
|
|
13
|
+
the same; recording it with a `ReplayLLM` recording turns "is this run
|
|
14
|
+
reproducible?" into a string comparison (`reactifact replay --verify <hash>`,
|
|
15
|
+
and the `fintech_audit` example).
|
|
16
|
+
|
|
17
|
+
Hashes are sha256 over canonical (sorted, JSON-safe) content, so the *hash*
|
|
18
|
+
fields are reproducible: the same state yields the same `context_sha256` and
|
|
19
|
+
per-artifact hashes, run to run. The report also carries `created_at` values
|
|
20
|
+
for a human reader, so the full JSON is not byte-identical across runs — the
|
|
21
|
+
fingerprint is.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
from .artifacts import Artifact
|
|
35
|
+
from .context import Context
|
|
36
|
+
|
|
37
|
+
#: Relations followed by default when walking an answer's provenance chain.
|
|
38
|
+
DEFAULT_RELATIONS: tuple[str, ...] = (
|
|
39
|
+
"supported_by",
|
|
40
|
+
"derived_from",
|
|
41
|
+
"extracted_from",
|
|
42
|
+
"materialized_from",
|
|
43
|
+
"resolved_from",
|
|
44
|
+
"calculated_from",
|
|
45
|
+
"refuted_by",
|
|
46
|
+
"contradicted_by",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _canonical(value: Any) -> str:
|
|
51
|
+
"""Canonical JSON for hashing (sorted keys; pydantic models dumped)."""
|
|
52
|
+
if isinstance(value, BaseModel):
|
|
53
|
+
payload: Any = value.model_dump(mode="json")
|
|
54
|
+
else:
|
|
55
|
+
payload = value
|
|
56
|
+
return json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def data_hash(value: Any) -> str:
|
|
60
|
+
"""sha256 of a value's canonical JSON — a stable content fingerprint."""
|
|
61
|
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def artifact_hash(artifact: Artifact[Any]) -> str:
|
|
65
|
+
"""Content hash of one artifact's data (its type + id are not included)."""
|
|
66
|
+
return data_hash(artifact.data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def context_hash(context: Context) -> str:
|
|
70
|
+
"""sha256 over the run's canonical state: version, artifacts, relations.
|
|
71
|
+
|
|
72
|
+
Includes each artifact's id, type, version and content hash, plus every
|
|
73
|
+
relation edge — everything that makes two runs' states the same or
|
|
74
|
+
different. Timestamps are deliberately excluded, so a re-run that reaches
|
|
75
|
+
the same state hashes identically.
|
|
76
|
+
"""
|
|
77
|
+
artifacts = [
|
|
78
|
+
{
|
|
79
|
+
"id": artifact.id,
|
|
80
|
+
"type": type(artifact.data).__name__,
|
|
81
|
+
"version": artifact.version,
|
|
82
|
+
"sha256": artifact_hash(artifact),
|
|
83
|
+
}
|
|
84
|
+
for artifact in sorted(context.list_artifacts(), key=lambda a: a.id)
|
|
85
|
+
]
|
|
86
|
+
relations = sorted(
|
|
87
|
+
(rel.source_id, rel.relation, rel.target_id) for rel in context.relations()
|
|
88
|
+
)
|
|
89
|
+
payload = {
|
|
90
|
+
"version": context.version,
|
|
91
|
+
"artifacts": artifacts,
|
|
92
|
+
"relations": relations,
|
|
93
|
+
}
|
|
94
|
+
return hashlib.sha256(_canonical(payload).encode("utf-8")).hexdigest()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ProvenanceEntry(BaseModel):
|
|
98
|
+
"""One artifact in an answer's provenance chain, with its content hash."""
|
|
99
|
+
|
|
100
|
+
artifact_id: str
|
|
101
|
+
data_type: str
|
|
102
|
+
version: int
|
|
103
|
+
sha256: str
|
|
104
|
+
produced_by: str = ""
|
|
105
|
+
created_at: datetime | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AuditReport(BaseModel):
|
|
109
|
+
"""A reproducible record of an answer and the evidence behind it."""
|
|
110
|
+
|
|
111
|
+
session_id: str = ""
|
|
112
|
+
context_version: int
|
|
113
|
+
context_sha256: str
|
|
114
|
+
answer: ProvenanceEntry
|
|
115
|
+
provenance: list[ProvenanceEntry] = []
|
|
116
|
+
relations: list[tuple[str, str, str]] = []
|
|
117
|
+
sources: list[str] = []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _entry(artifact: Artifact[Any], produced_by: str) -> ProvenanceEntry:
|
|
121
|
+
return ProvenanceEntry(
|
|
122
|
+
artifact_id=artifact.id,
|
|
123
|
+
data_type=type(artifact.data).__name__,
|
|
124
|
+
version=artifact.version,
|
|
125
|
+
sha256=artifact_hash(artifact),
|
|
126
|
+
produced_by=produced_by,
|
|
127
|
+
created_at=artifact.created_at,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _producers(context: Context) -> dict[str, str]:
|
|
132
|
+
"""artifact_id → author of the commit that last wrote it (from history)."""
|
|
133
|
+
producers: dict[str, str] = {}
|
|
134
|
+
for commit in context.history():
|
|
135
|
+
for write in commit.writes:
|
|
136
|
+
producers[write.artifact_id] = commit.author
|
|
137
|
+
return producers
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resolve_answer(context: Context, answer: Artifact[Any] | str) -> Artifact[Any]:
|
|
141
|
+
if isinstance(answer, Artifact):
|
|
142
|
+
return answer
|
|
143
|
+
artifact = context.get(answer)
|
|
144
|
+
if artifact is None:
|
|
145
|
+
raise KeyError(f"answer artifact {answer!r} not found in context")
|
|
146
|
+
return artifact
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_report(
|
|
150
|
+
context: Context,
|
|
151
|
+
answer: Artifact[Any] | str,
|
|
152
|
+
*,
|
|
153
|
+
relations: Sequence[str] | None = None,
|
|
154
|
+
session_id: str = "",
|
|
155
|
+
) -> AuditReport:
|
|
156
|
+
"""Walks the provenance chain behind `answer` into a verifiable report.
|
|
157
|
+
|
|
158
|
+
Breadth-first over `context.relations()`, following `relations` (all by
|
|
159
|
+
default), so every artifact that contributed to the answer is included with
|
|
160
|
+
its content hash and producing author. `SourceRef` locators in the chain
|
|
161
|
+
become `sources`.
|
|
162
|
+
"""
|
|
163
|
+
followed = set(relations) if relations is not None else set(DEFAULT_RELATIONS)
|
|
164
|
+
answer_artifact = _resolve_answer(context, answer)
|
|
165
|
+
producers = _producers(context)
|
|
166
|
+
|
|
167
|
+
entries: list[ProvenanceEntry] = []
|
|
168
|
+
edges: list[tuple[str, str, str]] = []
|
|
169
|
+
sources: list[str] = []
|
|
170
|
+
visited: set[str] = set()
|
|
171
|
+
queue = [answer_artifact]
|
|
172
|
+
while queue:
|
|
173
|
+
artifact = queue.pop(0)
|
|
174
|
+
if artifact.id in visited:
|
|
175
|
+
continue
|
|
176
|
+
visited.add(artifact.id)
|
|
177
|
+
entries.append(_entry(artifact, producers.get(artifact.id, "")))
|
|
178
|
+
locator = getattr(artifact.data, "locator", None)
|
|
179
|
+
if isinstance(locator, str) and locator and locator not in sources:
|
|
180
|
+
sources.append(locator)
|
|
181
|
+
for rel in context.relations(source_id=artifact.id):
|
|
182
|
+
if rel.relation not in followed:
|
|
183
|
+
continue
|
|
184
|
+
edges.append((rel.source_id, rel.relation, rel.target_id))
|
|
185
|
+
target = context.get(rel.target_id)
|
|
186
|
+
if target is not None and target.id not in visited:
|
|
187
|
+
queue.append(target)
|
|
188
|
+
|
|
189
|
+
return AuditReport(
|
|
190
|
+
session_id=session_id,
|
|
191
|
+
context_version=context.version,
|
|
192
|
+
context_sha256=context_hash(context),
|
|
193
|
+
answer=entries[0],
|
|
194
|
+
provenance=entries[1:],
|
|
195
|
+
relations=edges,
|
|
196
|
+
sources=sources,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def report_to_json(report: AuditReport, *, indent: int | None = 2) -> str:
|
|
201
|
+
"""The report as canonical JSON (sorted keys → reproducible bytes)."""
|
|
202
|
+
return json.dumps(
|
|
203
|
+
report.model_dump(mode="json"),
|
|
204
|
+
indent=indent,
|
|
205
|
+
sort_keys=True,
|
|
206
|
+
ensure_ascii=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def report_to_markdown(report: AuditReport) -> str:
|
|
211
|
+
"""The report as Markdown — for a PR comment, a ticket, or a regulator."""
|
|
212
|
+
lines = [
|
|
213
|
+
"# Audit report",
|
|
214
|
+
"",
|
|
215
|
+
f"- context version: {report.context_version}",
|
|
216
|
+
f"- context sha256: `{report.context_sha256}`",
|
|
217
|
+
f"- session: {report.session_id or '(none)'}",
|
|
218
|
+
"",
|
|
219
|
+
"## Answer",
|
|
220
|
+
"",
|
|
221
|
+
f"- `{report.answer.artifact_id}` ({report.answer.data_type}, "
|
|
222
|
+
f"v{report.answer.version})",
|
|
223
|
+
f"- sha256: `{report.answer.sha256}`",
|
|
224
|
+
f"- produced by: {report.answer.produced_by or '(seed)'}",
|
|
225
|
+
"",
|
|
226
|
+
"## Provenance",
|
|
227
|
+
"",
|
|
228
|
+
"| artifact | type | v | sha256 | produced by |",
|
|
229
|
+
"| --- | --- | --- | --- | --- |",
|
|
230
|
+
]
|
|
231
|
+
for entry in [report.answer, *report.provenance]:
|
|
232
|
+
lines.append(
|
|
233
|
+
f"| `{entry.artifact_id}` | {entry.data_type} | {entry.version} | "
|
|
234
|
+
f"`{entry.sha256[:12]}…` | {entry.produced_by or '(seed)'} |"
|
|
235
|
+
)
|
|
236
|
+
if report.relations:
|
|
237
|
+
lines += ["", "## Edges", ""]
|
|
238
|
+
lines += [f"- `{s}` —{r}→ `{t}`" for s, r, t in report.relations]
|
|
239
|
+
if report.sources:
|
|
240
|
+
lines += ["", "## Sources", ""]
|
|
241
|
+
lines += [f"- `{source}`" for source in report.sources]
|
|
242
|
+
return "\n".join(lines) + "\n"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = [
|
|
246
|
+
"DEFAULT_RELATIONS",
|
|
247
|
+
"AuditReport",
|
|
248
|
+
"ProvenanceEntry",
|
|
249
|
+
"artifact_hash",
|
|
250
|
+
"build_report",
|
|
251
|
+
"context_hash",
|
|
252
|
+
"data_hash",
|
|
253
|
+
"report_to_json",
|
|
254
|
+
"report_to_markdown",
|
|
255
|
+
]
|
|
@@ -35,6 +35,8 @@ class RunStats:
|
|
|
35
35
|
runs: int
|
|
36
36
|
iterations: int
|
|
37
37
|
outcome: RunOutcome
|
|
38
|
+
#: Wall-clock duration of the turn, in **seconds** (`time.monotonic()`).
|
|
39
|
+
#: Distinct from `RunTrace.duration_ms`, which is in milliseconds.
|
|
38
40
|
duration: float
|
|
39
41
|
#: Agent executions that raised and were isolated (`Runtime(isolate_errors=True)`).
|
|
40
42
|
#: Always 0 when isolation is off — an exception propagates instead (§69).
|