Skip to main content

is_costly_batch

Function is_costly_batch 

Source
pub(crate) fn is_costly_batch(calls: &[ToolCall]) -> bool
Expand 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.