pub(crate) fn is_costly_batch(calls: &[ToolCall]) -> boolExpand description
Return true if any call in calls costs something to repeat.
Read against the batch’s own tool names rather than against the active
observation_tools list, so a user-supplied list naming one of these is
bounded too. The harm is a property of the tool, not of who listed it.
§Why this does not reuse is_observation_batch’s rule
The two lists need opposite safety properties, so they cannot share a
matcher. Over-capturing as observation is permissive — it grants a larger
allowance — which is why that list can afford contains and only has to
ensure every captured name is itself read-only. Over-capturing here is
restrictive: it aborts a session. So this rule leans the other way —
it prefers to miss a costly tool over refusing a free one.
Unanchored contains cannot: click captures get_clickable_elements and
clickhouse_query, both genuinely read-only, and both would be refused at
the 16th call. Matching is therefore anchored to whole _/-/.-delimited
segments, which admits browser_navigate, mcp__playwright__click and
click_element_by_index while leaving clickhouse_query alone. Residue
remains — a bare navigate segment still catches an LSP-style
navigate_to_definition — and it is pinned by test rather than hidden: the
cost there is a ceiling on repeats, not a refusal of honest work.
§Two limits, both deliberate
Residue. Anchoring is not exact matching. A bare navigate or click
segment still catches read-only names built from the same word —
navigate_to_definition, get_click_count, ad_click_report,
click_house_query. Those are bounded at 15 rather than refused outright,
which is the pre-#928 behaviour and the cheaper of the two errors.
camelCase is missed. browserNavigate and clickElement carry no
separator, so they split to one segment and are not recognised as costly
— while is_observation_batch’s contains still classifies them, which
leaves them exempt and unbounded. Splitting on case boundaries would close
that, and would newly capture clickHouseQuery — the camelCase spelling of
the exact tool the anchoring exists to protect. Given the asymmetry above,
missing a bound is the better error than aborting a read-only session, so
the gap stays and is pinned by test rather than left to be rediscovered.
Narrowing it properly needs the two classifiers to share one anchored rule,
which is a change to observation_tools’ matching and out of scope here.
any, not all: one costly call in the batch is enough to spend the thing
that must not be spent without bound.