reactifact 0.8.0__tar.gz → 0.9.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.8.0/reactifact.egg-info → reactifact-0.9.0}/PKG-INFO +7 -7
- {reactifact-0.8.0 → reactifact-0.9.0}/README.md +6 -6
- {reactifact-0.8.0 → reactifact-0.9.0}/pyproject.toml +1 -1
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/__init__.py +3 -2
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/agents.py +78 -11
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/chat.py +73 -7
- reactifact-0.9.0/reactifact/consume.py +387 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/context_builder.py +60 -15
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/effects.py +2 -2
- reactifact-0.9.0/reactifact/native_tool_use.py +167 -0
- reactifact-0.9.0/reactifact/produce.py +322 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/__init__.py +26 -1
- reactifact-0.9.0/reactifact/recipes/cleanup.py +92 -0
- reactifact-0.9.0/reactifact/recipes/identity.py +94 -0
- reactifact-0.9.0/reactifact/recipes/memory.py +308 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/plan_execute.py +10 -14
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/reflection.py +13 -18
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/status.py +4 -9
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/supervisor.py +15 -10
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/runtime.py +60 -8
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/structured.py +191 -3
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/assertions.py +59 -9
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/fault.py +58 -10
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/lab.py +46 -8
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tool_use.py +8 -16
- reactifact-0.9.0/reactifact/triggers.py +69 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/verify.py +5 -9
- {reactifact-0.8.0 → reactifact-0.9.0/reactifact.egg-info}/PKG-INFO +7 -7
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact.egg-info/SOURCES.txt +10 -0
- reactifact-0.9.0/tests/test_absent_consume.py +111 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_agent_tool.py +15 -7
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_chat_web.py +4 -4
- reactifact-0.9.0/tests/test_consumes_produces.py +165 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_context_builder.py +51 -4
- reactifact-0.9.0/tests/test_correlated_consume.py +87 -0
- reactifact-0.9.0/tests/test_debounce.py +111 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_effects.py +4 -3
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_friendly_api.py +3 -3
- reactifact-0.9.0/tests/test_join_consume.py +121 -0
- reactifact-0.9.0/tests/test_native_tool_use.py +192 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_produce_styles.py +16 -15
- reactifact-0.9.0/tests/test_produce_trigger.py +204 -0
- reactifact-0.9.0/tests/test_reacts_to.py +160 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipe_supervisor.py +11 -3
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipes.py +234 -11
- reactifact-0.9.0/tests/test_recipes_memory.py +231 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_replay.py +2 -1
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_runtime_errors.py +4 -4
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_sessions.py +84 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_structured.py +162 -1
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_testing_lab.py +31 -3
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_tools.py +29 -28
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_viz.py +10 -2
- reactifact-0.8.0/reactifact/consume.py +0 -96
- reactifact-0.8.0/reactifact/produce.py +0 -182
- reactifact-0.8.0/reactifact/recipes/memory.py +0 -166
- reactifact-0.8.0/reactifact/triggers.py +0 -41
- reactifact-0.8.0/tests/test_consumes_produces.py +0 -54
- reactifact-0.8.0/tests/test_recipes_memory.py +0 -105
- {reactifact-0.8.0 → reactifact-0.9.0}/LICENSE +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/__main__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/_extras.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/agent_tool.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/branching.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/budget.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/checkpoints.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/commit.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/context.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/eval.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/events.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/llm_agent.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/operations.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/patches.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/prompts.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/chat.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/py.typed +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/relations.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/replay.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/resources.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/session.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/sources.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/streaming.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/mock.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tools.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/__init__.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/models.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/store.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/templates/ui_run.html +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/tracer.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/tracing/web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/viz.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact/web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact.egg-info/requires.txt +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/setup.cfg +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_backbone.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_branching.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_budget.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_checkpoint.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_cli.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_concurrency.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_devops.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_devops_web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_eval.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_forklab.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_hitl.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_incident_commander.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_mcp.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_multisource.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_patches.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_ports.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_prompts.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipe_plan_execute.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipe_reflection.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_relations.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_repair.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_research.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_resources.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_retry.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_runtime.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_sources.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_streaming.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_testing_mock.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_tracing.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_verify.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_view.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_web_source.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_workspace.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.8.0 → reactifact-0.9.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.9.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
|
|
@@ -106,21 +106,21 @@ DOCS = {
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
@produce(Evidence)
|
|
109
|
-
async def find_evidence(
|
|
110
|
-
question = next((a for a in inputs if isinstance(a.data, Question)), None)
|
|
109
|
+
async def find_evidence(call):
|
|
110
|
+
question = next((a for a in call.inputs if isinstance(a.data, Question)), None)
|
|
111
111
|
if question is None:
|
|
112
112
|
return None
|
|
113
113
|
hit = next((v for k, v in DOCS.items() if k in question.data.text.lower()), None)
|
|
114
114
|
if hit is not None:
|
|
115
|
-
effects.create(Evidence(text=hit))
|
|
115
|
+
call.effects.create(Evidence(text=hit))
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
@produce(Answer)
|
|
119
|
-
async def answer_from_evidence(
|
|
120
|
-
evidence = next((a for a in inputs if isinstance(a.data, Evidence)), None)
|
|
119
|
+
async def answer_from_evidence(call):
|
|
120
|
+
evidence = next((a for a in call.inputs if isinstance(a.data, Evidence)), None)
|
|
121
121
|
if evidence is None:
|
|
122
122
|
return None
|
|
123
|
-
effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
|
|
123
|
+
call.effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
search_agent = create_agent("search", consumes=[Consume(Question)], produces=[find_evidence])
|
|
@@ -62,21 +62,21 @@ DOCS = {
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
@produce(Evidence)
|
|
65
|
-
async def find_evidence(
|
|
66
|
-
question = next((a for a in inputs if isinstance(a.data, Question)), None)
|
|
65
|
+
async def find_evidence(call):
|
|
66
|
+
question = next((a for a in call.inputs if isinstance(a.data, Question)), None)
|
|
67
67
|
if question is None:
|
|
68
68
|
return None
|
|
69
69
|
hit = next((v for k, v in DOCS.items() if k in question.data.text.lower()), None)
|
|
70
70
|
if hit is not None:
|
|
71
|
-
effects.create(Evidence(text=hit))
|
|
71
|
+
call.effects.create(Evidence(text=hit))
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
@produce(Answer)
|
|
75
|
-
async def answer_from_evidence(
|
|
76
|
-
evidence = next((a for a in inputs if isinstance(a.data, Evidence)), None)
|
|
75
|
+
async def answer_from_evidence(call):
|
|
76
|
+
evidence = next((a for a in call.inputs if isinstance(a.data, Evidence)), None)
|
|
77
77
|
if evidence is None:
|
|
78
78
|
return None
|
|
79
|
-
effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
|
|
79
|
+
call.effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
search_agent = create_agent("search", consumes=[Consume(Question)], produces=[find_evidence])
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.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"
|
|
@@ -31,7 +31,7 @@ from .effects import Effects, Handle
|
|
|
31
31
|
from .events import Event, EventType
|
|
32
32
|
from .interrupt import PendingQuestion
|
|
33
33
|
from .patches import Create, Delete, Link, Patch, Relation, Unlink, Update
|
|
34
|
-
from .produce import Produce, produce
|
|
34
|
+
from .produce import Produce, ProduceCall, produce
|
|
35
35
|
from .providers import (
|
|
36
36
|
EmbeddingProvider,
|
|
37
37
|
FakeEmbedder,
|
|
@@ -48,7 +48,7 @@ from .session import Session, SessionStore
|
|
|
48
48
|
from .tools import FunctionTool, Tool, ToolOutput, tool
|
|
49
49
|
from .triggers import Trigger
|
|
50
50
|
|
|
51
|
-
__version__ = "0.
|
|
51
|
+
__version__ = "0.9.0"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Agent",
|
|
@@ -72,6 +72,7 @@ __all__ = [
|
|
|
72
72
|
"Patch",
|
|
73
73
|
"PendingQuestion",
|
|
74
74
|
"Produce",
|
|
75
|
+
"ProduceCall",
|
|
75
76
|
"Relation",
|
|
76
77
|
"RunOutcome",
|
|
77
78
|
"RunStats",
|
|
@@ -7,9 +7,9 @@ from typing import Any
|
|
|
7
7
|
from .artifacts import Artifact
|
|
8
8
|
from .consume import Consume
|
|
9
9
|
from .context import Context
|
|
10
|
-
from .events import Event
|
|
10
|
+
from .events import Event, EventType
|
|
11
11
|
from .patches import Patch
|
|
12
|
-
from .produce import Produce
|
|
12
|
+
from .produce import Produce, ProduceCall
|
|
13
13
|
from .triggers import Trigger
|
|
14
14
|
|
|
15
15
|
|
|
@@ -25,6 +25,15 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
25
25
|
produces: Sequence[Produce[Any]] | None = None
|
|
26
26
|
#: Declarative capability labels (§25), consumed by the adaptive policy.
|
|
27
27
|
capabilities: tuple[str, ...] = ()
|
|
28
|
+
#: Explicit trigger override — for the *imperative* style (`Agent`
|
|
29
|
+
#: subclass overriding `run()` directly, no `consumes`/`produces` at
|
|
30
|
+
#: all, see `run()`'s own docstring). That style never calls
|
|
31
|
+
#: `_collect_inputs()`, so `Consume` has nothing to attach to; `triggers`
|
|
32
|
+
#: is the only way such an agent says when to wake up. For the
|
|
33
|
+
#: declarative style (`consumes`/`produces`, the common case), leave this
|
|
34
|
+
#: unset — `triggers` auto-derives from `consumes` instead, and a
|
|
35
|
+
#: `Consume` that should feed inputs without also waking the agent uses
|
|
36
|
+
#: `Consume(..., wakes=False)` rather than a second, hand-maintained list.
|
|
28
37
|
triggers: list[Trigger] = []
|
|
29
38
|
# Run priority within a single generation: lower value runs earlier.
|
|
30
39
|
# Useful for "finishers"/evaluators that logically run last (§24).
|
|
@@ -80,7 +89,17 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
80
89
|
)
|
|
81
90
|
|
|
82
91
|
def matches(self, event: Event, context: Context | None = None) -> bool:
|
|
83
|
-
return
|
|
92
|
+
return bool(self.matching_triggers(event, context))
|
|
93
|
+
|
|
94
|
+
def matching_triggers(
|
|
95
|
+
self, event: Event, context: Context | None = None
|
|
96
|
+
) -> list[Trigger]:
|
|
97
|
+
"""Every trigger that matches `event` — `matches()` is just
|
|
98
|
+
`bool(...)` of this. `Runtime._arun_once_impl` uses the full list
|
|
99
|
+
(not just the bool) to decide whether *every* matching trigger asks
|
|
100
|
+
for debouncing (`Trigger.debounce`) before collapsing repeat events
|
|
101
|
+
into one run."""
|
|
102
|
+
return [trigger for trigger in self.triggers if trigger.matches(event, context)]
|
|
84
103
|
|
|
85
104
|
def collect_inputs(self, context: Context) -> list[Artifact[Any]]:
|
|
86
105
|
"""Public access to the consumed artifacts.
|
|
@@ -102,10 +121,7 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
102
121
|
return []
|
|
103
122
|
inputs: list[Artifact[Any]] = []
|
|
104
123
|
for c in self.consumes:
|
|
105
|
-
|
|
106
|
-
if c.condition:
|
|
107
|
-
artifacts = [a for a in artifacts if c.condition(a)]
|
|
108
|
-
inputs.extend(artifacts)
|
|
124
|
+
inputs.extend(c.collect(context))
|
|
109
125
|
builder = context.resources.context_builder
|
|
110
126
|
if builder is not None:
|
|
111
127
|
inputs = builder.build(context, self, inputs)
|
|
@@ -128,8 +144,9 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
128
144
|
async def execute(self, context: Context, event: Event | None = None) -> None:
|
|
129
145
|
"""Runs the agent's produces (usually on an event).
|
|
130
146
|
|
|
131
|
-
Effects-first (§24): produces write `self.effects.*`
|
|
132
|
-
the *runtime* compiles the effect slot into one
|
|
147
|
+
Effects-first (§24): produces write `self.effects.*`/`call.effects.*`
|
|
148
|
+
and return None; the *runtime* compiles the effect slot into one
|
|
149
|
+
atomic patch. This method
|
|
133
150
|
only *runs* the produces — it does not build a patch. (`run` remains the
|
|
134
151
|
agent-level escape hatch for custom Agent subclasses that assemble a
|
|
135
152
|
change-set by hand; the runtime merges its result after the effects.)
|
|
@@ -138,9 +155,56 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
138
155
|
return None
|
|
139
156
|
inputs = self._collect_inputs(context)
|
|
140
157
|
for p in self.produces:
|
|
141
|
-
|
|
158
|
+
runs, trigger = self._resolve_produce_call(p, event, context)
|
|
159
|
+
if not runs:
|
|
160
|
+
continue
|
|
161
|
+
call = ProduceCall(
|
|
162
|
+
context=context, inputs=inputs, event=event, trigger=trigger
|
|
163
|
+
)
|
|
164
|
+
await p.produce(call)
|
|
142
165
|
return None
|
|
143
166
|
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _resolve_produce_call(
|
|
169
|
+
p: Produce[Any], event: Event | None, context: Context
|
|
170
|
+
) -> tuple[bool, Artifact[Any] | None]:
|
|
171
|
+
"""Whether to call `p.produce()` for `event`, and the triggering
|
|
172
|
+
artifact to put on the `ProduceCall.trigger` field (see `Produce`'s
|
|
173
|
+
docstring, and `ProduceCall`'s, for the full contract).
|
|
174
|
+
|
|
175
|
+
`p.reacts_to is None` (the default): unrestricted, matching the
|
|
176
|
+
pre-`reacts_to` behavior where every produce ran on every event this
|
|
177
|
+
agent got — `trigger` is still resolved on a best-effort basis (for
|
|
178
|
+
a produce that wants it without narrowing `reacts_to`), but never a
|
|
179
|
+
reason to skip the call, since there's no per-type contract to hold
|
|
180
|
+
it to.
|
|
181
|
+
|
|
182
|
+
`p.reacts_to` set: exact type equality against `event.artifact_type`
|
|
183
|
+
(same convention as `Trigger.artifact_type`, not `issubclass` — that
|
|
184
|
+
one's a `Context.list_artifacts()` convention for querying by base
|
|
185
|
+
type). For a CREATED/UPDATED/STALE event, also requires
|
|
186
|
+
`context.get(event.artifact_id)` to still resolve — the artifact may
|
|
187
|
+
have been deleted by another agent earlier in the same generation
|
|
188
|
+
(the same race `Trigger.matches()` documents) — so `reacts_to`, once
|
|
189
|
+
set, guarantees `call.trigger` is always a live, correctly-typed
|
|
190
|
+
artifact. A DELETED event is exempt from that liveness requirement:
|
|
191
|
+
`context.get(...)` correctly returning `None` *is* the event there,
|
|
192
|
+
not a race, so the produce still runs with `trigger=None` —
|
|
193
|
+
deletion-reacting code is expected to handle that itself.
|
|
194
|
+
"""
|
|
195
|
+
if event is None:
|
|
196
|
+
return (p.reacts_to is None), None
|
|
197
|
+
if p.reacts_to is not None and event.artifact_type not in p.reacts_to:
|
|
198
|
+
return False, None
|
|
199
|
+
trigger = context.get(event.artifact_id)
|
|
200
|
+
if (
|
|
201
|
+
p.reacts_to is not None
|
|
202
|
+
and trigger is None
|
|
203
|
+
and event.type is not EventType.ARTIFACT_DELETED
|
|
204
|
+
):
|
|
205
|
+
return False, None
|
|
206
|
+
return True, trigger
|
|
207
|
+
|
|
144
208
|
|
|
145
209
|
def create_agent(
|
|
146
210
|
name: str,
|
|
@@ -166,7 +230,10 @@ def create_agent(
|
|
|
166
230
|
)
|
|
167
231
|
```
|
|
168
232
|
|
|
169
|
-
Falls back to `name` defaults the same way as `Agent.__init__`.
|
|
233
|
+
Falls back to `name` defaults the same way as `Agent.__init__`. Pass
|
|
234
|
+
`triggers` only for the rare imperative style with no `consumes` at all
|
|
235
|
+
(see `Agent.triggers`'s own docstring) — the ordinary declarative case
|
|
236
|
+
should leave it unset and use `Consume(..., wakes=False)` instead.
|
|
170
237
|
"""
|
|
171
238
|
agent = Agent(
|
|
172
239
|
name=name, triggers=triggers if triggers is not None else [], priority=priority
|
|
@@ -16,10 +16,11 @@ Two levels of use:
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
+
import inspect
|
|
19
20
|
import logging
|
|
20
21
|
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Sequence
|
|
21
22
|
from contextlib import asynccontextmanager
|
|
22
|
-
from typing import Any
|
|
23
|
+
from typing import Any, Literal
|
|
23
24
|
|
|
24
25
|
from pydantic import BaseModel
|
|
25
26
|
|
|
@@ -64,7 +65,7 @@ async def run_message(
|
|
|
64
65
|
user_message: type[BaseModel],
|
|
65
66
|
reply: Callable[[Context, str], dict[str, Any]],
|
|
66
67
|
session_id: str = "",
|
|
67
|
-
create_message: Callable[
|
|
68
|
+
create_message: Callable[..., str] | None = None,
|
|
68
69
|
status_kinds: Sequence[str] = ("status",),
|
|
69
70
|
fallback_reply: str = "No reply assembled.",
|
|
70
71
|
) -> AsyncIterator[ChatEvent]:
|
|
@@ -78,7 +79,11 @@ async def run_message(
|
|
|
78
79
|
`create_message(ctx, text) -> msg_id` overrides how the turn enters the
|
|
79
80
|
context (default: create a `user_message` artifact) — HITL apps where a
|
|
80
81
|
new turn resumes a pending question instead of appending a message
|
|
81
|
-
(devops-style clarify) pass their own.
|
|
82
|
+
(devops-style clarify) pass their own. Also accepts the 3-arg shape
|
|
83
|
+
`create_message(ctx, text, session_id) -> msg_id` for a hook that needs
|
|
84
|
+
the current turn's `session_id` (e.g. to stamp it onto the created
|
|
85
|
+
artifact) — detected from the callable's own arity, so the 2-arg shape
|
|
86
|
+
keeps working unchanged.
|
|
82
87
|
|
|
83
88
|
`status_kinds` selects which progress event kinds are forwarded as `status`
|
|
84
89
|
frames (default: only `status`; e.g. tool-announcing demos also forward
|
|
@@ -92,7 +97,7 @@ async def run_message(
|
|
|
92
97
|
ctx = runtime.context
|
|
93
98
|
try:
|
|
94
99
|
if create_message is not None:
|
|
95
|
-
msg_id = create_message
|
|
100
|
+
msg_id = _call_create_message(create_message, ctx, text, session_id)
|
|
96
101
|
else:
|
|
97
102
|
msg_id = ctx.create(user_message(text=text, session_id=session_id)).id
|
|
98
103
|
except Exception:
|
|
@@ -164,6 +169,45 @@ def _resolve(value: Any) -> Any:
|
|
|
164
169
|
return value() if callable(value) else value
|
|
165
170
|
|
|
166
171
|
|
|
172
|
+
def _accepts_arg(func: Callable[..., Any], count: int) -> bool:
|
|
173
|
+
"""Whether `func` (already known callable) declares at least `count`
|
|
174
|
+
positional-or-keyword parameters — used to detect the opt-in, per-request
|
|
175
|
+
call shapes below without breaking the plain zero/two-arg factories that
|
|
176
|
+
predate them. A callable whose signature can't be inspected (a builtin, a
|
|
177
|
+
C extension) is assumed to be the old, arg-less shape."""
|
|
178
|
+
try:
|
|
179
|
+
params = inspect.signature(func).parameters
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
return False
|
|
182
|
+
return len(params) >= count
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_with_session(value: Any, session_id: str) -> Any:
|
|
186
|
+
"""Like `_resolve`, but passes `session_id` to `value` when it declares a
|
|
187
|
+
parameter for it — the per-request factory shape
|
|
188
|
+
(`resources=lambda session_id: build_resources(session_id)`) alongside
|
|
189
|
+
the pre-existing zero-arg one (`resources=lambda: build_resources()`),
|
|
190
|
+
which keeps working unchanged. `ChatAssistant` already knows `session_id`
|
|
191
|
+
at the point it resolves `resources=`; this just lets a factory opt into
|
|
192
|
+
reading it instead of reaching for a contextvar/side-channel to get
|
|
193
|
+
per-request data (e.g. an authenticated user) into `RuntimeResources`.
|
|
194
|
+
"""
|
|
195
|
+
if not callable(value):
|
|
196
|
+
return value
|
|
197
|
+
return value(session_id) if _accepts_arg(value, 1) else value()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _call_create_message(
|
|
201
|
+
create_message: Callable[..., str], ctx: Context, text: str, session_id: str
|
|
202
|
+
) -> str:
|
|
203
|
+
"""Calls `create_message` with `session_id` appended when it declares a
|
|
204
|
+
third parameter for it (`(ctx, text, session_id) -> msg_id`), alongside
|
|
205
|
+
the pre-existing two-arg shape (`(ctx, text) -> msg_id`)."""
|
|
206
|
+
if _accepts_arg(create_message, 3):
|
|
207
|
+
return create_message(ctx, text, session_id)
|
|
208
|
+
return create_message(ctx, text)
|
|
209
|
+
|
|
210
|
+
|
|
167
211
|
# --- the canonical chat assistant ----------------------------------------- #
|
|
168
212
|
|
|
169
213
|
|
|
@@ -182,6 +226,23 @@ class ChatAssistant:
|
|
|
182
226
|
turns/sessions — that instance is never closed automatically; close it
|
|
183
227
|
yourself at real shutdown.
|
|
184
228
|
|
|
229
|
+
`session_save_policy=` passes straight through to `Runtime` — `"per_turn"`
|
|
230
|
+
trades finer crash-resilience granularity (a save at every commit, the
|
|
231
|
+
default) for one `session.save()` per turn instead of one per commit,
|
|
232
|
+
worthwhile once a multi-stage pipeline routinely produces several commits
|
|
233
|
+
per turn (see `Runtime.__init__`'s own docstring for the trade-off).
|
|
234
|
+
|
|
235
|
+
`resources=`/`create_message=` may optionally take the current turn's
|
|
236
|
+
`session_id` — `resources=lambda session_id: build_resources(session_id)`
|
|
237
|
+
(e.g. to attach an authenticated user looked up from the session) and
|
|
238
|
+
`create_message=lambda ctx, text, session_id: ...` — detected from each
|
|
239
|
+
callable's own arity, so the pre-existing zero/two-arg shapes keep
|
|
240
|
+
working unchanged. Without this, per-request data (who's asking, not
|
|
241
|
+
just what they asked) has no way into a turn short of a contextvar/
|
|
242
|
+
side-channel set by the caller before `stream()`/`invoke()` — `resources=`
|
|
243
|
+
and `create_message=` were otherwise the only two hooks in this class
|
|
244
|
+
that never saw it.
|
|
245
|
+
|
|
185
246
|
Base usage:
|
|
186
247
|
|
|
187
248
|
assistant = ChatAssistant(
|
|
@@ -203,15 +264,16 @@ class ChatAssistant:
|
|
|
203
264
|
user_message: type[BaseModel],
|
|
204
265
|
reply: Callable[[Context, str], dict[str, Any]],
|
|
205
266
|
session_state: Callable[[Context], dict[str, Any]] | None = None,
|
|
206
|
-
resources: RuntimeResources | Callable[
|
|
267
|
+
resources: RuntimeResources | Callable[..., RuntimeResources] | None = None,
|
|
207
268
|
budget: Budget | None = None,
|
|
208
269
|
max_concurrency: int | None = None,
|
|
209
270
|
tracer: Any = None,
|
|
210
|
-
create_message: Callable[
|
|
271
|
+
create_message: Callable[..., str] | None = None,
|
|
211
272
|
status_kinds: Sequence[str] = ("status",),
|
|
212
273
|
fallback_reply: str = "No reply assembled.",
|
|
213
274
|
isolate_errors: bool = False,
|
|
214
275
|
on_agent_error: Callable[[Agent, Event, BaseException], None] | None = None,
|
|
276
|
+
session_save_policy: Literal["per_commit", "per_turn"] = "per_commit",
|
|
215
277
|
):
|
|
216
278
|
self.store = store
|
|
217
279
|
self._agents = agents
|
|
@@ -227,6 +289,7 @@ class ChatAssistant:
|
|
|
227
289
|
self._fallback_reply = fallback_reply
|
|
228
290
|
self._isolate_errors = isolate_errors
|
|
229
291
|
self._on_agent_error = on_agent_error
|
|
292
|
+
self._session_save_policy = session_save_policy
|
|
230
293
|
# Serializes concurrent turns on the *same* session_id (a double
|
|
231
294
|
# submit, a client retry): without this, two overlapping stream()
|
|
232
295
|
# calls both load the same starting state and the later save() wins,
|
|
@@ -262,7 +325,9 @@ class ChatAssistant:
|
|
|
262
325
|
self._session_locks.pop(session_id, None)
|
|
263
326
|
|
|
264
327
|
async def _open(self, session_id: str) -> Session:
|
|
265
|
-
return await self.store.open(
|
|
328
|
+
return await self.store.open(
|
|
329
|
+
session_id, resources=_resolve_with_session(self._resources, session_id)
|
|
330
|
+
)
|
|
266
331
|
|
|
267
332
|
def _build_runtime(self, session: Session) -> Runtime:
|
|
268
333
|
return Runtime(
|
|
@@ -274,6 +339,7 @@ class ChatAssistant:
|
|
|
274
339
|
tracer=_resolve(self._tracer),
|
|
275
340
|
isolate_errors=self._isolate_errors,
|
|
276
341
|
on_agent_error=self._on_agent_error,
|
|
342
|
+
session_save_policy=self._session_save_policy,
|
|
277
343
|
)
|
|
278
344
|
|
|
279
345
|
async def stream(self, text: str, session_id: str = "") -> AsyncIterator[ChatEvent]:
|