Skip to content

feat: summarizer surface on LCMEngine + lcm_synthesize_around adapter — closes #156 (#164 PR-2)#166

Open
100yenadmin wants to merge 2 commits into
mainfrom
fix/164-pr2-summarizer-surface
Open

feat: summarizer surface on LCMEngine + lcm_synthesize_around adapter — closes #156 (#164 PR-2)#166
100yenadmin wants to merge 2 commits into
mainfrom
fix/164-pr2-summarizer-surface

Conversation

@100yenadmin
Copy link
Copy Markdown
Member

@100yenadmin 100yenadmin commented May 19, 2026

Summary

PR 2 of the unified 5-PR compaction-P0 sequence (#164). Builds the summarizer surface on LCMEngine and wires the 8th dispatch adapter, closing #156 at 8/8 tool-dispatch coverage. Builds on PR 1 (#165, merged) which landed HermesSummarizerDeps.

Follows the #164 unified plan's §4 (PR-2), Q-F (construction point), and Q-G (lcm_synthesize_around wiring).

1 · The summarizer surface (engine/__init__.py + engine/lifecycle.py)

on_session_start now constructs a HermesSummarizerDeps (PR-1's shim) and an LcmSummarizer from it, exposing three engine handles:

  • engine.deps — the public SummarizerDeps.
  • engine.summarize — the bound LcmSummarizer.summarize, the SummarizeFn-shaped callable ((text, aggressive=False, options=None) -> str) that CompactionEngine.compact_full_sweep (PR 3) and the lcm_synthesize_around build_llm_call factory consume.
  • engine._summarizer — the LcmSummarizer object (the build_llm_call factory reads .candidates off it for the Wave-12 F8 audit-honest model name).

Built once per DB-open inside the non-re-entrant if self._db is None: block — config-scoped, not session-scoped (a re-entrant on_session_start for a new session on an already-open DB does not rebuild it; identity stable). Nulled in on_session_end for symmetry with the four stores. Three Optional state fields added to LCMEngine.__init__, defaulting None; SummarizerDeps / LcmSummarizer / SummarizeFn imported under TYPE_CHECKING (annotation-only here — construction is lazy in lifecycle.py) to dodge any engine↔summarize cycle.

2 · The lcm_synthesize_around dispatch adapter (tools/_adapters.py)

_adapt_lcm_synthesize_around — the deferred #156 PR-3 work, unblocked by the summarizer surface. Unlike the four PR-1 attribute-bag adapters, SynthesizeAroundContext.build_llm_call is a factory (() -> tuple[LlmCall, str]).

  • _SynthesizeAroundCtx — a frozen dataclass structurally satisfying SynthesizeAroundContext (conn / conversation_store / timezone / build_llm_call). conn/conversation_store/timezone map from engine state like the other adapters; deps reuses the existing _build_deps helper.
  • _build_synthesizer_llm_call — the BuildLlmCall factory body, a faithful port of the TS buildLlmCallFromSummarizer (lcm-synthesize-around-tool.ts:608-618 @ 1f07fbd) composed with the TS handler's own summarizer wiring (:1019-1037). It closes over engine._summarizer, bridges sync→async (LlmCall is async, LcmSummarizer.summarize is sync per ADR-017), passes LcmSummarizeOptions(is_condensed=True) (synthesis is a condensed-tier rollup — TS {isCondensed: true}), measures latency, and records actual_model as the resolved primary candidate (Wave-12 F8 audit-honesty). It raises RuntimeError on an unset/empty summarizer; the handler's existing try/except around ctx.build_llm_call() converts that into the structured "No summarization model resolved" tool-error.
  • Registered: TOOL_DISPATCH["lcm_synthesize_around"] = _adapt_lcm_synthesize_around.

3 · #156 close-out — dispatch coverage 8/8

sorted(TOOL_DISPATCH) is now: lcm_compact, lcm_describe, lcm_doctor, lcm_get_entity, lcm_grep, lcm_search_entities, lcm_status, lcm_synthesize_around8 tools, equal to the advertised get_tool_schemas() surface. lcm_expand stays deferred per ADR-037 (absent from both TOOL_DISPATCH and TOOL_SCHEMAS).

  • _NOT_YET_ADAPTED in tests/test_dispatch_registry_coverage.py is now empty; the xfail ratchet is fully discharged (no XPASS failures — confirmed locally). The advertised-surface test additionally asserts set(TOOL_DISPATCH) equals the advertised surface.
  • tests/test_tool_dispatch.py's exact-membership test (test_tool_dispatch_holds_diagnostic_and_pr1_pr2_adapted_tools) asserted the un-wired state — it explicitly expected lcm_synthesize_around absent ("lands in [P0] Core lcm_* agent tools never dispatch — dispatch-adapter layer never built #156 PR-3"). Renamed to test_tool_dispatch_holds_all_eight_adapted_tools and updated to include it, preserving its real intent (a tight membership pin that fails if a tool is wired before its prerequisites or dropped).

4 · /lcm doctor apply side-effect (confirmed, not regressed)

commands/doctor.py:261-262 probes getattr(engine, "deps"/"summarize", None). Before this PR both always returned None, so apply_scoped_doctor_repair hit its kind="unavailable" arm unconditionally. Now engine.summarize is a non-None callable and engine.deps is a real SummarizerDeps_resolve_doctor_apply_summarize resolves a usable summarizer, so the "unavailable" arm no longer fires. A test pins both directions: the resolver returns a usable summarizer from the engine's surface, and still returns None on a pre-init engine (the arm is conditional, not dead, not always-on).

5 · README knob-docs

Documents the optional auxiliary.lcm_summary Hermes config entry (provider/model/timeout for LCM summarization + synthesis), folding in the PR #165 review MINOR — mirrors hermes_llm.py's module docstring into the operator-facing configuration section.

Test plan

New test files (mirror tests/tools/test_compact_adapter.py; the #161 review established an adapter must not ship with zero direct coverage):

  • tests/test_summarizer_surface.py — surface presence/null-out, SummarizerDeps conformance, on_session_start idempotence, engine.summarize round-trip against a fake complete(), both doctor-apply confirmations.
  • tests/tools/test_synthesize_around_adapter.py_build_synthesizer_llm_call (awaitable LlmCall, actual_model = primary candidate, raises on bare/empty-chain engine); _adapt_lcm_synthesize_around (engine-state-unset structured error, end-to-end dispatch writing a lcm_synthesis_cache row, degrade-to-tool-error when unconfigured); _SynthesizeAroundCtx structural conformance.
  • tests/test_dispatch_registry_coverage.py + tests/test_tool_dispatch.py — ratchet flipped to 8/8.

Verified locally (LEXAR repo): the new/changed test files + test_dispatch_registry_coverage.py + test_tool_dispatch.py + test_compact_adapter.py (104 passed), plus test_lcm_synthesize_around.py / test_hermes_llm.py / test_handle_tool_call_ingest.py / test_lifecycle.py / test_engine_state_fields.py (no regression). ruff check / ruff format / ty check src clean (the one pre-existing ty unused-ignore-comment warning in db/connection.py is untouched by this PR and ty exits 0). Full suite verification is via GitHub CI ({macOS, ubuntu} × {py3.11, 3.12, 3.13}).

Part of #164. Closes #156.

Summary by CodeRabbit

  • New Features

    • Completed lcm_synthesize_around tool implementation with full dispatch adapter support (8/8 coverage).
    • Added auxiliary LLM configuration pinning for summarization with fallback to auto-detection.
  • Documentation

    • Added README section explaining how to configure and pin the auxiliary LLM model for summarization tasks.
  • Tests

    • Added comprehensive test coverage for summarizer lifecycle and initialization.
    • Added end-to-end tests for the new synthesis tool adapter.
    • Updated dispatch registry coverage tests to reflect completed 8-tool coverage.

Review Change Stack

Eva added 2 commits May 19, 2026 23:10
#164 PR-2: build the summarizer surface on LCMEngine and wire the 8th
dispatch adapter, closing #156 at 8/8 tool-dispatch coverage.

Summarizer surface (engine/__init__.py + engine/lifecycle.py):
on_session_start now constructs a HermesSummarizerDeps (PR-1's shim) and
an LcmSummarizer from it, exposing engine.deps (SummarizerDeps),
engine.summarize (the bound SummarizeFn), and engine._summarizer (the
LcmSummarizer object the build_llm_call factory needs for .candidates).
Built once per DB-open inside the `if self._db is None:` block —
config-scoped, not session-scoped — and nulled in on_session_end for
symmetry with the stores. This also activates /lcm doctor apply's real
summarizer path: commands/doctor.py's getattr(engine, "deps") probe now
resolves, so apply_scoped_doctor_repair only renders "unavailable" when
the candidate chain is genuinely empty rather than unconditionally.

lcm_synthesize_around adapter (tools/_adapters.py): _adapt_lcm_synthesize
_around builds a frozen _SynthesizeAroundCtx and a build_llm_call factory
that wires the synthesis dispatcher's async LlmCall to the engine's
configured summarizer chain — a faithful port of TS buildLlmCallFrom
Summarizer (lcm-synthesize-around-tool.ts:608-618 @ 1f07fbd) plus the
TS handler's summarizer wiring (:1019-1037). The factory bridges sync
LcmSummarizer.summarize -> async LlmCall and records the resolved primary
candidate model (Wave-12 F8 audit-honesty). Registered into TOOL_DISPATCH
in engine/__init__.py.

#156 close-out: _NOT_YET_ADAPTED in test_dispatch_registry_coverage.py is
now empty; the advertised-surface test additionally asserts set(TOOL_
DISPATCH) equals the advertised surface (8/8). test_tool_dispatch.py's
exact-membership test updated to include lcm_synthesize_around.

Closes #156
…ADME knob

Direct tests for #164 PR-2 plus the operator-facing knob doc.

tests/test_summarizer_surface.py — pins the summarizer surface on
LCMEngine: the three handles (deps / summarize / _summarizer) are None
on a bare engine and post-on_session_end, populated between; deps
satisfies the SummarizerDeps Protocol; on_session_start is idempotent
(the surface is config-scoped, identity stable on re-entry);
engine.summarize round-trips against a fake complete(); and the /lcm
doctor apply confirmation — _resolve_doctor_apply_summarize resolves the
engine's real surface to a usable summarizer (not the "unavailable" arm)
and still returns None on a pre-init engine (the arm is conditional, not
dead and not always-on).

tests/tools/test_synthesize_around_adapter.py — direct coverage for the
adapter (the #161 review established an adapter must not ship with zero
direct tests; mirrors test_compact_adapter.py): _build_synthesizer_llm
_call yields an awaitable LlmCall whose output is the summary and whose
actual_model is the resolved primary candidate (Wave-12 F8), and raises
on a bare engine / empty candidate chain; _adapt_lcm_synthesize_around
degrades to a structured tool-error when engine state is unset, builds
SynthesizeAroundContext correctly and dispatches end-to-end (a non-error
tool_result + a lcm_synthesis_cache row in status='ready'), and degrades
to "No summarization model resolved" when the summarizer is unconfigured;
_SynthesizeAroundCtx structurally satisfies the SynthesizeAroundContext
Protocol.

README.md — documents the optional auxiliary.lcm_summary Hermes config
entry (provider/model/timeout for LCM summarization + synthesis), folding
in the PR #165 review MINOR; mirrors hermes_llm.py's module docstring
into the operator-facing configuration section.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

Completes dispatch adapter coverage by implementing lcm_synthesize_around adapter and underlying engine "summarizer surface" lifecycle. Adds type-safe summarizer state management to LCMEngine with initialization/teardown in on_session_start/on_session_end, implements the adapter as a bridge between sync summarization and async dispatch, and updates the #156 coverage ratchet to reflect 8/8 dispatch completion.

Changes

LCM Summarize-Around Dispatch Adapter & Summarizer Surface

Layer / File(s) Summary
Summarizer surface contract and lifecycle
src/lossless_hermes/engine/__init__.py, src/lossless_hermes/engine/lifecycle.py, README.md
LCMEngine declares deps, summarize, and _summarizer attributes (initialized to None), wired during on_session_start with deferred imports of HermesSummarizerDeps and LcmSummarizer, and cleared on on_session_end. Configuration hierarchy and precedence for auxiliary.lcm_summary provider/model/timeout documented.
Summarizer surface lifecycle tests
tests/test_summarizer_surface.py
Validates bare-engine state, population/clearing during session lifecycle, idempotent rebuilding (identity-stable on already-open DB), end-to-end summarization with fake LLM deps, candidate resolution from summary_provider/summary_model, and /lcm doctor apply resolver integration. Test doubles avoid external calls.
lcm_synthesize_around dispatch adapter implementation
src/lossless_hermes/tools/_adapters.py, src/lossless_hermes/engine/__init__.py
Introduces _SynthesizeAroundCtx dataclass with lazy build_llm_call factory, _build_synthesizer_llm_call helper validating summarizer readiness and wrapping sync calls in async LlmCall interface, and _adapt_lcm_synthesize_around main adapter performing engine-state checks and delegating to handler. Registered in TOOL_DISPATCH.
lcm_synthesize_around adapter and context tests
tests/tools/test_synthesize_around_adapter.py
Unit/integration tests for _build_synthesizer_llm_call (callable/model resolution, awaitable result, error cases), _SynthesizeAroundCtx protocol conformance, adapter engine-state error handling (structured JSON on uninitialized DB), end-to-end dispatch with seeded conversation and cache-row verification, and summarizer-unconfigured degradation. Deterministic test doubles bypass external LLM.
Dispatch coverage regression ratchet update (8/8)
tests/test_dispatch_registry_coverage.py, tests/test_tool_dispatch.py
Clears _NOT_YET_ADAPTED (now empty), adjusts lcm_synthesize_around test args, removes PR-sequence-specific xfail reasoning, updates assertions/messages for 8/8 coverage, and strengthens surface-consistency check: set(TOOL_DISPATCH) must exactly equal advertised tool set to catch advertised-but-not-dispatchable or dispatchable-but-not-advertised drift.

Sequence Diagram(s)

sequenceDiagram
  participant Engine as LCMEngine
  participant Lifecycle as on_session_start
  participant Summarizer as LcmSummarizer
  participant Adapter as _adapt_lcm_synthesize_around
  participant Handler as handle_lcm_synthesize_around
  participant DB as lcm_synthesis_cache

  Lifecycle->>Summarizer: __init__ with config<br/>(summary_provider hint)
  Summarizer-->>Lifecycle: self._summarizer ready<br/>(candidates resolved)
  Lifecycle->>Engine: engine.summarize = synthesizer.summarize
  Lifecycle->>Engine: engine._summarizer = summarizer
  
  Adapter->>Engine: check _db, _conversation_store
  Adapter->>Summarizer: _build_synthesizer_llm_call()<br/>lazy factory
  Adapter->>Handler: _SynthesizeAroundCtx with factory
  Handler->>Adapter: await build_llm_call()
  Adapter->>Summarizer: summarize() + timing
  Summarizer-->>Adapter: LlmCallResult (text, actual_model)
  Handler->>DB: INSERT lcm_synthesis_cache<br/>status='ready', resolved_model
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • electricsheephq/lossless-hermes#165: The main PR defers-imports and constructs HermesSummarizerDeps (introduced in #165) during on_session_start, providing the LLM completion backend for the synthesizer surface.
  • electricsheephq/lossless-hermes#30: Main PR extends the same on_session_start/on_session_end lifecycle introduced in #30 by layering summarizer-surface construction and cleanup on top of the DB-open and store-wiring behavior.
  • electricsheephq/lossless-hermes#159: Main PR extends the same per-tool dispatch adapter and TOOL_DISPATCH wiring infrastructure used for the other lcm_* tools in #159, adding registration and adapter support for lcm_synthesize_around to complete #156 coverage.

Poem

🐰 Whispers excitedly

Eight tools now dispatch with grace,
Summarizer surfaces find their place—
The factory builds, the adapter calls,
Cache rows dance through hallowed halls,
Coverage complete, the ratchet sleeps tight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a summarizer surface to LCMEngine and implementing the lcm_synthesize_around adapter, which are the primary objectives of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/164-pr2-summarizer-surface

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lossless_hermes/engine/lifecycle.py (1)

288-345: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against partial initialization when summarizer setup fails.

self._db and stores are assigned before summarizer construction. If that block throws once, the engine is left “DB-open but summarizer-unset”; later on_session_start calls short-circuit on self._db is not None, so the summarizer surface never recovers.

Suggested fix
-        # Wire state onto the shell class. The four stores share the
-        # one connection — ADR-017 §"Synchronous by design" means each
-        # store is a thin CRUD wrapper, not a lifecycle owner.
-        self._db = conn
-        self._conversation_store = ConversationStore(
+        # Build collaborators first; only publish to self after all
+        # construction succeeds to avoid partial-init state.
+        conversation_store = ConversationStore(
             conn,
             fts5_available=features.fts5_available,
         )
-        self._summary_store = SummaryStore(
+        summary_store = SummaryStore(
             conn,
             fts5_available=features.fts5_available,
             trigram_tokenizer_available=features.fts5_trigram_available,
         )
-        self._telemetry_store = CompactionTelemetryStore(conn)
-        self._maintenance_store = CompactionMaintenanceStore(conn)
+        telemetry_store = CompactionTelemetryStore(conn)
+        maintenance_store = CompactionMaintenanceStore(conn)
@@
-        self.deps = HermesSummarizerDeps()
+        deps = HermesSummarizerDeps()
@@
-        summarizer = LcmSummarizer(
-            deps=self.deps,
+        summarizer = LcmSummarizer(
+            deps=deps,
             config=self.config,
             provider_hint=self.config.summary_provider or None,
             model_hint=self.config.summary_model or None,
         )
-        self._summarizer = summarizer
-        # ``self.summarize`` is the bound ``LcmSummarizer.summarize`` —
-        # the ``SummarizeFn``-shaped callable downstream consumers
-        # (``CompactionEngine.compact_full_sweep``, the
-        # ``lcm_synthesize_around`` ``build_llm_call`` factory) expect.
-        self.summarize = summarizer.summarize
+        summarize = summarizer.summarize
+
+        # Publish once everything succeeded.
+        self._db = conn
+        self._conversation_store = conversation_store
+        self._summary_store = summary_store
+        self._telemetry_store = telemetry_store
+        self._maintenance_store = maintenance_store
+        self.deps = deps
+        self._summarizer = summarizer
+        self.summarize = summarize
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lossless_hermes/engine/lifecycle.py` around lines 288 - 345, The code
assigns self._db and the various stores before constructing the summarizer, so
if HermesSummarizerDeps/LcmSummarizer construction fails the engine remains
“DB-open but summarizer-unset” and future on_session_start short-circuits; fix
by constructing the summarizer (create HermesSummarizerDeps(), then
LcmSummarizer and bind self._summarizer/self.summarize) before assigning
self._db and initializing ConversationStore, SummaryStore,
CompactionTelemetryStore, and CompactionMaintenanceStore, or alternatively wrap
summarizer construction in try/except and on exception undo/reset self._db and
any created stores so the object stays in a consistent state (references:
HermesSummarizerDeps, LcmSummarizer, self._summarizer, self.summarize, self._db,
ConversationStore, SummaryStore, CompactionTelemetryStore,
CompactionMaintenanceStore, on_session_start).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/lossless_hermes/engine/lifecycle.py`:
- Around line 288-345: The code assigns self._db and the various stores before
constructing the summarizer, so if HermesSummarizerDeps/LcmSummarizer
construction fails the engine remains “DB-open but summarizer-unset” and future
on_session_start short-circuits; fix by constructing the summarizer (create
HermesSummarizerDeps(), then LcmSummarizer and bind
self._summarizer/self.summarize) before assigning self._db and initializing
ConversationStore, SummaryStore, CompactionTelemetryStore, and
CompactionMaintenanceStore, or alternatively wrap summarizer construction in
try/except and on exception undo/reset self._db and any created stores so the
object stays in a consistent state (references: HermesSummarizerDeps,
LcmSummarizer, self._summarizer, self.summarize, self._db, ConversationStore,
SummaryStore, CompactionTelemetryStore, CompactionMaintenanceStore,
on_session_start).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 21325fd9-46ed-4871-ad1e-a74e9d654c88

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce07b4 and d1d852c.

📒 Files selected for processing (8)
  • README.md
  • src/lossless_hermes/engine/__init__.py
  • src/lossless_hermes/engine/lifecycle.py
  • src/lossless_hermes/tools/_adapters.py
  • tests/test_dispatch_registry_coverage.py
  • tests/test_summarizer_surface.py
  • tests/test_tool_dispatch.py
  • tests/tools/test_synthesize_around_adapter.py

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