fabro/lib
fabro-sh-0530[bot] c987766641
agent: use API usage baseline for compaction context estimate (#366)
## Summary

Replaces the whole-history `chars / 4` compaction trigger with a Claude
Code-style hot-path estimator: find the latest assistant turn with real
provider-reported `usage.total_tokens()`, use that as the baseline, and
add local char estimates only for turns appended after it. This makes
compaction sensitive to actual provider-reported context usage
(including cache read/write and reasoning tokens) without adding any
provider token-count API calls.

### Plan Summary

- **New estimator** (`estimate_active_context_usage`): returns a
`ContextEstimate` with both a token count and an `ContextEstimateMethod`
enum tag (`ApiUsagePlusLocalDelta` or `LocalEstimate`).
- **`check_context_usage`** now returns `Option<ContextEstimate>`
instead of `bool`, and includes `estimate_method` in warning `details`.
The caller passes the estimate directly into `compact_context`, avoiding
a double-compute.
- **`compact_context`** signature drops `system_prompt` (no longer
needed) and takes the pre-computed `ContextEstimate`. The
`CompactionStarted` event is now emitted *after* the `turns.len() <=
preserve_count` no-op guard, so a no-op can never emit `Started` without
`Completed`.
- **`History::compact`** invalidates preserved assistant `usage` (resets
to `TokenCounts::default()`) so a preserved turn's pre-compaction
provider baseline never becomes the next estimate's anchor. Content,
tool calls, provider parts, and response IDs are untouched.
- **`session.compact_if_needed`** restructured to early-return on `None`
from `check_context_usage` or on compaction disabled, simplifying the
nesting.

## Key design decisions

**Why invalidate preserved assistant usage?** After compaction the prior
turns are gone, so a stored `total_tokens` from before compaction would
overstate the new context. The authoritative billing record is in
emitted run events, not in mutable runtime history.

**Why return `Option<ContextEstimate>` from `check_context_usage`?**
Avoids recomputing the estimate in `compact_context`. It also makes the
call-site idiom (`let Some(estimate) = ... else { return; }`) an
explicit gate, which is cleaner than a separate bool-then-compact
pattern.

**Why `strum::IntoStaticStr` on the method enum?** Lets the variant
serialize to a `&'static str` for the JSON `details` field without a
manual `match` or adding `serde` derives.


### Fabro Details

<details>
<summary>Ran 9 stages in 30m 54s for $7.93</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 52s | – | 0 |
| preflight_lint | 2m 6s | – | 0 |
| implement | 8m 11s | $3.47 | 0 |
| simplify_opus | 11m 7s | $3.53 | 0 |
| simplify_gpt | 2m 39s | $0.93 | 0 |
| verify | 4m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **30m 54s** | **$7.93** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-23 12:55:23 -04:00
..
crates agent: use API usage baseline for compaction context estimate (#366) 2026-05-23 12:55:23 -04:00
packages/fabro-api-client feat(fabro-types): promote transcript primitives and extend agent event… (#357) 2026-05-22 20:40:10 -04:00