reactifact 0.10.0__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.10.0/reactifact.egg-info → reactifact-0.11.0}/PKG-INFO +74 -9
- {reactifact-0.10.0 → reactifact-0.11.0}/README.md +73 -8
- {reactifact-0.10.0 → reactifact-0.11.0}/pyproject.toml +2 -2
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/__init__.py +1 -1
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/budget.py +2 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/context.py +2 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/effects.py +9 -3
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/replay.py +92 -1
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/resources.py +19 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/runtime.py +3 -1
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/fault.py +15 -8
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/lab.py +11 -8
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/mock.py +24 -10
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tracing/__init__.py +20 -2
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tracing/models.py +33 -0
- 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.10.0 → reactifact-0.11.0}/reactifact/tracing/tracer.py +11 -3
- reactifact-0.11.0/reactifact/tracing/web.py +291 -0
- {reactifact-0.10.0 → reactifact-0.11.0/reactifact.egg-info}/PKG-INFO +74 -9
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact.egg-info/SOURCES.txt +4 -0
- reactifact-0.11.0/tests/test_determinism.py +155 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_devops_web.py +2 -1
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_testing_lab.py +46 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_testing_mock.py +64 -1
- reactifact-0.11.0/tests/test_trace_tags.py +321 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_tracing.py +96 -0
- reactifact-0.10.0/reactifact/tracing/postgres.py +0 -220
- reactifact-0.10.0/reactifact/tracing/store.py +0 -254
- reactifact-0.10.0/reactifact/tracing/templates/ui.html +0 -196
- reactifact-0.10.0/reactifact/tracing/templates/ui_run.html +0 -264
- reactifact-0.10.0/reactifact/tracing/web.py +0 -117
- {reactifact-0.10.0 → reactifact-0.11.0}/LICENSE +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/__main__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/_extras.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/_httpx.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/agent_tool.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/agents.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/audit.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/branching.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/chat.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/checkpoints.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/commit.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/consume.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/context_builder.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/eval.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/events.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/llm_agent.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/operations.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/patches.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/produce.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/prompts.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/chat.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/py.typed +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/agent.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/chat.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/models.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/rag.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/recipes/tool_loop.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/redaction.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/relations.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/request.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/session.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/sources.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/streaming.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/structured.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/golden.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tool_use.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tools.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/triggers.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/verify.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/viz.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact/web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/setup.cfg +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_absent_consume.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_agent_tool.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_audit.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_backbone.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_branching.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_budget.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_chat_web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_checkpoint.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_cli.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_concurrency.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_context_builder.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_debounce.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_devops.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_effects.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_eval.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_forklab.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_golden.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_hitl.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_incident_commander.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_join_consume.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_mcp.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_multisource.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_patches.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_ports.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_produce_styles.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_prompts.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_quick.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_reacts_to.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipe_tool_loop.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipes.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_redaction.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_relations.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_repair.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_replay.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_repo_agent.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_request.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_research.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_resources.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_retry.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_runtime.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_sessions.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_sources.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_starter_app.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_streaming.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_structured.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_support_copilot.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_tools.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_verify.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_view.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_viz.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_web_source.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_workspace.py +0 -0
- {reactifact-0.10.0 → reactifact-0.11.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.10.0 → 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
|
|
|
@@ -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
|
|
|
@@ -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"]
|
|
@@ -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).
|
|
@@ -124,6 +124,8 @@ class Context:
|
|
|
124
124
|
"""
|
|
125
125
|
if id is not None and id in self._artifacts:
|
|
126
126
|
return self._artifacts[id]
|
|
127
|
+
if id is None and self.resources.id_factory is not None:
|
|
128
|
+
id = self.resources.id_factory(type(data).__name__)
|
|
127
129
|
artifact = Artifact(data=data, id=id)
|
|
128
130
|
self._artifacts[artifact.id] = artifact
|
|
129
131
|
self._by_type.setdefault(type(data), set()).add(artifact.id)
|
|
@@ -102,9 +102,15 @@ class Effects:
|
|
|
102
102
|
site when that "may already exist" intent should be explicit instead
|
|
103
103
|
of implicit in a plain `create(..., id=...)`.
|
|
104
104
|
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
if id is None:
|
|
106
|
+
factory = self._context.resources.id_factory
|
|
107
|
+
id = (
|
|
108
|
+
factory(type(data).__name__)
|
|
109
|
+
if factory is not None
|
|
110
|
+
else _auto_id(type(data).__name__)
|
|
111
|
+
)
|
|
112
|
+
self.operations.append(Create(data, id=id))
|
|
113
|
+
return Handle(self, id, data)
|
|
108
114
|
|
|
109
115
|
def create_once(self, data: Any, *, id: str) -> Handle | None:
|
|
110
116
|
"""Idempotent create (§42): `None` if `id` already exists in the
|
|
@@ -21,17 +21,23 @@ Two complementary halves:
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import hashlib
|
|
24
|
+
import itertools
|
|
24
25
|
import json
|
|
25
26
|
import logging
|
|
27
|
+
from collections.abc import Awaitable, Callable
|
|
26
28
|
from pathlib import Path
|
|
27
29
|
from typing import TYPE_CHECKING, Any, Literal
|
|
28
30
|
|
|
31
|
+
from pydantic import BaseModel
|
|
32
|
+
|
|
33
|
+
from .audit import context_hash
|
|
29
34
|
from .context import Context
|
|
30
35
|
from .providers import LLMProvider, LLMRequest, LLMResponse, LLMResponseChunk
|
|
31
36
|
|
|
32
37
|
if TYPE_CHECKING:
|
|
33
38
|
from collections.abc import AsyncIterator
|
|
34
39
|
|
|
40
|
+
from .resources import RuntimeResources
|
|
35
41
|
from .session import SessionStore
|
|
36
42
|
|
|
37
43
|
logger = logging.getLogger(__name__)
|
|
@@ -168,6 +174,83 @@ async def replay_context(
|
|
|
168
174
|
return context
|
|
169
175
|
|
|
170
176
|
|
|
177
|
+
def counter_ids(start: int = 0) -> Callable[[str], str]:
|
|
178
|
+
"""A deterministic id factory: `Model:0000`, `Model:0001`, … per run.
|
|
179
|
+
|
|
180
|
+
Assign it to `RuntimeResources(id_factory=…)` and every artifact created
|
|
181
|
+
without an explicit id gets a stable, order-derived id instead of a
|
|
182
|
+
`uuid4` — enough to make an otherwise-unmodified app's `context_hash`
|
|
183
|
+
reproducible run to run. Pair with `ReplayLLM` (recorded model calls) for
|
|
184
|
+
full reproducibility; `verify_run` wires both.
|
|
185
|
+
"""
|
|
186
|
+
counter = itertools.count(start)
|
|
187
|
+
|
|
188
|
+
def make(model_name: str) -> str:
|
|
189
|
+
return f"{model_name.lower()}:{next(counter):04d}"
|
|
190
|
+
|
|
191
|
+
return make
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ReproReport(BaseModel):
|
|
195
|
+
"""Outcome of `verify_run`: were repeated runs byte-identical?"""
|
|
196
|
+
|
|
197
|
+
ok: bool
|
|
198
|
+
hashes: list[str]
|
|
199
|
+
repeat: int
|
|
200
|
+
recording: str | None = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
#: A run builder: given resources, produce the finished `Context`.
|
|
204
|
+
RunBuilder = Callable[["RuntimeResources"], Awaitable["Context"]]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def verify_run(
|
|
208
|
+
build: RunBuilder,
|
|
209
|
+
*,
|
|
210
|
+
recording: str | Path | None = None,
|
|
211
|
+
repeat: int = 2,
|
|
212
|
+
resources_factory: Callable[[], RuntimeResources] | None = None,
|
|
213
|
+
) -> ReproReport:
|
|
214
|
+
"""Runs `build` `repeat` times and checks the `context_hash` is identical.
|
|
215
|
+
|
|
216
|
+
Each run gets fresh resources with **deterministic ids**
|
|
217
|
+
(`counter_ids()`) and — when `recording` is given — a `ReplayLLM` replaying
|
|
218
|
+
it, so a difference between runs is real nondeterminism in the app (time,
|
|
219
|
+
randomness, unstable ids, order), not model variance. `build(resources)`
|
|
220
|
+
must build the app on the given resources and return the finished
|
|
221
|
+
`Context` (it must not create resources itself).
|
|
222
|
+
|
|
223
|
+
report = await verify_run(build, recording="calls.jsonl")
|
|
224
|
+
assert report.ok, report.hashes
|
|
225
|
+
|
|
226
|
+
Returns a `ReproReport`; `ok` is False (with every hash) when they differ.
|
|
227
|
+
"""
|
|
228
|
+
if repeat < 2:
|
|
229
|
+
raise ValueError("verify_run needs repeat >= 2 to compare runs")
|
|
230
|
+
hashes: list[str] = []
|
|
231
|
+
for _ in range(repeat):
|
|
232
|
+
resources = (
|
|
233
|
+
resources_factory() if resources_factory is not None else _resources()
|
|
234
|
+
)
|
|
235
|
+
resources.id_factory = counter_ids()
|
|
236
|
+
if recording is not None:
|
|
237
|
+
resources.llm = ReplayLLM(recording, mode="replay")
|
|
238
|
+
context = await build(resources)
|
|
239
|
+
hashes.append(context_hash(context))
|
|
240
|
+
return ReproReport(
|
|
241
|
+
ok=len(set(hashes)) == 1,
|
|
242
|
+
hashes=hashes,
|
|
243
|
+
repeat=repeat,
|
|
244
|
+
recording=str(recording) if recording is not None else None,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _resources() -> RuntimeResources:
|
|
249
|
+
from .resources import RuntimeResources
|
|
250
|
+
|
|
251
|
+
return RuntimeResources()
|
|
252
|
+
|
|
253
|
+
|
|
171
254
|
def replay_summary(context: Context) -> dict[str, Any]:
|
|
172
255
|
"""A compact "state at this point" summary for the replay CLI."""
|
|
173
256
|
artifacts = context.list_artifacts()
|
|
@@ -184,4 +267,12 @@ def replay_summary(context: Context) -> dict[str, Any]:
|
|
|
184
267
|
}
|
|
185
268
|
|
|
186
269
|
|
|
187
|
-
__all__ = [
|
|
270
|
+
__all__ = [
|
|
271
|
+
"ReproReport",
|
|
272
|
+
"ReplayLLM",
|
|
273
|
+
"ReplayMiss",
|
|
274
|
+
"counter_ids",
|
|
275
|
+
"replay_context",
|
|
276
|
+
"replay_summary",
|
|
277
|
+
"verify_run",
|
|
278
|
+
]
|
|
@@ -15,6 +15,10 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
T = TypeVar("T")
|
|
17
17
|
|
|
18
|
+
#: Mints an artifact id from its model/type name — injectable for deterministic
|
|
19
|
+
#: runs (`reactifact.replay.counter_ids`). `None` keeps the uuid default.
|
|
20
|
+
IdFactory = Callable[[str], str]
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
class ResourceKey(Generic[T]):
|
|
20
24
|
"""A typed key for registering a resource when the *type* isn't a good key.
|
|
@@ -47,11 +51,17 @@ class RuntimeResources:
|
|
|
47
51
|
context_builder: ContextBuilder | None = None,
|
|
48
52
|
verification_threshold: float | None = None,
|
|
49
53
|
redactor: Redactor | None = None,
|
|
54
|
+
id_factory: IdFactory | None = None,
|
|
50
55
|
**additional: Any,
|
|
51
56
|
):
|
|
52
57
|
self.llm = llm
|
|
53
58
|
self.embedder = embedder
|
|
54
59
|
self.sources = sources or {}
|
|
60
|
+
# Injected id source for artifacts created without an explicit id
|
|
61
|
+
# (`None` = the uuid default). A deterministic factory
|
|
62
|
+
# (`reactifact.replay.counter_ids`) makes an unmodified app's
|
|
63
|
+
# `context_hash` reproducible run to run — see `reactifact.replay.verify_run`.
|
|
64
|
+
self.id_factory = id_factory
|
|
55
65
|
# Applied to trace text only (artifact `data`, LLM messages/responses,
|
|
56
66
|
# errors) before it reaches a sink — never to the live `Context` or a
|
|
57
67
|
# persisted session. `None` (default) reproduces the pre-hook behavior.
|
|
@@ -133,6 +143,15 @@ class RuntimeResources:
|
|
|
133
143
|
"""The keys of every registered typed resource (for diagnostics)."""
|
|
134
144
|
return frozenset(self._typed)
|
|
135
145
|
|
|
146
|
+
def typed_values(self) -> list[Any]:
|
|
147
|
+
"""Every registered typed resource value (read-only introspection).
|
|
148
|
+
|
|
149
|
+
`registered` gives the keys; this gives the values — e.g. so a generic
|
|
150
|
+
tool scanner (`reactifact.testing.fault`) can find a `list[Tool]`
|
|
151
|
+
registered with `register(...)`, not just one stashed via `set(...)`.
|
|
152
|
+
"""
|
|
153
|
+
return list(self._typed.values())
|
|
154
|
+
|
|
136
155
|
def set(self, name: str, value: Any) -> None:
|
|
137
156
|
self.additional[name] = value
|
|
138
157
|
|
|
@@ -498,7 +498,9 @@ class Runtime:
|
|
|
498
498
|
self._warn_no_runs()
|
|
499
499
|
await self._trace.end_turn(
|
|
500
500
|
session_id=self.session.session_id if self.session is not None else "",
|
|
501
|
-
|
|
501
|
+
# `time.monotonic()` is seconds; the trace's `duration_ms` (and every
|
|
502
|
+
# sink/UI reading it) is milliseconds — same unit as span latency.
|
|
503
|
+
duration_ms=(time.monotonic() - self._turn_started_at) * 1000,
|
|
502
504
|
outcome=self.outcome.value,
|
|
503
505
|
)
|
|
504
506
|
if self.session is not None and self.session_save_policy == "per_turn":
|
|
@@ -108,17 +108,24 @@ def _iter_tool_dicts(agents: Sequence[Agent]) -> Iterator[dict[str, Tool]]:
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def _iter_tool_lists(resources: RuntimeResources | None) -> Iterator[list[Tool]]:
|
|
111
|
-
"""Yields every `list[Tool]`-shaped value found
|
|
112
|
-
|
|
113
|
-
Covers tools resolved dynamically at produce-time
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
"""Yields every `list[Tool]`-shaped value found on `resources`.
|
|
112
|
+
|
|
113
|
+
Covers tools resolved dynamically at produce-time
|
|
114
|
+
(`context.resources.get(...)` / `resources.require(...)`) rather than fixed
|
|
115
|
+
on a `Produce` instance at construction — `_iter_tool_dicts` has nothing to
|
|
116
|
+
scan for those, since no static attribute holds them. Both storage shapes
|
|
117
|
+
are scanned: the string-keyed `additional` escape hatch, and resources
|
|
118
|
+
registered typedly via `resources.register(...)` (`_typed`, surfaced by
|
|
119
|
+
`RuntimeResources.typed_values()`). Missing the latter silently made
|
|
120
|
+
`result.tools.called/never_called` blind to tools registered that way.
|
|
121
|
+
|
|
122
|
+
Same duck-typing as `_iter_tool_dicts`: any non-empty list whose items all
|
|
123
|
+
have `.execute` is treated as a tool list, wherever it is stored. The
|
|
124
|
+
caller de-dupes by identity, so a list present in both is wrapped once.
|
|
118
125
|
"""
|
|
119
126
|
if resources is None:
|
|
120
127
|
return
|
|
121
|
-
for value in resources.additional.values():
|
|
128
|
+
for value in (*resources.additional.values(), *resources.typed_values()):
|
|
122
129
|
if (
|
|
123
130
|
isinstance(value, list)
|
|
124
131
|
and value
|
|
@@ -33,7 +33,7 @@ from pydantic import BaseModel
|
|
|
33
33
|
from reactifact.agents import Agent
|
|
34
34
|
from reactifact.budget import Budget, RunStats
|
|
35
35
|
from reactifact.context import Context
|
|
36
|
-
from reactifact.resources import RuntimeResources
|
|
36
|
+
from reactifact.resources import ResourceKey, RuntimeResources
|
|
37
37
|
from reactifact.runtime import Runtime
|
|
38
38
|
from reactifact.streaming import ProgressEvent, QueueEvent
|
|
39
39
|
from reactifact.tracing.models import RunTrace
|
|
@@ -176,27 +176,30 @@ class ScenarioLab:
|
|
|
176
176
|
|
|
177
177
|
def fail_resource(
|
|
178
178
|
self,
|
|
179
|
-
|
|
179
|
+
resource: str | type[Any] | ResourceKey[Any],
|
|
180
180
|
error: BaseException | Callable[[], BaseException],
|
|
181
181
|
*,
|
|
182
182
|
method: str | None = None,
|
|
183
183
|
times: int | None = None,
|
|
184
184
|
) -> None:
|
|
185
|
-
"""Queues a fault for a
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`
|
|
185
|
+
"""Queues a fault for a resource — the general-purpose analog of
|
|
186
|
+
`fail()` for anything that isn't a tool.
|
|
187
|
+
|
|
188
|
+
`resource` addresses it either by **string name** — `"llm"`,
|
|
189
|
+
`"embedder"`, a source id (`resources.sources[id]`), or a name set via
|
|
190
|
+
`resources.set(name, ...)` — or by a **typed key**: a class or a
|
|
191
|
+
`ResourceKey` registered with `resources.register(...)`.
|
|
189
192
|
|
|
190
193
|
Wraps the resource in a duck-typed proxy for the next `run()`/
|
|
191
194
|
`.turn()`: `method=None` (default) fails every callable on it;
|
|
192
195
|
naming one method (e.g. `"embed"`, `"search"`) faults only that
|
|
193
196
|
method. `times=None` faults every call; `times=N` faults the first
|
|
194
197
|
`N`, then delegates to the real resource — same shape as `fail()`.
|
|
195
|
-
Raises `ScenarioError` at run time if `
|
|
198
|
+
Raises `ScenarioError` at run time if `resource` doesn't match any
|
|
196
199
|
resource, or matches one that's `None` (nothing configured to fail).
|
|
197
200
|
"""
|
|
198
201
|
self._resource_faults.append(
|
|
199
|
-
ResourceFault(
|
|
202
|
+
ResourceFault(resource, error, method=method, times=times)
|
|
200
203
|
)
|
|
201
204
|
|
|
202
205
|
def _build_resources(self) -> RuntimeResources:
|
|
@@ -24,15 +24,17 @@ from typing import TYPE_CHECKING, Any
|
|
|
24
24
|
from .exceptions import ScenarioError
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
|
-
from reactifact.resources import RuntimeResources
|
|
27
|
+
from reactifact.resources import ResourceKey, RuntimeResources
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@dataclass
|
|
31
31
|
class ResourceFault:
|
|
32
|
-
"""A queued fault for one
|
|
32
|
+
"""A queued fault for one resource.
|
|
33
33
|
|
|
34
|
-
`resource`
|
|
35
|
-
[id]`), or a name set via
|
|
34
|
+
`resource` addresses it either by **string name** — `"llm"`, `"embedder"`,
|
|
35
|
+
a source id (`resources.sources[id]`), or a name set via
|
|
36
|
+
`resources.set(name, ...)` — or by a **typed key**: a class or a
|
|
37
|
+
`ResourceKey` registered with `resources.register(...)`. `method=None`
|
|
36
38
|
(default) fails every callable on the resource; naming one (e.g.
|
|
37
39
|
`"embed"`) faults only that method, leaving the rest of the resource
|
|
38
40
|
working normally. `times=None` faults every call; `times=N` faults the
|
|
@@ -40,15 +42,20 @@ class ResourceFault:
|
|
|
40
42
|
`fault.ToolFault`.
|
|
41
43
|
"""
|
|
42
44
|
|
|
43
|
-
resource: str
|
|
45
|
+
resource: str | type[Any] | ResourceKey[Any]
|
|
44
46
|
error: BaseException | Callable[[], BaseException]
|
|
45
47
|
method: str | None = None
|
|
46
48
|
times: int | None = None
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
def _get_resource(
|
|
51
|
+
def _get_resource(
|
|
52
|
+
resources: RuntimeResources, key: str | type[Any] | ResourceKey[Any]
|
|
53
|
+
) -> tuple[Any, bool]:
|
|
50
54
|
"""Returns `(value, found)` — `found=False` means no such resource exists
|
|
51
55
|
at all (as opposed to existing but being `None`)."""
|
|
56
|
+
if not isinstance(key, str):
|
|
57
|
+
return (resources.get(key), True) if resources.has(key) else (None, False)
|
|
58
|
+
name = key
|
|
52
59
|
if name == "llm":
|
|
53
60
|
return resources.llm, True
|
|
54
61
|
if name == "embedder":
|
|
@@ -60,7 +67,13 @@ def _get_resource(resources: RuntimeResources, name: str) -> tuple[Any, bool]:
|
|
|
60
67
|
return None, False
|
|
61
68
|
|
|
62
69
|
|
|
63
|
-
def _set_resource(
|
|
70
|
+
def _set_resource(
|
|
71
|
+
resources: RuntimeResources, key: str | type[Any] | ResourceKey[Any], value: Any
|
|
72
|
+
) -> None:
|
|
73
|
+
if not isinstance(key, str):
|
|
74
|
+
resources.register(key, value)
|
|
75
|
+
return
|
|
76
|
+
name = key
|
|
64
77
|
if name == "llm":
|
|
65
78
|
resources.llm = value
|
|
66
79
|
elif name == "embedder":
|
|
@@ -140,7 +153,7 @@ class ResourceFaultInstaller:
|
|
|
140
153
|
) -> None:
|
|
141
154
|
self._resources = resources
|
|
142
155
|
self._faults = {f.resource: f for f in faults}
|
|
143
|
-
self._originals: list[tuple[str, Any]] = []
|
|
156
|
+
self._originals: list[tuple[str | type[Any] | ResourceKey[Any], Any]] = []
|
|
144
157
|
|
|
145
158
|
def __enter__(self) -> ResourceFaultInstaller:
|
|
146
159
|
for name, fault in self._faults.items():
|
|
@@ -148,8 +161,9 @@ class ResourceFaultInstaller:
|
|
|
148
161
|
if not found:
|
|
149
162
|
raise ScenarioError(
|
|
150
163
|
f"fail_resource({name!r}, ...): no such resource — expected "
|
|
151
|
-
'"llm", "embedder", a source id,
|
|
152
|
-
"resources.set(...)"
|
|
164
|
+
'"llm", "embedder", a source id, a name set via '
|
|
165
|
+
"resources.set(...), or a key registered via "
|
|
166
|
+
"resources.register(Type | ResourceKey, ...)"
|
|
153
167
|
)
|
|
154
168
|
if original is None:
|
|
155
169
|
raise ScenarioError(
|
|
@@ -6,10 +6,24 @@ but without `web`, so that importing the package does not pull in FastAPI.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .langfuse import LangfuseTracer
|
|
9
|
-
from .models import
|
|
9
|
+
from .models import (
|
|
10
|
+
AgentSpan,
|
|
11
|
+
ArtifactRef,
|
|
12
|
+
LLMCall,
|
|
13
|
+
RelationRef,
|
|
14
|
+
RunTrace,
|
|
15
|
+
Tag,
|
|
16
|
+
TagAssignment,
|
|
17
|
+
)
|
|
10
18
|
from .otlp import OTLPTracer
|
|
11
19
|
from .postgres import PostgresStore
|
|
12
|
-
from .store import
|
|
20
|
+
from .store import (
|
|
21
|
+
TraceAnnotator,
|
|
22
|
+
TraceReader,
|
|
23
|
+
TraceSink,
|
|
24
|
+
TraceStore,
|
|
25
|
+
TraceStoreProtocol,
|
|
26
|
+
)
|
|
13
27
|
from .tracer import CompositeTracer, RecordingLLM, RunTracer, Tracer
|
|
14
28
|
|
|
15
29
|
__all__ = [
|
|
@@ -24,8 +38,12 @@ __all__ = [
|
|
|
24
38
|
"RelationRef",
|
|
25
39
|
"RunTrace",
|
|
26
40
|
"RunTracer",
|
|
41
|
+
"Tag",
|
|
42
|
+
"TagAssignment",
|
|
43
|
+
"TraceAnnotator",
|
|
27
44
|
"TraceReader",
|
|
28
45
|
"TraceSink",
|
|
29
46
|
"TraceStore",
|
|
47
|
+
"TraceStoreProtocol",
|
|
30
48
|
"Tracer",
|
|
31
49
|
]
|