Skip to content

feat(v3.0): wire API-based summarizer from config (supersedes #26)#27

Merged
RichardHightower merged 6 commits into
mainfrom
feature/v3.0-api-summarizer-wiring-v2
Apr 28, 2026
Merged

feat(v3.0): wire API-based summarizer from config (supersedes #26)#27
RichardHightower merged 6 commits into
mainfrom
feature/v3.0-api-summarizer-wiring-v2

Conversation

@RichardHightower

Copy link
Copy Markdown
Contributor

Summary

Replaces #26 with the same feature (wire ApiSummarizer into the daemon from config + env vars, replacing the hardcoded MockSummarizer TODO) plus the review feedback from #26 applied. All four task pr-precheck gates pass: fmt, clippy --all-targets --all-features -D warnings, full workspace test (~825 tests), and doc with RUSTDOCFLAGS=-D warnings.

What's the same as #26

File Change
crates/memory-daemon/src/commands.rs build_summarizer() reads provider/model from config, resolves API key from env var or config, constructs ApiSummarizer or falls back to MockSummarizer with warning.
crates/memory-types/src/config.rs api_key_env: Option<String> added to SummarizerSettings with serde(default) for backward compat.

What's improved over #26 (review-driven)

Decision logic split (functional core / imperative shell)

Five of #26's seven new tests ended with let _ = summarizer; — they only proved build_summarizer() didn't panic. They couldn't detect a regression that always returned Mock or always returned Api. This PR extracts a pure decision function:

pub(crate) fn pick_summarizer_kind(
    settings: &SummarizerSettings,
    key_available: bool,
) -> SummarizerKind  // Mock | OpenAi | Anthropic

Tests now assert on the exact SummarizerKind returned for every provider × key-availability combination.

API hygiene

  • Drop dead 2-tuple from resolve_api_key. Was fn resolve_api_key(...) -> (Option<String>, &'static str) but the only caller did let (api_key, _) = .... The discarded second element was also misleadingly the default env var name even when api_key_env override was active, so any future caller using it would log the wrong var on a cache miss. Now fn resolve_api_key(...) -> Option<String>.
  • Downgrade pubpub(crate) on build_summarizer, env_var_for_provider, pick_summarizer_kind. memory-daemon has both lib.rs and a [[bin]] target, so pub was leaking test helpers as part of the library's public API.

Helpers + duplication

  • Extract is_anthropic / is_openai / default_env_var_for_provider using eq_ignore_ascii_case (was: 3 sites of .to_lowercase() == "anthropic" with per-call allocations).
  • warn! on unknown providers before fail-open to OpenAI (was: silent fall-through — typos like "openi" or different vendors like "google" would silently route to api.openai.com).

Tests: 7 weak → 16 strong

Group Count Notes
env_var_for_provider mapping 4 Kept as-is
pick_summarizer_kind decision 6 (new) Assert exact SummarizerKind for openai / anthropic / mixed-case / unknown / no-key paths
resolve_api_key resolution 5 (new) Includes precedence regression guard (api_key wins over api_key_env even when env var is set with a different value)
build_summarizer smoke 1 Verifies wiring + Arc::strong_count == 1

The precedence test is the one #26's description claimed but didn't actually have.

Why a new PR instead of force-pushing #26

#26 also added a "v3.0 — Real Summarization / Phase 51: API Summarizer Wiring" section to ROADMAP.md and STATE.md. Local main (not yet on origin/main) has v3.0 Competitive Parity & Benchmarks with Phase 51 = Retrieval Orchestrator, Phase 52 = Simple CLI API, Phase 53 = Benchmark Suite — so #26's ROADMAP edits would collide on phase numbering once local main is pushed. This PR drops the planning edits in a separate revert commit; the summarizer work will be slotted into the existing v3.0 plan in a follow-up (likely a new Phase 54 "Real Summarization", or folded into Phase 51's LLM reranker work).

CI expectations

All checks should mirror #26 (which was fully green): Format, Clippy, Test (ubuntu+macos), Build (ubuntu+macos), Documentation, E2E Tests, and the full E2E CLI matrix across claude-code / gemini / opencode / copilot / codex on both ubuntu-24.04 and macos-latest.

Closes #26

- Read provider/model/api_key_env from SummarizerSettings config
- Construct ApiSummarizer (OpenAI or Anthropic) when API key available
- Fallback to MockSummarizer with warning when no API key found
- Add build_summarizer() function for testable summarizer construction
- Add unit tests for provider selection and env var lookup
- Update planning docs for v3.0
PR #26 added a "v3.0 — Real Summarization" section to ROADMAP.md and
a v3.0 in-progress block to STATE.md, both numbering this work as
Phase 51. The local main branch (not yet on origin/main) already has
"v3.0 Competitive Parity & Benchmarks" with Phase 51 = Retrieval
Orchestrator, Phase 52 = Simple CLI API, Phase 53 = Benchmark Suite.
Carrying PR #26's planning edits forward would create a phase-numbering
collision when local main is pushed.

Slot the summarizer wiring into the existing v3.0 plan in a follow-up
(likely a new Phase 54 "Real Summarization", or fold into Phase 51's
LLM reranker work).
Addresses code-review findings on PR #26 (closing #26 in favor of this
PR). All four pr-precheck gates pass: fmt, clippy --all-targets
--all-features -D warnings, full workspace test (~825 tests), doc with
RUSTDOCFLAGS=-D warnings.

Decision logic split (functional core / imperative shell):
- Extract pure pick_summarizer_kind(settings, key_available)
  -> SummarizerKind so tests assert on the branch taken instead of
  only verifying non-panic. Five of seven original tests ended with
  `let _ = summarizer;` which couldn't detect a regression that, e.g.,
  always returned Mock.

API hygiene:
- Drop dead 2-tuple from resolve_api_key. The second element was always
  discarded by the only caller and misleadingly returned the *default*
  env var name even when api_key_env override was active, so any future
  caller using it would log the wrong var on a cache miss.
- Downgrade pub -> pub(crate) on build_summarizer, env_var_for_provider,
  and pick_summarizer_kind. memory-daemon has both lib.rs and a [[bin]]
  target, so pub leaked test helpers as part of the library's public
  API.

Helpers + duplication:
- Extract is_anthropic / is_openai / default_env_var_for_provider using
  eq_ignore_ascii_case (was: 3 sites of `.to_lowercase() == "anthropic"`
  with per-call allocations).
- Warn on unknown providers before fail-open to OpenAI (was: silent
  fall-through that would have routed an OPENAI_API_KEY to api.openai.com
  for typos like "openi" or genuinely different providers like "google").

Tests (7 weak -> 16 strong):
- 4 env_var_for_provider mapping tests (kept as-is)
- 6 pick_summarizer_kind decision tests (NEW: assert exact SummarizerKind
  for openai / anthropic / mixed-case / unknown-provider / no-key paths)
- 5 resolve_api_key tests (NEW: regression guard for the precedence
  ordering api_key > api_key_env > provider default; explicit override
  vs custom env var; missing custom env var)
- 1 build_summarizer smoke test (verifies wiring + Arc allocation)

Closes #26
CI runs `dtolnay/rust-toolchain@stable` which now ships clippy from
Rust 1.95, where `clippy::unnecessary_sort_by` is enforced under
`-D warnings`. The pre-existing `sort_by(|a, b| b.1.cmp(&a.1))` on
stale_filter.rs:163 fires the lint and blocks CI for any branch.

Replace with the canonical `sort_by_key(|b| Reverse(b.1))` form
suggested by clippy. Semantically equivalent (descending order by .1).

Local toolchain (1.93) does not have this lint yet, which is why this
slipped through `task pr-precheck` here. Fixed in this PR to unblock
CI; benefits #25 and any other open branch.
Round 2 of the Rust 1.95 stable upgrade adaptation. Previous commit
fixed stale_filter.rs:163 which let CI advance past memory-retrieval,
where it then surfaced the same lint in three more sites. Workspace
grep for the simple `cmp(&)` form (which the lint flags) covers all
remaining cases:

- memory-storage/src/db.rs:462 (sort_by_key on start_time, ascending)
- memory-storage/src/db.rs:481 (same in get_child_nodes)
- memory-service/src/agents.rs:85 (Reverse for descending last_seen_ms)
- memory-toc/src/summarizer/mock.rs:122 (Reverse for descending count)

The remaining sort_by sites in the workspace use multi-line closures
or partial_cmp (Option-returning) which the lint cannot auto-simplify;
they pass clippy as-is.

All semantically equivalent. Tests on the three modified crates
remain green (110 + 42 + 70 + doctests).
@RichardHightower RichardHightower merged commit 3a73582 into main Apr 28, 2026
20 checks passed
@RichardHightower RichardHightower deleted the feature/v3.0-api-summarizer-wiring-v2 branch April 28, 2026 00:03
RichardHightower added a commit that referenced this pull request Apr 28, 2026
…al phase stack

Sync planning files with origin/main reality after PR #27 merged as `3a73582`:

- ROADMAP.md: insert Phase 51.5 (API Summarizer Wiring) between Phases 51
  and 52 as a decimal out-of-band phase. Marks complete (merged 2026-04-28).
- STATE.md: bump status to in_progress, total_phases 3 -> 4, completed 0 -> 1.
  Add Out-of-band Work section flagging:
  - PR #25 (cross-project memory) phase-numbering conflict with local Phase 51
  - Local-only gsd/phase-{51..58} stack (~80 commits, no PRs yet)

Phase 51.5 supersedes closed PR #26 with stronger tests (16 vs 7) and a
pick_summarizer_kind decision split. Inserted out-of-band because the
summarizer wiring shipped before Phase 51 (Retrieval Orchestrator) itself.

No code changes. pr-precheck (fmt + clippy + test + doc) passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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