reactifact 0.15.0__tar.gz → 0.15.2__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.0/reactifact.egg-info → reactifact-0.15.2}/PKG-INFO +2 -2
- {reactifact-0.15.0 → reactifact-0.15.2}/README.md +1 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/pyproject.toml +1 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/__init__.py +1 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/a2a/server.py +37 -9
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/agents.py +42 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/chat.py +18 -4
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/produce.py +35 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/runtime.py +54 -16
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/streaming.py +13 -2
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/__init__.py +2 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/langfuse.py +73 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/models.py +20 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/otlp.py +11 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/postgres.py +31 -13
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/store.py +29 -13
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/templates/app.css +12 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/templates/ui_run.html +30 -4
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/tracer.py +13 -1
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/web.py +10 -3
- {reactifact-0.15.0 → reactifact-0.15.2/reactifact.egg-info}/PKG-INFO +2 -2
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_a2a.py +66 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_chat_web.py +74 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_produce_styles.py +17 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_public_api.py +1 -0
- reactifact-0.15.2/tests/test_streaming.py +156 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_tracing.py +117 -2
- reactifact-0.15.0/tests/test_streaming.py +0 -68
- {reactifact-0.15.0 → reactifact-0.15.2}/LICENSE +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/__main__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/_extras.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/_httpx.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/a2a/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/a2a/client.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/a2a/models.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/a2a/remote.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/agent_tool.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/artifacts.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/audit.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/authz.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/branching.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/budget.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cache.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/checkpoints.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/branch.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/common.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/context.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/eval.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/graph.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/replay.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/skills.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/cli/trace.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/commit.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/commit_log.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/consume.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/context.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/context_builder.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/effects.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/dataset.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/evaluators.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/judge.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/online.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/runner.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/eval/scoring.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/events.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/guardrails.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/interrupt.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/llm_agent.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/logging.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/mcp/client.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/mcp/server.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/metrics.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/native_tool_use.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/operations.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/patches.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/pricing.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/prompts.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/azure.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/chat.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/fake.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/groq.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/image.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/openai.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/speech.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/together.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/video.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/xai.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/providers/zai.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/py.typed +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/agent.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/chat.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/models.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/rag.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/quota.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/conversation.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/search.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/status.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/text.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/recipes/tool_loop.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/redaction.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/relations.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/replay.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/request.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/resources.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/routing.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/scheduler.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/session.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-eval/references/online.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/sources.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/structured.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/fault.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/golden.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/lab.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/mock.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/pytest_plugin.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/record.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/testing/registry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tool_use.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tools.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/columns.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/templates/sessions.html +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/tracing/web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/triggers.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/types.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/verify.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact/viz.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact.egg-info/SOURCES.txt +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/setup.cfg +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_a2a_example.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_a2a_interop.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_absent_consume.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_adaptive.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_agent_tool.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_artifacts.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_audit.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_authz.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_backbone.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_branching.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_budget.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_cache.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_chat_example.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_chat_memory.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_chat_outbox.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_checkpoint.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_cli.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_commit_log.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_compaction.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_concurrency.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_context_builder.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_debounce.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_determinism.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_devops.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_devops_online.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_devops_web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_effects.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_eval.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_eval_dataset.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_eval_judge.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_eval_online.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_eval_trajectory.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_forklab.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_forklab_web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_friendly_api.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_golden.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_guardrails.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_hitl.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_image_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_incident_commander.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_invalidation.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_join_consume.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_knowledge.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_logging.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_mcp.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_medic_lab.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_metrics.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_multisource.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_openai_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_outbox.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_patches.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_pending_actions.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_ports.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_prompts.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_provider_auth.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_provider_retry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_providers_integration.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_quick.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_quota.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_reacts_to.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipe_conversation.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipe_tool_loop.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipes.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_redaction.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_relation_graph.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_relations.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_repair.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_repair_web.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_replay.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_repo_agent.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_request.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_research.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_resources.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_retry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_routing.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_runtime.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_sessions.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_shutdown.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_skills.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_sources.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_sources_search.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_sources_vector.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_speech_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_starter_app.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_structured.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_support_copilot.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_golden.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_lab.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_mock.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_provenance.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_pytest_plugin.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_testing_registry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_token_budget.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_tools.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_trace_columns.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_trace_tags.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_type_registry.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_verify.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_video_provider.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_view.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_viz.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_web_source.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_workspace.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.15.0 → reactifact-0.15.2}/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.2
|
|
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
|
|
@@ -380,7 +380,7 @@ uv sync --extra dev --extra web
|
|
|
380
380
|
|
|
381
381
|
## Status & contributing
|
|
382
382
|
|
|
383
|
-
Active, `0.
|
|
383
|
+
Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
384
384
|
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
385
385
|
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
386
386
|
[GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
|
|
@@ -334,7 +334,7 @@ uv sync --extra dev --extra web
|
|
|
334
334
|
|
|
335
335
|
## Status & contributing
|
|
336
336
|
|
|
337
|
-
Active, `0.
|
|
337
|
+
Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
|
|
338
338
|
(see the [roadmap](docs/roadmap.md)); releases follow
|
|
339
339
|
[Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
|
|
340
340
|
[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.2"
|
|
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"
|
|
@@ -25,6 +25,8 @@ tasks persists the `Context` with a `Session` itself.
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
import asyncio
|
|
28
|
+
import contextlib
|
|
29
|
+
from collections import OrderedDict
|
|
28
30
|
from collections.abc import AsyncIterator, Callable, Sequence
|
|
29
31
|
from dataclasses import dataclass, field
|
|
30
32
|
from typing import Any
|
|
@@ -59,6 +61,10 @@ CreateMessage = Callable[[Context, str], str]
|
|
|
59
61
|
Reply = Callable[[Context, str], str]
|
|
60
62
|
ContextFactory = Callable[[], Context]
|
|
61
63
|
|
|
64
|
+
#: In-process task store bound. A long-lived A2A server must not grow `_tasks`
|
|
65
|
+
#: without limit; the oldest completed task is evicted first (see `_evict`).
|
|
66
|
+
_MAX_TASKS = 512
|
|
67
|
+
|
|
62
68
|
|
|
63
69
|
def default_create_message(context: Context, text: str) -> str:
|
|
64
70
|
"""Default input hook: seed a `UserMessage` artifact (apps override this)."""
|
|
@@ -106,10 +112,11 @@ class _A2AServer:
|
|
|
106
112
|
self._reply = reply
|
|
107
113
|
self._budget = budget
|
|
108
114
|
self._max_iterations = max_iterations
|
|
109
|
-
self._tasks:
|
|
115
|
+
self._tasks: OrderedDict[str, _TaskRecord] = OrderedDict()
|
|
110
116
|
|
|
111
117
|
def _record_for(self, message: Message) -> _TaskRecord:
|
|
112
118
|
if message.taskId and message.taskId in self._tasks:
|
|
119
|
+
self._tasks.move_to_end(message.taskId)
|
|
113
120
|
return self._tasks[message.taskId]
|
|
114
121
|
record = _TaskRecord(
|
|
115
122
|
id=message.taskId or uuid4().hex,
|
|
@@ -117,8 +124,20 @@ class _A2AServer:
|
|
|
117
124
|
context=self._context_factory(),
|
|
118
125
|
)
|
|
119
126
|
self._tasks[record.id] = record
|
|
127
|
+
self._evict()
|
|
120
128
|
return record
|
|
121
129
|
|
|
130
|
+
def _evict(self) -> None:
|
|
131
|
+
"""Keep `_tasks` bounded: drop the oldest terminal task first."""
|
|
132
|
+
if len(self._tasks) <= _MAX_TASKS:
|
|
133
|
+
return
|
|
134
|
+
for task_id, record in list(self._tasks.items()):
|
|
135
|
+
if record.state in TERMINAL_STATES:
|
|
136
|
+
del self._tasks[task_id]
|
|
137
|
+
break
|
|
138
|
+
else:
|
|
139
|
+
self._tasks.popitem(last=False)
|
|
140
|
+
|
|
122
141
|
async def _execute(
|
|
123
142
|
self,
|
|
124
143
|
record: _TaskRecord,
|
|
@@ -185,14 +204,23 @@ class _A2AServer:
|
|
|
185
204
|
run = asyncio.create_task(
|
|
186
205
|
self._execute(record, params.message.text, on_event=queue.put_nowait)
|
|
187
206
|
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
try:
|
|
208
|
+
while not run.done() or not queue.empty():
|
|
209
|
+
try:
|
|
210
|
+
event = await asyncio.wait_for(queue.get(), timeout=0.05)
|
|
211
|
+
except TimeoutError:
|
|
212
|
+
continue
|
|
213
|
+
yield _sse(request_id, _status_update(record, "working", event.message))
|
|
214
|
+
await run
|
|
215
|
+
yield _sse(request_id, self.task(record))
|
|
216
|
+
finally:
|
|
217
|
+
# A dropped SSE client closes this generator mid-run: cancel and
|
|
218
|
+
# await the run, or it keeps executing (tokens!) and is destroyed
|
|
219
|
+
# pending ("Task was destroyed but it is pending!").
|
|
220
|
+
if not run.done():
|
|
221
|
+
run.cancel()
|
|
222
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
223
|
+
await run
|
|
196
224
|
|
|
197
225
|
def get(self, task_id: str) -> Task | None:
|
|
198
226
|
record = self._tasks.get(task_id)
|
|
@@ -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
|
|
@@ -26,7 +26,7 @@ from collections.abc import (
|
|
|
26
26
|
Mapping,
|
|
27
27
|
Sequence,
|
|
28
28
|
)
|
|
29
|
-
from contextlib import asynccontextmanager
|
|
29
|
+
from contextlib import asynccontextmanager, suppress
|
|
30
30
|
from dataclasses import dataclass
|
|
31
31
|
from typing import Any, Literal
|
|
32
32
|
|
|
@@ -414,9 +414,16 @@ class ChatAssistant:
|
|
|
414
414
|
self._session_locks.pop(session_id, None)
|
|
415
415
|
|
|
416
416
|
async def _open(self, session_id: str) -> Session:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
resources = _resolve_with_session(self._resources, session_id)
|
|
418
|
+
try:
|
|
419
|
+
return await self.store.open(session_id, resources=resources)
|
|
420
|
+
except Exception:
|
|
421
|
+
# A callable `resources=` built a fresh per-turn instance; if the
|
|
422
|
+
# store fails to open the session nothing else will close it.
|
|
423
|
+
if callable(self._resources):
|
|
424
|
+
with suppress(Exception):
|
|
425
|
+
await resources.aclose()
|
|
426
|
+
raise
|
|
420
427
|
|
|
421
428
|
def _memory_agents(self) -> list[Agent]:
|
|
422
429
|
"""The bounded-memory agent implied by `memory=`, or none."""
|
|
@@ -497,6 +504,7 @@ class ChatAssistant:
|
|
|
497
504
|
design (see the module docstring).
|
|
498
505
|
"""
|
|
499
506
|
async with self._locked_session(session_id):
|
|
507
|
+
session: Session | None = None
|
|
500
508
|
try:
|
|
501
509
|
session = await self._open(session_id)
|
|
502
510
|
runtime = self._build_runtime(session)
|
|
@@ -504,6 +512,12 @@ class ChatAssistant:
|
|
|
504
512
|
logger.exception(
|
|
505
513
|
"chat.ChatAssistant: failed to open session %r", session_id
|
|
506
514
|
)
|
|
515
|
+
# If the session did open but building the runtime failed, its
|
|
516
|
+
# per-turn resources still need closing (a callable `resources=`
|
|
517
|
+
# is never referenced again).
|
|
518
|
+
if session is not None and callable(self._resources):
|
|
519
|
+
with suppress(Exception):
|
|
520
|
+
await session.context.resources.aclose()
|
|
507
521
|
yield ChatEvent(
|
|
508
522
|
kind="message",
|
|
509
523
|
session_id=session_id,
|
|
@@ -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).
|
|
@@ -59,6 +67,11 @@ class RunInfo:
|
|
|
59
67
|
_ACTIVE_RUNS: dict[str, RunInfo] = {}
|
|
60
68
|
_RUN_TASKS: dict[str, "asyncio.Task[Any]"] = {}
|
|
61
69
|
|
|
70
|
+
#: How long `astream` waits for a cancelled runner to unwind before giving up
|
|
71
|
+
#: (a well-behaved turn stops immediately; this only guards a misbehaving agent
|
|
72
|
+
#: that swallows `CancelledError`).
|
|
73
|
+
_CANCEL_GRACE_SECONDS = 5.0
|
|
74
|
+
|
|
62
75
|
|
|
63
76
|
def active_runs() -> list[RunInfo]:
|
|
64
77
|
"""Snapshots of the turns currently executing in this process.
|
|
@@ -645,18 +658,22 @@ class Runtime:
|
|
|
645
658
|
"""
|
|
646
659
|
patches_to_apply: list[PatchWork] = []
|
|
647
660
|
runs = 0
|
|
648
|
-
for patch, agent, event, reads, latency, error in results:
|
|
661
|
+
for patch, agent, event, reads, latency, error, produce_run_list in results:
|
|
649
662
|
if self._budget_exhausted():
|
|
650
663
|
break
|
|
651
664
|
runs += 1
|
|
652
665
|
self._runs_used += 1
|
|
653
666
|
if error is not None:
|
|
654
667
|
self._errors_used += 1
|
|
655
|
-
self._trace.record_span(
|
|
668
|
+
self._trace.record_span(
|
|
669
|
+
agent, event, reads, latency, error=error, produces=produce_run_list
|
|
670
|
+
)
|
|
656
671
|
continue
|
|
657
672
|
if patch is None or patch.is_empty():
|
|
658
673
|
continue
|
|
659
|
-
span = self._trace.record_span(
|
|
674
|
+
span = self._trace.record_span(
|
|
675
|
+
agent, event, reads, latency, produces=produce_run_list
|
|
676
|
+
)
|
|
660
677
|
self._validate_patch_types(patch, agent)
|
|
661
678
|
self._enforce_authorization(patch)
|
|
662
679
|
patch = self._apply_guardrails(patch)
|
|
@@ -789,6 +806,7 @@ class Runtime:
|
|
|
789
806
|
task = asyncio.current_task()
|
|
790
807
|
self._trace.register_task(task, agent.name)
|
|
791
808
|
effects_token = set_effects(Effects(self.context))
|
|
809
|
+
produce_runs_token = begin_produce_runs()
|
|
792
810
|
slot: Effects | None = None
|
|
793
811
|
patch: Patch | None = None
|
|
794
812
|
error: BaseException | None = None
|
|
@@ -812,6 +830,8 @@ class Runtime:
|
|
|
812
830
|
if self.on_agent_error is not None:
|
|
813
831
|
self.on_agent_error(agent, event, exc)
|
|
814
832
|
finally:
|
|
833
|
+
runs = produce_runs()
|
|
834
|
+
end_produce_runs(produce_runs_token)
|
|
815
835
|
reset_effects(effects_token)
|
|
816
836
|
self._trace.unregister_task(task)
|
|
817
837
|
latency = (time.monotonic() - started) * 1000
|
|
@@ -824,7 +844,7 @@ class Runtime:
|
|
|
824
844
|
"error": repr(error),
|
|
825
845
|
},
|
|
826
846
|
)
|
|
827
|
-
return None, agent, event, reads, latency, error
|
|
847
|
+
return None, agent, event, reads, latency, error, runs
|
|
828
848
|
logger.debug(
|
|
829
849
|
"agent finished",
|
|
830
850
|
extra={"agent": agent.name, "latency_ms": round(latency, 2)},
|
|
@@ -838,7 +858,7 @@ class Runtime:
|
|
|
838
858
|
combined.merge(patch)
|
|
839
859
|
combined.merge(slot.to_patch())
|
|
840
860
|
patch = combined
|
|
841
|
-
return patch, agent, event, reads, latency, None
|
|
861
|
+
return patch, agent, event, reads, latency, None, runs
|
|
842
862
|
|
|
843
863
|
async def arun(
|
|
844
864
|
self,
|
|
@@ -1031,13 +1051,22 @@ class Runtime:
|
|
|
1031
1051
|
break
|
|
1032
1052
|
get_event = asyncio.ensure_future(queue.get())
|
|
1033
1053
|
wait_done = asyncio.ensure_future(done.wait())
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1054
|
+
try:
|
|
1055
|
+
finished, _ = await asyncio.wait(
|
|
1056
|
+
{get_event, wait_done}, return_when=asyncio.FIRST_COMPLETED
|
|
1057
|
+
)
|
|
1058
|
+
if get_event in finished:
|
|
1059
|
+
yield get_event.result()
|
|
1060
|
+
finally:
|
|
1061
|
+
# Always cancel *and await* the losing future. A task that is
|
|
1062
|
+
# cancelled but never awaited stays pending until the loop
|
|
1063
|
+
# closes, producing "Task was destroyed but it is pending!"
|
|
1064
|
+
# (`Queue.get`/`Event.wait`) on every early generator close
|
|
1065
|
+
# (e.g. a dropped SSE client).
|
|
1066
|
+
for fut in (get_event, wait_done):
|
|
1067
|
+
if not fut.done():
|
|
1068
|
+
fut.cancel()
|
|
1069
|
+
await asyncio.gather(get_event, wait_done, return_exceptions=True)
|
|
1041
1070
|
# Re-raise any agent/runtime exception instead of silently dropping it:
|
|
1042
1071
|
# an error inside a run must reach the caller, not hide in the task.
|
|
1043
1072
|
await task
|
|
@@ -1053,6 +1082,15 @@ class Runtime:
|
|
|
1053
1082
|
)
|
|
1054
1083
|
finally:
|
|
1055
1084
|
task.cancel()
|
|
1085
|
+
# Settle the runner too, so an early generator close (client
|
|
1086
|
+
# disconnect) does not leave the run task pending at loop shutdown.
|
|
1087
|
+
# Bounded: an agent that swallows `CancelledError` must never hang
|
|
1088
|
+
# the caller's disconnect/shutdown path.
|
|
1089
|
+
with contextlib.suppress(Exception):
|
|
1090
|
+
await asyncio.wait_for(
|
|
1091
|
+
asyncio.gather(task, return_exceptions=True),
|
|
1092
|
+
timeout=_CANCEL_GRACE_SECONDS,
|
|
1093
|
+
)
|
|
1056
1094
|
self.context.unsubscribe(queue)
|
|
1057
1095
|
|
|
1058
1096
|
def _apply_patch(self, patch: Patch, commit: Commit) -> list[Write]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from datetime import UTC, datetime
|
|
6
7
|
from typing import Any
|
|
@@ -30,6 +31,11 @@ class ProgressEvent:
|
|
|
30
31
|
|
|
31
32
|
QueueEvent = asyncio.Queue[ProgressEvent]
|
|
32
33
|
|
|
34
|
+
#: Per-subscriber queue bound. Status events are best-effort UI hints: a
|
|
35
|
+
#: subscriber that never drains must not grow the queue without bound, so the
|
|
36
|
+
#: oldest event is dropped when this is reached.
|
|
37
|
+
_DEFAULT_SUBSCRIBER_QUEUE_SIZE = 1024
|
|
38
|
+
|
|
33
39
|
|
|
34
40
|
class EventHub:
|
|
35
41
|
"""Broadcaster of agent status events into a single- or multi-stream.
|
|
@@ -38,15 +44,16 @@ class EventHub:
|
|
|
38
44
|
runs pay nothing for announce.
|
|
39
45
|
"""
|
|
40
46
|
|
|
41
|
-
def __init__(self) -> None:
|
|
47
|
+
def __init__(self, maxsize: int = _DEFAULT_SUBSCRIBER_QUEUE_SIZE) -> None:
|
|
42
48
|
self._subscribers: set[QueueEvent] = set()
|
|
49
|
+
self._maxsize = maxsize
|
|
43
50
|
|
|
44
51
|
@property
|
|
45
52
|
def has_subscribers(self) -> bool:
|
|
46
53
|
return bool(self._subscribers)
|
|
47
54
|
|
|
48
55
|
def subscribe(self) -> QueueEvent:
|
|
49
|
-
queue: QueueEvent = asyncio.Queue()
|
|
56
|
+
queue: QueueEvent = asyncio.Queue(maxsize=self._maxsize)
|
|
50
57
|
self._subscribers.add(queue)
|
|
51
58
|
return queue
|
|
52
59
|
|
|
@@ -55,4 +62,8 @@ class EventHub:
|
|
|
55
62
|
|
|
56
63
|
def publish(self, event: ProgressEvent) -> None:
|
|
57
64
|
for queue in self._subscribers:
|
|
65
|
+
if queue.full():
|
|
66
|
+
# Drop the oldest status hint rather than block or grow forever.
|
|
67
|
+
with contextlib.suppress(asyncio.QueueEmpty):
|
|
68
|
+
queue.get_nowait()
|
|
58
69
|
queue.put_nowait(event)
|
|
@@ -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}"),
|