reactifact 0.15.1__tar.gz → 0.15.3__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.15.1/reactifact.egg-info → reactifact-0.15.3}/PKG-INFO +19 -2
- {reactifact-0.15.1 → reactifact-0.15.3}/README.md +18 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/pyproject.toml +1 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/__init__.py +1 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/agents.py +42 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/audit.py +22 -2
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/interrupt.py +12 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/produce.py +35 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/runtime.py +24 -9
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/__init__.py +2 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/langfuse.py +73 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/models.py +20 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/otlp.py +11 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/postgres.py +31 -13
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/store.py +29 -13
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/app.css +12 -1
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/ui_run.html +30 -4
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/tracer.py +13 -1
- {reactifact-0.15.1 → reactifact-0.15.3/reactifact.egg-info}/PKG-INFO +19 -2
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_audit.py +33 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_outbox.py +10 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_produce_styles.py +17 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_public_api.py +1 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_tracing.py +117 -2
- {reactifact-0.15.1 → reactifact-0.15.3}/LICENSE +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/__main__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/_extras.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/_httpx.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/client.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/models.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/remote.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/server.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/agent_tool.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/artifacts.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/authz.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/branching.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/budget.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cache.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/chat.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/checkpoints.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/branch.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/common.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/context.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/eval.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/graph.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/replay.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/skills.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/trace.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/commit.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/commit_log.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/consume.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/context.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/context_builder.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/effects.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/dataset.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/evaluators.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/judge.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/online.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/runner.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/scoring.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/events.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/guardrails.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/llm_agent.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/logging.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/client.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/server.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/metrics.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/operations.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/patches.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/pricing.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/prompts.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/azure.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/chat.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/fake.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/groq.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/image.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/openai.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/speech.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/together.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/video.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/xai.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/zai.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/py.typed +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/agent.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/chat.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/models.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/rag.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quota.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/conversation.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/search.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/status.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/text.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/tool_loop.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/redaction.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/relations.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/replay.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/request.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/resources.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/routing.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/scheduler.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/session.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-eval/references/online.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/sources.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/streaming.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/structured.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/fault.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/golden.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/lab.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/mock.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/pytest_plugin.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/record.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/registry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tool_use.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tools.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/columns.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/sessions.html +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/triggers.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/types.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/verify.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/viz.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/SOURCES.txt +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/setup.cfg +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a_example.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a_interop.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_absent_consume.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_adaptive.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_agent_tool.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_artifacts.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_authz.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_backbone.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_branching.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_budget.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cache.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_example.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_memory.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_outbox.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_checkpoint.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cli.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_commit_log.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_compaction.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_concurrency.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_context_builder.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_debounce.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_determinism.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops_online.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_effects.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_dataset.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_judge.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_online.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_trajectory.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_forklab.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_forklab_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_friendly_api.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_golden.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_guardrails.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_hitl.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_image_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_incident_commander.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_invalidation.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_join_consume.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_knowledge.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_logging.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_mcp.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_medic_lab.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_metrics.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_multisource.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_openai_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_patches.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_pending_actions.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_ports.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_prompts.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_provider_auth.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_provider_retry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_providers_integration.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_quick.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_quota.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_reacts_to.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_conversation.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_tool_loop.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_redaction.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_relation_graph.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_relations.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repair.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repair_web.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_replay.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repo_agent.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_request.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_research.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_resources.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_retry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_routing.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_runtime.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sessions.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_shutdown.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_skills.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources_search.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources_vector.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_speech_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_starter_app.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_streaming.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_structured.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_support_copilot.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_golden.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_lab.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_mock.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_provenance.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_pytest_plugin.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_registry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_token_budget.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_tools.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_trace_columns.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_trace_tags.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_type_registry.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_verify.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_video_provider.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_view.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_viz.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_web_source.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_workspace.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.15.1 → reactifact-0.15.3}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.3
|
|
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
|
|
@@ -323,6 +323,23 @@ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
|
323
323
|
- **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
|
|
324
324
|
`reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
|
|
325
325
|
|
|
326
|
+
## Agent Skills
|
|
327
|
+
|
|
328
|
+
reactifact ships official **Agent Skills** — reusable instructions that teach a
|
|
329
|
+
coding agent (Claude Code, Cursor, Codex, Copilot, …) how to build *with*
|
|
330
|
+
reactifact. They're bundled with the package, so they always match your version:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
python -m reactifact skills list # what is available
|
|
334
|
+
python -m reactifact skills install # -> .agents/skills/
|
|
335
|
+
python -m reactifact skills install --target both # + .claude/skills/
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Skills: `reactifact` (mental model), `reactifact-agents`, `reactifact-llm`,
|
|
339
|
+
`reactifact-rag`, `reactifact-testing`, `reactifact-eval`,
|
|
340
|
+
`reactifact-observability`, `reactifact-from-langchain` — see
|
|
341
|
+
[Agent skills](docs/en/skills.md).
|
|
342
|
+
|
|
326
343
|
## Run a demo
|
|
327
344
|
|
|
328
345
|
Offline-capable, no API keys required (deterministic fallbacks):
|
|
@@ -380,7 +397,7 @@ uv sync --extra dev --extra web
|
|
|
380
397
|
|
|
381
398
|
## Status & contributing
|
|
382
399
|
|
|
383
|
-
Active, `0.
|
|
400
|
+
Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
384
401
|
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
385
402
|
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
386
403
|
[GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
|
|
@@ -277,6 +277,23 @@ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
|
|
|
277
277
|
- **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
|
|
278
278
|
`reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
|
|
279
279
|
|
|
280
|
+
## Agent Skills
|
|
281
|
+
|
|
282
|
+
reactifact ships official **Agent Skills** — reusable instructions that teach a
|
|
283
|
+
coding agent (Claude Code, Cursor, Codex, Copilot, …) how to build *with*
|
|
284
|
+
reactifact. They're bundled with the package, so they always match your version:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
python -m reactifact skills list # what is available
|
|
288
|
+
python -m reactifact skills install # -> .agents/skills/
|
|
289
|
+
python -m reactifact skills install --target both # + .claude/skills/
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Skills: `reactifact` (mental model), `reactifact-agents`, `reactifact-llm`,
|
|
293
|
+
`reactifact-rag`, `reactifact-testing`, `reactifact-eval`,
|
|
294
|
+
`reactifact-observability`, `reactifact-from-langchain` — see
|
|
295
|
+
[Agent skills](docs/en/skills.md).
|
|
296
|
+
|
|
280
297
|
## Run a demo
|
|
281
298
|
|
|
282
299
|
Offline-capable, no API keys required (deterministic fallbacks):
|
|
@@ -334,7 +351,7 @@ uv sync --extra dev --extra web
|
|
|
334
351
|
|
|
335
352
|
## Status & contributing
|
|
336
353
|
|
|
337
|
-
Active, `0.
|
|
354
|
+
Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
338
355
|
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
339
356
|
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
340
357
|
[GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.3"
|
|
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"
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from abc import ABC
|
|
4
5
|
from collections.abc import Sequence
|
|
6
|
+
from contextvars import ContextVar, Token
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
7
9
|
from .artifacts import Artifact
|
|
8
10
|
from .consume import Consume
|
|
9
11
|
from .context import Context
|
|
12
|
+
from .effects import current_effects
|
|
10
13
|
from .events import Event, EventType
|
|
11
14
|
from .patches import Patch
|
|
12
15
|
from .produce import Produce, ProduceCall
|
|
13
16
|
from .triggers import Trigger
|
|
14
17
|
|
|
18
|
+
#: Per-task log of the produces that ran for the current agent execution:
|
|
19
|
+
#: ``(produce_class_name, operations_authored, latency_ms)``. ``Runtime._execute``
|
|
20
|
+
#: opens it (and later records the span), ``Agent.execute`` appends — a
|
|
21
|
+
#: contextvar so concurrent executions sharing one ``Agent`` instance never race.
|
|
22
|
+
_produce_runs: ContextVar[list[tuple[str, int, float]] | None] = ContextVar(
|
|
23
|
+
"reactifact_produce_runs", default=None
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def begin_produce_runs() -> Token[Any]:
|
|
28
|
+
"""Opens the per-task produce-run log (see ``_produce_runs``)."""
|
|
29
|
+
return _produce_runs.set([])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def produce_runs() -> list[tuple[str, int, float]]:
|
|
33
|
+
"""The produce runs recorded for the current task (empty if none)."""
|
|
34
|
+
return _produce_runs.get() or []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def end_produce_runs(token: Token[Any]) -> None:
|
|
38
|
+
_produce_runs.reset(token)
|
|
39
|
+
|
|
15
40
|
|
|
16
41
|
class Agent(ABC): # noqa: B024 — interface without abstract methods, run() has a default
|
|
17
42
|
"""Base container: consumes some artifacts and produces others.
|
|
@@ -154,14 +179,30 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
154
179
|
if not self.produces:
|
|
155
180
|
return None
|
|
156
181
|
inputs = self._collect_inputs(context)
|
|
182
|
+
slot = current_effects()
|
|
183
|
+
log = _produce_runs.get()
|
|
157
184
|
for p in self.produces:
|
|
158
185
|
runs, trigger = self._resolve_produce_call(p, event, context)
|
|
159
186
|
if not runs:
|
|
160
187
|
continue
|
|
188
|
+
before = len(slot.operations) if slot is not None else 0
|
|
189
|
+
started = time.monotonic()
|
|
161
190
|
call = ProduceCall(
|
|
162
191
|
context=context, inputs=inputs, event=event, trigger=trigger
|
|
163
192
|
)
|
|
164
|
-
|
|
193
|
+
try:
|
|
194
|
+
await p.produce(call)
|
|
195
|
+
finally:
|
|
196
|
+
# Record the produce even when it raises — a failing produce is
|
|
197
|
+
# exactly what a trace should show (the span is then the error
|
|
198
|
+
# span, with this produce's partial operations/latency).
|
|
199
|
+
if log is not None:
|
|
200
|
+
operations = (
|
|
201
|
+
len(slot.operations) if slot is not None else 0
|
|
202
|
+
) - before
|
|
203
|
+
log.append(
|
|
204
|
+
(p.name, operations, (time.monotonic() - started) * 1000)
|
|
205
|
+
)
|
|
165
206
|
return None
|
|
166
207
|
|
|
167
208
|
@staticmethod
|
|
@@ -47,10 +47,30 @@ DEFAULT_RELATIONS: tuple[str, ...] = (
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _transient_fields(model: type[BaseModel]) -> set[str]:
|
|
51
|
+
"""Field names a model declares as transactional *envelope*, not state.
|
|
52
|
+
|
|
53
|
+
A model lists them as ``transient_fields: ClassVar[frozenset[str]]``
|
|
54
|
+
(e.g. `PendingAction.dispatched_at`, `PendingQuestion.resolved_at`).
|
|
55
|
+
Wall-clock transitions are excluded from the content hash so an otherwise
|
|
56
|
+
identical state doesn't hash differently run to run — the *fact* of the
|
|
57
|
+
transition stays hashed (`status`/`resolution`); only its timestamp drops.
|
|
58
|
+
"""
|
|
59
|
+
names = getattr(model, "transient_fields", ())
|
|
60
|
+
return set(names) if names else set()
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
def _canonical(value: Any) -> str:
|
|
51
|
-
"""Canonical JSON for hashing (sorted keys; pydantic models dumped).
|
|
64
|
+
"""Canonical JSON for hashing (sorted keys; pydantic models dumped).
|
|
65
|
+
|
|
66
|
+
A model's `transient_fields` are dropped before hashing — envelope, not
|
|
67
|
+
state. Everything else is serialized exactly as before, so a state with no
|
|
68
|
+
transient fields hashes the same as it always did.
|
|
69
|
+
"""
|
|
52
70
|
if isinstance(value, BaseModel):
|
|
53
|
-
payload: Any = value.model_dump(
|
|
71
|
+
payload: Any = value.model_dump(
|
|
72
|
+
mode="json", exclude=_transient_fields(type(value)) or None
|
|
73
|
+
)
|
|
54
74
|
else:
|
|
55
75
|
payload = value
|
|
56
76
|
return json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Any, Literal
|
|
4
|
+
from typing import Any, ClassVar, Literal
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
@@ -21,6 +21,11 @@ class PendingQuestion(BaseModel):
|
|
|
21
21
|
resolution: str | None = None
|
|
22
22
|
resolved_at: datetime | None = None
|
|
23
23
|
|
|
24
|
+
#: Wall-clock fields excluded from `context_hash` (transactional envelope,
|
|
25
|
+
#: not state): a resolved question's *state* is `resolution`, the moment it
|
|
26
|
+
#: was resolved is not something two equivalent runs must agree on.
|
|
27
|
+
transient_fields: ClassVar[frozenset[str]] = frozenset({"resolved_at"})
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
class PendingAction(BaseModel):
|
|
26
31
|
"""An outbound side effect awaiting dispatch (outbox, §42/§55/§59).
|
|
@@ -52,3 +57,9 @@ class PendingAction(BaseModel):
|
|
|
52
57
|
attempts: int = 0
|
|
53
58
|
dispatched_at: datetime | None = None
|
|
54
59
|
error: str | None = None
|
|
60
|
+
|
|
61
|
+
#: Wall-clock field excluded from `context_hash` (transactional envelope,
|
|
62
|
+
#: not state): the *fact* that the action dispatched (`status`/`error`/
|
|
63
|
+
#: `attempts`) is hashed, the moment it happened is not — so an outbox run
|
|
64
|
+
#: stays run-to-run reproducible.
|
|
65
|
+
transient_fields: ClassVar[frozenset[str]] = frozenset({"dispatched_at"})
|
|
@@ -266,6 +266,37 @@ class Produce(Generic[TOut]):
|
|
|
266
266
|
"""
|
|
267
267
|
return None
|
|
268
268
|
|
|
269
|
+
#: Explicit display name (set by e.g. tool-use produces); when unset,
|
|
270
|
+
#: `name` derives a readable one from the class/function/artifact type.
|
|
271
|
+
_name: str | None = None
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def name(self) -> str:
|
|
275
|
+
"""A human-readable name for traces/logs.
|
|
276
|
+
|
|
277
|
+
Subclass-style produces use their class name; an ``@produce``-decorated
|
|
278
|
+
function uses the wrapped function's name; a bare ``Produce(Model)``
|
|
279
|
+
(declared only to widen an agent's allowed ``Create`` types) falls back
|
|
280
|
+
to its output artifact type — otherwise every such no-op would show up
|
|
281
|
+
as a generic ``Produce``. An explicit ``name`` (e.g. a tool-use produce's)
|
|
282
|
+
always wins.
|
|
283
|
+
"""
|
|
284
|
+
explicit = getattr(self, "_name", None)
|
|
285
|
+
if explicit:
|
|
286
|
+
return str(explicit)
|
|
287
|
+
cls = type(self).__name__
|
|
288
|
+
if cls not in ("Produce", "_FunctionProduce"):
|
|
289
|
+
return cls
|
|
290
|
+
func = getattr(self, "_func", None)
|
|
291
|
+
if func is not None:
|
|
292
|
+
return str(getattr(func, "__name__", cls))
|
|
293
|
+
artifact = getattr(self.artifact_type, "__name__", "")
|
|
294
|
+
return f"Produce[{artifact}]" if artifact else cls
|
|
295
|
+
|
|
296
|
+
@name.setter
|
|
297
|
+
def name(self, value: str) -> None:
|
|
298
|
+
self._name = value
|
|
299
|
+
|
|
269
300
|
def _apply_result(self, result: Any) -> None:
|
|
270
301
|
"""Writes a `@produce`-decorated function's return-style result into
|
|
271
302
|
the effect slot (§24).
|
|
@@ -319,6 +350,8 @@ def produce(
|
|
|
319
350
|
)
|
|
320
351
|
|
|
321
352
|
class _FunctionProduce(Produce[Any]):
|
|
353
|
+
_func: Callable[[ProduceCall], Any]
|
|
354
|
+
|
|
322
355
|
async def produce(self, call: ProduceCall) -> None:
|
|
323
356
|
result = func(call)
|
|
324
357
|
if asyncio.iscoroutine(result):
|
|
@@ -329,6 +362,8 @@ def produce(
|
|
|
329
362
|
instance = _FunctionProduce(
|
|
330
363
|
artifact_type=artifact_type, also_creates=also_creates, reacts_to=reacts_to
|
|
331
364
|
)
|
|
365
|
+
# Keep the function for `Produce.name` (a readable trace/log label).
|
|
366
|
+
instance._func = func
|
|
332
367
|
return instance
|
|
333
368
|
|
|
334
369
|
return decorator
|
|
@@ -7,7 +7,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import Any, Literal, cast
|
|
9
9
|
|
|
10
|
-
from .agents import Agent
|
|
10
|
+
from .agents import Agent, begin_produce_runs, end_produce_runs, produce_runs
|
|
11
11
|
from .artifacts import Artifact
|
|
12
12
|
from .budget import Budget, BudgetLLM, BudgetTracker, RunOutcome, RunStats
|
|
13
13
|
from .commit import Commit, Read, Write
|
|
@@ -31,9 +31,17 @@ logger = get_logger(__name__)
|
|
|
31
31
|
#: A scheduled patch with its trigger reads and (optional) trace span.
|
|
32
32
|
PatchWork = tuple[Patch, Agent, list[Read], AgentSpan | None]
|
|
33
33
|
#: One agent execution result: patch (if any), the agent/event/reads that
|
|
34
|
-
#: produced it, latency,
|
|
35
|
-
#: `isolate_errors` was set.
|
|
36
|
-
AgentResult = tuple[
|
|
34
|
+
#: produced it, latency, the exception if the produce raised and
|
|
35
|
+
#: `isolate_errors` was set, and the produces that ran (name/ops/latency).
|
|
36
|
+
AgentResult = tuple[
|
|
37
|
+
Patch | None,
|
|
38
|
+
Agent,
|
|
39
|
+
Event,
|
|
40
|
+
list[Read],
|
|
41
|
+
float,
|
|
42
|
+
BaseException | None,
|
|
43
|
+
list[tuple[str, int, float]],
|
|
44
|
+
]
|
|
37
45
|
#: Performs one committed `PendingAction` (outbox). Called by the runtime after
|
|
38
46
|
#: the intent is committed; the app performs the real I/O and returns (or
|
|
39
47
|
#: raises). Must be idempotent on `idempotency_key` (§42).
|
|
@@ -650,18 +658,22 @@ class Runtime:
|
|
|
650
658
|
"""
|
|
651
659
|
patches_to_apply: list[PatchWork] = []
|
|
652
660
|
runs = 0
|
|
653
|
-
for patch, agent, event, reads, latency, error in results:
|
|
661
|
+
for patch, agent, event, reads, latency, error, produce_run_list in results:
|
|
654
662
|
if self._budget_exhausted():
|
|
655
663
|
break
|
|
656
664
|
runs += 1
|
|
657
665
|
self._runs_used += 1
|
|
658
666
|
if error is not None:
|
|
659
667
|
self._errors_used += 1
|
|
660
|
-
self._trace.record_span(
|
|
668
|
+
self._trace.record_span(
|
|
669
|
+
agent, event, reads, latency, error=error, produces=produce_run_list
|
|
670
|
+
)
|
|
661
671
|
continue
|
|
662
672
|
if patch is None or patch.is_empty():
|
|
663
673
|
continue
|
|
664
|
-
span = self._trace.record_span(
|
|
674
|
+
span = self._trace.record_span(
|
|
675
|
+
agent, event, reads, latency, produces=produce_run_list
|
|
676
|
+
)
|
|
665
677
|
self._validate_patch_types(patch, agent)
|
|
666
678
|
self._enforce_authorization(patch)
|
|
667
679
|
patch = self._apply_guardrails(patch)
|
|
@@ -794,6 +806,7 @@ class Runtime:
|
|
|
794
806
|
task = asyncio.current_task()
|
|
795
807
|
self._trace.register_task(task, agent.name)
|
|
796
808
|
effects_token = set_effects(Effects(self.context))
|
|
809
|
+
produce_runs_token = begin_produce_runs()
|
|
797
810
|
slot: Effects | None = None
|
|
798
811
|
patch: Patch | None = None
|
|
799
812
|
error: BaseException | None = None
|
|
@@ -817,6 +830,8 @@ class Runtime:
|
|
|
817
830
|
if self.on_agent_error is not None:
|
|
818
831
|
self.on_agent_error(agent, event, exc)
|
|
819
832
|
finally:
|
|
833
|
+
runs = produce_runs()
|
|
834
|
+
end_produce_runs(produce_runs_token)
|
|
820
835
|
reset_effects(effects_token)
|
|
821
836
|
self._trace.unregister_task(task)
|
|
822
837
|
latency = (time.monotonic() - started) * 1000
|
|
@@ -829,7 +844,7 @@ class Runtime:
|
|
|
829
844
|
"error": repr(error),
|
|
830
845
|
},
|
|
831
846
|
)
|
|
832
|
-
return None, agent, event, reads, latency, error
|
|
847
|
+
return None, agent, event, reads, latency, error, runs
|
|
833
848
|
logger.debug(
|
|
834
849
|
"agent finished",
|
|
835
850
|
extra={"agent": agent.name, "latency_ms": round(latency, 2)},
|
|
@@ -843,7 +858,7 @@ class Runtime:
|
|
|
843
858
|
combined.merge(patch)
|
|
844
859
|
combined.merge(slot.to_patch())
|
|
845
860
|
patch = combined
|
|
846
|
-
return patch, agent, event, reads, latency, None
|
|
861
|
+
return patch, agent, event, reads, latency, None, runs
|
|
847
862
|
|
|
848
863
|
async def arun(
|
|
849
864
|
self,
|
|
@@ -11,6 +11,7 @@ from .models import (
|
|
|
11
11
|
AgentSpan,
|
|
12
12
|
ArtifactRef,
|
|
13
13
|
LLMCall,
|
|
14
|
+
ProduceRun,
|
|
14
15
|
RelationRef,
|
|
15
16
|
RunTrace,
|
|
16
17
|
Tag,
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"LangfuseTracer",
|
|
36
37
|
"OTLPTracer",
|
|
37
38
|
"PostgresStore",
|
|
39
|
+
"ProduceRun",
|
|
38
40
|
"RecordingLLM",
|
|
39
41
|
"RelationRef",
|
|
40
42
|
"RunTrace",
|
|
@@ -96,6 +96,11 @@ class LangfuseTracer(Tracer):
|
|
|
96
96
|
)
|
|
97
97
|
span_start = span.started_at or run_start
|
|
98
98
|
agent_span_id = _span_id(f"{trace_id}:span:{i}:{span.agent}")
|
|
99
|
+
spans.extend(
|
|
100
|
+
self._produce_spans(
|
|
101
|
+
trace_id, agent_span_id, trace_attrs, run_start, i, span
|
|
102
|
+
)
|
|
103
|
+
)
|
|
99
104
|
for j, call in enumerate(span.llm_calls):
|
|
100
105
|
spans.append(
|
|
101
106
|
self._llm_span(
|
|
@@ -145,8 +150,29 @@ class LangfuseTracer(Tracer):
|
|
|
145
150
|
_type_summary(span.writes),
|
|
146
151
|
),
|
|
147
152
|
]
|
|
153
|
+
if span.event_artifact_type:
|
|
154
|
+
attrs.append(
|
|
155
|
+
_attr(
|
|
156
|
+
"langfuse.observation.metadata.event_artifact_type",
|
|
157
|
+
span.event_artifact_type,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
148
160
|
if span.error:
|
|
149
161
|
attrs.append(_attr("langfuse.observation.metadata.error", span.error))
|
|
162
|
+
if span.produces:
|
|
163
|
+
attrs.append(
|
|
164
|
+
_attr(
|
|
165
|
+
"langfuse.observation.metadata.produces",
|
|
166
|
+
[
|
|
167
|
+
{
|
|
168
|
+
"name": p.name,
|
|
169
|
+
"operations": p.operations,
|
|
170
|
+
"latency_ms": p.latency_ms,
|
|
171
|
+
}
|
|
172
|
+
for p in span.produces
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
)
|
|
150
176
|
return {
|
|
151
177
|
"traceId": trace_id,
|
|
152
178
|
"spanId": _span_id(f"{trace_id}:span:{index}:{span.agent}"),
|
|
@@ -158,6 +184,53 @@ class LangfuseTracer(Tracer):
|
|
|
158
184
|
"attributes": attrs,
|
|
159
185
|
}
|
|
160
186
|
|
|
187
|
+
def _produce_spans(
|
|
188
|
+
self,
|
|
189
|
+
trace_id: str,
|
|
190
|
+
parent_span_id: str,
|
|
191
|
+
trace_attrs: list[dict[str, Any]],
|
|
192
|
+
run_start: datetime,
|
|
193
|
+
span_index: int,
|
|
194
|
+
span: AgentSpan,
|
|
195
|
+
) -> list[dict[str, Any]]:
|
|
196
|
+
"""One child observation per produce the agent ran, so Langfuse's
|
|
197
|
+
waterfall shows the ``Consume → Produce`` steps instead of a single
|
|
198
|
+
opaque agent span.
|
|
199
|
+
|
|
200
|
+
Produces run sequentially within an agent, so they are laid out
|
|
201
|
+
back-to-back from the span start by each one's measured latency.
|
|
202
|
+
"""
|
|
203
|
+
if not span.produces:
|
|
204
|
+
return []
|
|
205
|
+
cursor = span.started_at or run_start
|
|
206
|
+
out: list[dict[str, Any]] = []
|
|
207
|
+
for k, produce in enumerate(span.produces):
|
|
208
|
+
end = cursor + timedelta(milliseconds=produce.latency_ms)
|
|
209
|
+
out.append(
|
|
210
|
+
{
|
|
211
|
+
"traceId": trace_id,
|
|
212
|
+
"spanId": _span_id(
|
|
213
|
+
f"{trace_id}:produce:{span_index}:{k}:{produce.name}"
|
|
214
|
+
),
|
|
215
|
+
"parentSpanId": parent_span_id,
|
|
216
|
+
"name": produce.name,
|
|
217
|
+
"kind": 1,
|
|
218
|
+
"startTimeUnixNano": _unix_nanos(cursor),
|
|
219
|
+
"endTimeUnixNano": _unix_nanos(end),
|
|
220
|
+
"attributes": [
|
|
221
|
+
*trace_attrs,
|
|
222
|
+
_attr("langfuse.observation.type", "span"),
|
|
223
|
+
_attr("langfuse.observation.metadata.agent", span.agent),
|
|
224
|
+
_attr(
|
|
225
|
+
"langfuse.observation.metadata.operations",
|
|
226
|
+
produce.operations,
|
|
227
|
+
),
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
cursor = end
|
|
232
|
+
return out
|
|
233
|
+
|
|
161
234
|
def _llm_span(
|
|
162
235
|
self,
|
|
163
236
|
trace_id: str,
|
|
@@ -88,11 +88,31 @@ class TagAssignment(BaseModel):
|
|
|
88
88
|
updated_at: datetime | None = None
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
class ProduceRun(BaseModel):
|
|
92
|
+
"""One produce execution within an agent span (§54).
|
|
93
|
+
|
|
94
|
+
An agent runs one or more ``Produce``s per event; the span records which
|
|
95
|
+
ones actually ran and how much each contributed (``operations`` = authored
|
|
96
|
+
effect operations, so the produce that did the work is distinguishable from
|
|
97
|
+
one that merely matched the event).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
operations: int = 0
|
|
102
|
+
latency_ms: float = 0.0
|
|
103
|
+
|
|
104
|
+
|
|
91
105
|
class AgentSpan(BaseModel):
|
|
92
106
|
"""A single agent execution within a run (§54)."""
|
|
93
107
|
|
|
94
108
|
agent: str
|
|
95
109
|
event_type: str = ""
|
|
110
|
+
#: The artifact type whose change triggered this agent (its matched
|
|
111
|
+
#: ``Consume``) — e.g. ``DraftAnswer`` vs ``FinalResponse`` for an agent that
|
|
112
|
+
#: consumes both. ``event_type`` alone (created/updated) can't tell them apart.
|
|
113
|
+
event_artifact_type: str = ""
|
|
114
|
+
#: The produces that ran for this event, in execution order.
|
|
115
|
+
produces: list[ProduceRun] = Field(default_factory=list)
|
|
96
116
|
reads: list[ArtifactRef] = Field(default_factory=list)
|
|
97
117
|
writes: list[ArtifactRef] = Field(default_factory=list)
|
|
98
118
|
relations: list[RelationRef] = Field(default_factory=list)
|
|
@@ -123,8 +123,19 @@ class OTLPTracer(Tracer):
|
|
|
123
123
|
attr("reactifact.reads", type_summary(span.reads)),
|
|
124
124
|
attr("reactifact.writes", type_summary(span.writes)),
|
|
125
125
|
]
|
|
126
|
+
if span.event_artifact_type:
|
|
127
|
+
attrs.append(
|
|
128
|
+
attr("reactifact.event_artifact_type", span.event_artifact_type)
|
|
129
|
+
)
|
|
126
130
|
if span.error:
|
|
127
131
|
attrs.append(attr("error.type", span.error))
|
|
132
|
+
if span.produces:
|
|
133
|
+
attrs.append(
|
|
134
|
+
attr(
|
|
135
|
+
"reactifact.produces",
|
|
136
|
+
", ".join(f"{p.name}({p.operations})" for p in span.produces),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
128
139
|
return {
|
|
129
140
|
"traceId": tid,
|
|
130
141
|
"spanId": span_id(f"{tid}:span:{index}:{span.agent}"),
|
|
@@ -27,6 +27,7 @@ from .models import (
|
|
|
27
27
|
AgentSpan,
|
|
28
28
|
ArtifactRef,
|
|
29
29
|
LLMCall,
|
|
30
|
+
ProduceRun,
|
|
30
31
|
RelationRef,
|
|
31
32
|
RunTrace,
|
|
32
33
|
TagAssignment,
|
|
@@ -70,13 +71,15 @@ class PostgresStore:
|
|
|
70
71
|
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
71
72
|
agent TEXT NOT NULL,
|
|
72
73
|
event_type TEXT NOT NULL DEFAULT '',
|
|
74
|
+
event_artifact_type TEXT NOT NULL DEFAULT '',
|
|
73
75
|
latency_ms REAL NOT NULL DEFAULT 0,
|
|
74
76
|
error TEXT,
|
|
75
77
|
started_at TIMESTAMPTZ,
|
|
76
78
|
reads JSONB NOT NULL DEFAULT '[]',
|
|
77
79
|
writes JSONB NOT NULL DEFAULT '[]',
|
|
78
80
|
relations JSONB NOT NULL DEFAULT '[]',
|
|
79
|
-
llm_calls JSONB NOT NULL DEFAULT '[]'
|
|
81
|
+
llm_calls JSONB NOT NULL DEFAULT '[]',
|
|
82
|
+
produces JSONB NOT NULL DEFAULT '[]'
|
|
80
83
|
)
|
|
81
84
|
"""
|
|
82
85
|
)
|
|
@@ -122,6 +125,14 @@ class PostgresStore:
|
|
|
122
125
|
await cur.execute(
|
|
123
126
|
"ALTER TABLE spans ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ"
|
|
124
127
|
)
|
|
128
|
+
await cur.execute(
|
|
129
|
+
"ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
|
|
130
|
+
"produces JSONB NOT NULL DEFAULT '[]'"
|
|
131
|
+
)
|
|
132
|
+
await cur.execute(
|
|
133
|
+
"ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
|
|
134
|
+
"event_artifact_type TEXT NOT NULL DEFAULT ''"
|
|
135
|
+
)
|
|
125
136
|
await conn.commit()
|
|
126
137
|
finally:
|
|
127
138
|
await conn.close()
|
|
@@ -159,13 +170,15 @@ class PostgresStore:
|
|
|
159
170
|
if span_started is not None and span_started.tzinfo is None:
|
|
160
171
|
span_started = span_started.replace(tzinfo=UTC)
|
|
161
172
|
await cur.execute(
|
|
162
|
-
"INSERT INTO spans (run_id, agent, event_type,
|
|
163
|
-
"started_at, reads, writes, relations, llm_calls
|
|
164
|
-
"
|
|
173
|
+
"INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
|
|
174
|
+
"latency_ms, error, started_at, reads, writes, relations, llm_calls, "
|
|
175
|
+
"produces) "
|
|
176
|
+
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
|
165
177
|
(
|
|
166
178
|
trace.id,
|
|
167
179
|
span.agent,
|
|
168
180
|
span.event_type,
|
|
181
|
+
span.event_artifact_type,
|
|
169
182
|
span.latency_ms,
|
|
170
183
|
span.error,
|
|
171
184
|
span_started,
|
|
@@ -181,6 +194,9 @@ class PostgresStore:
|
|
|
181
194
|
psycopg.types.json.Jsonb(
|
|
182
195
|
[c.model_dump(mode="json") for c in span.llm_calls]
|
|
183
196
|
),
|
|
197
|
+
psycopg.types.json.Jsonb(
|
|
198
|
+
[p.model_dump(mode="json") for p in span.produces]
|
|
199
|
+
),
|
|
184
200
|
),
|
|
185
201
|
)
|
|
186
202
|
await conn.commit()
|
|
@@ -387,8 +403,8 @@ class PostgresStore:
|
|
|
387
403
|
if row is None:
|
|
388
404
|
return None
|
|
389
405
|
await cur.execute(
|
|
390
|
-
"SELECT agent, event_type, latency_ms, error,
|
|
391
|
-
"reads, writes, relations, llm_calls "
|
|
406
|
+
"SELECT agent, event_type, event_artifact_type, latency_ms, error, "
|
|
407
|
+
"started_at, reads, writes, relations, llm_calls, produces "
|
|
392
408
|
"FROM spans WHERE run_id = %s ORDER BY id",
|
|
393
409
|
(trace_id,),
|
|
394
410
|
)
|
|
@@ -408,13 +424,15 @@ class PostgresStore:
|
|
|
408
424
|
AgentSpan(
|
|
409
425
|
agent=r[0],
|
|
410
426
|
event_type=r[1],
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
427
|
+
event_artifact_type=r[2],
|
|
428
|
+
latency_ms=r[3],
|
|
429
|
+
error=r[4],
|
|
430
|
+
started_at=r[5],
|
|
431
|
+
reads=[ArtifactRef(**d) for d in r[6]],
|
|
432
|
+
writes=[ArtifactRef(**d) for d in r[7]],
|
|
433
|
+
relations=[RelationRef(**d) for d in r[8]],
|
|
434
|
+
llm_calls=[LLMCall(**d) for d in r[9]],
|
|
435
|
+
produces=[ProduceRun(**d) for d in r[10]],
|
|
418
436
|
)
|
|
419
437
|
for r in span_rows
|
|
420
438
|
]
|