# -*- coding: utf-8 -*- """Кэп результатов инструментов в истории тика (burn-аудит 14.10, рек.1). Запуск: ./venv/bin/python tests/test_tool_result_cap.py Hermetic: без LLM, на живом strands-агенте (только реестр хуков). Диагноз из аудита (docs/aum/burn-audit-2026-10-14.md): 99.9% бюджета — промпт-токены; каждый агентный шаг пересылает накопленную историю с полными tool-result'ами (до 75k ток/шаг). Кэп срезает гигантские результаты до попадания в историю; полный текст остаётся в трейсе (events.db, ToolResult пишет runtime-хук). """ import os import sys import tempfile as _iso_tf sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ["SIMBIOT_DB"] = os.path.join(_iso_tf.mkdtemp(prefix="simbiot_test_"), "harness.db") from strands import Agent from strands.hooks import AfterToolCallEvent from harness import _llm_worker fails = [] def check(name, cond, extra=""): print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "") if not cond: fails.append(name) agent = Agent(callback_handler=None) agent = _llm_worker._wire_tool_result_cap(agent) _probe = AfterToolCallEvent(agent=agent, selected_tool=None, tool_use={}, invocation_state={}, result={"content": [{"text": ""}], "status": "success", "toolUseId": "t0"}) _cbs = list(agent.hooks.get_callbacks_for(_probe)) check("хук зарегистрирован", len(_cbs) >= 1) def fire(text): ev = AfterToolCallEvent(agent=agent, selected_tool=None, tool_use={"name": "shell", "input": {}}, invocation_state={}, result={"content": [{"text": text}], "status": "success", "toolUseId": "t"}) for cb in _cbs: cb(ev) return ev.result["content"][0]["text"] # 1) гигантский результат срезается сразу, с маркером t1 = fire("x" * 20000) check("20k срезан до кэпа", len(t1) < _llm_worker._TOOL_RESULT_CAP_CHARS + 200, len(t1)) check("маркер среза в тексте", "срез" in t1) # 2) маленький результат не трогается check("маленький не тронут", fire("ok") == "ok") # 3) мягкий порог: после ~60k накопленных результатов средний (6k) тоже режется for _ in range(12): fire("z" * 5000) t3 = fire("y" * 6000) check("soft-порог включил срез средних", len(t3) < 6000, len(t3)) # 4) не-текстовый контент (json/image) не валит хук _ev = AfterToolCallEvent(agent=agent, selected_tool=None, tool_use={"name": "portal_mcp", "input": {}}, invocation_state={}, result={"content": [{"json": {"a": 1}}, {"image": b"x"}], "status": "success", "toolUseId": "t4"}) for cb in _cbs: cb(_ev) check("не-текстовый контент жив", _ev.result["content"][0] == {"json": {"a": 1}}) # 5) мусорный result (не dict) не валит хук try: from strands.hooks import AfterToolCallEvent as _ATE ev5 = _ATE(agent=agent, selected_tool=None, tool_use={}, invocation_state={}, result=None) for cb in _cbs: cb(ev5) check("result=None не валит хук", True) except Exception as e: check("result=None не валит хук", False, str(e)[:120]) print() if fails: print("FAIL:", fails) sys.exit(1) print("все проверки пройдены")