Repository navigation
token efficiency: lazy tool registration + tool-result trimming policy #211
Copy link
Copy link
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Summary
Feature-research ring 2 (2026-10-06, validated: all tools register upfront in the system prompt; per-tool output caps exist but there is no cross-turn trimming/compaction policy). Original levers (from Cursor's Sept-2026 token-efficiency work): (1) register tool schemas lazily/progressively; (2) a trimming policy for old tool RESULTS in re-sent history. Design settled by a grilling session on 2026-10-09 after a fact-finding pass over the code; the decisions below supersede the original sketch.
Facts that drove the decisions (develop @ 4c90467, v0.8.0)
- The wire body is a pure function of (system prompt, tool specs, entries) (
OpenAiProvider::request_body); history is append-only and nothing is reshuffled per step; specs are sorted by name and the system prompt is pinned in the session header. No provider cache markers exist; caching relies on the gateway prefix cache. - Archived GLM traces: first-step prompt ≈ 3.65k tokens (system + tool specs with the 18-tool cora MCP preset), history grows only ≈ 40–100 tokens per step in short missions, cache hit ≈ 96–98% on later steps.
cached_tokensis already in the usage ledger but is not reported anywhere. mission.rs::tokens_used()sumsprompt_tokens + completion_tokensover every step, i.e. the re-sent (mostly cached) prefix is charged against--max-tokensevery step: the issue is as much budget ACCOUNTING as real cost.- No tokenizer and no local token estimator exist; mock providers emit no
usage.UsageRecord.usageis a raw providerValue, so additive keys need no storage format change. - Largest history bodies:
read_file(up to 1 MiB, no head cap, unlike git/web at 20k chars), MCP results (256 KiB),--skillbodies in the system prompt,load_skillresults (16 KiB). - An unknown tool call aborts the turn (settles to Final); changing the
toolsarray mid-session breaks the prefix cache for the whole history.
Decisions
- Scope / phasing: measure first. Lazy tool registration is OUT of scope (deferred with reasons: it trades a small saving — the prefix is already ~96% cached — for risk to two invariants: cache-prefix stability and the unknown-tool safety behavior; it would also need a new meta-tool). It can be re-opened if the numbers justify it.
- Success measure: reference missions (deterministic mock missions with large tool results and many steps) with a loose CI upper bound; live missions are judged by the real usage report. The CI metric is serialized request CHARACTERS (tokenizer-free), see 4.
- History trimming is deferred until the instrumentation shows history is a material share on long missions. Design is ready if needed: wire-only (in
request_body, never touching the write-once log), applied once at a fixed age, deterministic function of the entry sequence only (no clock/process state), only for tool results that can be re-fetched deterministically (read_file,git,web_fetch), replaced by a stub inside the existing fence with the intent input kept visible;run_command/job output never trimmed; a no-op below its threshold so the Tier-1 wire golden is unaffected. - Metric + storage: per step, compute the serialized request characters split into
system,tools,historyand store them under a namespaced keytole_wireinside the existingUsageRecord.usageobject (provider values untouched, no storage format change). - Report: derive from the ledger at read time —
tole statusandGET /sessions/{id}/statusshow, for ANY session: total prompt/completion/cached tokens, cache-hit rate, the system+tools vs history character split at the first and last step, and the step count. Thefact/missionregister gains an additivecached_tokens. Check that consumers of the status JSON (mobile app, docs/mobile-control.md) tolerate new fields. - Budget accounting: unchanged for now (only
cached_tokensis REPORTED). Whether--max-tokensshould count non-cached tokens only is a follow-up decision once the report shows the size of the gap (it changes the meaning of a user-visible flag and depends on providers reportingcached_tokens). read_filecontract (separate PR): head cap of 20,000 characters (like git/web) with optionaloffsetandlimitin characters (UTF-8 boundary-safe); the result carriestruncated,total_charsandnext_offsetso the model can continue reading.
Staging
- PR 1 —
tole_wireper step + report intole status/REST +cached_tokensinfact/mission(no wire change, no behavior change). - PR 2 — reference mock missions + loose CI bound on request characters (depends on PR 1).
- PR 3 —
read_filecap withoffset/limit(independent; needs a test that a mission can still read a large file). - Follow-ups, only if the numbers justify them: history trimming (3 above), non-cached budget accounting (6 above), lazy registration (out of scope for now).
Acceptance
- PR 1: per-step wire split stored;
tole status+ REST status show tokens, cache-hit rate and the split for any session; additive fields only - PR 2: reference missions with a CI bound that fails on a deliberate prompt-size regression
- PR 3:
read_filecapped with a continuation contract and a large-file mission test - Docs (README/mobile-control status fields) + CHANGELOG same-PR; no change to the wire golden or to durable entry shapes
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Activity
fix(cli): wire-only usage rows do not count as provider steps (#211)
feat(core,cli): per-step wire-size breakdown and usage report (#211 p…
feat(core): cap read_file at 20,000 chars with offset/limit continuat…
test(core,cli): reference wire-size missions with a loose CI bound (#211
ajianaz commentedon Oct 9, 2026
Scope done and released in v0.8.1: usage/wire-size report (#356), read_file cap with offset/limit (#357), wire-size reference missions with a CI bound (#358). Follow-ups remain conditional on the numbers and are intentionally deferred: history trimming, non-cached budget accounting, lazy tool registration (out of scope).