atomicguard 2.26.1__tar.gz → 2.28.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.
- {atomicguard-2.26.1/src/atomicguard.egg-info → atomicguard-2.28.0}/PKG-INFO +2 -1
- {atomicguard-2.26.1 → atomicguard-2.28.0}/pyproject.toml +22 -1
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/__init__.py +1 -1
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/agent.py +2 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/builder.py +64 -3
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/workflow.py +42 -5
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/cli/run.py +1 -1
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/cli/runner.py +1 -1
- atomicguard-2.28.0/src/atomicguard/contrib/__init__.py +9 -0
- {atomicguard-2.26.1/src/atomicguard/infrastructure → atomicguard-2.28.0/src/atomicguard/contrib}/effectors/bash_executor.py +5 -0
- atomicguard-2.28.0/src/atomicguard/contrib/generators/__init__.py +0 -0
- atomicguard-2.28.0/src/atomicguard/contrib/generators/command_template.py +184 -0
- atomicguard-2.28.0/src/atomicguard/contrib/guards/__init__.py +0 -0
- atomicguard-2.28.0/src/atomicguard/contrib/guards/pattern_guard.py +40 -0
- atomicguard-2.28.0/src/atomicguard/contrib/guards/rendered_command_guard.py +78 -0
- atomicguard-2.28.0/src/atomicguard/contrib/guards/script_result_guard.py +158 -0
- {atomicguard-2.26.1/src/atomicguard/infrastructure → atomicguard-2.28.0/src/atomicguard/contrib}/registries.py +3 -3
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/__init__.py +53 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/cache.py +78 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/embedding_classifier.py +173 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/examples/__init__.py +0 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/examples/demo.py +135 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/examples/smoke_embedding.py +140 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/handlers/__init__.py +6 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/handlers/tmux_forward_handler.py +65 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/handlers/workflow_handler.py +106 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/protocols.py +104 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/registry_seed.py +107 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/results.py +46 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/switchboard.py +120 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/__init__.py +0 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/conftest.py +84 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_embedding_classifier.py +225 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_protocols.py +73 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_registry_seed.py +47 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_switchboard.py +232 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_tmux_forward_handler.py +100 -0
- atomicguard-2.28.0/src/atomicguard/contrib/switchboard/tests/test_workflow_handler.py +121 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/dynamic/test_runner.py +11 -5
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/guards/__init__.py +1 -5
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/precommit.py +4 -4
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/persistence/sqlite.py +15 -0
- atomicguard-2.28.0/src/atomicguard/tools/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0/src/atomicguard.egg-info}/PKG-INFO +2 -1
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard.egg-info/SOURCES.txt +33 -5
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard.egg-info/requires.txt +1 -0
- atomicguard-2.26.1/src/atomicguard/infrastructure/generators/command_template.py +0 -82
- {atomicguard-2.26.1 → atomicguard-2.28.0}/LICENSE +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/README.md +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/setup.cfg +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/__main__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/action_pair.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/agent_rl.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/feedback_summarizer.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/policy_export.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/application/runner.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/cli/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/cli/_queue.py +0 -0
- {atomicguard-2.26.1/src/atomicguard/tools → atomicguard-2.28.0/src/atomicguard/contrib/effectors}/__init__.py +0 -0
- {atomicguard-2.26.1/src/atomicguard/infrastructure → atomicguard-2.28.0/src/atomicguard/contrib}/guards/all_passed_guard.py +0 -0
- {atomicguard-2.26.1/src/atomicguard/infrastructure → atomicguard-2.28.0/src/atomicguard/contrib}/guards/exit_code_guard.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/exceptions.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/extraction.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/feedback_summarizer.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/gym.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/interfaces.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/models.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/multiagent.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/policy_export.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/prompts.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/actions.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/models.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/reward.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/state.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/rl/transitions.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/runner.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/domain/workflow.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/composite/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/composite/base.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/dynamic/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/interactive/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/interactive/human.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/static/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/static/imports.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/guards/static/syntax.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/gym/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/gym/__main__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/effectors/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/effectors/multi_dispatch_effector.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/effectors/tmux_broadcast_effector.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/effectors/tmux_session_effector.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/effectors/workflow_effector.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/generators/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/generators/subprocess_generator.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/guards/container_subprocess_guard.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/guards/diff_empty_guard.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/guards/human_review_adapter.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/diff_parser.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/precommit_data/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/precommit_data/dataset.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/precommit_data/schemas.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/gym/precommit_generators.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/human_guard_db.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/human_guard_stdin.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/llm/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/llm/factory.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/llm/huggingface.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/llm/mock.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/llm/ollama.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/persistence/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/persistence/filesystem.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/persistence/memory.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/policy_registry.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/queue_inprocess.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/queue_sqlite.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/registry.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/checkpoint.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/environment.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/policy.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/replay_buffer.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/rl/rewards.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/infrastructure/status_callback.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/run_policy.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/schemas/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/tools/__main__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/tools/export_training.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/tools/export_workflow.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/training/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/training/curriculum.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/training/trainer.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/visualization/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/visualization/html_exporter.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/visualization/workflow_config_exporter.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/__main__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/config_loader.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/dag_reader.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/dependencies.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/discovery.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/experiment_locator.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/policy_loader.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/rl_loader.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/__init__.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/ap_catalogue_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/gym_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/library_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/ollama_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/policy_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/rl_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/run_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/spark_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/training_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/workflow_pages.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/routes/ws_routes.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/server.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/training_launcher.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/utils.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard/web/ws.py +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard.egg-info/dependency_links.txt +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard.egg-info/entry_points.txt +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/src/atomicguard.egg-info/top_level.txt +0 -0
- {atomicguard-2.26.1 → atomicguard-2.28.0}/tests/test_run_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atomicguard
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.28.0
|
|
4
4
|
Summary: A Dual-State Agent Framework for reliable LLM code generation with guard-validated loops
|
|
5
5
|
Author-email: Matthew Thompson <thompsonson@gmail.com>
|
|
6
6
|
Maintainer-email: Matthew Thompson <thompsonson@gmail.com>
|
|
@@ -31,6 +31,7 @@ Requires-Dist: openhands-ai>=0.27.0
|
|
|
31
31
|
Requires-Dist: pydantic-ai>=1.0.0
|
|
32
32
|
Requires-Dist: pyflakes>=3.0
|
|
33
33
|
Requires-Dist: click>=8.1.0
|
|
34
|
+
Requires-Dist: pyyaml>=6.0
|
|
34
35
|
Provides-Extra: web
|
|
35
36
|
Requires-Dist: fastapi>=0.115.0; extra == "web"
|
|
36
37
|
Requires-Dist: uvicorn[standard]>=0.34.0; extra == "web"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "atomicguard"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.28.0"
|
|
4
4
|
description = "A Dual-State Agent Framework for reliable LLM code generation with guard-validated loops"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -40,6 +40,7 @@ dependencies = [
|
|
|
40
40
|
"pydantic-ai>=1.0.0",
|
|
41
41
|
"pyflakes>=3.0",
|
|
42
42
|
"click>=8.1.0",
|
|
43
|
+
"pyyaml>=6.0",
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
[project.urls]
|
|
@@ -180,6 +181,9 @@ ignore = [
|
|
|
180
181
|
]
|
|
181
182
|
|
|
182
183
|
[tool.ruff.lint.per-file-ignores]
|
|
184
|
+
# Vendored Raschka source — keep the diff minimal; don't lint upstream's
|
|
185
|
+
# unused params or other style choices we don't own.
|
|
186
|
+
"agents/double_oh_seven/agent.py" = ["ARG001", "ARG002", "ARG003"]
|
|
183
187
|
# Concrete reward functions must match RewardFunctionInterface.compute()
|
|
184
188
|
# signature — unused params are required by the interface contract.
|
|
185
189
|
"src/atomicguard/infrastructure/rl/rewards.py" = ["ARG002"]
|
|
@@ -227,6 +231,14 @@ mypy_path = "src"
|
|
|
227
231
|
module = "tests.*"
|
|
228
232
|
disallow_untyped_defs = false
|
|
229
233
|
|
|
234
|
+
[[tool.mypy.overrides]]
|
|
235
|
+
module = "atomicguard.contrib.switchboard.tests.*"
|
|
236
|
+
ignore_errors = true
|
|
237
|
+
|
|
238
|
+
[[tool.mypy.overrides]]
|
|
239
|
+
module = "atomicguard.contrib.switchboard.examples.*"
|
|
240
|
+
ignore_errors = true
|
|
241
|
+
|
|
230
242
|
[[tool.mypy.overrides]]
|
|
231
243
|
module = ["atomicguard.web.*", "atomicguard.web.routes.*"]
|
|
232
244
|
warn_return_any = false
|
|
@@ -236,6 +248,15 @@ disallow_untyped_defs = false
|
|
|
236
248
|
module = ["examples.*"]
|
|
237
249
|
ignore_errors = true
|
|
238
250
|
|
|
251
|
+
[[tool.mypy.overrides]]
|
|
252
|
+
# Vendored Raschka source — don't typecheck upstream code we don't own.
|
|
253
|
+
module = "agents.double_oh_seven.agent"
|
|
254
|
+
ignore_errors = true
|
|
255
|
+
|
|
256
|
+
[[tool.mypy.overrides]]
|
|
257
|
+
module = "agents.double_oh_seven.tests.*"
|
|
258
|
+
ignore_errors = true
|
|
259
|
+
|
|
239
260
|
[dependency-groups]
|
|
240
261
|
dev = [
|
|
241
262
|
"mypy>=1.19.1",
|
|
@@ -335,6 +335,7 @@ class DualStateAgent:
|
|
|
335
335
|
escalation_feedback=escalation_feedback,
|
|
336
336
|
dependency_types=dep_types,
|
|
337
337
|
dependency_artifact_contexts=dep_contexts,
|
|
338
|
+
workflow_id=self._workflow_id,
|
|
338
339
|
)
|
|
339
340
|
|
|
340
341
|
def _refine_context(
|
|
@@ -371,4 +372,5 @@ class DualStateAgent:
|
|
|
371
372
|
escalation_feedback=escalation_feedback,
|
|
372
373
|
dependency_types=dep_types,
|
|
373
374
|
dependency_artifact_contexts=dep_contexts,
|
|
375
|
+
workflow_id=self._workflow_id,
|
|
374
376
|
)
|
|
@@ -8,15 +8,26 @@ adapter-agnostic factory that takes explicit registries.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
+
import re
|
|
11
12
|
from typing import TYPE_CHECKING
|
|
12
13
|
|
|
13
14
|
from atomicguard.application.action_pair import ActionPair
|
|
14
|
-
from atomicguard.application.workflow import
|
|
15
|
+
from atomicguard.application.workflow import (
|
|
16
|
+
PreStepCallback,
|
|
17
|
+
StepCallback,
|
|
18
|
+
WorkflowOrchestrator,
|
|
19
|
+
)
|
|
15
20
|
from atomicguard.domain.prompts import PromptTemplate
|
|
16
21
|
from atomicguard.guards.composite import CompositeGuard
|
|
17
22
|
|
|
23
|
+
# Matches `{dep.<ap_id>.<field>}` placeholders in task / feedback / context
|
|
24
|
+
# strings. Used at orchestrator-build time to validate that every referenced
|
|
25
|
+
# producer AP is declared in `requires` — catches cross-AP coupling bugs
|
|
26
|
+
# before runtime instead of as a misleading KeyError mid-execution.
|
|
27
|
+
_DEP_PLACEHOLDER_RE = re.compile(r"\{dep\.(\w+)\.\w+\}")
|
|
28
|
+
|
|
18
29
|
if TYPE_CHECKING:
|
|
19
|
-
from atomicguard.application.workflow import StepCallback
|
|
30
|
+
from atomicguard.application.workflow import PreStepCallback, StepCallback
|
|
20
31
|
from atomicguard.domain.interfaces import (
|
|
21
32
|
ArtifactDAGInterface,
|
|
22
33
|
EffectorInterface,
|
|
@@ -40,6 +51,7 @@ def build_orchestrator(
|
|
|
40
51
|
artifact_dag: ArtifactDAGInterface,
|
|
41
52
|
step_callback: StepCallback | None = None,
|
|
42
53
|
effector_registry: dict[str, EffectorInterface] | None = None,
|
|
54
|
+
pre_step_callback: PreStepCallback | None = None,
|
|
43
55
|
) -> WorkflowOrchestrator:
|
|
44
56
|
"""Build a configured WorkflowOrchestrator from a definition and registries.
|
|
45
57
|
|
|
@@ -49,8 +61,12 @@ def build_orchestrator(
|
|
|
49
61
|
generator_registry: Map of generator name -> concrete instance.
|
|
50
62
|
guard_registry: Map of guard name -> concrete instance.
|
|
51
63
|
artifact_dag: Artifact storage implementation.
|
|
52
|
-
step_callback: Optional callback passed to the orchestrator
|
|
64
|
+
step_callback: Optional callback passed to the orchestrator (fires
|
|
65
|
+
after each step's verdict).
|
|
53
66
|
effector_registry: Map of effector name -> concrete instance (optional).
|
|
67
|
+
pre_step_callback: Optional callback passed to the orchestrator
|
|
68
|
+
(fires just before each step's ``agent.execute()``). Used to
|
|
69
|
+
display per-AP progress on long-running workflows.
|
|
54
70
|
|
|
55
71
|
Returns:
|
|
56
72
|
A fully configured ``WorkflowOrchestrator`` ready to ``execute()``.
|
|
@@ -61,11 +77,14 @@ def build_orchestrator(
|
|
|
61
77
|
"""
|
|
62
78
|
effector_registry = effector_registry or {}
|
|
63
79
|
|
|
80
|
+
_validate_dep_references(definition, ap_context)
|
|
81
|
+
|
|
64
82
|
orchestrator = WorkflowOrchestrator(
|
|
65
83
|
artifact_dag,
|
|
66
84
|
rmax=definition.rmax,
|
|
67
85
|
constraints=definition.constraints,
|
|
68
86
|
step_callback=step_callback,
|
|
87
|
+
pre_step_callback=pre_step_callback,
|
|
69
88
|
)
|
|
70
89
|
|
|
71
90
|
for ap_def in definition.action_pairs:
|
|
@@ -107,6 +126,48 @@ def build_orchestrator(
|
|
|
107
126
|
return orchestrator
|
|
108
127
|
|
|
109
128
|
|
|
129
|
+
def _validate_dep_references(
|
|
130
|
+
definition: WorkflowDefinition,
|
|
131
|
+
ap_context: dict[str, APContextEntry],
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Reject workflows where a `{dep.X.field}` reference points outside `requires`.
|
|
134
|
+
|
|
135
|
+
Scans every AP's `task` template for `{dep.<ap_id>.<field>}` placeholders
|
|
136
|
+
and asserts that the referenced AP id is declared in the consuming AP's
|
|
137
|
+
`requires` list. Fails fast at orchestrator-build time with a precise
|
|
138
|
+
error rather than producing a misleading runtime KeyError when
|
|
139
|
+
`CommandTemplateGenerator` looks up the missing dependency.
|
|
140
|
+
|
|
141
|
+
Type-based references (`{type.<artifact_type>.<field>}`) are independent
|
|
142
|
+
of `requires` by design and are not checked here.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: when any AP's task references a `{dep.X.*}` for an X
|
|
146
|
+
not in that AP's `requires`.
|
|
147
|
+
"""
|
|
148
|
+
errors: list[str] = []
|
|
149
|
+
for ap_def in definition.action_pairs:
|
|
150
|
+
ctx = ap_context.get(ap_def.ap_id)
|
|
151
|
+
if ctx is None or not ctx.task:
|
|
152
|
+
continue
|
|
153
|
+
referenced = set(_DEP_PLACEHOLDER_RE.findall(ctx.task))
|
|
154
|
+
if not referenced:
|
|
155
|
+
continue
|
|
156
|
+
declared = set(ap_def.requires or [])
|
|
157
|
+
missing = referenced - declared
|
|
158
|
+
if missing:
|
|
159
|
+
for ref in sorted(missing):
|
|
160
|
+
errors.append(
|
|
161
|
+
f"AP '{ap_def.ap_id}' task references {{dep.{ref}.<field>}} "
|
|
162
|
+
f"but does not declare '{ref}' in `requires` "
|
|
163
|
+
f"(declared: {sorted(declared)})."
|
|
164
|
+
)
|
|
165
|
+
if errors:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"Workflow has unresolved dep references:\n " + "\n ".join(errors)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
110
171
|
def _resolve_generator(
|
|
111
172
|
ap_def: ActionPairDefinition,
|
|
112
173
|
registry: dict[str, GeneratorInterface],
|
|
@@ -67,6 +67,14 @@ logger = logging.getLogger(__name__)
|
|
|
67
67
|
# Callback type: (ap_id, artifact, guard_result) -> None
|
|
68
68
|
StepCallback = Callable[[str, "Artifact", "GuardResult"], None]
|
|
69
69
|
|
|
70
|
+
# Pre-step callback: (ap_id,) -> None. Invoked once just before each
|
|
71
|
+
# AP's agent.execute() runs (after deps are resolved, before the
|
|
72
|
+
# generator/effector/guard chain starts). Fires once per workflow-level
|
|
73
|
+
# entry into the step — inner retries inside the DualStateAgent loop are
|
|
74
|
+
# not surfaced here, but an Extension-09 backtrack re-entry to the same
|
|
75
|
+
# AP will re-fire the callback (which is the right signal).
|
|
76
|
+
PreStepCallback = Callable[[str], None]
|
|
77
|
+
|
|
70
78
|
|
|
71
79
|
class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
72
80
|
"""
|
|
@@ -83,6 +91,7 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
83
91
|
constraints: str = "",
|
|
84
92
|
agent_factory: AgentFactory | None = None,
|
|
85
93
|
step_callback: StepCallback | None = None,
|
|
94
|
+
pre_step_callback: PreStepCallback | None = None,
|
|
86
95
|
):
|
|
87
96
|
"""
|
|
88
97
|
Args:
|
|
@@ -94,12 +103,18 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
94
103
|
step_callback: Optional callback invoked after each step completes
|
|
95
104
|
(both pass and fail). Signature:
|
|
96
105
|
``(ap_id: str, artifact: Artifact, guard_result: GuardResult) -> None``
|
|
106
|
+
pre_step_callback: Optional callback invoked just before each
|
|
107
|
+
step's ``agent.execute()`` runs. Signature: ``(ap_id: str) -> None``.
|
|
108
|
+
Useful for displaying per-AP progress in long-running workflows
|
|
109
|
+
where the user otherwise sees no output between the previous
|
|
110
|
+
step's verdict and this step's.
|
|
97
111
|
"""
|
|
98
112
|
self._dag = artifact_dag
|
|
99
113
|
self._rmax = rmax
|
|
100
114
|
self._constraints = constraints
|
|
101
115
|
self._agent_factory = agent_factory
|
|
102
116
|
self._step_callback = step_callback
|
|
117
|
+
self._pre_step_callback = pre_step_callback
|
|
103
118
|
self._steps: list[WorkflowStep] = []
|
|
104
119
|
self._steps_by_id: dict[str, WorkflowStep] = {}
|
|
105
120
|
self._workflow_state = WorkflowState()
|
|
@@ -293,6 +308,11 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
293
308
|
step_escalation_feedback = tuple(
|
|
294
309
|
self._escalation_feedback.get(step.guard_id, [])
|
|
295
310
|
)
|
|
311
|
+
# Invoke pre-step callback before the AP actually runs, so
|
|
312
|
+
# consumers can show "step X starting" output before the
|
|
313
|
+
# (potentially slow) generator/effector/guard chain executes.
|
|
314
|
+
if self._pre_step_callback is not None:
|
|
315
|
+
self._pre_step_callback(step.guard_id)
|
|
296
316
|
artifact = agent.execute(
|
|
297
317
|
specification,
|
|
298
318
|
dependencies,
|
|
@@ -304,14 +324,14 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
304
324
|
)
|
|
305
325
|
|
|
306
326
|
# Invoke step callback on success
|
|
307
|
-
|
|
308
|
-
self._step_callback is not None
|
|
309
|
-
and artifact.guard_result is not None
|
|
310
|
-
):
|
|
311
|
-
self._step_callback(step.guard_id, artifact, artifact.guard_result)
|
|
327
|
+
self._fire_step_callback(step.guard_id, artifact)
|
|
312
328
|
|
|
313
329
|
except StagnationDetected as e:
|
|
314
330
|
# Level 2: WorkflowOrchestrator backtracking
|
|
331
|
+
# Fire post-step callback to close the entry (paired with the
|
|
332
|
+
# pre_step_callback fired above). On the continue sub-path the
|
|
333
|
+
# next workflow entry will fire a fresh pre+post pair.
|
|
334
|
+
self._fire_step_callback(step.guard_id, e.artifact)
|
|
315
335
|
if self._escalation_count[step.guard_id] < step.e_max:
|
|
316
336
|
logger.info(
|
|
317
337
|
"[%s] Escalation %d/%d triggered. Targets: %s",
|
|
@@ -355,6 +375,7 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
355
375
|
|
|
356
376
|
except EscalationRequired as e:
|
|
357
377
|
# Level 4: Human intervention (fatal guard)
|
|
378
|
+
self._fire_step_callback(step.guard_id, e.artifact)
|
|
358
379
|
return WorkflowResult(
|
|
359
380
|
status=WorkflowStatus.ESCALATION,
|
|
360
381
|
artifacts=self._artifacts,
|
|
@@ -364,6 +385,11 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
364
385
|
)
|
|
365
386
|
|
|
366
387
|
except RmaxExhausted as e:
|
|
388
|
+
# Last attempt's artifact is the head of the most recent
|
|
389
|
+
# provenance entry; provenance is non-empty when this is raised.
|
|
390
|
+
last_artifact = e.provenance[-1][0] if e.provenance else None
|
|
391
|
+
if last_artifact is not None:
|
|
392
|
+
self._fire_step_callback(step.guard_id, last_artifact)
|
|
367
393
|
return WorkflowResult(
|
|
368
394
|
status=WorkflowStatus.FAILED,
|
|
369
395
|
artifacts=self._artifacts,
|
|
@@ -476,6 +502,17 @@ class WorkflowOrchestrator(ActionPairPoolInterface, WorkflowExecutorInterface):
|
|
|
476
502
|
|
|
477
503
|
return dependents
|
|
478
504
|
|
|
505
|
+
def _fire_step_callback(self, ap_id: str, artifact: "Artifact") -> None:
|
|
506
|
+
"""Invoke step_callback for an AP entry if both callback and guard_result exist.
|
|
507
|
+
|
|
508
|
+
Centralises the None-guard so success and failure paths share the same
|
|
509
|
+
contract: exactly one call per workflow-level entry, whenever a
|
|
510
|
+
guard_result is available to report.
|
|
511
|
+
"""
|
|
512
|
+
if self._step_callback is None or artifact.guard_result is None:
|
|
513
|
+
return
|
|
514
|
+
self._step_callback(ap_id, artifact, artifact.guard_result)
|
|
515
|
+
|
|
479
516
|
def _inject_failure_context(self, target_id: str, summary: str) -> None:
|
|
480
517
|
"""Add failure summary to constraints for target re-execution (Definition 48).
|
|
481
518
|
|
|
@@ -247,7 +247,7 @@ def _execute_in_process(
|
|
|
247
247
|
|
|
248
248
|
# Build registries
|
|
249
249
|
click.echo(f"\nModel: {model} ({provider})")
|
|
250
|
-
from atomicguard.
|
|
250
|
+
from atomicguard.contrib.registries import (
|
|
251
251
|
build_generator_registry,
|
|
252
252
|
build_guard_registry,
|
|
253
253
|
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
contrib — reusable building blocks for AtomicGuard agents.
|
|
3
|
+
|
|
4
|
+
Generic guards, effectors, and generators that are useful across agents
|
|
5
|
+
but do not belong in the core framework. Agents import from here;
|
|
6
|
+
core (domain, application, infrastructure) never imports from here.
|
|
7
|
+
|
|
8
|
+
One-way dependency: contrib → core, never core → contrib.
|
|
9
|
+
"""
|
|
@@ -101,6 +101,11 @@ class BashCommandExecutor(EffectorInterface):
|
|
|
101
101
|
"command": command,
|
|
102
102
|
}
|
|
103
103
|
),
|
|
104
|
+
# Preserve artifact_type from the plan so type-based dependency
|
|
105
|
+
# resolution ({type.<artifact_type>.<field>}) can find this artifact
|
|
106
|
+
# in R. Without this, the effector's output artifact loses the type
|
|
107
|
+
# tag the action_pair attached to the plan.
|
|
108
|
+
artifact_type=plan.artifact_type,
|
|
104
109
|
)
|
|
105
110
|
|
|
106
111
|
def undo(self, plan: Artifact) -> bool: # noqa: ARG002
|
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CommandTemplateGenerator: Deterministic generator that resolves {spec.*},
|
|
3
|
+
{dep.<ap_id>.<field>}, and {type.<artifact_type>.<field>} placeholders.
|
|
4
|
+
|
|
5
|
+
For command-based APs, the task field in the prompt template IS the command
|
|
6
|
+
template. The generator resolves placeholders from the specification and from
|
|
7
|
+
upstream artifact content (parsed as JSON).
|
|
8
|
+
|
|
9
|
+
Three placeholder forms:
|
|
10
|
+
{spec.<key>} — workflow specification key/value pair
|
|
11
|
+
{dep.<ap_id>.<field>} — direct dep by AP id (legacy)
|
|
12
|
+
{type.<artifact_type>.<field>} — by artifact_type, scanning R for the
|
|
13
|
+
most recent ACCEPTED artifact whose
|
|
14
|
+
producer declared this artifact_type.
|
|
15
|
+
Independent of `requires` declarations.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import uuid
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
from atomicguard.domain.interfaces import GeneratorInterface
|
|
24
|
+
from atomicguard.domain.models import (
|
|
25
|
+
ActionPairId,
|
|
26
|
+
Artifact,
|
|
27
|
+
ArtifactId,
|
|
28
|
+
ArtifactStatus,
|
|
29
|
+
Context,
|
|
30
|
+
ContextSnapshot,
|
|
31
|
+
WorkflowId,
|
|
32
|
+
)
|
|
33
|
+
from atomicguard.domain.prompts import PromptTemplate
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CommandTemplateGenerator(GeneratorInterface):
|
|
37
|
+
"""Resolves {spec.*}, {dep.<ap_id>.<field>}, and {type.<artifact_type>.<field>}
|
|
38
|
+
placeholders in the task template.
|
|
39
|
+
|
|
40
|
+
{spec.<key>} — resolved from the specification string (key=value pairs).
|
|
41
|
+
{dep.<ap_id>.<field>} — resolved by parsing the named dependency artifact's
|
|
42
|
+
content as JSON and extracting <field>. Requires <ap_id> to be in the
|
|
43
|
+
AP's `requires`. If the JSON value is a list, the first element is used.
|
|
44
|
+
{type.<artifact_type>.<field>} — resolved by scanning R for the most recent
|
|
45
|
+
ACCEPTED artifact with matching artifact_type. Does NOT require the
|
|
46
|
+
producing AP to be in `requires`; type-based hydration sidesteps the
|
|
47
|
+
ordering-vs-visibility conflation. The producer AP must declare the
|
|
48
|
+
artifact_type via `output_artifact_type` (or equivalently the AP's
|
|
49
|
+
artifact_type field on its definition).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def generate(
|
|
53
|
+
self,
|
|
54
|
+
context: Context,
|
|
55
|
+
template: PromptTemplate,
|
|
56
|
+
action_pair_id: str = "unknown",
|
|
57
|
+
workflow_id: str = "unknown",
|
|
58
|
+
workflow_ref: str | None = None,
|
|
59
|
+
) -> Artifact:
|
|
60
|
+
command = template.task
|
|
61
|
+
|
|
62
|
+
# Resolve {spec.*} placeholders from the specification
|
|
63
|
+
placeholders = re.findall(r"\{spec\.(\w+)\}", command)
|
|
64
|
+
if placeholders:
|
|
65
|
+
spec_values = self._parse_specification(context.specification)
|
|
66
|
+
for key in placeholders:
|
|
67
|
+
if key not in spec_values:
|
|
68
|
+
msg = f"Missing specification key: spec.{key}"
|
|
69
|
+
raise KeyError(msg)
|
|
70
|
+
command = command.replace(f"{{spec.{key}}}", spec_values[key])
|
|
71
|
+
|
|
72
|
+
# Resolve {dep.<ap_id>.<field>} placeholders from direct dependency artifacts
|
|
73
|
+
dep_placeholders = re.findall(r"\{dep\.(\w+)\.(\w+)\}", command)
|
|
74
|
+
if dep_placeholders and context.ambient and context.ambient.repository:
|
|
75
|
+
dep_map = dict(context.dependency_artifacts)
|
|
76
|
+
for ap_id, field in dep_placeholders:
|
|
77
|
+
artifact_id = dep_map.get(ap_id)
|
|
78
|
+
if artifact_id is None:
|
|
79
|
+
msg = f"Missing dependency artifact: dep.{ap_id}"
|
|
80
|
+
raise KeyError(msg)
|
|
81
|
+
artifact = context.ambient.repository.get_artifact(artifact_id)
|
|
82
|
+
value = self._extract_json_field(artifact.content, field, ap_id)
|
|
83
|
+
command = command.replace(f"{{dep.{ap_id}.{field}}}", value)
|
|
84
|
+
|
|
85
|
+
# Resolve {type.<artifact_type>.<field>} placeholders by scanning R
|
|
86
|
+
type_placeholders = re.findall(r"\{type\.(\w+)\.(\w+)\}", command)
|
|
87
|
+
if type_placeholders and context.ambient and context.ambient.repository:
|
|
88
|
+
for artifact_type, field in type_placeholders:
|
|
89
|
+
producer = self._find_artifact_by_type(context, artifact_type)
|
|
90
|
+
if producer is None:
|
|
91
|
+
msg = (
|
|
92
|
+
f"No ACCEPTED artifact with artifact_type={artifact_type!r} "
|
|
93
|
+
f"found in R. Producer AP may not have run yet, or its "
|
|
94
|
+
f"definition does not declare artifact_type={artifact_type!r}."
|
|
95
|
+
)
|
|
96
|
+
raise KeyError(msg)
|
|
97
|
+
value = self._extract_json_field(
|
|
98
|
+
producer.content, field, f"type.{artifact_type}"
|
|
99
|
+
)
|
|
100
|
+
command = command.replace(f"{{type.{artifact_type}.{field}}}", value)
|
|
101
|
+
|
|
102
|
+
return Artifact(
|
|
103
|
+
artifact_id=ArtifactId(str(uuid.uuid4())),
|
|
104
|
+
workflow_id=WorkflowId(workflow_id),
|
|
105
|
+
content=command,
|
|
106
|
+
previous_attempt_id=None,
|
|
107
|
+
parent_action_pair_id=None,
|
|
108
|
+
action_pair_id=ActionPairId(action_pair_id),
|
|
109
|
+
created_at=datetime.now().isoformat(),
|
|
110
|
+
attempt_number=1,
|
|
111
|
+
status=ArtifactStatus.PENDING,
|
|
112
|
+
guard_result=None,
|
|
113
|
+
context=ContextSnapshot(
|
|
114
|
+
workflow_id=workflow_id,
|
|
115
|
+
specification=context.specification,
|
|
116
|
+
constraints=context.ambient.constraints if context.ambient else "",
|
|
117
|
+
feedback_history=(),
|
|
118
|
+
),
|
|
119
|
+
workflow_ref=workflow_ref,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _parse_specification(spec: str) -> dict[str, str]:
|
|
124
|
+
"""Parse specification as key=value pairs (one per line)."""
|
|
125
|
+
result = {}
|
|
126
|
+
for line in spec.strip().splitlines():
|
|
127
|
+
line = line.strip()
|
|
128
|
+
if "=" in line:
|
|
129
|
+
key, _, value = line.partition("=")
|
|
130
|
+
result[key.strip()] = value.strip()
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _find_artifact_by_type(context: Context, artifact_type: str) -> Artifact | None:
|
|
135
|
+
"""Find the most recent ACCEPTED artifact in R with a matching artifact_type.
|
|
136
|
+
|
|
137
|
+
Independent of `requires` — scans the full repository for the current
|
|
138
|
+
workflow_id. If multiple matches exist, returns the most recent by
|
|
139
|
+
`created_at`. Returns None if no match is found.
|
|
140
|
+
"""
|
|
141
|
+
if context.ambient is None or context.ambient.repository is None:
|
|
142
|
+
return None
|
|
143
|
+
if context.workflow_id is None:
|
|
144
|
+
return None
|
|
145
|
+
artifacts = context.ambient.repository.get_by_workflow(context.workflow_id)
|
|
146
|
+
matching = [
|
|
147
|
+
a
|
|
148
|
+
for a in artifacts
|
|
149
|
+
if a.artifact_type == artifact_type and a.status == ArtifactStatus.ACCEPTED
|
|
150
|
+
]
|
|
151
|
+
if not matching:
|
|
152
|
+
return None
|
|
153
|
+
return max(matching, key=lambda a: a.created_at)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _extract_json_field(content: str, field: str, ap_id: str) -> str:
|
|
157
|
+
"""Parse content as JSON and extract field. Lists yield their first element."""
|
|
158
|
+
try:
|
|
159
|
+
parsed = json.loads(content)
|
|
160
|
+
except json.JSONDecodeError as e:
|
|
161
|
+
msg = f"dep.{ap_id} artifact content is not valid JSON: {e}"
|
|
162
|
+
raise ValueError(msg) from e
|
|
163
|
+
|
|
164
|
+
if isinstance(parsed, list):
|
|
165
|
+
if not parsed:
|
|
166
|
+
msg = f"dep.{ap_id} artifact is an empty JSON array"
|
|
167
|
+
raise ValueError(msg)
|
|
168
|
+
parsed = parsed[0]
|
|
169
|
+
|
|
170
|
+
if not isinstance(parsed, dict):
|
|
171
|
+
msg = f"dep.{ap_id} artifact JSON is not an object or array of objects"
|
|
172
|
+
raise ValueError(msg)
|
|
173
|
+
|
|
174
|
+
if field not in parsed:
|
|
175
|
+
msg = f"dep.{ap_id} artifact has no field '{field}'"
|
|
176
|
+
raise KeyError(msg)
|
|
177
|
+
|
|
178
|
+
value = parsed[field]
|
|
179
|
+
# Nested arrays/objects are re-serialised as JSON so consumers (e.g.
|
|
180
|
+
# shell scripts piping into jq) get a valid JSON string rather than
|
|
181
|
+
# Python's repr. Primitives are str-coerced as before.
|
|
182
|
+
if isinstance(value, (list, dict)):
|
|
183
|
+
return json.dumps(value)
|
|
184
|
+
return str(value)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""PatternGuard — checks for required/forbidden substrings in artifact content."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from atomicguard.domain.interfaces import GuardInterface
|
|
6
|
+
from atomicguard.domain.models import Artifact, GuardResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PatternGuard(GuardInterface):
|
|
10
|
+
"""Passes if all required patterns are found and no forbidden patterns are found."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
required: list[str] | None = None,
|
|
16
|
+
forbidden: list[str] | None = None,
|
|
17
|
+
):
|
|
18
|
+
self._required = required or []
|
|
19
|
+
self._forbidden = forbidden or []
|
|
20
|
+
|
|
21
|
+
def validate(self, artifact: Artifact, **deps: Artifact) -> GuardResult: # noqa: ARG002
|
|
22
|
+
content = artifact.content
|
|
23
|
+
|
|
24
|
+
for pattern in self._forbidden:
|
|
25
|
+
if pattern in content:
|
|
26
|
+
return GuardResult(
|
|
27
|
+
passed=False,
|
|
28
|
+
feedback=f"Forbidden pattern found: {pattern!r}",
|
|
29
|
+
guard_name="PatternGuard",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for pattern in self._required:
|
|
33
|
+
if pattern not in content:
|
|
34
|
+
return GuardResult(
|
|
35
|
+
passed=False,
|
|
36
|
+
feedback=f"Required pattern not found: {pattern!r}",
|
|
37
|
+
guard_name="PatternGuard",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return GuardResult(passed=True, guard_name="PatternGuard")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""RenderedCommandGuard — pre-guard for command-based APs.
|
|
2
|
+
|
|
3
|
+
Validates that the generator's rendered command is well-formed BEFORE the
|
|
4
|
+
effector runs. Catches:
|
|
5
|
+
|
|
6
|
+
- Empty rendered command (template was empty, or all placeholders blanked)
|
|
7
|
+
- Unresolved template placeholders: `{spec.X}`, `{dep.X.Y}`, `{type.X.Y}`
|
|
8
|
+
that survived the generator. These indicate a generator bug or a missing
|
|
9
|
+
spec/dependency value the generator should have raised on.
|
|
10
|
+
|
|
11
|
+
Layers as ``G_pre`` in the formal AP tuple ⟨ρ, a_gen, G_pre, E, G_post⟩.
|
|
12
|
+
The corresponding ``G_post`` (e.g. ScriptResultGuard + a domain guard)
|
|
13
|
+
validates the effector's output.
|
|
14
|
+
|
|
15
|
+
Why this matters: failures caught at G_pre cost zero subprocess execution.
|
|
16
|
+
A malformed command running through ``BashCommandExecutor`` still uses a
|
|
17
|
+
slot of the AP's r_max budget and produces a confusing failure mode (the
|
|
18
|
+
shell sees `{spec.foo}` literally and probably exits non-zero with an
|
|
19
|
+
unhelpful message). Catching it before the effector gives a precise
|
|
20
|
+
diagnosis.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from atomicguard.domain.interfaces import GuardInterface
|
|
29
|
+
from atomicguard.domain.models import Artifact, GuardResult
|
|
30
|
+
|
|
31
|
+
# Match any of the three placeholder forms used by CommandTemplateGenerator.
|
|
32
|
+
_UNRESOLVED = re.compile(r"\{(?:spec|dep|type)\.[\w.]+\}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RenderedCommandGuard(GuardInterface):
|
|
36
|
+
"""Pre-guard for command-template-based APs.
|
|
37
|
+
|
|
38
|
+
Pass when ``artifact.content`` is a non-empty string with no
|
|
39
|
+
``{spec.*}``, ``{dep.*.*}``, or ``{type.*.*}`` placeholders remaining.
|
|
40
|
+
Fail with a precise diagnostic otherwise.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, **_kwargs: Any) -> None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def validate(self, artifact: Artifact, **dependencies: Artifact) -> GuardResult: # noqa: ARG002
|
|
47
|
+
command = artifact.content or ""
|
|
48
|
+
stripped = command.strip()
|
|
49
|
+
if not stripped:
|
|
50
|
+
return GuardResult(
|
|
51
|
+
passed=False,
|
|
52
|
+
feedback=(
|
|
53
|
+
"Rendered command is empty. The generator produced an empty "
|
|
54
|
+
"task template — check ap_context.json `task` field for the "
|
|
55
|
+
"current AP."
|
|
56
|
+
),
|
|
57
|
+
guard_name="RenderedCommandGuard",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
unresolved = _UNRESOLVED.findall(command)
|
|
61
|
+
if unresolved:
|
|
62
|
+
unique = sorted(set(unresolved))
|
|
63
|
+
return GuardResult(
|
|
64
|
+
passed=False,
|
|
65
|
+
feedback=(
|
|
66
|
+
f"Rendered command contains unresolved placeholders: "
|
|
67
|
+
f"{unique}. The generator should have raised KeyError; "
|
|
68
|
+
f"if it didn't, the placeholder syntax is malformed or "
|
|
69
|
+
f"the regex in CommandTemplateGenerator is out of sync."
|
|
70
|
+
),
|
|
71
|
+
guard_name="RenderedCommandGuard",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return GuardResult(
|
|
75
|
+
passed=True,
|
|
76
|
+
feedback="",
|
|
77
|
+
guard_name="RenderedCommandGuard",
|
|
78
|
+
)
|