Preserve user guidance through history compression, stabilize Flash index targets, and retire withdrawn checks without applying stale verdicts. Improve runtime diagnostics, tool responses, and history rendering. Simplify documentation and comments. Validation: 2076 tests passed in the full suite; 381 affected tests and 26 frontend tests passed after cleanup. Ruff and the quality ratchet passed.
191 lines
7.6 KiB
Python
191 lines
7.6 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""§1.3 assembly discipline: prompts are functions of configuration x scenario.
|
|
Both check gates off => zero context pollution; with a gate on, the check-line
|
|
guidance lives once in the static system prompt, worded for that gate."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from artemis.agents.operator.prompts import (
|
|
FeedbackPromptComponent,
|
|
PromptBuilder,
|
|
TemplatePromptComponent,
|
|
render_transcript_static_system,
|
|
)
|
|
from artemis.agents.planner.planner import build_planner_system_blocks
|
|
from artemis.context import ArtemisContext, ExecutionSetup
|
|
from artemis.graph.state import State
|
|
|
|
PLANNER_JSON = Path(__file__).resolve().parents[3] / "artemis/agents/planner/planner.json"
|
|
OPERATOR_JSON = Path(__file__).resolve().parents[3] / "artemis/agents/operator/operator.json"
|
|
|
|
|
|
def _planner_data():
|
|
return json.loads(PLANNER_JSON.read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_planner_blocks_without_checks_are_byte_identical_to_legacy():
|
|
data = _planner_data()
|
|
baseline = list(data["modes"]["initial_plan"]["system"])
|
|
assert build_planner_system_blocks(data, "initial_plan", include_checks=False) == baseline
|
|
# And the disabled assembly never references the check blocks
|
|
rendered = "\n\n".join(
|
|
data["blocks"][b]
|
|
for b in build_planner_system_blocks(data, "initial_plan", include_checks=False)
|
|
)
|
|
assert "- verify:" not in rendered
|
|
assert "- assert:" not in rendered
|
|
|
|
|
|
def test_planner_blocks_with_checks_mount_generation_and_audit():
|
|
data = _planner_data()
|
|
initial = build_planner_system_blocks(data, "initial_plan", include_checks=True)
|
|
assert initial[-1] == "check_generation"
|
|
validator = build_planner_system_blocks(data, "validator", include_checks=True)
|
|
assert validator[-1] == "check_audit"
|
|
|
|
generation = data["blocks"]["check_generation"]
|
|
# Restraint, honesty about post-hoc auditing, and no silent ordering promises
|
|
assert "restraint" in generation.lower() or "Do NOT attach" in generation
|
|
assert "post-hoc" in generation.lower()
|
|
assert "Never silently promise" in generation
|
|
|
|
audit = data["blocks"]["check_audit"]
|
|
# Advisory review: the audit flags weakened check standards, never blocks.
|
|
assert "FLAG" in audit
|
|
assert "REJECT" not in audit
|
|
|
|
|
|
def _mock_ctx(tmp_path=None, plan: str | None = None, **setup_kwargs):
|
|
ctx = MagicMock(spec=ArtemisContext)
|
|
ctx.execution_setup = ExecutionSetup(**setup_kwargs) if setup_kwargs is not None else None
|
|
if plan is not None and tmp_path is not None:
|
|
notes = tmp_path / "notes"
|
|
notes.mkdir(parents=True, exist_ok=True)
|
|
(notes / "task_plan.md").write_text(plan, encoding="utf-8")
|
|
ctx.data_engine = MagicMock()
|
|
ctx.data_engine.base_dir = tmp_path
|
|
else:
|
|
ctx.data_engine = None
|
|
return ctx
|
|
|
|
|
|
def _state(**overrides):
|
|
state = MagicMock(spec=State)
|
|
state.operator_feedback = None
|
|
for k, v in overrides.items():
|
|
setattr(state, k, v)
|
|
return state
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_line_guidance_lives_in_the_static_prompt_once():
|
|
"""The check-line guidance is a static-prompt section gated on the check
|
|
gates (never a per-turn observation block), worded for the active gate."""
|
|
off = await _render_main_template(disable_checker=True)
|
|
assert "*Check Lines*" not in off
|
|
assert "never declare a check passed" not in off
|
|
|
|
final_only = await _render_main_template(disable_midway_checks=True, disable_final_check=False)
|
|
assert final_only.count("*Check Lines*") == 1
|
|
assert "never declare a check passed" in final_only
|
|
assert "restores deleted ones unless user guidance called for the change" in final_only
|
|
# No midway repair loop -> no repair budget to recite, and the grammar says so.
|
|
assert "reopens its milestone at most" not in final_only
|
|
assert "there is no midway repair loop" in final_only
|
|
|
|
midway = await _render_main_template(disable_midway_checks=False, checkpoint_max_repairs=3)
|
|
assert midway.count("*Check Lines*") == 1
|
|
assert "reopens its milestone at most 3 times" in midway
|
|
assert "- finding:" in midway
|
|
|
|
|
|
def test_transcript_static_system_recites_the_repair_budget_from_setup():
|
|
ctx = _mock_ctx(disable_midway_checks=False, checkpoint_max_repairs=5)
|
|
ctx.actuator = None
|
|
prompts = json.loads(OPERATOR_JSON.read_text(encoding="utf-8"))
|
|
text = render_transcript_static_system(prompts, ctx, _state(initial_goal="G"))
|
|
assert text.count("*Check Lines*") == 1
|
|
assert "reopens its milestone at most 5 times" in text
|
|
|
|
quiet = _mock_ctx(disable_checker=True)
|
|
quiet.actuator = None
|
|
assert "*Check Lines*" not in render_transcript_static_system(
|
|
prompts, quiet, _state(initial_goal="G")
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_static_prompt_declares_the_user_guidance_channel_once():
|
|
text = await _render_main_template(disable_checker=True)
|
|
assert text.count("--- User Guidance ---") == 1
|
|
assert "outranks the task plan and its check lines" in text
|
|
|
|
|
|
async def _render_main_template(**setup_kwargs) -> str:
|
|
component = TemplatePromptComponent()
|
|
builder = PromptBuilder()
|
|
ctx = _mock_ctx(**setup_kwargs)
|
|
ctx.actuator = None
|
|
state = _state(initial_goal="G")
|
|
prompts = json.loads(OPERATOR_JSON.read_text(encoding="utf-8"))
|
|
await component(builder, state, ctx, prompts=prompts, plan_and_history="")
|
|
return "".join(builder.system_parts) + (builder.human_footer or "")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_main_template_diagnosis_trigger_gated_on_verification():
|
|
"""The rejection/finding diagnosis trigger is a dead instruction unless a
|
|
mechanism that can produce rejections or findings is active (§1.3)."""
|
|
# Factory default: checks off (master alias) AND planner validation off.
|
|
default = await _render_main_template(disable_checker=True, disable_planner_validation=True)
|
|
assert "Verification Finding" not in default
|
|
assert "Ambiguous Validation Rejection" not in default
|
|
|
|
# Checks on -> the trigger is real and must be documented.
|
|
with_checks = await _render_main_template(
|
|
disable_checker=False, disable_planner_validation=True
|
|
)
|
|
assert "Ambiguous Verification Finding" in with_checks
|
|
|
|
# Planner validation alone can also produce (advisory) findings.
|
|
with_validation = await _render_main_template(
|
|
disable_checker=True, disable_planner_validation=False
|
|
)
|
|
assert "Ambiguous Verification Finding" in with_validation
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_feedback_component_appends_findings_only_when_present():
|
|
component = FeedbackPromptComponent()
|
|
|
|
builder = PromptBuilder()
|
|
await component(builder, _state(operator_feedback=None), _mock_ctx())
|
|
assert builder.human_parts == []
|
|
|
|
builder2 = PromptBuilder()
|
|
await component(
|
|
builder2,
|
|
_state(operator_feedback=["[verify failed] 'X': missing"]),
|
|
_mock_ctx(),
|
|
)
|
|
joined = "\n".join(p for p in builder2.human_parts if isinstance(p, str))
|
|
assert "Verification Findings" in joined
|
|
assert "[verify failed] 'X': missing" in joined
|