reactifact 0.7.0__tar.gz → 0.8.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.7.0/reactifact.egg-info → reactifact-0.8.0}/PKG-INFO +10 -3
- {reactifact-0.7.0 → reactifact-0.8.0}/README.md +2 -1
- {reactifact-0.7.0 → reactifact-0.8.0}/pyproject.toml +14 -2
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/__init__.py +1 -1
- reactifact-0.8.0/reactifact/agent_tool.py +115 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/agents.py +11 -1
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/branching.py +2 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/checkpoints.py +9 -2
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/context.py +43 -3
- reactifact-0.8.0/reactifact/context_builder.py +157 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/llm_agent.py +2 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/__init__.py +18 -0
- reactifact-0.8.0/reactifact/recipes/plan_execute.py +207 -0
- reactifact-0.8.0/reactifact/recipes/reflection.py +262 -0
- reactifact-0.8.0/reactifact/recipes/supervisor.py +224 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/resources.py +14 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/runtime.py +58 -2
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tool_use.py +129 -6
- reactifact-0.8.0/reactifact/verify.py +161 -0
- {reactifact-0.7.0 → reactifact-0.8.0/reactifact.egg-info}/PKG-INFO +10 -3
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/SOURCES.txt +13 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/requires.txt +5 -0
- reactifact-0.8.0/tests/test_agent_tool.py +190 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_checkpoint.py +22 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_concurrency.py +56 -0
- reactifact-0.8.0/tests/test_context_builder.py +165 -0
- reactifact-0.8.0/tests/test_incident_commander.py +114 -0
- reactifact-0.8.0/tests/test_recipe_plan_execute.py +106 -0
- reactifact-0.8.0/tests/test_recipe_reflection.py +124 -0
- reactifact-0.8.0/tests/test_recipe_supervisor.py +162 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_tools.py +137 -0
- reactifact-0.8.0/tests/test_verify.py +129 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/LICENSE +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/__main__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/_extras.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/artifacts.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/budget.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/chat.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/__init__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/branch.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/common.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/context.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/graph.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/replay.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/scenario.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/cli/trace.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/commit.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/commit_log.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/consume.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/effects.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/eval.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/events.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/interrupt.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/__init__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/client.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/oauth.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/mcp/server.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/operations.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/patches.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/produce.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/prompts.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/__init__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/_retry.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/anthropic.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/azure.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/cerebras.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/chat.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/contracts.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/deepseek.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/fake.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/fireworks.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/gemini.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/github_models.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/groq.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/image.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/mistral.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/nvidia.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/ollama.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/openai.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/openrouter.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/perplexity.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/qwen.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/speech.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/together.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/video.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/xai.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/providers/zai.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/py.typed +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/inputs.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/memory.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/resolve.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/rollback.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/search.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/skills.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/status.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/recipes/text.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/relations.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/replay.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/scheduler.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/session.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/sources.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/streaming.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/structured.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/__init__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/assertions.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/exceptions.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/fault.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/lab.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/mock.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/record.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/testing/registry.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tools.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/__init__.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/_otlp.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/langfuse.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/models.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/otlp.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/postgres.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/store.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/templates/ui.html +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/templates/ui_run.html +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/tracer.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/tracing/web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/triggers.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/viz.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact/web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/dependency_links.txt +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/entry_points.txt +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/reactifact.egg-info/top_level.txt +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/setup.cfg +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_adaptive.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_anthropic_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_artifacts.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_backbone.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_branching.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_budget.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_canonical_ports.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_chat_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_checkpoints_concurrency.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_cli.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_cli_scenario.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_commit_log.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_consumes_produces.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_devops.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_devops_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_effects.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_eval.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_forklab.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_forklab_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_friendly_api.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_gemini_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_hitl.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_image_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_invalidation.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_knowledge.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_knowledge_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_llm_ladder.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_mcp.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_medic_lab.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_medic_lab_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_multisource.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_openai_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_patches.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_ports.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_produce_styles.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_prompts.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_provider_auth.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_provider_retry.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_providers_integration.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_inputs.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_memory.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_recipes_skills.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_relation_graph.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_relations.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_repair.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_repair_web.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_replay.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_research.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_resources.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_retry.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_runtime.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_runtime_errors.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sessions.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources_search.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_sources_vector.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_speech_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_streaming.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_structured.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_assertions.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_lab.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_mock.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_testing_registry.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_tracing.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_vendor_factories.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_vendor_multimodal_factories.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_video_provider.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_view.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_viz.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_web_source.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_workspace.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/test_workspace_with_sources.py +0 -0
- {reactifact-0.7.0 → reactifact-0.8.0}/tests/tests_checkpoints_sqlite.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reactifact
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
7
7
|
Project-URL: Repository, https://github.com/bzdvdn/reactifact
|
|
8
|
-
Project-URL: Documentation, https://github.
|
|
8
|
+
Project-URL: Documentation, https://bzdvdn.github.io/reactifact/
|
|
9
9
|
Project-URL: Changelog, https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md
|
|
10
10
|
Keywords: agents,llm,ai-agents,reactive,provenance,orchestration,agentic
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -15,6 +15,8 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
20
|
Classifier: Typing :: Typed
|
|
19
21
|
Requires-Python: >=3.11
|
|
20
22
|
Description-Content-Type: text/markdown
|
|
@@ -34,6 +36,10 @@ Provides-Extra: pg
|
|
|
34
36
|
Requires-Dist: psycopg[binary]>=3.2; extra == "pg"
|
|
35
37
|
Provides-Extra: mcp
|
|
36
38
|
Requires-Dist: mcp>=2.2; extra == "mcp"
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: mkdocs>=1.6; extra == "docs"
|
|
41
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
42
|
+
Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
|
|
37
43
|
Dynamic: license-file
|
|
38
44
|
|
|
39
45
|
<p align="center">
|
|
@@ -44,10 +50,11 @@ Dynamic: license-file
|
|
|
44
50
|
|
|
45
51
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
46
52
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
47
|
-
[](https://github.com/bzdvdn/reactifact)
|
|
53
|
+
[](https://github.com/bzdvdn/reactifact)
|
|
48
54
|
[](https://pypi.org/project/reactifact/)
|
|
49
55
|
[](LICENSE)
|
|
50
56
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
57
|
+
[](https://bzdvdn.github.io/reactifact/)
|
|
51
58
|
|
|
52
59
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
53
60
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/bzdvdn/reactifact/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/bzdvdn/reactifact)
|
|
9
|
-
[](https://github.com/bzdvdn/reactifact)
|
|
9
|
+
[](https://github.com/bzdvdn/reactifact)
|
|
10
10
|
[](https://pypi.org/project/reactifact/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://deepwiki.com/bzdvdn/reactifact)
|
|
13
|
+
[](https://bzdvdn.github.io/reactifact/)
|
|
13
14
|
|
|
14
15
|
Most agent frameworks make you **draw the graph**: connect nodes, wire memory,
|
|
15
16
|
declare control flow. But a knowledge question — *"why did infra costs jump in
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reactifact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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"
|
|
@@ -19,6 +19,8 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
22
24
|
"Typing :: Typed",
|
|
23
25
|
]
|
|
24
26
|
dependencies = [
|
|
@@ -30,7 +32,7 @@ dependencies = [
|
|
|
30
32
|
[project.urls]
|
|
31
33
|
Homepage = "https://github.com/bzdvdn/reactifact"
|
|
32
34
|
Repository = "https://github.com/bzdvdn/reactifact"
|
|
33
|
-
Documentation = "https://github.
|
|
35
|
+
Documentation = "https://bzdvdn.github.io/reactifact/"
|
|
34
36
|
Changelog = "https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md"
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
|
@@ -50,6 +52,11 @@ pg = [
|
|
|
50
52
|
mcp = [
|
|
51
53
|
"mcp>=2.2",
|
|
52
54
|
]
|
|
55
|
+
docs = [
|
|
56
|
+
"mkdocs>=1.6",
|
|
57
|
+
"mkdocs-material>=9.5",
|
|
58
|
+
"mkdocstrings[python]>=0.27",
|
|
59
|
+
]
|
|
53
60
|
|
|
54
61
|
# uv-native groups mirror the extras so `uv run`/`uv sync` installs the web demo
|
|
55
62
|
# deps by default (no `--extra` flag needed to boot the examples). Extras stay
|
|
@@ -74,6 +81,11 @@ pg = [
|
|
|
74
81
|
mcp = [
|
|
75
82
|
"mcp>=2.2",
|
|
76
83
|
]
|
|
84
|
+
docs = [
|
|
85
|
+
"mkdocs>=1.6",
|
|
86
|
+
"mkdocs-material>=9.5",
|
|
87
|
+
"mkdocstrings[python]>=0.27",
|
|
88
|
+
]
|
|
77
89
|
|
|
78
90
|
[tool.uv]
|
|
79
91
|
default-groups = ["dev", "web", "mcp"]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""reactifact.agent_tool — a sub-agent as a `Tool` (delegation, §46).
|
|
2
|
+
|
|
3
|
+
`AgentAsTool` lets an `LLMAgent`/`HITLLMAgent` loop call another agent as an
|
|
4
|
+
ordinary tool: "delegate to X" runs a fresh, isolated nested `Runtime` to
|
|
5
|
+
completion and returns its final answer as the tool result. The delegating
|
|
6
|
+
loop sees only the final text — not the sub-agent's own tool calls, retries,
|
|
7
|
+
or internal reasoning.
|
|
8
|
+
|
|
9
|
+
Two deliberate constraints, not omissions:
|
|
10
|
+
|
|
11
|
+
- **Isolation is a fresh `Context`, not `context.branch()`.** The sub-agent
|
|
12
|
+
sees only the `query` it's given, nothing of the parent's history — no
|
|
13
|
+
prompt bloat from an unrelated conversation, and no accidental fork/merge
|
|
14
|
+
semantics (`branch()` exists for alternative-state exploration meant to be
|
|
15
|
+
merged back later, §39/§40; a one-shot delegate-and-discard call needs
|
|
16
|
+
neither). If a sub-agent genuinely needs to see the parent's state, build
|
|
17
|
+
its `Context` yourself and don't use this tool.
|
|
18
|
+
- **HITL sub-agents are rejected, not silently broken.** `Tool.execute` has
|
|
19
|
+
no channel back to a human — nothing would ever answer a `PendingQuestion`
|
|
20
|
+
the sub-agent raises inside its own isolated `Context`, and the nested run
|
|
21
|
+
would just finish with no `ToolAnswer`. `execute()` checks for this and
|
|
22
|
+
returns a `ToolOutput(error=...)` naming the unanswered question, instead
|
|
23
|
+
of returning empty text with no explanation.
|
|
24
|
+
|
|
25
|
+
Because `Tool.execute` receives only `args` (no `context`, see `tools.py`),
|
|
26
|
+
the sub-agent's `LLMProvider`/budget cannot be inherited implicitly from
|
|
27
|
+
whatever loop is calling this tool — pass them explicitly via `resources=`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel
|
|
36
|
+
|
|
37
|
+
from .agents import Agent
|
|
38
|
+
from .budget import Budget
|
|
39
|
+
from .context import Context
|
|
40
|
+
from .resources import RuntimeResources
|
|
41
|
+
from .runtime import Runtime
|
|
42
|
+
from .tool_use import ToolAnswer
|
|
43
|
+
from .tools import Tool, ToolOutput
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SubTask(BaseModel):
|
|
47
|
+
"""Default input artifact for `AgentAsTool` — override via `input_type`
|
|
48
|
+
with any model that has a `text: str` field (the convention `ToolUse`'s
|
|
49
|
+
own `_goal` reads, `tool_use.py`)."""
|
|
50
|
+
|
|
51
|
+
text: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentAsTool(Tool):
|
|
55
|
+
"""Wraps `agent_factory()` as a callable tool (see module docstring)."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
name: str,
|
|
61
|
+
description: str,
|
|
62
|
+
agent_factory: Callable[[], Agent],
|
|
63
|
+
resources: RuntimeResources,
|
|
64
|
+
input_type: type[BaseModel] = SubTask,
|
|
65
|
+
output_type: type[BaseModel] | None = None,
|
|
66
|
+
max_runs: int = 20,
|
|
67
|
+
destructive: bool = False,
|
|
68
|
+
):
|
|
69
|
+
self.name = name
|
|
70
|
+
self.description = description
|
|
71
|
+
self.agent_factory = agent_factory
|
|
72
|
+
self.resources = resources
|
|
73
|
+
self.input_type = input_type
|
|
74
|
+
self.output_type = output_type or ToolAnswer
|
|
75
|
+
self.max_runs = max_runs
|
|
76
|
+
self.destructive = destructive
|
|
77
|
+
self.schema = {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"query": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "The task/question to delegate to the sub-agent.",
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"required": ["query"],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async def execute(self, args: dict[str, Any]) -> ToolOutput:
|
|
89
|
+
query = str(args.get("query", ""))
|
|
90
|
+
sub_context = Context(resources=self.resources)
|
|
91
|
+
sub_agent = self.agent_factory()
|
|
92
|
+
sub_runtime = Runtime(
|
|
93
|
+
sub_context, agents=[sub_agent], budget=Budget(max_runs=self.max_runs)
|
|
94
|
+
)
|
|
95
|
+
sub_context.create(self.input_type(text=query))
|
|
96
|
+
await sub_runtime.arun()
|
|
97
|
+
|
|
98
|
+
if sub_context.has_pending_question():
|
|
99
|
+
pending = sub_context.latest_pending_question()
|
|
100
|
+
question = pending.data.question if pending is not None else ""
|
|
101
|
+
return ToolOutput(
|
|
102
|
+
error=(
|
|
103
|
+
f"sub-agent '{sub_agent.name}' needs clarification it wasn't "
|
|
104
|
+
f"given and can't ask for: {question!r}. Give '{self.name}' a "
|
|
105
|
+
"more complete query and try again."
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
outputs = sub_context.list_artifacts(self.output_type)
|
|
110
|
+
if not outputs:
|
|
111
|
+
return ToolOutput(
|
|
112
|
+
error=f"sub-agent '{sub_agent.name}' produced no {self.output_type.__name__}"
|
|
113
|
+
)
|
|
114
|
+
text = getattr(outputs[-1].data, "text", "") or ""
|
|
115
|
+
return ToolOutput(text=text)
|
|
@@ -90,7 +90,14 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
90
90
|
return self._collect_inputs(context)
|
|
91
91
|
|
|
92
92
|
def _collect_inputs(self, context: Context) -> list[Artifact[Any]]:
|
|
93
|
-
"""Collects all artifacts matching consumes and conditions.
|
|
93
|
+
"""Collects all artifacts matching consumes and conditions.
|
|
94
|
+
|
|
95
|
+
Ranking/truncation, if any, is a `Runtime`-level policy
|
|
96
|
+
(`context.resources.context_builder`, see `context_builder.py`), not
|
|
97
|
+
this agent's — applied here so both this and `collect_inputs()`
|
|
98
|
+
(used by the runtime for provenance) see the identical, already
|
|
99
|
+
built list.
|
|
100
|
+
"""
|
|
94
101
|
if not self.consumes:
|
|
95
102
|
return []
|
|
96
103
|
inputs: list[Artifact[Any]] = []
|
|
@@ -99,6 +106,9 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
|
|
|
99
106
|
if c.condition:
|
|
100
107
|
artifacts = [a for a in artifacts if c.condition(a)]
|
|
101
108
|
inputs.extend(artifacts)
|
|
109
|
+
builder = context.resources.context_builder
|
|
110
|
+
if builder is not None:
|
|
111
|
+
inputs = builder.build(context, self, inputs)
|
|
102
112
|
return inputs
|
|
103
113
|
|
|
104
114
|
async def run(self, event: Event, context: Context) -> Patch | None:
|
|
@@ -63,6 +63,7 @@ def clone_context(source: Context) -> Context:
|
|
|
63
63
|
new_ws._log = source._log.copy()
|
|
64
64
|
new_ws._relations = source._relations.copy()
|
|
65
65
|
new_ws._recompute_stale()
|
|
66
|
+
new_ws._reindex_by_type()
|
|
66
67
|
return new_ws
|
|
67
68
|
|
|
68
69
|
|
|
@@ -209,6 +210,7 @@ def merge_contexts(
|
|
|
209
210
|
# `delete()` — resync `_stale` from scratch rather than risk it
|
|
210
211
|
# drifting from the post-merge state.
|
|
211
212
|
target._recompute_stale()
|
|
213
|
+
target._reindex_by_type()
|
|
212
214
|
|
|
213
215
|
|
|
214
216
|
class BranchStore:
|
|
@@ -112,11 +112,18 @@ class FileBackend(CheckpointBackend):
|
|
|
112
112
|
"""File backend: state is stored in a single JSON file."""
|
|
113
113
|
|
|
114
114
|
def __init__(self, path: str):
|
|
115
|
-
self.path = path
|
|
115
|
+
self.path = Path(path)
|
|
116
116
|
|
|
117
117
|
def _save_sync(self, data: dict[str, Any]) -> None:
|
|
118
|
-
|
|
118
|
+
# Atomic write (tmp + rename), same reasoning as `FileKVBackend._set_sync`:
|
|
119
|
+
# a direct write left a truncated/corrupt file on a process kill mid-write
|
|
120
|
+
# (OOM, deploy, kill -9) — the next `load()` would raise JSONDecodeError
|
|
121
|
+
# and the session would be unrecoverable.
|
|
122
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
tmp = self.path.with_suffix(self.path.suffix + ".tmp")
|
|
124
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
119
125
|
json.dump(data, f, indent=2)
|
|
126
|
+
tmp.replace(self.path)
|
|
120
127
|
|
|
121
128
|
async def save(self, data: dict[str, Any]) -> None:
|
|
122
129
|
await asyncio.to_thread(self._save_sync, data)
|
|
@@ -81,6 +81,16 @@ class Context:
|
|
|
81
81
|
# version. Kept up to date by `update()`/`log_commit()` so
|
|
82
82
|
# `stale_artifacts()`/`has_stale()` never rescan the whole context.
|
|
83
83
|
self._stale: set[str] = set()
|
|
84
|
+
# Incrementally maintained: exact `type(data)` -> ids of that exact
|
|
85
|
+
# type. Kept up to date by `create()`/`update()`/`delete()` so
|
|
86
|
+
# `list_artifacts(T)` doesn't call `isinstance()` per artifact — it
|
|
87
|
+
# unions the ids of every *distinct type ever created* that is an
|
|
88
|
+
# `issubclass` of `T` (a small set — bounded by distinct types, not
|
|
89
|
+
# artifact count) into an O(1) membership test, still walked in
|
|
90
|
+
# insertion order to preserve tie-break order in callers' own sorts.
|
|
91
|
+
# Any bulk rewrite that bypasses create/update/delete must call
|
|
92
|
+
# `_reindex_by_type()` (mirrors `_recompute_stale()`, same reasoning).
|
|
93
|
+
self._by_type: dict[type, set[str]] = {}
|
|
84
94
|
|
|
85
95
|
# ---- announce: agent progress events streamed out ----
|
|
86
96
|
|
|
@@ -116,6 +126,7 @@ class Context:
|
|
|
116
126
|
return self._artifacts[id]
|
|
117
127
|
artifact = Artifact(data=data, id=id)
|
|
118
128
|
self._artifacts[artifact.id] = artifact
|
|
129
|
+
self._by_type.setdefault(type(data), set()).add(artifact.id)
|
|
119
130
|
self._events.append(
|
|
120
131
|
Event(
|
|
121
132
|
type=EventType.ARTIFACT_CREATED,
|
|
@@ -140,7 +151,12 @@ class Context:
|
|
|
140
151
|
return None
|
|
141
152
|
if artifact.data == new_data:
|
|
142
153
|
return artifact
|
|
154
|
+
old_type = type(artifact.data)
|
|
143
155
|
artifact.update(new_data)
|
|
156
|
+
new_type = type(new_data)
|
|
157
|
+
if new_type is not old_type:
|
|
158
|
+
self._by_type.get(old_type, set()).discard(artifact_id)
|
|
159
|
+
self._by_type.setdefault(new_type, set()).add(artifact_id)
|
|
144
160
|
self._events.append(
|
|
145
161
|
Event(
|
|
146
162
|
type=EventType.ARTIFACT_UPDATED,
|
|
@@ -165,6 +181,7 @@ class Context:
|
|
|
165
181
|
if artifact is None:
|
|
166
182
|
return False
|
|
167
183
|
self._stale.discard(artifact_id)
|
|
184
|
+
self._by_type.get(type(artifact.data), set()).discard(artifact_id)
|
|
168
185
|
self._events.append(
|
|
169
186
|
Event(
|
|
170
187
|
type=EventType.ARTIFACT_DELETED,
|
|
@@ -185,13 +202,22 @@ class Context:
|
|
|
185
202
|
def list_artifacts(
|
|
186
203
|
self, artifact_type: type[TArtifact] | None = None
|
|
187
204
|
) -> list[Artifact[Any]]:
|
|
188
|
-
"""Returns a list of artifacts, optionally filtered by data type.
|
|
205
|
+
"""Returns a list of artifacts, optionally filtered by data type.
|
|
206
|
+
|
|
207
|
+
Preserves insertion order (matters: ties in a caller's own sort key,
|
|
208
|
+
e.g. `updated_at`, break in creation order, same as before this
|
|
209
|
+
method stopped `isinstance`-scanning every artifact).
|
|
210
|
+
"""
|
|
189
211
|
if artifact_type is None:
|
|
190
212
|
return list(self._artifacts.values())
|
|
213
|
+
matching_ids: set[str] = set()
|
|
214
|
+
for t, ids in self._by_type.items():
|
|
215
|
+
if issubclass(t, artifact_type):
|
|
216
|
+
matching_ids |= ids
|
|
191
217
|
return [
|
|
192
218
|
cast(Artifact[TArtifact], a)
|
|
193
|
-
for a in self._artifacts.
|
|
194
|
-
if
|
|
219
|
+
for aid, a in self._artifacts.items()
|
|
220
|
+
if aid in matching_ids
|
|
195
221
|
]
|
|
196
222
|
|
|
197
223
|
def latest(self, artifact_type: type[TArtifact]) -> Artifact[TArtifact] | None:
|
|
@@ -493,6 +519,18 @@ class Context:
|
|
|
493
519
|
break
|
|
494
520
|
self._stale = stale
|
|
495
521
|
|
|
522
|
+
def _reindex_by_type(self) -> None:
|
|
523
|
+
"""Full rebuild of `_by_type` from current artifacts.
|
|
524
|
+
|
|
525
|
+
Same reasoning as `_recompute_stale()`: only needed after a bulk
|
|
526
|
+
rewrite that bypasses `create`/`update`/`delete` — call it alongside
|
|
527
|
+
`_recompute_stale()` at every such site, never on its own.
|
|
528
|
+
"""
|
|
529
|
+
by_type: dict[type, set[str]] = {}
|
|
530
|
+
for aid, artifact in self._artifacts.items():
|
|
531
|
+
by_type.setdefault(type(artifact.data), set()).add(aid)
|
|
532
|
+
self._by_type = by_type
|
|
533
|
+
|
|
496
534
|
def _rebuild_artifacts_from_commits(
|
|
497
535
|
self, upto_version: int
|
|
498
536
|
) -> dict[str, Artifact[Any]]:
|
|
@@ -534,6 +572,7 @@ class Context:
|
|
|
534
572
|
self._events = []
|
|
535
573
|
self._log.truncate(version)
|
|
536
574
|
self._recompute_stale()
|
|
575
|
+
self._reindex_by_type()
|
|
537
576
|
|
|
538
577
|
def to_dict(self) -> dict[str, Any]:
|
|
539
578
|
return {
|
|
@@ -559,6 +598,7 @@ class Context:
|
|
|
559
598
|
ws._fork_name = d.get("fork_name", "")
|
|
560
599
|
ws._base = Context.from_dict(d["base"]) if d.get("base") is not None else None
|
|
561
600
|
ws._recompute_stale()
|
|
601
|
+
ws._reindex_by_type()
|
|
562
602
|
return ws
|
|
563
603
|
|
|
564
604
|
async def save_checkpoint(self, backend_or_path: str | CheckpointBackend) -> None:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""reactifact.context_builder — Runtime-level policy for what goes into an
|
|
2
|
+
agent's inputs (ranking/truncation), separate from the agent itself.
|
|
3
|
+
|
|
4
|
+
An `Agent` only declares *which* artifact types/conditions it consumes
|
|
5
|
+
(`Consume`); it says nothing about *how many* or *which* matching artifacts
|
|
6
|
+
actually go into a produce call when there are more candidates than fit a
|
|
7
|
+
prompt. That decision is a `Runtime`-level concern — set it once via
|
|
8
|
+
`RuntimeResources(context_builder=...)` and every agent's
|
|
9
|
+
`collect_inputs()` (`Agent._collect_inputs`, `agents.py`) routes through it,
|
|
10
|
+
so a builder set on the `Context` a `Runtime` was constructed with applies
|
|
11
|
+
uniformly, without each `Agent` subclass reimplementing ranking/truncation.
|
|
12
|
+
|
|
13
|
+
Two implementations ship here: `DefaultContextBuilder` (no-op — today's
|
|
14
|
+
"every matching artifact, unranked" behavior) and `TokenBudgetContextBuilder`
|
|
15
|
+
(rank candidates, keep a prefix that fits a token budget).
|
|
16
|
+
|
|
17
|
+
Known limitation (not solved here): `build()` sees the *combined* candidate
|
|
18
|
+
list across every `Consume` an agent declares, not each `Consume` separately.
|
|
19
|
+
A high-volume type (e.g. many `Evidence`) can starve out a low-volume one
|
|
20
|
+
(e.g. the single triggering `Question`) under a tight budget if the
|
|
21
|
+
low-volume artifact ranks lower (e.g. it's older). If that bites, pass a
|
|
22
|
+
`rank_key` that accounts for it, or budget per `Consume` with several
|
|
23
|
+
smaller agents instead of one broad one.
|
|
24
|
+
|
|
25
|
+
One specific case of that limitation *is* solved: an agent often consumes
|
|
26
|
+
one real content type (`Evidence`) plus a marker type it only needs for
|
|
27
|
+
triggering (a completion/wake-up artifact with little or no meaningful
|
|
28
|
+
text) — see `TokenBudgetContextBuilder`'s `exempt_types`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
36
|
+
|
|
37
|
+
from .artifacts import Artifact
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from .agents import Agent
|
|
41
|
+
from .context import Context
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TokenCounter(Protocol):
|
|
45
|
+
"""Estimates how many tokens a piece of text costs.
|
|
46
|
+
|
|
47
|
+
Exactness is not the contract — see `HeuristicTokenCounter`. Plug in a
|
|
48
|
+
real tokenizer (tiktoken, a provider's count-tokens endpoint, ...) via
|
|
49
|
+
this protocol when you need precision for a specific provider.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def count(self, text: str) -> int: ...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class HeuristicTokenCounter:
|
|
56
|
+
"""Provider-agnostic token estimate: zero dependencies, deliberately
|
|
57
|
+
conservative (overestimates rather than underestimates).
|
|
58
|
+
|
|
59
|
+
~3.5 chars/token instead of the commonly quoted ~4: a budget built on
|
|
60
|
+
this errs toward trimming a bit more context, not toward overflowing a
|
|
61
|
+
provider's window. Good enough for a soft truncation threshold; not a
|
|
62
|
+
substitute for the real `usage` a provider returns after the call
|
|
63
|
+
(`LLMResponse.usage`, `providers/contracts.py`).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
chars_per_token: float = 3.5
|
|
67
|
+
|
|
68
|
+
def count(self, text: str) -> int:
|
|
69
|
+
if not text:
|
|
70
|
+
return 0
|
|
71
|
+
return max(1, int(len(text) / self.chars_per_token) + 1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _default_render(artifact: Artifact[Any]) -> str:
|
|
75
|
+
return str(artifact.data.model_dump_json())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ContextBuilder(ABC):
|
|
79
|
+
"""Ranks/truncates the artifacts a single agent run would otherwise see.
|
|
80
|
+
|
|
81
|
+
Called once per agent run with every candidate matched by that agent's
|
|
82
|
+
`consumes` (combined across all of them). Returning the list unchanged
|
|
83
|
+
reproduces today's behavior.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def build(
|
|
88
|
+
self,
|
|
89
|
+
context: Context,
|
|
90
|
+
agent: Agent,
|
|
91
|
+
candidates: list[Artifact[Any]],
|
|
92
|
+
) -> list[Artifact[Any]]: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DefaultContextBuilder(ContextBuilder):
|
|
96
|
+
"""No-op: every consumes-matched artifact, in collection order."""
|
|
97
|
+
|
|
98
|
+
def build(
|
|
99
|
+
self, context: Context, agent: Agent, candidates: list[Artifact[Any]]
|
|
100
|
+
) -> list[Artifact[Any]]:
|
|
101
|
+
return candidates
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TokenBudgetContextBuilder(ContextBuilder):
|
|
105
|
+
"""Ranks candidates (newest-first by default) and keeps a prefix that
|
|
106
|
+
fits `max_tokens`, estimated via `token_counter`.
|
|
107
|
+
|
|
108
|
+
At least one *costed* (non-exempt) artifact is always kept, even if it
|
|
109
|
+
alone exceeds the budget — an agent silently getting zero real content
|
|
110
|
+
is worse than one that gets an oversized item; `max_tokens` is a soft
|
|
111
|
+
cap, not a hard clip.
|
|
112
|
+
|
|
113
|
+
`exempt_types`: artifact types that cost nothing and never trigger that
|
|
114
|
+
clip — for a marker/trigger type an agent only consumes to wake up
|
|
115
|
+
(little or no meaningful text), not to reason over. Without this, a
|
|
116
|
+
freshly created marker can rank first (newest) and either eat the whole
|
|
117
|
+
budget itself or — worse — silently consume the "at least one" slot,
|
|
118
|
+
leaving zero real content if the budget is then too tight for the next
|
|
119
|
+
(real) candidate. Exempt artifacts are still ranked and still kept
|
|
120
|
+
(still wake the agent up); they just never count toward `max_tokens` or
|
|
121
|
+
toward what counts as "at least one" content item.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
max_tokens: int | None = None,
|
|
128
|
+
token_counter: TokenCounter | None = None,
|
|
129
|
+
rank_key: Callable[[Artifact[Any]], Any] | None = None,
|
|
130
|
+
render: Callable[[Artifact[Any]], str] | None = None,
|
|
131
|
+
exempt_types: tuple[type, ...] = (),
|
|
132
|
+
):
|
|
133
|
+
self.max_tokens = max_tokens
|
|
134
|
+
self.token_counter = token_counter or HeuristicTokenCounter()
|
|
135
|
+
self.rank_key = rank_key or (lambda a: a.updated_at)
|
|
136
|
+
self.render = render or _default_render
|
|
137
|
+
self.exempt_types = exempt_types
|
|
138
|
+
|
|
139
|
+
def build(
|
|
140
|
+
self, context: Context, agent: Agent, candidates: list[Artifact[Any]]
|
|
141
|
+
) -> list[Artifact[Any]]:
|
|
142
|
+
ranked = sorted(candidates, key=self.rank_key, reverse=True)
|
|
143
|
+
if self.max_tokens is None:
|
|
144
|
+
return ranked
|
|
145
|
+
kept: list[Artifact[Any]] = []
|
|
146
|
+
used = 0
|
|
147
|
+
kept_content = False
|
|
148
|
+
for artifact in ranked:
|
|
149
|
+
exempt = type(artifact.data) in self.exempt_types
|
|
150
|
+
cost = 0 if exempt else self.token_counter.count(self.render(artifact))
|
|
151
|
+
if kept_content and used + cost > self.max_tokens:
|
|
152
|
+
break
|
|
153
|
+
kept.append(artifact)
|
|
154
|
+
used += cost
|
|
155
|
+
if not exempt:
|
|
156
|
+
kept_content = True
|
|
157
|
+
return kept
|
|
@@ -137,6 +137,7 @@ class HITLLMAgent(Agent):
|
|
|
137
137
|
tools: Sequence[Tool] | dict[str, Tool] = ()
|
|
138
138
|
max_steps: int = 8
|
|
139
139
|
max_asks: int = 2
|
|
140
|
+
max_approvals: int = 3
|
|
140
141
|
resume_announce: Callable[[str], str] | None = None
|
|
141
142
|
temperature: float | None = None
|
|
142
143
|
max_tokens: int | None = None
|
|
@@ -162,6 +163,7 @@ class HITLLMAgent(Agent):
|
|
|
162
163
|
tools=self.tools,
|
|
163
164
|
max_steps=self.max_steps,
|
|
164
165
|
max_asks=self.max_asks,
|
|
166
|
+
max_approvals=self.max_approvals,
|
|
165
167
|
resume_announce=self.resume_announce,
|
|
166
168
|
temperature=self.temperature,
|
|
167
169
|
max_tokens=self.max_tokens,
|
|
@@ -13,6 +13,17 @@ keep reappearing across agent codebases and the bundled examples:
|
|
|
13
13
|
- `StatusMachine` — a `Produce` that deterministically advances an artifact's
|
|
14
14
|
`status` lifecycle driven by a pure `next_status(context, key)` (§67, §69) —
|
|
15
15
|
see `recipes.status`;
|
|
16
|
+
- `PlanExecute` — sequential plan → execute → finish: ordering, gating each
|
|
17
|
+
step on its predecessor's result, and completion detection owned by the
|
|
18
|
+
recipe; domain supplies `plan`/`execute_step`/`finish` (§24, §42, §69) —
|
|
19
|
+
see `recipes.plan_execute`;
|
|
20
|
+
- `Router` / `ApprovalGate` — classify a request into a route with a
|
|
21
|
+
deterministic fallback, and ask a human to sign off on a report before
|
|
22
|
+
finalizing it (§60, §67) — see `recipes.supervisor`;
|
|
23
|
+
- `ReflectionLoop` — generate → critique → regenerate: round-capping, the
|
|
24
|
+
accept threshold, and completion detection owned by the recipe; domain
|
|
25
|
+
supplies `draft`/`critique`/`rewrite`/`finish` (§24, §42, §69) — see
|
|
26
|
+
`recipes.reflection`;
|
|
16
27
|
- `WindowSummarizer` / `WindowPruner` / `llm_summarizer` — bounded
|
|
17
28
|
conversation memory: periodic summarization + pruning as two plain
|
|
18
29
|
`Produce`s, domain owns the summarizer callback and the summary artifact
|
|
@@ -34,15 +45,22 @@ from __future__ import annotations
|
|
|
34
45
|
|
|
35
46
|
from .inputs import find, find_all
|
|
36
47
|
from .memory import WindowPruner, WindowSummarizer, llm_summarizer
|
|
48
|
+
from .plan_execute import PlanExecute
|
|
49
|
+
from .reflection import ReflectionLoop
|
|
37
50
|
from .resolve import materialize_doc
|
|
38
51
|
from .rollback import changed_fields, downstream_fields, earliest_stage
|
|
39
52
|
from .search import fan_out_sources
|
|
40
53
|
from .skills import Skill, load_skills, match_skills
|
|
41
54
|
from .status import StatusMachine
|
|
55
|
+
from .supervisor import ApprovalGate, Router
|
|
42
56
|
from .text import EN_STOPWORDS, keyword_score, stem, stem_words
|
|
43
57
|
|
|
44
58
|
__all__ = [
|
|
59
|
+
"ApprovalGate",
|
|
45
60
|
"EN_STOPWORDS",
|
|
61
|
+
"PlanExecute",
|
|
62
|
+
"ReflectionLoop",
|
|
63
|
+
"Router",
|
|
46
64
|
"Skill",
|
|
47
65
|
"StatusMachine",
|
|
48
66
|
"WindowPruner",
|