Skip to content

Summarization Framework

Composition-based summarization — Source × Strategy × Store with a shared refresh orchestrator. Two compositions today: conversation sessions (layered disclosure → durable store) and Chrome tabs (flat extraction → TTL cache).

Details

A protocol-based composition framework for content summarization, modeled on the artifact system. Summarizer = Source × Strategy × Store — three pluggable axes plus a written-once shared core (refresh orchestrator, composer, construction-time coherence checks, provenance stamping).

The three axes

  • Source — domain adapter. discover(window) -> [(item_id, freshness_token)], render(item_id) -> prompt text. Per domain (sessions, web pages, event streams). May declare BATCHED and implement render_batch. Sources for INCREMENTAL strategies additionally provide total_turns(item_id), render_from(item_id, from_turn), and render_range(item_id, from_turn, to_turn) so the algorithm can slice fresh-tail input without re-feeding finalized history.
  • SummaryStrategy — output-shape adapter. Owns system prompt + output JSON schema + parse(structured_output, raw) -> SummaryNode. Per output shape (layered disclosure, flat extraction, incremental layered).
  • Store — persistence + staleness adapter. is_fresh, select_stale, save, load, record_error. INCREMENTAL strategies additionally require apply_incremental for the merge step. Two implementations: DurableSummaryStore (SQLite, version-stamped, tree-shaped) and TtlCacheStore (wraps work_buddy.llm.cache).

Provenance (model / backend / version / timestamp stamping) is uniform and baked into the core — not a pluggable axis. The framework's Store is responsible for caching; LLMRunner is called without cache_ttl_minutes to avoid double-caching.

Tree-shaped record invariant

Every stored summary is a SummaryNode tree: {summary, source_ref?, children: [], extra}. Flat extraction = depth-1 (root only, empty children, null source_ref). Layered disclosure = root + children, each child carrying a source_ref pointer to exact source events. Incremental layered = same shape; each refresh preserves finalized children verbatim and rewrites only the trailing-onward region. The durable store schema persists arbitrary-depth trees with a source_ref slot on every node.

Composer + coherence

Summarizer(source=, strategy=, store=) validates coherence at construction. Today's checks: LAYERED strategy requires PERSISTS_TREE store; FLAT strategy requires PERSISTS_FLAT or PERSISTS_TREE; BATCHED must be declared on both source and strategy or neither; INCREMENTAL requires the source to provide render_from / total_turns (duck-typed) and the store to provide apply_incremental. Violations raise IncoherentComposition.

At each refresh / refresh_one the composer re-bridges the strategy's prompt_version / schema_version into the store via set_strategy_versions, so a version bump invalidates stored rows on the next staleness check.

Current compositions

Composition Source Strategy Store Used by
conversation_session SessionSource LayeredDisclosureStrategy (v1; legacy callers) OR IncrementalLayeredStrategy (v2; queue worker) DurableSummaryStore(namespace="conversation_session") — selection_version=1 for v1, 2 for v2 dashboard /api/chats/<id>/topics, /wb-session-identify, claude_session_summary collector, session_summary_get MCP capability, the summarization-worker sidecar job
chrome_page ChromeSource (per-call) FlatExtractionStrategy (BATCHED) TtlCacheStore(key_prefix="summarize_tab", ttl=30m) chrome_infer._summarize_tabs, pipelines/chrome.py

The lazy singleton get_session_summarizer() always builds v1; the worker explicitly constructs v2 via build_session_summarizer(use_incremental=True). The split keeps legacy v1-shape callers (tests, query helpers) on v1 strategy without flipping under them when the production flag changes.

Consumers / search surface

Summaries are made searchable and drillable by two separate but coordinated layers:

  • IR summary source (work_buddy/ir/sources/summary.py) — emits one Document per SummaryNode row. BM25 fields (title 1.75x, summary 1.0x, keywords 2.0x) plus a combined dense_text. Rebuilt by the summary-index-rebuild sidecar job every 5 minutes; built ad-hoc via ir_index(source="summary").
  • summary_search capability (work_buddy/summarization/funnel.py) — the coarse-to-fine retrieval funnel. Stage 1 ranks summary nodes; stage 2 drills via session_search (or any registered per-namespace drill handler). See summarization/summary_search.
  • drill_tree capability (work_buddy/disclosure/) — the unified navigation contract. domain="summary" walks the per-node tree at three depths (index / summary / full). See disclosure/.

A new summarizable domain becomes searchable + drillable as soon as it ships a composition: no per-domain IR source or navigator to write.

Adding a new composition

  1. Implement a Source for the domain (discover + render, optionally render_batch if BATCHED, optionally render_from/total_turns if INCREMENTAL).
  2. Pick (or implement) a SummaryStrategy — flat, layered, or incremental layered.
  3. Pick a Store — durable for version-stamped, TTL for cache-style.
  4. Build a binding factory build_<name>_summarizer() -> Summarizer in the consumer package.
  5. Optional: surface read/write shims in the consumer package if existing call-sites depend on a legacy API.

Unit-test the composition by injecting a stub LLM via as_caller(stub_fn) — the framework normalizes legacy bare-dict-returning stubs.

Key files

  • work_buddy/summarization/protocol.pySummaryNode, Source/SummaryStrategy/Store Protocols, Provenance, SummaryCapability (LAYERED, FLAT, BATCHED, INCREMENTAL, PERSISTS_TREE, PERSISTS_FLAT, VERSION_STAMPED, TTL_EVICTED), LLMCaller, exceptions.
  • work_buddy/summarization/summarizer.pySummarizer composer + RefreshReport.
  • work_buddy/summarization/orchestrator.pyrun_refresh (per-item + batch + incremental paths), as_caller, default_llm_caller (config-driven model chain), provenance assembly.
  • work_buddy/summarization/strategies.pyLayeredDisclosureStrategy, IncrementalLayeredStrategy, FlatExtractionStrategy.
  • work_buddy/summarization/stores.pyDurableSummaryStore (with apply_incremental), TtlCacheStore.
  • work_buddy/summarization/incremental.pyrefresh_one_incremental + build_incremental_prompt + pathway selection (single-call vs chunked) + per-tier budget resolution.
  • work_buddy/summarization/queue.py + worker.py — SQLite-backed queue + worker tick with cooldown + daily-budget circuit-breaker.
  • work_buddy/summarization/funnel.pysummary_search coarse-to-fine funnel + default drill handler.
  • work_buddy/summarization/db.py + schema.py — durable SQLite (WAL, idempotent schema; tree-shaped summary_items + summary_nodes tables + summarization_queue).
  • work_buddy/conversation_observability/summarizer_binding.pySessionSource, build_session_summarizer.
  • work_buddy/collectors/chrome_summarizer_binding.pyChromeSource, build_chrome_summarizer, summarize_tabs (the public Chrome entry).
  • work_buddy/ir/sources/summary.py — IR adapter for the per-node summary store.
  • work_buddy/disclosure/summary_tree.pySummaryTreeDrillable for the unified drill_tree capability.

Tests: tests/unit/test_summarization_framework.py, tests/unit/test_summarization_store.py, tests/unit/test_incremental_strategy.py, tests/unit/test_summarization_queue.py, tests/unit/test_chrome_summarization.py, tests/unit/test_conversation_observability_summaries.py, tests/unit/test_ir_summary_source.py, tests/unit/test_summarization_funnel.py, tests/unit/test_disclosure.py.