Files
hermes-agent/tests/tools/test_delegate_child_cache_ttl.py
Teknium 0edb6b928a fix(delegate): subagents never inherit the 1h prompt-cache tier
A delegated child copies the parent's prompt_caching.cache_ttl. The 1h tier
is priced for a person who steps away between turns (2x write vs 1.25x for
5m); a subagent calls every few seconds for minutes and is gone, so it paid
2x on every tool result and never collected the retention. Live measurement
(Sep 5, 40 concurrent Fable 5.1 children via OpenRouter, 1h markers on the
wire): cache writes billed at $20/M against $12.51/M for the same run at
5m, ~60% of a write-dominated bill.

_apply_child_cache_ttl runs right after child construction: 1h -> 5m,
5m stays, disabled stays disabled, parent untouched. Tests: unit (markers
on the wire drop ttl; parent's 1h layout still differs) and the real spawn
path with cache_ttl: 1h in a temp HERMES_HOME.
2026-09-06 02:20:49 -07:00

65 lines
3.1 KiB
Python

"""Subagents never inherit the 1h prompt-cache tier: with ``prompt_caching.cache_ttl: 1h`` the parent
keeps 1h, the child is built at 5m, and the child's wire markers carry no ``ttl``. A disabled cache
stays disabled (the child is not re-enabled to 5m)."""
from types import SimpleNamespace
from agent.prompt_caching import apply_anthropic_cache_control
from tools.delegate_tool import _apply_child_cache_ttl
def _markers(messages):
out = []
for m in messages:
c = m.get("content")
if isinstance(c, list):
out += [b["cache_control"] for b in c if isinstance(b, dict) and "cache_control" in b]
if "cache_control" in m:
out.append(m["cache_control"])
return out
def test_child_1h_becomes_5m_and_wire_markers_drop_ttl():
child = SimpleNamespace(_cache_ttl="1h")
_apply_child_cache_ttl(child)
assert child._cache_ttl == "5m"
msgs = [{"role": "system", "content": "sys"}, {"role": "user", "content": "hi"},
{"role": "assistant", "content": "ok"}, {"role": "user", "content": "go"}]
wire = apply_anthropic_cache_control([dict(m) for m in msgs], cache_ttl=child._cache_ttl)
marks = _markers(wire)
assert marks and all("ttl" not in m for m in marks), marks
# and the parent's 1h layout really is different, so this is not a vacuous check
parent_marks = _markers(apply_anthropic_cache_control([dict(m) for m in msgs], cache_ttl="1h"))
assert any(m.get("ttl") == "1h" for m in parent_marks)
def test_disabled_and_5m_are_left_alone():
for ttl in (None, "5m"):
child = SimpleNamespace(_cache_ttl=ttl)
_apply_child_cache_ttl(child)
assert child._cache_ttl == ttl
def test_real_spawn_path_applies_it(tmp_path, monkeypatch):
"""Through ``_build_child_agent`` with a real AIAgent: parent configured 1h, child ends at 5m, and the
parent is untouched."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
"prompt_caching:\n cache_ttl: 1h\nmodel:\n default: anthropic/claude-sonnet-4.6\n", encoding="utf-8")
from run_agent import AIAgent
from tools import delegate_tool as dt
import tools.delegate_tool_config as dtc
monkeypatch.setattr(dt, "_load_config", lambda: {})
monkeypatch.setattr(dtc, "_load_config", lambda: {})
kw = dict(api_key="k", base_url="https://openrouter.ai/api/v1", provider="openrouter",
api_mode="chat_completions", model="anthropic/claude-sonnet-4.6", platform="cli", quiet_mode=True,
skip_context_files=True, skip_memory=True, save_trajectories=False, enabled_toolsets=["file"])
parent = AIAgent(session_id="p", **kw)
assert parent._cache_ttl == "1h", "fixture: the operator's 1h must actually be in effect"
child = dt._build_child_agent(task_index=0, goal="goal", context=None, toolsets=["file"], model=None,
max_iterations=4, task_count=1, parent_agent=parent)
try:
assert child._cache_ttl == "5m"
assert parent._cache_ttl == "1h"
finally:
child.close()
parent.close()