Skip to content

feat(ai): summariser selector + summarisation prompt builder - #455

Merged
gemone merged 1 commit into
mainfrom
feat/compaction-summariser
Aug 7, 2026
Merged

feat(ai): summariser selector + summarisation prompt builder#455
gemone merged 1 commit into
mainfrom
feat/compaction-summariser

Conversation

@gemone

@gemone gemone commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

Compaction epic slice A (of ~3): land the two pure building blocks for
conversation compaction — a summariser selector and a summarisation
prompt builder — as a self-contained, unit-testable module. Built on
the context-budget primitive (#448). No run-loop wiring yet.

Changes

New pure module packages/cothis-ai/src/cothis/ai/compaction.py (sibling
to context_budget.py; no I/O, no SDK imports — preserves the
lazy-client invariant):

  • resolve_summary_model — picks the model/provider that performs
    the summarisation. Precedence: explicit override arg >
    COTHIS_SUMMARY_MODEL env var > the session pair. A "provider/model"
    spec splits on the first slash; a bare model inherits the session
    provider; empty/whitespace specs fall through to the next level.
  • build_summarisation_request — renders a window of older turns
    into a one-shot summarisation request (a system instruction + a single
    user turn with a labelled transcript). Inline-render (not pass-through)
    so no tool schemas are shipped and it stays provider-agnostic.
    tool_use/tool_result semantics are preserved; thinking blocks are
    skipped; the oldest turns are truncated above a char cap so the most
    recent survive verbatim. Empty window does not crash.

Re-exported the four new symbols (SummaryTarget, SummarisationRequest,
resolve_summary_model, build_summarisation_request) from
cothis.ai.__init__.

Contracts preserved

Pure additions only — no changes to the budget primitive, the agent run
loop, provider call semantics, or any existing test. The eviction policy
(slice B) and the run-loop wiring via SessionPhase="compaction" (slice C)
are deferred.

Tests

New tests/test_compaction.py — 30 tests: selector precedence / slash-
parse / fallback / env-not-read-when-override-set, and builder shape /
tool semantics / thinking-skip / length-cap (oldest truncated, recent
preserved) / empty window / system_text override, plus a static source
guard that the module has no top-level provider-SDK import. Full suite:
uv run pytest -q → 1545 passed (2 pre-existing local-only environmental
flakes deselected; both reproduce on clean main).

CI

uv run ruff check — clean. uv run ty check — clean. uv run pytest
green (see Tests).

Compaction epic slice A (of ~3): land the two pure building blocks that
later slices consume, without wiring them into the run loop.

- resolve_summary_model: pick the model/provider that performs the
  summarisation. Precedence: explicit override > COTHIS_SUMMARY_MODEL
  env var > the session pair. A "provider/model" spec splits on the
  first slash; a bare model inherits the session provider. Empty or
  whitespace-only specs fall through to the next level.
- build_summarisation_request: render a window of older turns into a
  single-user-turn summarisation request (a system instruction plus a
  labelled transcript). Inline-render (not pass-through) so no tool
  schemas are shipped and it stays provider-agnostic. tool_use and
  tool_result semantics are preserved; thinking blocks are skipped; the
  oldest turns are truncated above a char cap so the most recent survive
  verbatim.

Pure module (no I/O, no SDK imports) -- preserves the lazy-client
invariant. Self-contained and unit-testable. The eviction policy
(slice B) and the run-loop wiring via SessionPhase="compaction" (slice C)
are deferred.

Iteration I25 (compaction epic slice A, built on the context-budget
primitive). 30 new tests.
@gemone
gemone force-pushed the feat/compaction-summariser branch from 87ff9df to 6486e0d Compare August 6, 2026 20:22
@gemone
gemone merged commit 61351e6 into main Aug 7, 2026
gemone added a commit that referenced this pull request Aug 7, 2026
Compaction epic slice B (of ~3): the pure eviction policy that decides
WHICH older turns to compact vs retain, driven by context-window
pressure.

plan_eviction(*, messages, budget, min_retained_turns=4,
high_target_ratio=0.75, critical_floor_ratio=0.50, ...) ->
EvictionDecision(window, retained, pressure, evicted_token_estimate,
reason). The window is the contiguous prefix slice A's
build_summarisation_request consumes; retained is the verbatim tail.

Pressure gate: no eviction when the budget is unknown or under
NONE/LOW/MEDIUM (MEDIUM is "plan, not act"); HIGH evicts the smallest
safe cut that brings the retained estimate down to
high_target_ratio*capacity; CRITICAL targets the lower
critical_floor_ratio*capacity. Both fall back to the largest safe cut
(down to the retention floor) if no cut meets the target -- CRITICAL
evicts a token-superset of HIGH (monotonic).

The cut respects three invariants: tool_use/tool_result pair closure
(no dangling ids -- a safe-cut walk), a retention floor (the last
min_retained_turns turn-groups always stay verbatim), and strict role
alternation (inherited from the storage layer).

Pure module (no I/O, no SDK imports, no cothis.agent import -- duck-typed
on list[dict]). Self-contained and unit-testable; run-loop wiring, the
actual summarisation call, summary persistence, and auto-trigger are
deferred to slice C. 18 new tests.

Iteration I26 (compaction epic slice B, built on slice A #455 + the
context-budget primitive #448).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant