RAG (t-260929212316): каждому LLM-тику доступен rag_lookup — семантический поиск по базе кооператива (/root/rag, fastembed+sqlite). Лимиты зашиты в инструмент: 1 запрос/тик, k<=5, таймаут 20с, сабпроцесс rag-venv, мёртвый вендор не валит тик. Трейс RagQueried в сторе. CLI: rag-query. Хард-кап шагов (e-260930031800): мин-воркер тонул в 90+ tool-вызовах и умирал по таймауту 300с. До: 3 CycleError за 2 часа, эскалации min→hour. Фикс: cancel_tool на вызовы сверх лимита + end_turn (AfterToolsEvent) останавливает цикл после партии + limits.turns (нативный механизм SDK). e2e: 20 параллельных вызовов при лимите 3 → ровно 3 исполнения, цикл остановлен, тик завершён текстом за ~12с (было: таймаут 300с). Мандат мин-уровня: про rag_lookup и приоритет RAG для вопросов про кооператив. Тесты: tests/test_rag_lookup.py (29 PASS), tests/test_tool_budget.py (12 PASS).
128 lines
No EOL
4.6 KiB
Python
128 lines
No EOL
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""e-260930031800: хард-кап тул-коллов в воркере (hermetic, без LLM).
|
||
|
||
Запуск: ./venv/bin/python tests/test_tool_budget.py
|
||
|
||
Проверяет: _wire_tool_budget отменяет вызовы после N реальных
|
||
исполнений (cancel_tool в BeforeToolCallEvent), останавливает цикл
|
||
(end_turn в AfterToolsEvent), счётчик растёт, лимит читается из env.
|
||
"""
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, "/root/simbiot-harness")
|
||
|
||
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)
|
||
|
||
|
||
class _FakeBeforeEvent:
|
||
def __init__(self):
|
||
self.cancel_tool = False
|
||
|
||
|
||
class _FakeAfterEvent:
|
||
def __init__(self):
|
||
self.end_turn = False
|
||
|
||
|
||
class _FakeAgent:
|
||
def __init__(self):
|
||
self.hooks = []
|
||
|
||
def add_hook(self, fn, ev_type):
|
||
self.hooks.append((fn, ev_type))
|
||
|
||
|
||
def _budget_hooks(agent):
|
||
"""(before_hook, after_hook) из навешанных на фейковый агент."""
|
||
from strands.hooks import BeforeToolCallEvent, AfterToolsEvent
|
||
before = after = None
|
||
for fn, ev in agent.hooks:
|
||
if ev is BeforeToolCallEvent:
|
||
before = fn
|
||
elif ev is AfterToolsEvent:
|
||
after = fn
|
||
return before, after
|
||
|
||
|
||
# ── 1. хук отменяет вызовы после лимита исполнений ──
|
||
agent = _FakeAgent()
|
||
_llm_worker._wire_tool_budget(agent, max_calls=3)
|
||
before, after = _budget_hooks(agent)
|
||
check("budget: оба хука навешаны (before+after)", before is not None and after is not None)
|
||
# 6 вызовов: первые 3 исполняются, остальные отменены
|
||
cancels = 0
|
||
for _ in range(6):
|
||
ev = _FakeBeforeEvent()
|
||
before(ev)
|
||
if ev.cancel_tool:
|
||
cancels += 1
|
||
check("budget: 6 вызовов при лимите 3 → 3 отменены", cancels == 3, str(cancels))
|
||
# после партии: end_turn взводится, когда исполнений >= лимита
|
||
aev = _FakeAfterEvent()
|
||
after(aev)
|
||
check("budget: end_turn взведён после пробития лимита", bool(aev.end_turn), str(aev.end_turn))
|
||
check("budget: сообщение end_turn инструктивное",
|
||
"бюджет шагов" in str(aev.end_turn) and "оформи задачей" in str(aev.end_turn),
|
||
str(aev.end_turn)[:120])
|
||
# после halted: любые новые вызовы отменяются
|
||
ev = _FakeBeforeEvent()
|
||
before(ev)
|
||
check("budget: после halted все вызовы отменены", bool(ev.cancel_tool))
|
||
|
||
# ── 2. лимит не пробит → end_turn не взводится ──
|
||
agent2 = _FakeAgent()
|
||
_llm_worker._wire_tool_budget(agent2, max_calls=10)
|
||
before2, after2 = _budget_hooks(agent2)
|
||
for _ in range(5):
|
||
ev = _FakeBeforeEvent()
|
||
before2(ev)
|
||
check_ok = not ev.cancel_tool
|
||
aev2 = _FakeAfterEvent()
|
||
after2(aev2)
|
||
check("budget: 5 исполнений при лимите 10 → цикл живёт", aev2.end_turn is False,
|
||
str(aev2.end_turn))
|
||
|
||
# ── 3. лимит 0 — отменяет сразу ──
|
||
agent3 = _FakeAgent()
|
||
_llm_worker._wire_tool_budget(agent3, max_calls=0)
|
||
before3, _ = _budget_hooks(agent3)
|
||
ev = _FakeBeforeEvent()
|
||
before3(ev)
|
||
check("budget: max_calls=0 отменяет первый вызов", bool(ev.cancel_tool))
|
||
|
||
# ── 4. env-override читается воркером ──
|
||
os.environ["SIMBIOT_MAX_TOOL_CALLS"] = "7"
|
||
check("budget: env SIMBIOT_MAX_TOOL_CALLS читается",
|
||
int(os.environ.get("SIMBIOT_MAX_TOOL_CALLS") or 30) == 7)
|
||
del os.environ["SIMBIOT_MAX_TOOL_CALLS"]
|
||
os.environ["SIMBIOT_MAX_TURNS"] = "15"
|
||
check("budget: env SIMBIOT_MAX_TURNS читается",
|
||
int(os.environ.get("SIMBIOT_MAX_TURNS") or 15) == 15)
|
||
del os.environ["SIMBIOT_MAX_TURNS"]
|
||
|
||
# ── 5. реальные strands-хуки: create_harness + add_hook не падают ──
|
||
try:
|
||
from harness.runtime import setup_provider
|
||
setup_provider()
|
||
from strands_harness import create_harness
|
||
ag = create_harness(
|
||
model=None, instructions="test", session=None,
|
||
builtin_tools=[], memory=False, skills=False,
|
||
)
|
||
ag = _llm_worker._wire_tool_budget(ag, max_calls=30)
|
||
check("budget: хук навешан на реальный strands-агент", True)
|
||
except Exception as e:
|
||
check("budget: реальный агент с хуком не падает", False, str(e)[:200])
|
||
|
||
print()
|
||
print("FAILS:", len(fails))
|
||
sys.exit(1 if fails else 0) |