reactifact 0.15.2__tar.gz → 0.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reactifact-0.15.2/reactifact.egg-info → reactifact-0.16.0}/PKG-INFO +18 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/README.md +17 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/pyproject.toml +1 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/__init__.py +3 -2
- reactifact-0.16.0/reactifact/_persistence.py +78 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/agents.py +3 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/audit.py +22 -2
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/branching.py +28 -5
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/chat.py +67 -17
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/checkpoints.py +104 -8
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/consume.py +52 -2
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/context.py +185 -14
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/online.py +2 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/interrupt.py +12 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/metrics.py +20 -2
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/native_tool_use.py +97 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/__init__.py +8 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/memory.py +48 -10
- reactifact-0.16.0/reactifact/recipes/tool_loop.py +436 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/resources.py +13 -7
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/runtime.py +57 -7
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/session.py +40 -6
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tool_use.py +2 -2
- reactifact-0.16.0/reactifact/tools.py +351 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/__init__.py +2 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/langfuse.py +68 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/models.py +4 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/otlp.py +57 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/postgres.py +14 -4
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/store.py +12 -4
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/templates/app.css +5 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/templates/ui_run.html +52 -5
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/tracer.py +20 -11
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/triggers.py +19 -1
- {reactifact-0.15.2 → reactifact-0.16.0/reactifact.egg-info}/PKG-INFO +18 -1
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact.egg-info/SOURCES.txt +3 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_audit.py +33 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_chat_memory.py +58 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_consumes_produces.py +65 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_devops.py +10 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_metrics.py +26 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_outbox.py +10 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_public_api.py +5 -0
- reactifact-0.16.0/tests/test_recipe_tool_loop.py +479 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipes_memory.py +53 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_repair_web.py +9 -4
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_request.py +62 -0
- reactifact-0.16.0/tests/test_session_append.py +222 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_token_budget.py +43 -0
- reactifact-0.16.0/tests/test_trace_ui.py +33 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_tracing.py +118 -0
- reactifact-0.15.2/reactifact/recipes/tool_loop.py +0 -190
- reactifact-0.15.2/reactifact/tools.py +0 -131
- reactifact-0.15.2/tests/test_recipe_tool_loop.py +0 -160
- {reactifact-0.15.2 → reactifact-0.16.0}/LICENSE +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/__main__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/_extras.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/_httpx.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/a2a/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/a2a/client.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/a2a/models.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/a2a/remote.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/a2a/server.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/agent_tool.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/authz.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/budget.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cache.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/eval.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/skills.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/commit.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/context_builder.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/effects.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/dataset.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/evaluators.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/judge.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/runner.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/eval/scoring.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/events.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/guardrails.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/llm_agent.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/logging.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/operations.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/patches.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/pricing.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/produce.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/prompts.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/chat.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/py.typed +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/_shared.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/agent.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/chat.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/models.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/rag.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quick/tools_agent.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/quota.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/cleanup.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/conversation.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/identity.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/plan_execute.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/reflection.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/supervisor.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/redaction.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/relations.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/replay.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/request.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/routing.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-eval/references/online.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/sources.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/streaming.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/structured.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/fault.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/golden.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/lab.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/mock.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/pytest_plugin.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/columns.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/templates/sessions.html +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/tracing/web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/types.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/verify.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/viz.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact/web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/setup.cfg +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_a2a.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_a2a_example.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_a2a_interop.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_absent_consume.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_agent_tool.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_authz.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_backbone.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_branching.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_budget.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_cache.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_chat_example.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_chat_outbox.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_chat_web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_checkpoint.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_cli.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_compaction.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_concurrency.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_context_builder.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_correlated_consume.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_debounce.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_determinism.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_devops_online.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_devops_web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_effects.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_eval.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_eval_dataset.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_eval_judge.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_eval_online.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_eval_trajectory.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_fintech_audit.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_forklab.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_golden.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_guardrails.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_hitl.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_incident_commander.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_join_consume.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_logging.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_loop_bound_client.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_mcp.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_multisource.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_native_tool_use.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_patches.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_pending_actions.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_ports.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_produce_styles.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_produce_trigger.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_prompts.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_quick.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_quota.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_reacts_to.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipe_conversation.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipe_supervisor.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipes.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_redaction.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_relations.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_repair.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_replay.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_repo_agent.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_research.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_resources.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_retry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_routing.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_runtime.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_scheduler_semantics.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_sessions.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_shutdown.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_skills.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_sources.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_starter_app.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_streaming.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_structured.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_support_copilot.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_golden.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_lab.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_mock.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_provenance.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_pytest_plugin.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_tools.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_trace_columns.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_trace_tags.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_type_registry.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_verify.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_view.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_viz.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_web_source.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_workspace.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.15.2 → reactifact-0.16.0}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/bzdvdn/reactifact
|
|
@@ -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):
|
|
@@ -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):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.16.0"
|
|
8
8
|
description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -47,11 +47,11 @@ from .request import current_request
|
|
|
47
47
|
from .resources import ResourceKey, ResourceScope, RuntimeResources
|
|
48
48
|
from .runtime import Runtime
|
|
49
49
|
from .session import Session, SessionStore
|
|
50
|
-
from .tools import FunctionTool, Tool, ToolOutput, tool
|
|
50
|
+
from .tools import FunctionTool, Tool, ToolError, ToolOutput, tool
|
|
51
51
|
from .triggers import Trigger
|
|
52
52
|
from .types import register as register_type
|
|
53
53
|
|
|
54
|
-
__version__ = "0.
|
|
54
|
+
__version__ = "0.16.0"
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"Agent",
|
|
@@ -92,6 +92,7 @@ __all__ = [
|
|
|
92
92
|
"Session",
|
|
93
93
|
"SessionStore",
|
|
94
94
|
"Tool",
|
|
95
|
+
"ToolError",
|
|
95
96
|
"ToolOutput",
|
|
96
97
|
"Trigger",
|
|
97
98
|
"Unlink",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Internal: append-only persistence helpers shared by session/branch stores.
|
|
2
|
+
|
|
3
|
+
Not public API — `SessionStore`/`BranchStore` use these when the backend
|
|
4
|
+
implements `AppendBackend` (SQLite/Postgres). A save writes a full snapshot on
|
|
5
|
+
the first save, after a compaction, or after a rollback; otherwise it appends
|
|
6
|
+
only the new commits plus the current pending-event queue.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from .checkpoints import AppendBackend
|
|
14
|
+
from .commit import Commit
|
|
15
|
+
from .context import Context
|
|
16
|
+
from .events import Event
|
|
17
|
+
|
|
18
|
+
#: Default number of appended saves before a snapshot rewrite. Bounds load
|
|
19
|
+
#: replay (and log size) for long-lived sessions without an explicit
|
|
20
|
+
#: `context.compact()`.
|
|
21
|
+
DEFAULT_COMPACT_EVERY = 100
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def save_append(
|
|
25
|
+
backend: AppendBackend,
|
|
26
|
+
key: str,
|
|
27
|
+
context: Context,
|
|
28
|
+
*,
|
|
29
|
+
compact_every: int | None = DEFAULT_COMPACT_EVERY,
|
|
30
|
+
) -> None:
|
|
31
|
+
persisted, persisted_compacted = context.persisted_state(key)
|
|
32
|
+
needs_snapshot = (
|
|
33
|
+
persisted < 0
|
|
34
|
+
or persisted > context.version
|
|
35
|
+
or context.compacted_at > persisted_compacted
|
|
36
|
+
or await backend.get(key) is None
|
|
37
|
+
)
|
|
38
|
+
if (
|
|
39
|
+
not needs_snapshot
|
|
40
|
+
and compact_every is not None
|
|
41
|
+
and await backend.entry_count(key) + 1 >= compact_every
|
|
42
|
+
):
|
|
43
|
+
needs_snapshot = True
|
|
44
|
+
if needs_snapshot:
|
|
45
|
+
await backend.set(key, cast(dict[str, Any], context.to_dict()))
|
|
46
|
+
await backend.clear_entries(key)
|
|
47
|
+
context.mark_persisted(key=key)
|
|
48
|
+
return
|
|
49
|
+
new_commits = [
|
|
50
|
+
commit.to_dict()
|
|
51
|
+
for commit in context.commit_log()
|
|
52
|
+
if (commit.context_version or 0) > persisted
|
|
53
|
+
]
|
|
54
|
+
await backend.append(
|
|
55
|
+
key,
|
|
56
|
+
{
|
|
57
|
+
"commits": new_commits,
|
|
58
|
+
"events": [event.to_dict() for event in context.pending_events()],
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
context.mark_persisted(key=key)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def load_append(backend: AppendBackend, key: str) -> Context | None:
|
|
65
|
+
snapshot = await backend.get(key)
|
|
66
|
+
if snapshot is None:
|
|
67
|
+
return None
|
|
68
|
+
context = Context.from_dict(snapshot)
|
|
69
|
+
for entry in await backend.entries(key):
|
|
70
|
+
for commit_dict in entry.get("commits", []):
|
|
71
|
+
context.apply_commit(Commit.from_dict(commit_dict), reindex=False)
|
|
72
|
+
if "events" in entry:
|
|
73
|
+
context.replace_events(
|
|
74
|
+
[Event.from_dict(event) for event in entry["events"]]
|
|
75
|
+
)
|
|
76
|
+
context.rebuild_indices()
|
|
77
|
+
context.mark_persisted(key=key)
|
|
78
|
+
return context
|
|
@@ -13,6 +13,7 @@ from .effects import current_effects
|
|
|
13
13
|
from .events import Event, EventType
|
|
14
14
|
from .patches import Patch
|
|
15
15
|
from .produce import Produce, ProduceCall
|
|
16
|
+
from .tools import reset_current_produce, set_current_produce
|
|
16
17
|
from .triggers import Trigger
|
|
17
18
|
|
|
18
19
|
#: Per-task log of the produces that ran for the current agent execution:
|
|
@@ -190,9 +191,11 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
190
191
|
call = ProduceCall(
|
|
191
192
|
context=context, inputs=inputs, event=event, trigger=trigger
|
|
192
193
|
)
|
|
194
|
+
produce_token = set_current_produce(p.name)
|
|
193
195
|
try:
|
|
194
196
|
await p.produce(call)
|
|
195
197
|
finally:
|
|
198
|
+
reset_current_produce(produce_token)
|
|
196
199
|
# Record the produce even when it raises — a failing produce is
|
|
197
200
|
# exactly what a trace should show (the span is then the error
|
|
198
201
|
# span, with this produce's partial operations/latency).
|
|
@@ -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)
|
|
@@ -29,8 +29,9 @@ from typing import Any
|
|
|
29
29
|
|
|
30
30
|
from pydantic import BaseModel
|
|
31
31
|
|
|
32
|
+
from ._persistence import DEFAULT_COMPACT_EVERY, load_append, save_append
|
|
32
33
|
from .artifacts import Artifact
|
|
33
|
-
from .checkpoints import KVBackend
|
|
34
|
+
from .checkpoints import AppendBackend, KVBackend
|
|
34
35
|
from .commit import Commit
|
|
35
36
|
from .context import Context
|
|
36
37
|
from .interrupt import PendingAction
|
|
@@ -242,10 +243,22 @@ def merge_contexts(
|
|
|
242
243
|
|
|
243
244
|
|
|
244
245
|
class BranchStore:
|
|
245
|
-
"""Persists named branches (`branch:<session>:<name>`) over a KV backend.
|
|
246
|
+
"""Persists named branches (`branch:<session>:<name>`) over a KV backend.
|
|
246
247
|
|
|
247
|
-
|
|
248
|
+
With an `AppendBackend` (SQLite/Postgres) branches use the same
|
|
249
|
+
snapshot + append-only commit log as sessions, keyed per branch name.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
backend: KVBackend,
|
|
255
|
+
*,
|
|
256
|
+
compact_every: int | None = DEFAULT_COMPACT_EVERY,
|
|
257
|
+
):
|
|
258
|
+
if compact_every is not None and compact_every < 1:
|
|
259
|
+
raise ValueError("compact_every must be >= 1 or None")
|
|
248
260
|
self.backend = backend
|
|
261
|
+
self.compact_every = compact_every
|
|
249
262
|
|
|
250
263
|
@staticmethod
|
|
251
264
|
def _key(session_id: str, name: str) -> str:
|
|
@@ -255,11 +268,21 @@ class BranchStore:
|
|
|
255
268
|
self, context: Context, *, session_id: str, name: str
|
|
256
269
|
) -> None:
|
|
257
270
|
"""Saves a branch (including its fork base snapshot, §40)."""
|
|
258
|
-
|
|
271
|
+
key = self._key(session_id, name)
|
|
272
|
+
if isinstance(self.backend, AppendBackend):
|
|
273
|
+
await save_append(
|
|
274
|
+
self.backend, key, context, compact_every=self.compact_every
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
await context.to_kv(self.backend, key)
|
|
278
|
+
context.mark_persisted(key=key)
|
|
259
279
|
|
|
260
280
|
async def load_branch(self, session_id: str, name: str) -> Context | None:
|
|
261
281
|
"""Loads a branch, or None if it does not exist."""
|
|
262
|
-
|
|
282
|
+
key = self._key(session_id, name)
|
|
283
|
+
if isinstance(self.backend, AppendBackend):
|
|
284
|
+
return await load_append(self.backend, key)
|
|
285
|
+
return await Context.from_kv(self.backend, key)
|
|
263
286
|
|
|
264
287
|
async def list_branches(self, session_id: str) -> list[str]:
|
|
265
288
|
prefix = f"branch:{session_id}:"
|
|
@@ -37,6 +37,7 @@ from .budget import Budget
|
|
|
37
37
|
from .consume import Consume
|
|
38
38
|
from .context import Context
|
|
39
39
|
from .events import Event
|
|
40
|
+
from .request import reset_request, set_request
|
|
40
41
|
from .resources import RuntimeResources
|
|
41
42
|
from .runtime import Dispatcher, DispatchErrorHandler, Runtime
|
|
42
43
|
from .session import Session, SessionStore
|
|
@@ -101,8 +102,12 @@ async def run_message(
|
|
|
101
102
|
(devops-style clarify) pass their own. Also accepts the 3-arg shape
|
|
102
103
|
`create_message(ctx, text, session_id) -> msg_id` for a hook that needs
|
|
103
104
|
the current turn's `session_id` (e.g. to stamp it onto the created
|
|
104
|
-
artifact)
|
|
105
|
-
|
|
105
|
+
artifact) and the 4-arg shape `create_message(ctx, text, session_id,
|
|
106
|
+
request) -> msg_id` for per-request metadata (a category, tenant, user)
|
|
107
|
+
— detected from the callable's own arity, so the 2-arg shape keeps
|
|
108
|
+
working unchanged. During the hook `ctx.request` is the same mapping
|
|
109
|
+
passed as `request=`, so a 2/3-arg hook can read it too without a
|
|
110
|
+
`ContextVar` of its own.
|
|
106
111
|
|
|
107
112
|
`status_kinds` selects which progress event kinds are forwarded as `status`
|
|
108
113
|
frames (default: only `status`; e.g. tool-announcing demos also forward
|
|
@@ -115,10 +120,17 @@ async def run_message(
|
|
|
115
120
|
forwarded = set(status_kinds)
|
|
116
121
|
ctx = runtime.context
|
|
117
122
|
try:
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
token = set_request(request) if request is not None else None
|
|
124
|
+
try:
|
|
125
|
+
if create_message is not None:
|
|
126
|
+
msg_id = _call_create_message(
|
|
127
|
+
create_message, ctx, text, session_id, request
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
msg_id = ctx.create(user_message(text=text, session_id=session_id)).id
|
|
131
|
+
finally:
|
|
132
|
+
if token is not None:
|
|
133
|
+
reset_request(token)
|
|
122
134
|
except Exception:
|
|
123
135
|
logger.exception("chat.run_message: failed to enter the turn")
|
|
124
136
|
yield ChatEvent(
|
|
@@ -217,11 +229,20 @@ def _resolve_with_session(value: Any, session_id: str) -> Any:
|
|
|
217
229
|
|
|
218
230
|
|
|
219
231
|
def _call_create_message(
|
|
220
|
-
create_message: Callable[..., str],
|
|
232
|
+
create_message: Callable[..., str],
|
|
233
|
+
ctx: Context,
|
|
234
|
+
text: str,
|
|
235
|
+
session_id: str,
|
|
236
|
+
request: Mapping[str, Any] | None = None,
|
|
221
237
|
) -> str:
|
|
222
|
-
"""Calls `create_message` with
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
"""Calls `create_message` with the widest shape it declares.
|
|
239
|
+
|
|
240
|
+
Four params get `(ctx, text, session_id, request)`, three get
|
|
241
|
+
`(ctx, text, session_id)`, and the pre-existing two-arg shape gets
|
|
242
|
+
`(ctx, text)`. Detected from arity so old hooks keep working unchanged.
|
|
243
|
+
"""
|
|
244
|
+
if _accepts_arg(create_message, 4):
|
|
245
|
+
return create_message(ctx, text, session_id, request)
|
|
225
246
|
if _accepts_arg(create_message, 3):
|
|
226
247
|
return create_message(ctx, text, session_id)
|
|
227
248
|
return create_message(ctx, text)
|
|
@@ -274,9 +295,16 @@ class ChatMemory:
|
|
|
274
295
|
`message_type` (needed for `keep`/`summarize`) is any model with
|
|
275
296
|
`role`/`text`; for compaction-only it isn't needed at all, so
|
|
276
297
|
`ChatMemory(compact_commits=…)` is a complete policy on its own.
|
|
298
|
+
|
|
299
|
+
`message_types` (or a sequence passed as `message_type`) bounds a
|
|
300
|
+
heterogeneous conversation — the natural `UserQuery`/`Answer` pair, or a
|
|
301
|
+
marker base like `ChatMessage` whose concrete types inherit from it. The
|
|
302
|
+
memory agent wakes on every listed type (and, for a marker base, on its
|
|
303
|
+
subclasses), so one window covers the whole thread.
|
|
277
304
|
"""
|
|
278
305
|
|
|
279
|
-
message_type: type[BaseModel] | None = None
|
|
306
|
+
message_type: type[BaseModel] | Sequence[type[BaseModel]] | None = None
|
|
307
|
+
message_types: Sequence[type[BaseModel]] | None = None
|
|
280
308
|
keep: int | None = None
|
|
281
309
|
summarize: Callable[[Context, str, list[Any]], Awaitable[str | None]] | None = None
|
|
282
310
|
summary_type: type[BaseModel] | None = None
|
|
@@ -284,6 +312,20 @@ class ChatMemory:
|
|
|
284
312
|
digest_id: str = "digest"
|
|
285
313
|
compact_commits: int | None = None
|
|
286
314
|
|
|
315
|
+
def resolved_message_types(self) -> tuple[type[BaseModel], ...]:
|
|
316
|
+
"""The message types to bound, normalized from the two fields."""
|
|
317
|
+
if self.message_types is not None:
|
|
318
|
+
types = tuple(self.message_types)
|
|
319
|
+
elif self.message_type is None:
|
|
320
|
+
return ()
|
|
321
|
+
elif isinstance(self.message_type, type):
|
|
322
|
+
types = (self.message_type,)
|
|
323
|
+
else:
|
|
324
|
+
types = tuple(self.message_type)
|
|
325
|
+
if not types:
|
|
326
|
+
raise ValueError("message_type must name at least one artifact type")
|
|
327
|
+
return types
|
|
328
|
+
|
|
287
329
|
|
|
288
330
|
class ChatAssistant:
|
|
289
331
|
"""Session-persisted chat over the runtime, for the canonical contract.
|
|
@@ -298,7 +340,9 @@ class ChatAssistant:
|
|
|
298
340
|
its provider's HTTP client doesn't leak. Pass a plain `RuntimeResources`
|
|
299
341
|
instance instead when you want one shared, long-lived provider across
|
|
300
342
|
turns/sessions — that instance is never closed automatically; close it
|
|
301
|
-
yourself at real shutdown.
|
|
343
|
+
yourself at real shutdown. It is safe to share: the runtime installs
|
|
344
|
+
per-turn budget/tracing wrappers on a shallow copy, so concurrent turns do
|
|
345
|
+
not mutate the shared instance.
|
|
302
346
|
|
|
303
347
|
`session_save_policy=` passes straight through to `Runtime` — `"per_turn"`
|
|
304
348
|
trades finer crash-resilience granularity (a save at every generation
|
|
@@ -434,12 +478,17 @@ class ChatAssistant:
|
|
|
434
478
|
|
|
435
479
|
produce: Any
|
|
436
480
|
if memory.summarize is not None:
|
|
437
|
-
if memory.
|
|
481
|
+
if memory.summary_type is None:
|
|
482
|
+
raise ValueError(
|
|
483
|
+
"ChatMemory.summarize needs message_type and summary_type"
|
|
484
|
+
)
|
|
485
|
+
message_types = memory.resolved_message_types()
|
|
486
|
+
if not message_types:
|
|
438
487
|
raise ValueError(
|
|
439
488
|
"ChatMemory.summarize needs message_type and summary_type"
|
|
440
489
|
)
|
|
441
490
|
produce = RollingDigestSummarizer(
|
|
442
|
-
message_type=
|
|
491
|
+
message_type=message_types,
|
|
443
492
|
artifact_type=memory.summary_type,
|
|
444
493
|
summarize=memory.summarize,
|
|
445
494
|
build=_digest_build(memory.summary_type, memory.build_summary),
|
|
@@ -447,15 +496,16 @@ class ChatAssistant:
|
|
|
447
496
|
digest_id=memory.digest_id,
|
|
448
497
|
)
|
|
449
498
|
elif memory.keep is not None:
|
|
450
|
-
|
|
499
|
+
message_types = memory.resolved_message_types()
|
|
500
|
+
if not message_types:
|
|
451
501
|
raise ValueError("ChatMemory.keep needs message_type")
|
|
452
|
-
produce = WindowPruner(
|
|
502
|
+
produce = WindowPruner(message_types, keep=memory.keep)
|
|
453
503
|
else:
|
|
454
504
|
return [] # compaction-only: no message-level memory agent
|
|
455
505
|
return [
|
|
456
506
|
create_agent(
|
|
457
507
|
"memory",
|
|
458
|
-
consumes=[Consume(
|
|
508
|
+
consumes=[Consume.by_base(t) for t in message_types],
|
|
459
509
|
produces=[produce],
|
|
460
510
|
)
|
|
461
511
|
]
|
|
@@ -48,6 +48,36 @@ class KVBackend(ABC):
|
|
|
48
48
|
"""Releases held resources (a pooled connection, ...). No-op by default."""
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class AppendBackend(KVBackend):
|
|
52
|
+
"""KV backend that can append commit entries without rewriting the snapshot.
|
|
53
|
+
|
|
54
|
+
`SessionStore` uses this when available to persist a session as
|
|
55
|
+
*snapshot + append-only commit log* instead of rewriting the whole Context
|
|
56
|
+
on every commit. Backends without it keep the full-blob behavior.
|
|
57
|
+
|
|
58
|
+
Entries are opaque JSON dicts; `SessionStore` writes one per save with the
|
|
59
|
+
new commits and the current pending-event queue. `clear_entries` is used
|
|
60
|
+
when the snapshot is rewritten (first save, compaction, rollback).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def append(self, key: str, entry: dict[str, Any]) -> None: ...
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def entries(self, key: str) -> list[dict[str, Any]]: ...
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def clear_entries(self, key: str) -> None: ...
|
|
71
|
+
|
|
72
|
+
async def entry_count(self, key: str) -> int:
|
|
73
|
+
"""Number of appended entries for `key`.
|
|
74
|
+
|
|
75
|
+
Backends with a native `COUNT(*)` should override this; the default
|
|
76
|
+
loads the entries and counts them.
|
|
77
|
+
"""
|
|
78
|
+
return len(await self.entries(key))
|
|
79
|
+
|
|
80
|
+
|
|
51
81
|
class InMemoryKVBackend(KVBackend):
|
|
52
82
|
"""Dict-backed KV store — no files, no external service.
|
|
53
83
|
|
|
@@ -178,9 +208,9 @@ class _AsyncSQLite:
|
|
|
178
208
|
writers across processes via the database file).
|
|
179
209
|
"""
|
|
180
210
|
|
|
181
|
-
def __init__(self, db_path: str, init_sql: str):
|
|
211
|
+
def __init__(self, db_path: str, init_sql: str | tuple[str, ...]):
|
|
182
212
|
self.db_path = db_path
|
|
183
|
-
self._init_sql = init_sql
|
|
213
|
+
self._init_sql = (init_sql,) if isinstance(init_sql, str) else init_sql
|
|
184
214
|
self._conn: sqlite3.Connection | None = None
|
|
185
215
|
self._lock = asyncio.Lock()
|
|
186
216
|
|
|
@@ -197,7 +227,8 @@ class _AsyncSQLite:
|
|
|
197
227
|
try:
|
|
198
228
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
199
229
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
200
|
-
|
|
230
|
+
for statement in self._init_sql:
|
|
231
|
+
conn.execute(statement)
|
|
201
232
|
conn.commit()
|
|
202
233
|
return conn
|
|
203
234
|
except sqlite3.OperationalError:
|
|
@@ -227,15 +258,23 @@ class _AsyncSQLite:
|
|
|
227
258
|
self._conn = None
|
|
228
259
|
|
|
229
260
|
|
|
230
|
-
class SQLiteKVBackend(
|
|
231
|
-
"""SQLite KV backend:
|
|
261
|
+
class SQLiteKVBackend(AppendBackend):
|
|
262
|
+
"""SQLite KV backend: snapshot in `kv_entries`, commit log in
|
|
263
|
+
`append_entries` (snapshot + append-only commits for sessions)."""
|
|
232
264
|
|
|
233
265
|
def __init__(self, db_path: str):
|
|
234
266
|
self.db_path = db_path
|
|
235
267
|
self._sql = _AsyncSQLite(
|
|
236
268
|
db_path,
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
(
|
|
270
|
+
"CREATE TABLE IF NOT EXISTS kv_entries "
|
|
271
|
+
"(key TEXT PRIMARY KEY, data TEXT NOT NULL)",
|
|
272
|
+
"CREATE TABLE IF NOT EXISTS append_entries "
|
|
273
|
+
"(seq INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
274
|
+
"key TEXT NOT NULL, data TEXT NOT NULL)",
|
|
275
|
+
"CREATE INDEX IF NOT EXISTS idx_append_entries_key "
|
|
276
|
+
"ON append_entries(key, seq)",
|
|
277
|
+
),
|
|
239
278
|
)
|
|
240
279
|
|
|
241
280
|
async def set(self, key: str, data: dict[str, Any]) -> None:
|
|
@@ -252,11 +291,33 @@ class SQLiteKVBackend(KVBackend):
|
|
|
252
291
|
|
|
253
292
|
async def delete(self, key: str) -> None:
|
|
254
293
|
await self._sql.execute("DELETE FROM kv_entries WHERE key = ?", (key,))
|
|
294
|
+
await self._sql.execute("DELETE FROM append_entries WHERE key = ?", (key,))
|
|
255
295
|
|
|
256
296
|
async def keys(self) -> list[str]:
|
|
257
297
|
rows = await self._sql.execute("SELECT key FROM kv_entries")
|
|
258
298
|
return [cast(str, r[0]) for r in rows]
|
|
259
299
|
|
|
300
|
+
async def append(self, key: str, entry: dict[str, Any]) -> None:
|
|
301
|
+
await self._sql.execute(
|
|
302
|
+
"INSERT INTO append_entries (key, data) VALUES (?, ?)",
|
|
303
|
+
(key, json.dumps(entry)),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
async def entries(self, key: str) -> list[dict[str, Any]]:
|
|
307
|
+
rows = await self._sql.execute(
|
|
308
|
+
"SELECT data FROM append_entries WHERE key = ? ORDER BY seq", (key,)
|
|
309
|
+
)
|
|
310
|
+
return [cast(dict[str, Any], json.loads(r[0])) for r in rows]
|
|
311
|
+
|
|
312
|
+
async def clear_entries(self, key: str) -> None:
|
|
313
|
+
await self._sql.execute("DELETE FROM append_entries WHERE key = ?", (key,))
|
|
314
|
+
|
|
315
|
+
async def entry_count(self, key: str) -> int:
|
|
316
|
+
rows = await self._sql.execute(
|
|
317
|
+
"SELECT COUNT(*) FROM append_entries WHERE key = ?", (key,)
|
|
318
|
+
)
|
|
319
|
+
return int(rows[0][0]) if rows else 0
|
|
320
|
+
|
|
260
321
|
async def aclose(self) -> None:
|
|
261
322
|
await self._sql.aclose()
|
|
262
323
|
|
|
@@ -288,7 +349,7 @@ class SQLiteBackend(CheckpointBackend):
|
|
|
288
349
|
await self._sql.aclose()
|
|
289
350
|
|
|
290
351
|
|
|
291
|
-
class PostgreSQLKVBackend(
|
|
352
|
+
class PostgreSQLKVBackend(AppendBackend):
|
|
292
353
|
"""PostgreSQL KV backend (`kv_entries(key, data)`) — behind the `pg` extra.
|
|
293
354
|
|
|
294
355
|
Like the SQLite KV backend, but shared across processes: a natural store
|
|
@@ -327,6 +388,15 @@ class PostgreSQLKVBackend(KVBackend):
|
|
|
327
388
|
"CREATE TABLE IF NOT EXISTS kv_entries "
|
|
328
389
|
"(key TEXT PRIMARY KEY, data TEXT NOT NULL)"
|
|
329
390
|
)
|
|
391
|
+
await cur.execute(
|
|
392
|
+
"CREATE TABLE IF NOT EXISTS append_entries "
|
|
393
|
+
"(seq BIGSERIAL PRIMARY KEY, "
|
|
394
|
+
"key TEXT NOT NULL, data TEXT NOT NULL)"
|
|
395
|
+
)
|
|
396
|
+
await cur.execute(
|
|
397
|
+
"CREATE INDEX IF NOT EXISTS idx_append_entries_key "
|
|
398
|
+
"ON append_entries(key, seq)"
|
|
399
|
+
)
|
|
330
400
|
await conn.commit()
|
|
331
401
|
self._conn = conn
|
|
332
402
|
return conn
|
|
@@ -384,11 +454,37 @@ class PostgreSQLKVBackend(KVBackend):
|
|
|
384
454
|
|
|
385
455
|
async def delete(self, key: str) -> None:
|
|
386
456
|
await self._run("DELETE FROM kv_entries WHERE key = %s", (key,))
|
|
457
|
+
await self._run("DELETE FROM append_entries WHERE key = %s", (key,))
|
|
387
458
|
|
|
388
459
|
async def keys(self) -> list[str]:
|
|
389
460
|
rows = await self._run("SELECT key FROM kv_entries", fetch="fetchall")
|
|
390
461
|
return [cast(str, r[0]) for r in rows]
|
|
391
462
|
|
|
463
|
+
async def append(self, key: str, entry: dict[str, Any]) -> None:
|
|
464
|
+
await self._run(
|
|
465
|
+
"INSERT INTO append_entries (key, data) VALUES (%s, %s)",
|
|
466
|
+
(key, json.dumps(entry)),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
async def entries(self, key: str) -> list[dict[str, Any]]:
|
|
470
|
+
rows = await self._run(
|
|
471
|
+
"SELECT data FROM append_entries WHERE key = %s ORDER BY seq",
|
|
472
|
+
(key,),
|
|
473
|
+
fetch="fetchall",
|
|
474
|
+
)
|
|
475
|
+
return [cast(dict[str, Any], json.loads(r[0])) for r in rows]
|
|
476
|
+
|
|
477
|
+
async def clear_entries(self, key: str) -> None:
|
|
478
|
+
await self._run("DELETE FROM append_entries WHERE key = %s", (key,))
|
|
479
|
+
|
|
480
|
+
async def entry_count(self, key: str) -> int:
|
|
481
|
+
row = await self._run(
|
|
482
|
+
"SELECT COUNT(*) FROM append_entries WHERE key = %s",
|
|
483
|
+
(key,),
|
|
484
|
+
fetch="fetchone",
|
|
485
|
+
)
|
|
486
|
+
return int(row[0]) if row is not None else 0
|
|
487
|
+
|
|
392
488
|
async def aclose(self) -> None:
|
|
393
489
|
async with self._lock:
|
|
394
490
|
await self._reset()
|