93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
# -*- 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("все проверки пройдены")
|