- фото/документ/видео без подписи выпадали из триажа (_pending_inbox и build_context пускали только текст/голос): висели до 23 ч, все 4 неотвеченных за неделю — такие. Один фильтр _is_pending, медиа с тегом и путём файла; окно свежести SIMBIOT_MEDIA_PENDING_H=6 — старые не будят LLM задним числом; стикер без текста — не повод. - MinNudge: мин-тик через 3 с тишины после MessageReceived/Transcribed, не ждём границы минуты (было: медиана 36 с до старта LLM, p90 93 с при 24 с самой модели). Слот следующего такта, граница его пропускает; SIMBIOT_MIN_NUDGE=0 — выключить. - сек-уровень: CycleTicked+ReflexObserved каждую секунду = 1.35 млн подписанных событий/нед (98% стора 1.37 ГБ). Теперь heartbeat раз в 60 с + такты с событием (алерт, команды, каналы); «reflex ok» не спамит журнал. - 6 тестов писали в боевой стор под SIMBIOT_HOME тика (TTFTWatchdogRetry с бюджетом 0.5 с в проде — это они): изоляция SIMBIOT_DB. Тест: tests/test_tick_latency_noise.py 20 PASS; полный набор — без регрессий. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
No EOL
4.8 KiB
Python
130 lines
No EOL
4.8 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")
|
||
|
||
# изоляция стора (аудит 2026-10-16): тест не пишет в боевой стор, даже
|
||
# когда его гоняет тик харнеса под SIMBIOT_HOME (TTFTWatchdogRetry с
|
||
# бюджетом 0.5с из этого набора оседали в проде)
|
||
import tempfile as _iso_tf # noqa: E402
|
||
os.environ["SIMBIOT_DB"] = os.path.join(_iso_tf.mkdtemp(prefix="simbiot_test_"), "harness.db")
|
||
|
||
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"]
|
||
|
||
# ── 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) |