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.
148 lines
5.2 KiB
Python
148 lines
5.2 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.
|
|
|
|
"""Tests for the Operator reasoning reminder.
|
|
|
|
Thinking-mode models satisfy "reason before acting" inside their thought channel
|
|
and emit bare tool calls. After such a silent turn the next observation tail
|
|
carries a one-line reminder; nothing is bounced and no extra model call is made.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from artemis.agents.operator.operator import OperatorNode
|
|
from artemis.agents.operator.prompts import (
|
|
REASONING_REMINDER,
|
|
apply_operator_prompt_contract,
|
|
load_operator_prompts,
|
|
)
|
|
from artemis.config.agent import MemoryTranscriptConfig
|
|
from artemis.context import ArtemisContext
|
|
|
|
LEGACY_TRANSCRIPT = MemoryTranscriptConfig(enabled=False)
|
|
|
|
PLAN_WITH_LEAF = """- [x] Open Google Maps and search for SFO
|
|
- [/] Read the commute duration and record it into note `commute_eta_info`
|
|
- [/] Read the driving duration
|
|
- [/] Tap the "Driving" tab so the fastest route is highlighted
|
|
- [ ] Draft the ETA message
|
|
"""
|
|
|
|
|
|
def _make_node(tmp_path):
|
|
notes = tmp_path / "notes"
|
|
notes.mkdir(parents=True)
|
|
(notes / "task_plan.md").write_text(PLAN_WITH_LEAF, encoding="utf-8")
|
|
|
|
ctx = MagicMock(spec=ArtemisContext)
|
|
ctx.execution_setup = None
|
|
ctx.data_engine = MagicMock()
|
|
ctx.data_engine.base_dir = str(tmp_path)
|
|
ctx.data_engine.current_session_id = "s"
|
|
ctx.data_engine.get_agent_friendly_steps.return_value = []
|
|
|
|
state = MagicMock()
|
|
state.subagent_calls = []
|
|
state.initial_goal = "goal"
|
|
state.open_incident = None
|
|
return OperatorNode(ctx, transcript_config=LEGACY_TRANSCRIPT), state
|
|
|
|
|
|
def _click_response(content):
|
|
response = MagicMock()
|
|
response.content = content
|
|
response.tool_calls = [
|
|
{
|
|
"name": "click",
|
|
"args": {"target": [50, 50], "target_description": "button"},
|
|
"id": "call_0",
|
|
}
|
|
]
|
|
return response
|
|
|
|
|
|
def _texts(messages):
|
|
out = []
|
|
for m in messages:
|
|
content = getattr(m, "content", None)
|
|
if isinstance(content, str):
|
|
out.append(content)
|
|
elif isinstance(content, list):
|
|
out.extend(b.get("text", "") for b in content if isinstance(b, dict))
|
|
return "\n".join(out)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reminder_follows_a_silent_turn_and_costs_no_extra_call(tmp_path):
|
|
node, state = _make_node(tmp_path)
|
|
seen = []
|
|
replies = iter(
|
|
[
|
|
_click_response(""), # turn 1: silent
|
|
_click_response("Tapping the Driving tab."), # turn 2: reminded, speaks
|
|
_click_response([{"type": "thinking", "thinking": "internal"}]), # turn 3: silent
|
|
_click_response("Done."), # turn 4
|
|
]
|
|
)
|
|
|
|
async def ainvoke(messages, *args, **kwargs):
|
|
seen.append(list(messages))
|
|
return next(replies)
|
|
|
|
llm = MagicMock()
|
|
llm.ainvoke = AsyncMock(side_effect=ainvoke)
|
|
llm.bind_tools.return_value = llm
|
|
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
|
|
for _ in range(4):
|
|
await node(state)
|
|
|
|
assert llm.ainvoke.await_count == 4, "one model call per turn, never a bounce"
|
|
assert REASONING_REMINDER not in _texts(seen[0]) # nothing to remind on turn 1
|
|
assert REASONING_REMINDER in _texts(seen[1]) # turn 1 was silent
|
|
assert REASONING_REMINDER not in _texts(seen[2]) # turn 2 spoke
|
|
assert REASONING_REMINDER in _texts(seen[3]) # thinking-only turn 3 counts as silent
|
|
assert _texts(seen[3]).count(REASONING_REMINDER) == 1
|
|
|
|
|
|
def test_ledger_silence_verdict_outranks_the_local_flag(tmp_path):
|
|
"""With a transcript ledger, the ledger's own judgment of the previous
|
|
turn is authoritative; the local flag only serves the legacy path."""
|
|
from artemis.memory import TranscriptLedger
|
|
|
|
node, _ = _make_node(tmp_path)
|
|
node._previous_turn_silent = True
|
|
assert node._previous_turn_was_silent() is True # no ledger: local flag
|
|
|
|
ledger = MagicMock(spec=TranscriptLedger)
|
|
ledger.last_turn_silent = False
|
|
node.ctx.transcript_ledger = ledger
|
|
assert node._previous_turn_was_silent() is False
|
|
|
|
ledger.last_turn_silent = True
|
|
node._previous_turn_silent = False
|
|
assert node._previous_turn_was_silent() is True
|
|
|
|
|
|
def test_prompt_asks_for_one_paragraph_not_phase_sections():
|
|
prompt = apply_operator_prompt_contract(load_operator_prompts()["main_template"])
|
|
assert prompt.count("one short paragraph of connected prose reflecting these three phases") == 1
|
|
assert "in your raw thinking" not in prompt
|
|
assert "in your written reply" not in prompt
|
|
assert (
|
|
"bounced"
|
|
not in prompt.split("# THE THREE-PHASE COGNITIVE PROTOCOL")[1].split("## Phase 1")[0]
|
|
)
|