fix(worker): убрать limits.turns — стоп без текста, дайджесты пустели

Репродукция: мин-тик с реальным контекстом → модель (auto/low effort)
каждый ход делала tool-вызов, никогда не выдавая чистый текст;
limits={'turns': 15} обрывал цикл на границе хода, result.message
оставался toolResult-сообщением → str(result)='' → DigestWritten len 0.

limit_turns — нативный стоп SDK, но он не кладёт в историю итоговый
ассистентский текст. Наш end_turn (AfterToolsEvent) кладёт: дайджест
не пустеет. Защита от таймаутов остаётся двойной: cancel_tool на
вызовы сверх лимита + end_turn после партии.

E2E-подтверждение: тот же промпт, stop_reason=end_turn, текст 465
символов ('Тик чистый. Проверил: ...').
This commit is contained in:
Simbiot Harness 2026-09-30 04:24:58 +00:00
parent 1b8c27b292
commit 2d70740e76
3 changed files with 13 additions and 15 deletions

View file

@ -123,9 +123,11 @@ sqlite, rag.db ~12.5k чанков по контексту/навыкам, herme
Защита в `_llm_worker`: `BeforeToolCallEvent.cancel_tool` отменяет
вызова сверх лимита, `AfterToolsEvent.end_turn` останавливает цикл
агента ПОСЛЕ партии инструментов (без него модель шлёт шаги снова —
e2e: 20 отмен подряд), `limits={"turns": N}` — нативный кап ходов SDK.
Лимиты: `SIMBIOT_MAX_TOOL_CALLS` (по умолчанию 30, счёт по реальным
исполнениям), `SIMBIOT_MAX_TURNS` (по умолчанию 15).
e2e: 20 отмен подряд), кладя в историю ассистентский текст-итог.
Нативный `limits={"turns": N}` НЕ используется: стоп на границе хода
оставляет `result.message` = toolResult → пустой дайджест (04:18+).
Лимит: `SIMBIOT_MAX_TOOL_CALLS` (по умолчанию 30, счёт по реальным
исполнениям).
## Дашборд задач

View file

@ -238,16 +238,16 @@ def main():
)
agent = _wire_trace_hooks(agent, es, trace_id)
agent = _wire_budget_hooks(agent, level, trace_id)
# хард-кап шагов (e-260930031800): двойная защита.
# 1) limits={"turns": N} — нативный механизм SDK: цикл агента
# останавливается на границе хода (stop_reason="limit_turns"),
# модель не может уйти в бесконечную цепочку tool-вызовов.
# 2) хук cancel_tool — страховка на случай параллельного блока
# (20 вызовов одним блоком: turns их не режет, хук отменяет).
max_turns = int(os.environ.get("SIMBIOT_MAX_TURNS") or 15)
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
# обрывает цикл на границе хода, и result.message остаётся
# toolResult-сообщением → str(result)='' → пустой дайджест
# (репродукция: stop_reason=limit_turns, text='', тики 04:18+).
# Наш end_turn (AfterToolsEvent) кладёт в историю ассистентский
# текст с итогом — дайджест не пустеет.
agent = _wire_tool_budget(
agent, int(os.environ.get("SIMBIOT_MAX_TOOL_CALLS") or 30))
result = agent(payload.get("prompt", ""), limits={"turns": max_turns})
result = agent(payload.get("prompt", ""))
usage = _extract_usage(agent)
_emit(payload, {"ok": True, "text": str(result), "usage": usage, "trace_id": trace_id})
except Exception as e:

View file

@ -104,10 +104,6 @@ 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: