reactifact 0.9.0__tar.gz → 0.9.1__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.0/reactifact.egg-info → reactifact-0.9.1}/PKG-INFO +1 -1
- {reactifact-0.9.0 → reactifact-0.9.1}/pyproject.toml +1 -1
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/__init__.py +1 -1
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/tracer.py +152 -78
- {reactifact-0.9.0 → reactifact-0.9.1/reactifact.egg-info}/PKG-INFO +1 -1
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_tracing.py +77 -1
- {reactifact-0.9.0 → reactifact-0.9.1}/LICENSE +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/README.md +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/__main__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/_extras.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/agent_tool.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/agents.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/artifacts.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/branching.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/budget.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/chat.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/checkpoints.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/branch.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/common.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/context.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/graph.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/replay.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/trace.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/commit.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/commit_log.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/consume.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/context.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/context_builder.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/effects.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/eval.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/events.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/interrupt.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/llm_agent.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/client.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/server.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/operations.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/patches.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/produce.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/prompts.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/azure.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/chat.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/fake.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/groq.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/image.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/openai.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/speech.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/together.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/video.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/xai.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/zai.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/py.typed +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/search.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/status.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/text.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/relations.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/replay.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/resources.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/runtime.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/scheduler.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/session.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/sources.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/streaming.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/structured.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/fault.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/lab.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/mock.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/record.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/registry.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tool_use.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tools.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/__init__.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/models.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/store.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/templates/ui_run.html +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/triggers.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/verify.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/viz.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/SOURCES.txt +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/setup.cfg +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_absent_consume.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_adaptive.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_agent_tool.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_artifacts.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_backbone.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_branching.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_budget.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_chat_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_checkpoint.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_cli.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_commit_log.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_concurrency.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_context_builder.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_debounce.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_devops.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_devops_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_effects.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_eval.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_forklab.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_forklab_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_friendly_api.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_hitl.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_image_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_incident_commander.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_invalidation.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_join_consume.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_knowledge.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_mcp.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_medic_lab.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_multisource.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_openai_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_patches.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_ports.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_produce_styles.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_prompts.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_provider_auth.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_provider_retry.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_providers_integration.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_reacts_to.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_relation_graph.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_relations.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_repair.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_repair_web.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_replay.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_research.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_resources.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_retry.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_runtime.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sessions.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources_search.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources_vector.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_speech_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_streaming.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_structured.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_lab.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_mock.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_registry.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_tools.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_verify.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_video_provider.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_view.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_viz.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_web_source.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_workspace.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.9.0 → reactifact-0.9.1}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.1"
|
|
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"
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import json
|
|
19
|
+
import logging
|
|
19
20
|
import time
|
|
20
21
|
import uuid
|
|
21
22
|
from collections.abc import AsyncIterator, Callable, Iterable
|
|
@@ -34,24 +35,52 @@ if TYPE_CHECKING:
|
|
|
34
35
|
from ..events import Event
|
|
35
36
|
from ..patches import Patch
|
|
36
37
|
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
37
40
|
#: Up to what size to truncate artifact/response data in a trace.
|
|
38
41
|
TRACE_TRUNCATE = 1500
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
def _swallow_tracing_error(where: str, exc: BaseException) -> None:
|
|
45
|
+
"""Reports a tracing failure and lets the run continue (§54).
|
|
46
|
+
|
|
47
|
+
Observability is best-effort by contract: a sink that is unreachable
|
|
48
|
+
(Langfuse down, Postgres refusing connections), a custom `Tracer`
|
|
49
|
+
callback that raises, or a malformed payload must never abort the
|
|
50
|
+
business run it was only supposed to observe. Every tracer call site
|
|
51
|
+
funnels its exceptions here and carries on; the trace for that step is
|
|
52
|
+
simply dropped. `exc_info` keeps the failure debuggable in logs.
|
|
53
|
+
"""
|
|
54
|
+
logger.warning(
|
|
55
|
+
"tracing step %r failed; run continues without it: %s", where, exc, exc_info=exc
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
41
59
|
def _clip(value: str, limit: int = TRACE_TRUNCATE) -> str:
|
|
42
60
|
if len(value) <= limit:
|
|
43
61
|
return value
|
|
44
62
|
return value[:limit] + "…"
|
|
45
63
|
|
|
46
64
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
def _message_fields(message: Any) -> tuple[str, Any]:
|
|
66
|
+
"""Reads role/content off a `Message` dataclass, dict, or pydantic-like object.
|
|
67
|
+
|
|
68
|
+
`LLMRequest.messages` is typed `list[Message]` (a plain dataclass without
|
|
69
|
+
`model_dump`), but providers/hand-rolled callers can also hand over dicts or
|
|
70
|
+
pydantic models — only these two fields are ever traced, so attribute access
|
|
71
|
+
covers all of them without assuming a serialization method.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(message, dict):
|
|
74
|
+
return str(message.get("role", "")), message.get("content")
|
|
75
|
+
return str(getattr(message, "role", "")), getattr(message, "content", None)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _clip_messages(messages: Iterable[Any]) -> list[dict[str, Any]]:
|
|
79
|
+
clipped: list[dict[str, Any]] = []
|
|
80
|
+
for message in messages:
|
|
81
|
+
role, content = _message_fields(message)
|
|
82
|
+
clipped.append({"role": role, "content": _clip(str(content or ""))})
|
|
83
|
+
return clipped
|
|
55
84
|
|
|
56
85
|
|
|
57
86
|
class Tracer:
|
|
@@ -87,7 +116,10 @@ class Tracer:
|
|
|
87
116
|
|
|
88
117
|
async def on_turn_end(self, trace: RunTrace) -> None:
|
|
89
118
|
for sink in self.sinks:
|
|
90
|
-
|
|
119
|
+
try:
|
|
120
|
+
await sink.export(trace)
|
|
121
|
+
except Exception as exc: # noqa: BLE001 — one bad sink must not fail the run
|
|
122
|
+
_swallow_tracing_error(f"{type(sink).__name__}.export", exc)
|
|
91
123
|
|
|
92
124
|
|
|
93
125
|
class RecordingLLM(LLMProvider):
|
|
@@ -110,14 +142,33 @@ class RecordingLLM(LLMProvider):
|
|
|
110
142
|
try:
|
|
111
143
|
response = await self._inner.complete(request)
|
|
112
144
|
except Exception as exc: # noqa: BLE001 — record and re-raise
|
|
113
|
-
self.
|
|
145
|
+
self._safe_record(
|
|
146
|
+
request, None, (time.monotonic() - started) * 1000, str(exc)
|
|
147
|
+
)
|
|
114
148
|
raise
|
|
115
|
-
self.
|
|
149
|
+
self._safe_record(request, response, (time.monotonic() - started) * 1000, None)
|
|
116
150
|
return response
|
|
117
151
|
|
|
118
152
|
def stream(self, request: LLMRequest) -> AsyncIterator[LLMResponseChunk]:
|
|
119
153
|
return self._inner.stream(request)
|
|
120
154
|
|
|
155
|
+
def _safe_record(
|
|
156
|
+
self,
|
|
157
|
+
request: LLMRequest,
|
|
158
|
+
response: LLMResponse | None,
|
|
159
|
+
latency_ms: float,
|
|
160
|
+
error: str | None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""`_record`, isolated: losing one LLMCall must not fail the LLM call.
|
|
163
|
+
|
|
164
|
+
The error path matters most: if recording raised while handling the
|
|
165
|
+
provider's own exception, it would mask that original error.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
self._record(request, response, latency_ms, error)
|
|
169
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
170
|
+
_swallow_tracing_error("RecordingLLM._record", exc)
|
|
171
|
+
|
|
121
172
|
def _record(
|
|
122
173
|
self,
|
|
123
174
|
request: LLMRequest,
|
|
@@ -130,12 +181,7 @@ class RecordingLLM(LLMProvider):
|
|
|
130
181
|
agent=self._agent_of(),
|
|
131
182
|
provider=self._provider,
|
|
132
183
|
model=str(getattr(self._inner, "model", "") or ""),
|
|
133
|
-
messages=_clip_messages(
|
|
134
|
-
[
|
|
135
|
-
m.model_dump() if hasattr(m, "model_dump") else m.__dict__
|
|
136
|
-
for m in request.messages
|
|
137
|
-
]
|
|
138
|
-
),
|
|
184
|
+
messages=_clip_messages(request.messages),
|
|
139
185
|
response=_clip(response.text) if response is not None else "",
|
|
140
186
|
prompt_tokens=int(usage.get("prompt_tokens") or usage.get("prompt") or 0),
|
|
141
187
|
completion_tokens=int(
|
|
@@ -157,15 +203,26 @@ class CompositeTracer:
|
|
|
157
203
|
self, run_id: str, *, session_id: str, started_at: datetime
|
|
158
204
|
) -> None:
|
|
159
205
|
for tracer in self.tracers:
|
|
160
|
-
|
|
206
|
+
try:
|
|
207
|
+
tracer.on_turn_begin(
|
|
208
|
+
run_id, session_id=session_id, started_at=started_at
|
|
209
|
+
)
|
|
210
|
+
except Exception as exc: # noqa: BLE001 — isolate one bad tracer
|
|
211
|
+
_swallow_tracing_error(f"{type(tracer).__name__}.on_turn_begin", exc)
|
|
161
212
|
|
|
162
213
|
def on_span(self, span: AgentSpan) -> None:
|
|
163
214
|
for tracer in self.tracers:
|
|
164
|
-
|
|
215
|
+
try:
|
|
216
|
+
tracer.on_span(span)
|
|
217
|
+
except Exception as exc: # noqa: BLE001 — isolate one bad tracer
|
|
218
|
+
_swallow_tracing_error(f"{type(tracer).__name__}.on_span", exc)
|
|
165
219
|
|
|
166
220
|
async def on_turn_end(self, trace: RunTrace) -> None:
|
|
167
221
|
for tracer in self.tracers:
|
|
168
|
-
|
|
222
|
+
try:
|
|
223
|
+
await tracer.on_turn_end(trace)
|
|
224
|
+
except Exception as exc: # noqa: BLE001 — isolate one bad tracer
|
|
225
|
+
_swallow_tracing_error(f"{type(tracer).__name__}.on_turn_end", exc)
|
|
169
226
|
|
|
170
227
|
|
|
171
228
|
class RunTracer:
|
|
@@ -272,44 +329,52 @@ class RunTracer:
|
|
|
272
329
|
]
|
|
273
330
|
|
|
274
331
|
def write_refs(self, patch: Patch, writes: list[Write]) -> list[ArtifactRef]:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
332
|
+
try:
|
|
333
|
+
ops_by_id: dict[str, tuple[str, Any | None]] = {}
|
|
334
|
+
for op in patch.operations:
|
|
335
|
+
artifact_id = getattr(op, "artifact_id", None)
|
|
336
|
+
if artifact_id is None:
|
|
337
|
+
continue
|
|
338
|
+
if isinstance(op, Create):
|
|
339
|
+
model: Any | None = op.data
|
|
340
|
+
elif isinstance(op, Update):
|
|
341
|
+
model = op.new_data
|
|
342
|
+
else:
|
|
343
|
+
model = None
|
|
344
|
+
ops_by_id[artifact_id] = (op.to_dict().get("type", ""), model)
|
|
345
|
+
return [
|
|
346
|
+
self.artifact_ref(
|
|
347
|
+
w.artifact_id,
|
|
348
|
+
w.version,
|
|
349
|
+
ops_by_id.get(w.artifact_id, ("", None))[0],
|
|
350
|
+
ops_by_id.get(w.artifact_id, ("", None))[1],
|
|
351
|
+
)
|
|
352
|
+
for w in writes
|
|
353
|
+
]
|
|
354
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
355
|
+
_swallow_tracing_error("write_refs", exc)
|
|
356
|
+
return []
|
|
296
357
|
|
|
297
358
|
def relation_refs(self, patch: Patch) -> list[RelationRef]:
|
|
298
359
|
"""Provenance edges (`patch.link`) recorded for the span (§34)."""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
360
|
+
try:
|
|
361
|
+
refs: list[RelationRef] = []
|
|
362
|
+
for op in patch.operations:
|
|
363
|
+
if not isinstance(op, Link):
|
|
364
|
+
continue
|
|
365
|
+
refs.append(
|
|
366
|
+
RelationRef(
|
|
367
|
+
source_id=op.artifact_id,
|
|
368
|
+
relation=op.relation,
|
|
369
|
+
target_id=op.target_id,
|
|
370
|
+
source_type=self._type_name(op.artifact_id, self._context),
|
|
371
|
+
target_type=self._type_name(op.target_id, self._context),
|
|
372
|
+
)
|
|
310
373
|
)
|
|
311
|
-
|
|
312
|
-
|
|
374
|
+
return refs
|
|
375
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
376
|
+
_swallow_tracing_error("relation_refs", exc)
|
|
377
|
+
return []
|
|
313
378
|
|
|
314
379
|
# ---- per-turn lifecycle ----
|
|
315
380
|
|
|
@@ -319,9 +384,12 @@ class RunTracer:
|
|
|
319
384
|
self.run_id = str(uuid.uuid4())
|
|
320
385
|
self.spans = []
|
|
321
386
|
self._trace_data_cache = {}
|
|
322
|
-
|
|
323
|
-
self.
|
|
324
|
-
|
|
387
|
+
try:
|
|
388
|
+
self.tracer.on_turn_begin(
|
|
389
|
+
self.run_id, session_id=session_id, started_at=datetime.now(UTC)
|
|
390
|
+
)
|
|
391
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
392
|
+
_swallow_tracing_error("on_turn_begin", exc)
|
|
325
393
|
|
|
326
394
|
def record_span(
|
|
327
395
|
self,
|
|
@@ -340,31 +408,37 @@ class RunTracer:
|
|
|
340
408
|
"""
|
|
341
409
|
if self.tracer is None:
|
|
342
410
|
return None
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
try:
|
|
412
|
+
span = AgentSpan(
|
|
413
|
+
agent=agent.name,
|
|
414
|
+
event_type=event.type.value,
|
|
415
|
+
reads=self.read_refs(reads),
|
|
416
|
+
latency_ms=latency_ms,
|
|
417
|
+
llm_calls=self._pending_llm.pop(agent.name, []),
|
|
418
|
+
error=f"{type(error).__name__}: {error}" if error is not None else None,
|
|
419
|
+
started_at=datetime.now(UTC),
|
|
420
|
+
)
|
|
421
|
+
self.spans.append(span)
|
|
422
|
+
self.tracer.on_span(span)
|
|
423
|
+
return span
|
|
424
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
425
|
+
_swallow_tracing_error("on_span", exc)
|
|
426
|
+
return None
|
|
355
427
|
|
|
356
428
|
async def end_turn(
|
|
357
429
|
self, *, session_id: str, duration_ms: float, outcome: str
|
|
358
430
|
) -> None:
|
|
359
431
|
if self.tracer is None:
|
|
360
432
|
return
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
spans=self.spans,
|
|
369
|
-
)
|
|
433
|
+
trace = RunTrace(
|
|
434
|
+
id=self.run_id,
|
|
435
|
+
session_id=session_id,
|
|
436
|
+
started_at=datetime.now(UTC),
|
|
437
|
+
duration_ms=duration_ms,
|
|
438
|
+
outcome=outcome,
|
|
439
|
+
spans=self.spans,
|
|
370
440
|
)
|
|
441
|
+
try:
|
|
442
|
+
await self.tracer.on_turn_end(trace)
|
|
443
|
+
except Exception as exc: # noqa: BLE001 — best-effort observability
|
|
444
|
+
_swallow_tracing_error("on_turn_end", exc)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -3,11 +3,12 @@ import json
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
from reactifact import Agent, Consume, Context, Patch, Runtime, RuntimeResources
|
|
6
|
-
from reactifact.providers import LLMProvider, LLMRequest, LLMResponse
|
|
6
|
+
from reactifact.providers import LLMProvider, LLMRequest, LLMResponse, Message
|
|
7
7
|
from reactifact.tracing import (
|
|
8
8
|
AgentSpan,
|
|
9
9
|
ArtifactRef,
|
|
10
10
|
LLMCall,
|
|
11
|
+
RecordingLLM,
|
|
11
12
|
RelationRef,
|
|
12
13
|
RunTrace,
|
|
13
14
|
Tracer,
|
|
@@ -202,6 +203,81 @@ def test_composite_tracer_fans_out(tmp_path):
|
|
|
202
203
|
assert run(store_b.query())["total"] == 1
|
|
203
204
|
|
|
204
205
|
|
|
206
|
+
class BoomSink:
|
|
207
|
+
"""A sink that is always unreachable (e.g. a down Langfuse)."""
|
|
208
|
+
|
|
209
|
+
async def export(self, trace: RunTrace) -> None:
|
|
210
|
+
raise RuntimeError("langfuse is down")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class CapturingSink:
|
|
214
|
+
"""Records the traces it received, to prove fan-out continued."""
|
|
215
|
+
|
|
216
|
+
def __init__(self) -> None:
|
|
217
|
+
self.traces: list[RunTrace] = []
|
|
218
|
+
|
|
219
|
+
async def export(self, trace: RunTrace) -> None:
|
|
220
|
+
self.traces.append(trace)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_failing_sink_does_not_break_the_run():
|
|
224
|
+
"""An unreachable sink is skipped; the run and healthy sinks still proceed."""
|
|
225
|
+
ok = CapturingSink()
|
|
226
|
+
ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
|
|
227
|
+
runtime = Runtime(ctx, agents=[Greeter()], tracer=Tracer(sinks=[BoomSink(), ok]))
|
|
228
|
+
ctx.create(Question(text="привет"))
|
|
229
|
+
|
|
230
|
+
assert asyncio.run(runtime.arun()) >= 1
|
|
231
|
+
assert ctx.list_artifacts(Answer) # business outcome still produced
|
|
232
|
+
assert len(ok.traces) == 1 # the healthy sibling still got the trace
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class BoomTracer(Tracer):
|
|
236
|
+
"""A custom tracer whose every callback raises."""
|
|
237
|
+
|
|
238
|
+
def on_turn_begin(self, run_id, *, session_id, started_at) -> None:
|
|
239
|
+
raise RuntimeError("boom")
|
|
240
|
+
|
|
241
|
+
def on_span(self, span) -> None:
|
|
242
|
+
raise RuntimeError("boom")
|
|
243
|
+
|
|
244
|
+
async def on_turn_end(self, trace: RunTrace) -> None:
|
|
245
|
+
raise RuntimeError("boom")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_failing_custom_tracer_does_not_break_the_run():
|
|
249
|
+
ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
|
|
250
|
+
runtime = Runtime(ctx, agents=[Greeter()], tracer=BoomTracer())
|
|
251
|
+
ctx.create(Question(text="привет"))
|
|
252
|
+
|
|
253
|
+
assert asyncio.run(runtime.arun()) >= 1
|
|
254
|
+
assert ctx.list_artifacts(Answer)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_composite_tracer_isolates_a_failing_member():
|
|
258
|
+
ok = CapturingSink()
|
|
259
|
+
ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
|
|
260
|
+
runtime = Runtime(
|
|
261
|
+
ctx, agents=[Greeter()], tracer=[BoomTracer(), Tracer(sinks=[ok])]
|
|
262
|
+
)
|
|
263
|
+
ctx.create(Question(text="привет"))
|
|
264
|
+
|
|
265
|
+
assert asyncio.run(runtime.arun()) >= 1
|
|
266
|
+
assert len(ok.traces) == 1 # the second tracer still received the trace
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_recording_llm_ignores_a_failing_recorder():
|
|
270
|
+
"""A raising `on_call` must not fail the wrapped LLM call."""
|
|
271
|
+
|
|
272
|
+
def boom(call: LLMCall) -> None:
|
|
273
|
+
raise RuntimeError("tracer is down")
|
|
274
|
+
|
|
275
|
+
llm = RecordingLLM(ReplyLLM(), on_call=boom, agent_of=lambda: "agent")
|
|
276
|
+
response = asyncio.run(llm.complete(LLMRequest(messages=[Message.user("hi")])))
|
|
277
|
+
|
|
278
|
+
assert response.text == '{"text":"привет"}'
|
|
279
|
+
|
|
280
|
+
|
|
205
281
|
def test_trace_store_migrates_old_schema(tmp_path):
|
|
206
282
|
"""Old DB without the llm_calls column — migrated without data loss."""
|
|
207
283
|
import sqlite3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|