mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +00:00
Phase 2 of #3875. Two guardrail axes can end a subagent run early — the turn budget (GraphRecursionError) and the token budget (TokenBudgetMiddleware) — and both now surface *why* through one additive `subagent_stop_reason` field instead of a status enum. This completes and course-corrects Phase 1 (#3949), which shipped the turn-budget cap as a `max_turns_reached` status enum. The agreed Phase 2 design replaces that enum with an optional `stop_reason` field (token_capped | turn_capped | loop_capped): a new enum value would break v1 consumers, while an additive field is ignored by older frontends and ledger readers. `max_turns_reached` and SubagentStatus.MAX_TURNS_REACHED are removed. - subagents.token_budget config (default enabled, 2,000,000 tokens, warn 0.7) with per-agent override; TokenBudgetMiddleware is now attached in build_subagent_runtime_middlewares so the cost-ceiling backstop engages for every subagent. The hard-stop does not raise — it strips tool_calls and lets the run finish with a final answer, recording the cap on a per-run consume_stop_reason() accessor. - executor.py: on normal completion it reads consume_stop_reason() and stamps completed + token_capped when the budget fired; on GraphRecursionError it recovers the last AIMessage partial (completed + turn_capped) or, if nothing usable survived, failed + turn_capped. SubagentResult gains stop_reason. - status_contract.py / contracts/subagent_status_contract.json (v2) / frontend subtask-result.ts: additive subagent_stop_reason field, pinned by test_status_values_match_contract / test_stop_reason_values_match_contract. - task_tool.py + delegation_ledger.py: drop the max_turns_reached paths; the ledger captures stop_reason and renders model-facing "capped" guidance so the lead reuses a capped completion knowingly. The 2,000,000-token default is deliberately loose (tighten to taste) — it would have roughly halved the reported 4.4M burn while leaving legitimate deep-research runs (max_turns=150) room. Subagent summarization is a follow-up.
33 lines
1.2 KiB
Python
33 lines
1.2 KiB
Python
"""A small bounded ``OrderedDict`` shared by guard middlewares.
|
|
|
|
Guard middlewares (``TokenBudgetMiddleware``, ``LoopDetectionMiddleware``) keep
|
|
per-``run_id`` state that must not grow without bound on abandoned or reused
|
|
runs. This module provides the single shared implementation so both middlewares
|
|
cap identically and a future guard does not reinvent it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import OrderedDict
|
|
from typing import Any
|
|
|
|
|
|
class BoundedDict(OrderedDict):
|
|
"""An ``OrderedDict`` that evicts the oldest entry once ``maxsize`` is reached.
|
|
|
|
Used for per-``run_id`` state (stop-reason flags, pending warnings, usage
|
|
accumulators) so a long-lived middleware instance on the lead agent cannot
|
|
leak memory across many runs. Insertion order is preserved, so the
|
|
least-recently-inserted key is evicted first.
|
|
"""
|
|
|
|
def __init__(self, maxsize: int = 1000, *args: Any, **kwds: Any) -> None:
|
|
self.maxsize = maxsize
|
|
super().__init__(*args, **kwds)
|
|
|
|
def __setitem__(self, key: Any, value: Any) -> None:
|
|
if key not in self:
|
|
if len(self) >= self.maxsize:
|
|
self.popitem(last=False)
|
|
super().__setitem__(key, value)
|