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 declareBATCHEDand implementrender_batch. Sources for INCREMENTAL strategies additionally providetotal_turns(item_id),render_from(item_id, from_turn), andrender_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 requireapply_incrementalfor the merge step. Two implementations:DurableSummaryStore(SQLite, version-stamped, tree-shaped) andTtlCacheStore(wrapswork_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
summarysource (work_buddy/ir/sources/summary.py) — emits one Document perSummaryNoderow. BM25 fields (title1.75x,summary1.0x,keywords2.0x) plus a combined dense_text. Rebuilt by thesummary-index-rebuildsidecar job every 5 minutes; built ad-hoc viair_index(source="summary"). summary_searchcapability (work_buddy/summarization/funnel.py) — the coarse-to-fine retrieval funnel. Stage 1 ranks summary nodes; stage 2 drills viasession_search(or any registered per-namespace drill handler). Seesummarization/summary_search.drill_treecapability (work_buddy/disclosure/) — the unified navigation contract.domain="summary"walks the per-node tree at three depths (index / summary / full). Seedisclosure/.
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¶
- Implement a
Sourcefor the domain (discover+render, optionallyrender_batchifBATCHED, optionallyrender_from/total_turnsifINCREMENTAL). - Pick (or implement) a
SummaryStrategy— flat, layered, or incremental layered. - Pick a
Store— durable for version-stamped, TTL for cache-style. - Build a binding factory
build_<name>_summarizer() -> Summarizerin the consumer package. - 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.py—SummaryNode,Source/SummaryStrategy/StoreProtocols,Provenance,SummaryCapability(LAYERED, FLAT, BATCHED, INCREMENTAL, PERSISTS_TREE, PERSISTS_FLAT, VERSION_STAMPED, TTL_EVICTED),LLMCaller, exceptions.work_buddy/summarization/summarizer.py—Summarizercomposer +RefreshReport.work_buddy/summarization/orchestrator.py—run_refresh(per-item + batch + incremental paths),as_caller,default_llm_caller(config-driven model chain), provenance assembly.work_buddy/summarization/strategies.py—LayeredDisclosureStrategy,IncrementalLayeredStrategy,FlatExtractionStrategy.work_buddy/summarization/stores.py—DurableSummaryStore(withapply_incremental),TtlCacheStore.work_buddy/summarization/incremental.py—refresh_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.py—summary_searchcoarse-to-fine funnel + default drill handler.work_buddy/summarization/db.py+schema.py— durable SQLite (WAL, idempotent schema; tree-shapedsummary_items+summary_nodestables +summarization_queue).work_buddy/conversation_observability/summarizer_binding.py—SessionSource,build_session_summarizer.work_buddy/collectors/chrome_summarizer_binding.py—ChromeSource,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.py—SummaryTreeDrillablefor the unifieddrill_treecapability.
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.