Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ installable release; see the roadmap in [README.md](README.md).

### Changed

- **`query_strategy` default flipped from `legacy-bm25` to `stack-r1-r3`** ([#718](https://github.com/robotrocketscience/aelfrice/issues/718)). PR-3 of the #291 sequencing. `DEFAULT_STRATEGY` in `aelfrice.query_understanding.strategy` now resolves to `STACK_R1_R3_STRATEGY`, which cascades through `RebuilderConfig.query_strategy` (default + malformed-config fallback) and `rebuild_v14`'s kwarg default. Bench evidence captured 2026-05-12 on the labeled query-strategy corpus (30 rows): mean NDCG@k 0.3006 → 0.5858 (**+0.2851 absolute, +94.8%**, ~5.7× the +0.05 flip-default threshold); p99 latency 3.84 ms → 4.50 ms (+0.66 ms, 13% of the documented 5 ms budget). Both `tests/bench_gate/test_query_strategy_uplift` and `test_query_strategy_latency` PASS on `github/main`. `legacy-bm25` remains callable via explicit `query_strategy="legacy-bm25"` (TOML `[rebuilder] query_strategy = "legacy-bm25"` or kwarg) until PR-4 removes the code path one minor release after the flip.

- **`WonderResult.coverage` clamped to `[0.0, 1.0]`** ([#667](https://github.com/robotrocketscience/aelfrice/issues/667)). The `coverage` scalar emitted by `aelf wonder --axes QUERY` (and the structured `WonderResult` dataclass) is now `min(1.0, phantoms_created / max(1, len(research_axes)))` instead of the raw ratio. Prior to #667 the field could exceed `1.0` if any axis produced more than one ingested phantom — dormant because `phantoms_created` is currently always `0` until #229's store-write lane lands, but it would have started producing values outside the documented bound the moment that changed. The clamp picks the cheap path (Option A in the issue) without re-plumbing per-axis success counts through `wonder_ingest`. New `aelfrice.wonder.result.axes_coverage(phantoms_created, n_axes) -> float` helper is the shared construction surface (cli.py call site + tests use the same expression). Six new property-flavoured tests pin the bound; the dataclass docstring is updated so prose and formula match.

- **`aelf wonder` query agent-count shorthand** ([#645](https://github.com/robotrocketscience/aelfrice/issues/645)). The QUERY string (both the positional and the explicit `--axes QUERY` flag) recognises agent-count shorthand of the form `quick N-agent`, `deep N-agent`, or bare `N-agent` (with an optional trailing literal "wonder"), e.g. `aelf wonder "quick 2-agent wonder about indentation"` parses to `agent_count=2` and gap-analysis query `"about indentation"`. Matches the agentmemory ergonomic. New `_parse_wonder_query_shorthand(query) -> (cleaned_query, count|None)` is the parser; case-insensitive; shorthand mid-query is stripped cleanly. Explicit `--axes-agents N` takes precedence over shorthand. No effect on no-arg `aelf wonder` (graph-walk).
Expand Down
22 changes: 11 additions & 11 deletions src/aelfrice/context_rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def rebuild_v14(
session_id_for_log: str | None = None,
floor_session: float = 0.0,
floor_l1: float = 0.0,
query_strategy: str = LEGACY_STRATEGY,
query_strategy: str = DEFAULT_QUERY_STRATEGY,
working_state: "WorkingState | None" = None,
) -> str:
"""v1.4 rebuild: L0 + session-scoped + L2.5/L1 via `retrieve()`.
Expand Down Expand Up @@ -348,11 +348,10 @@ def rebuild_v14(
are unaffected.

v1.7 (#291 PR-2): `query_strategy` selects the query rewriter.
Default `legacy-bm25` is byte-identical to the v1.4 path.
`stack-r1-r3` opts into the ratified R1 entity-expansion + R3
per-store IDF-clip stack from `aelfrice.query_understanding`.
The default flip lands in PR-3 after a clean #288 phase-1b
operator-week.
Default `stack-r1-r3` (since #291 PR-3 / #718) runs the ratified
R1 entity-expansion + R3 per-store IDF-clip stack from
`aelfrice.query_understanding`. `legacy-bm25` is byte-identical
to the v1.4 path and remains opt-in until PR-4 removes it.
"""
locked: list[Belief] = store.list_locked_beliefs()
locked_ids: set[str] = {b.id for b in locked}
Expand Down Expand Up @@ -631,11 +630,12 @@ class RebuilderConfig:
`[rebuild_floor] l1` in .aelfrice.toml. Calibration lands
in a follow-up after #288 phase-1b."""
query_strategy: str = DEFAULT_QUERY_STRATEGY
"""v1.7 (#291 PR-2) opt-in for the R1+R3 query-understanding
stack. `legacy-bm25` (default) is byte-identical to v1.4.
`stack-r1-r3` opts in. Operator-tunable via
`[rebuilder] query_strategy` in .aelfrice.toml. Default flip
lands in PR-3 after a clean #288 phase-1b operator-week."""
"""v1.7 (#291 PR-2) selector for the R1+R3 query-understanding
stack. Default `stack-r1-r3` (since #291 PR-3 / #718, v3.0)
runs entity expansion + per-store IDF clipping; `legacy-bm25`
is the v1.4-byte-identical opt-in escape hatch (removal
sequenced as PR-4). Operator-tunable via `[rebuilder]
query_strategy` in .aelfrice.toml."""


def load_rebuilder_config(start: Path | None = None) -> RebuilderConfig:
Expand Down
3 changes: 2 additions & 1 deletion src/aelfrice/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
read_recent_turns_claude_transcript,
rebuild_v14,
)
from aelfrice.query_understanding import DEFAULT_STRATEGY
from aelfrice.hook_search import search_for_prompt
from aelfrice.models import (
BELIEF_CORRECTION,
Expand Down Expand Up @@ -1691,7 +1692,7 @@ def _rebuild_and_format(
rebuild_log_enabled: bool = True,
floor_session: float = 0.0,
floor_l1: float = 0.0,
query_strategy: str = "legacy-bm25",
query_strategy: str = DEFAULT_STRATEGY,
) -> str:
"""Open the store and run the v1.4 rebuild.

Expand Down
13 changes: 8 additions & 5 deletions src/aelfrice/query_understanding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
2. R3 IDF clip with per-store quantile thresholds
(`clip_with_quantile_thresholds`, `compute_idf_quantile_thresholds`)
3. PR-2 wiring: `transform_query(raw_query, store, strategy)`
dispatches between `legacy-bm25` (default) and `stack-r1-r3`.
dispatches between `stack-r1-r3` (default since #291 PR-3) and
`legacy-bm25` (opt-in escape hatch, removal sequenced as PR-4).
The per-store BM25Index + quantile cache lives in
`store_cache.get_bm25_and_quantiles`.

PR-1 landed the rewriters + per-store quantile helper + unit tests.
PR-2 lands the dispatcher + cache + rebuilder/hook plumbing behind
the `query_strategy = "legacy-bm25"` setting (default unchanged).
The default flip to `stack-r1-r3` lands in PR-3 after a clean
#288 phase-1b operator-week.
PR-2 landed the dispatcher + cache + rebuilder/hook plumbing behind
the `query_strategy` setting (initially defaulting to `legacy-bm25`
for safety). PR-3 (#718) flipped the default to `stack-r1-r3` after
the bench gate cleared (+94.8% NDCG@k uplift, +0.66 ms p99 — well
inside the documented thresholds). PR-4 removes the `legacy-bm25`
code path one minor release after the flip.

The synthetic-tuned IDF constants (1.5, 2.5) from the lab R3.5
campaign do not transfer to live stores (live IDF medians 7.5-9.5
Expand Down
13 changes: 7 additions & 6 deletions src/aelfrice/query_understanding/strategy.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""Query-strategy dispatcher: legacy-bm25 (default) vs stack-r1-r3.
"""Query-strategy dispatcher: stack-r1-r3 (default) vs legacy-bm25.

The rebuilder calls `transform_query(raw_query, store, strategy)` to
produce the final query string fed to `retrieve()`. Two strategies
exist at v1.7 (#291):

* `legacy-bm25` -- the v1.4-era query string is passed through
unchanged. Default until #291 PR-3 flips after a clean
#288 phase-1b operator-week.
* `stack-r1-r3` -- the ratified R1+R3 stack: capitalised-token
entity expansion, then per-store IDF-quantile clipping, against
the cached `BM25Index` for the store. Returns the rewritten term
list joined with spaces (FTS5 MATCH consumes the whitespace-
separated form; duplicated terms boost their effective query
frequency the same way the lab campaign measured).
frequency the same way the lab campaign measured). Default since
#291 PR-3 (v3.0) after the bench gate cleared on the lab corpus.
* `legacy-bm25` -- the v1.4-era query string is passed through
unchanged. Retained as an opt-in escape hatch; removal is
sequenced as PR-4 one minor release after the flip.

This module owns no state; the per-store BM25Index + quantile cache
lives in `query_understanding.store_cache`.
Expand All @@ -38,7 +39,7 @@
VALID_STRATEGIES: Final[frozenset[str]] = frozenset(
{LEGACY_STRATEGY, STACK_R1_R3_STRATEGY},
)
DEFAULT_STRATEGY: Final[str] = LEGACY_STRATEGY
DEFAULT_STRATEGY: Final[str] = STACK_R1_R3_STRATEGY


def transform_query(
Expand Down
22 changes: 11 additions & 11 deletions tests/test_query_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def test_transform_legacy_returns_query_unchanged(tmp_path: Path) -> None:
store.close()


def test_transform_default_is_legacy() -> None:
assert DEFAULT_STRATEGY == LEGACY_STRATEGY
def test_transform_default_is_stack_r1_r3() -> None:
assert DEFAULT_STRATEGY == STACK_R1_R3_STRATEGY


def test_transform_stack_lowercases_capitalised(tmp_path: Path) -> None:
Expand Down Expand Up @@ -203,9 +203,9 @@ def test_cache_quantile_args_passed_through(tmp_path: Path) -> None:
# --- RebuilderConfig --------------------------------------------------------


def test_config_default_query_strategy_is_legacy() -> None:
def test_config_default_query_strategy_is_stack_r1_r3() -> None:
cfg = RebuilderConfig()
assert cfg.query_strategy == LEGACY_STRATEGY
assert cfg.query_strategy == STACK_R1_R3_STRATEGY


def test_config_loads_query_strategy_override(tmp_path: Path) -> None:
Expand All @@ -223,7 +223,7 @@ def test_config_invalid_query_strategy_falls_back_to_default(
'[rebuilder]\nquery_strategy = "nonsense"\n'
)
cfg = load_rebuilder_config(tmp_path)
assert cfg.query_strategy == LEGACY_STRATEGY
assert cfg.query_strategy == STACK_R1_R3_STRATEGY
err = capsys.readouterr().err
assert "query_strategy" in err
assert "nonsense" not in err # don't echo the bad value verbatim
Expand All @@ -236,7 +236,7 @@ def test_config_non_string_query_strategy_falls_back(
'[rebuilder]\nquery_strategy = 42\n'
)
cfg = load_rebuilder_config(tmp_path)
assert cfg.query_strategy == LEGACY_STRATEGY
assert cfg.query_strategy == STACK_R1_R3_STRATEGY
assert "query_strategy" in capsys.readouterr().err


Expand All @@ -251,10 +251,10 @@ def test_config_explicit_legacy_value_loads(tmp_path: Path) -> None:
# --- rebuild_v14 plumbing ---------------------------------------------------


def test_rebuild_default_query_strategy_is_legacy(tmp_path: Path) -> None:
def test_rebuild_default_query_strategy_is_stack_r1_r3(tmp_path: Path) -> None:
"""Calling rebuild_v14 with no `query_strategy` argument is
byte-identical to calling it with `query_strategy='legacy-bm25'`.
Default must not change behavior."""
byte-identical to calling it with `query_strategy='stack-r1-r3'`
after the #291 PR-3 default flip."""
store = _seed(
tmp_path / "m.db",
[_mk(
Expand All @@ -265,8 +265,8 @@ def test_rebuild_default_query_strategy_is_legacy(tmp_path: Path) -> None:
try:
turns = [RecentTurn(role="user", text="Tell me about the Rebuilder")]
without_arg = rebuild_v14(turns, store)
with_legacy = rebuild_v14(turns, store, query_strategy=LEGACY_STRATEGY)
assert without_arg == with_legacy
with_stack = rebuild_v14(turns, store, query_strategy=STACK_R1_R3_STRATEGY)
assert without_arg == with_stack
finally:
store.close()

Expand Down
Loading