Exact targeting is strong. Broad lifecycle prompts are the remaining bottleneck.
+
+ This run combines the 0.9.1 scenario benchmark, a broad public naked/RAG/Vaner comparison,
+ and the deep-run maturation suite. It uses public corpora or public-layout fixtures and reports
+ aggregate benchmark evidence without exposing local workspace details.
+
Scenario related coverage 100% related, 95% exact; promotion gate passed.
+
+
+
+
5/8
+
Quality A/B Vaner-context answers won 62% of judged pairs.
+
+
+
+
8.04
+
Broad public quality +3.24 vs naked, +0.41 vs RAG across 125 cases.
+
+
+
+
+0.842
+
Deep-run delta effective pass; raw 3/5 gates passed with scaffold-only non-blocking gates
+
+
+
+
+
+
Layer Results
+
+
Layer
Benchmark
Result
Read
+
+
Prediction / exact targeting
20 Vaner repo mechanism queries
19 exact, 1 partial, 0 no-scenario cycles
Exact targeting is now strong on named components; remaining partials are mostly broad process questions.
+
Answer quality
8 Claude-judged pairs
5 Vaner wins, 3 cold wins, 0 ties
When useful context is present, the answer improves; weak/empty context can lose.
+
Naked vs RAG vs Vaner
125 public cases
Vaner quality 8.04 vs RAG 7.62 vs naked 4.80
Vaner beats naked and edges RAG in this deterministic public harness, with similar hit/recall and lower estimated cost than RAG.
+
Deep-run maturation
80 sessions / 80 outcomes
Mean improvement +0.842; stale rate 0%
Strong reference-ceiling maturation signal; raw anti-self-judging gates remain visible and are non-blocking only in this scaffold mode.
+
Agent-with-tools layer
Harness restored and CLI import validated
Not included as a scored release claim
Needs a configured OpenAI-compatible tool-calling answer endpoint and running Vaner daemon; not faked in this pass.
+
+
+
+
+
+
Answer The Question
+
+
1
When Vaner is betterBest on codebase tasks where the needed object/path is present in prepared context. The quality judge favored Vaner in 5 of 8 paired scenario answers, and the broad public harness shows +3.24 quality over naked and +0.41 over RAG.
+
2
Why it worksIt front-loads context selection before the final answer: exact component hints, symbol/path signals, and cached packages reduce answer-time search and can correct cold-answer misunderstandings.
+
3
Where it is weakerWeakness is precision under broad lifecycle prompts: 1 of 20 turns were only partial, with cold-start still under-specified. No turns lacked relevant context.
+
4
Cost and performanceScenario precompute averaged 46.3s; cache lookup averaged 6.0s. Warm answer latency was 61.0s vs 169.0s cold in the judged sample. Broad public estimated cost was $1.155 for Vaner vs $1.230 for RAG and $0.883 naked.
+
+
+
+
+
+
+
Scenario Gate
+
+
+
Engine LLM
wired via ollama:qwen3.6:27b
+
Mean relevance
0.87
+
No-scenario cycles
0/20 (0%)
+
Gate status
5/5 passed; PASS
+
+
+
+
+
+
Broad Public Arms
+
+
Arm
Hit@3
Recall@5
Quality
Cost
+
+
Naked
0%
0%
4.80
$0.883
+
RAG
64%
68%
7.62
$1.230
+
Vaner
64%
69%
8.04
$1.155
+
+
+
+
+
+
Eval/Fix Loop
+
+
A
Promote only with gatesSeparate coverage, quality, latency, cost, and leak gates. Failed gates become tracked fixes, not rewritten conclusions.
+
B
Debug by failure sliceFirst slice for 0.9.2: named-component queries that produced zero scenarios or weak primary evidence.
+
C
Publish clean evidencePublic reports should include methodology, aggregate results, limitations, and leak scans without naming local paths or internal workspaces.
Vaner helps AI stop starting from scratch. The reactive floor is strong; continuation still needs proof.
+
+ This canvas reframes the medium two-hour run around Vaner's real product claim: preparation before the prompt.
+ The corrected A/B quality slice is strong, but the telemetry also shows that normal continuation did not activate
+ during this run.
+
+
+
+
Run date
+
2026-05-05
+
Models
+
Vaner prep: local qwen Answer: claude:sonnet Judge: claude:opus
+
+
+
+
+
+
4/4
+
Corrected quality A/B Vaner-context answers won every recovered medium case.
+
+
+
7189s
+
Preparation observed The two-hour window reached 99.8% before the original quality phase crashed.
+
+
+
0
+
Continuation rounds No normal continuation activity was recorded in telemetry.
+
+
+
21
+
Ready predictions 73 total predictions at the end: 21 ready, 52 queued.
+
+
+
+
+
+
Layer Results
+
+
Layer
Benchmark
Result
Read
+
+
+
Reactive floor
+
4 Claude-judged repo mechanism questions
+
4 Vaner wins, 0 naked wins, 0 ties
+
When prepared context is available, the primary AI answers with more exact files, identifiers, and mechanisms.
+
+
+
Preparation window
+
Normal background run, target 7200s
+
Last sample at 7188.9s; 236 telemetry samples
+
Vaner stayed active through almost the whole preparation window.
This proves background work happened. It does not prove the desired continuation policy yet.
+
+
+
+
+
+
Before / After Pattern
+
+
Without Vaner
With Vaner
User-visible improvement
+
+
+
Generic answer or caveat that repo details are unknown.
+
Specific files such as learning/reward.py, intent/scorer.py, engine.py, and store/artefacts.py.
+
Less setup, fewer clarifying turns, better grounded follow-up work.
+
+
+
Broad architecture pattern.
+
Exact constants, methods, schema fields, and flow stages when evidence was present.
+
The assistant starts from project evidence instead of rediscovery.
+
+
+
Safer but often non-actionable uncertainty.
+
More useful specificity, but still needs hallucination-risk checks in public claims.
+
Better answer quality with a clear need for evidence-precision gates.
+
+
+
+
+
+
+
Release-safe Language
+
+
OK
Safe claimIn a recovered medium quality slice after background preparation, Vaner context helped Claude Sonnet produce better answers than naked prompting on all four tested mechanism questions.
+
NO
Do not claim yetDo not say this run proves Vaner continuously explores and hardens many future scenarios over a two-hour idle window.
+
FIT
Best fitRecurring project work, codebases, research, writing, planning, and team knowledge where prepared context can be reused.
+
LOW
Weak fitGeneric one-off prompts, vague unrelated asks, and tasks where the base model already knows enough.
+
+
+
+
+
+
+
Needed Harness Fixes
+
+
A
Normalize context payloadsStructured Vaner payloads must be converted to text before prompt assembly.
+
B
Keep A/B artifacts explicitMark recovered, failed, and release-harness files separately.
+
C
Gate hallucination riskMedium-risk wins need identifier checks before marketing copy.
+
+
+
+
+
Preparation Benchmark Next
+
+
Window
Measure
+
+
0m
Cold reactive floor.
+
5m
Fast preparation payoff.
+
15m
Short work-session continuity.
+
60m
Idle lunch-break preparation.
+
2h
Normal background exploration, not deep-run.
+
Overnight
Deep preparation and stale-rate pressure.
+
+
+
+
+
+
Metrics To Add
+
+
H
Prepared Work Hit RateHow often useful work was ready before the user asked.
+
E
Evidence PrecisionWhether prepared evidence was actually the decisive evidence.
+
S
Stale RateHow often prepared work was outdated or irrelevant.
+
C
Compounding ScoreWhether usefulness improves over repeated sessions.
+
+
+
+
+
+ Source run: .vaner/bench-runs/normal-bg-medium-2h-20260505T182339Z.
+ This canvas is a recovery and interpretation artifact, not a final versioned release benchmark.
+
+
+
+
diff --git a/src/vaner/broker/answerable.py b/src/vaner/broker/answerable.py
index 19417a9..903ca09 100644
--- a/src/vaner/broker/answerable.py
+++ b/src/vaner/broker/answerable.py
@@ -5,6 +5,7 @@
import re
from pathlib import Path
+from vaner.broker.compressor import EvidenceSpan, extract_evidence_spans
from vaner.broker.selector import _prompt_terms
from vaner.models.answerable import (
Answerability,
@@ -43,9 +44,9 @@ def build_answerable_briefing(
cost_sensitivity: str = "balanced",
) -> AnswerableBriefing:
terms = _prompt_terms(query)
- scored = [
- _build_item(query, terms, artefact, repo_root=repo_root, channel=_channel_for(artefact, channels_by_key)) for artefact in artefacts
- ]
+ scored: list[tuple[float, int, AnswerableEvidenceItem]] = []
+ for artefact in artefacts:
+ scored.extend(_build_items(query, terms, artefact, repo_root=repo_root, channel=_channel_for(artefact, channels_by_key)))
scored.sort(key=lambda row: (row[0], -row[1], row[2].path), reverse=True)
direct: list[AnswerableEvidenceItem] = []
@@ -170,6 +171,8 @@ def build_answerable_briefing(
}
)
transport_limited_count = dropped_direct + dropped_supporting + dropped_lower
+ if assembly_mode == "safe" and (direct_truncated or supporting_truncated or lower_truncated):
+ transport_limited_count = max(1, transport_limited_count)
if transport_limited_count:
assembly_plan = _mark_transport_limited(assembly_plan, direct + supporting + lower, transport_limited_count)
truncation_applied = direct_truncated or supporting_truncated or lower_truncated or dropped_direct > 0
@@ -280,35 +283,84 @@ def build_answerable_briefing_from_text(
return AnswerableBriefing(text=text, answer_plan=answer_plan, sections=sections, metadata=metadata)
-def _build_item(
+def _build_items(
query: str,
terms: list[str],
artefact: Artefact,
*,
repo_root: Path | None,
channel: EvidenceChannel,
-) -> tuple[float, int, AnswerableEvidenceItem]:
+) -> list[tuple[float, int, AnswerableEvidenceItem]]:
source_text = _source_text(artefact, repo_root)
- excerpt, truncated = _best_excerpt(source_text or artefact.content, terms)
+ source_artefact = artefact.model_copy(update={"content": source_text}) if source_text else artefact
path = artefact.source_path
+ spans = extract_evidence_spans(source_artefact, query, base_score=float(artefact.relevance_score or 0.0), max_spans_per_file=7)
+ if spans:
+ return [_item_from_span(query, terms, artefact, span, channel=channel) for span in spans]
+
+ excerpt, truncated = _best_excerpt(source_text or artefact.content, terms)
score = _score_path_and_excerpt(query, path.lower(), excerpt.lower(), terms)
title = _title_for(path, excerpt)
confidence = max(0.0, min(1.0, score / 24.0))
+ return [
+ (
+ score,
+ -len(path),
+ AnswerableEvidenceItem(
+ path=path,
+ title=title,
+ source=str(artefact.metadata.get("corpus_id", "default")),
+ channel=channel,
+ why_selected=_why_selected(path, terms, score),
+ excerpt=excerpt,
+ relevance_to_query=_relevance(query, path, terms, score),
+ confidence=confidence,
+ token_count=count_tokens(excerpt),
+ revision_or_hash=str(artefact.metadata.get("revision") or artefact.metadata.get("hash") or "") or None,
+ truncated=truncated,
+ ),
+ )
+ ]
+
+
+def _item_from_span(
+ query: str,
+ terms: list[str],
+ artefact: Artefact,
+ span: EvidenceSpan,
+ *,
+ channel: EvidenceChannel,
+) -> tuple[float, int, AnswerableEvidenceItem]:
+ path = artefact.source_path
+ role_bonus = {
+ "direct": 8.0,
+ "constant_or_default": 6.0,
+ "schema_or_storage": 6.0,
+ "caller_or_downstream": 5.0,
+ "test_or_example": 4.0,
+ "supporting": 1.0,
+ "provenance": 0.5,
+ }[span.role]
+ score = span.score + role_bonus + _score_path_and_excerpt(query, path.lower(), span.excerpt.lower(), terms)
+ confidence = max(0.0, min(1.0, score / 32.0))
+ symbol = f" {span.symbol}" if span.symbol else ""
+ title = f"{span.role}{symbol} L{span.start_line}-{span.end_line}"
+ why = f"{_why_selected(path, terms, score)}; role={span.role}; lines={span.start_line}-{span.end_line}; {span.reason}"
return (
score,
- -len(path),
+ -span.start_line,
AnswerableEvidenceItem(
path=path,
title=title,
source=str(artefact.metadata.get("corpus_id", "default")),
channel=channel,
- why_selected=_why_selected(path, terms, score),
- excerpt=excerpt,
+ why_selected=why,
+ excerpt=span.excerpt,
relevance_to_query=_relevance(query, path, terms, score),
confidence=confidence,
- token_count=count_tokens(excerpt),
+ token_count=count_tokens(span.excerpt),
revision_or_hash=str(artefact.metadata.get("revision") or artefact.metadata.get("hash") or "") or None,
- truncated=truncated,
+ truncated=False,
),
)
@@ -440,6 +492,15 @@ def _mark_transport_limited(
marked += 1
else:
updated.append(decision)
+ if transport_limited_count and marked == 0 and updated:
+ updated[-1] = updated[-1].model_copy(
+ update={
+ "decision": "transport_limited",
+ "reason": "evidence was constrained by the caller transport limit",
+ "would_change_output_in_shadow": True,
+ }
+ )
+ marked = 1
return assembly.model_copy(
update={
"items_transport_limited": assembly.items_transport_limited + max(transport_limited_count, marked),
@@ -465,6 +526,14 @@ def _dedupe_key(item: AnswerableEvidenceItem) -> str:
def _protected_reason(item: AnswerableEvidenceItem, role: str, conflict_paths: set[str]) -> str:
if role == "direct_answer_evidence":
return "direct answer evidence"
+ for evidence_role, reason in (
+ ("role=constant_or_default", "constant/default evidence"),
+ ("role=schema_or_storage", "schema/storage evidence"),
+ ("role=caller_or_downstream", "caller/downstream evidence"),
+ ("role=test_or_example", "test/example evidence"),
+ ):
+ if evidence_role in item.why_selected:
+ return reason
if item.path in conflict_paths:
return "conflict evidence"
if item.channel == "prediction":
diff --git a/src/vaner/broker/assembler.py b/src/vaner/broker/assembler.py
index 9f0aa81..43d6b96 100644
--- a/src/vaner/broker/assembler.py
+++ b/src/vaner/broker/assembler.py
@@ -12,6 +12,7 @@
from vaner.models.answerable import EvidenceAssemblyMode
from vaner.models.artefact import Artefact
from vaner.models.context import ContextPackage, ContextSelection
+from vaner.models.context_preparation import PreparedContextDiagnostics
from vaner.models.decision import DecisionRecord, ScoreFactor, SelectionDecision
from vaner.policy.staleness import is_stale_timestamp
@@ -42,6 +43,7 @@ def assemble_context_package(
evidence_assembly_mode: EvidenceAssemblyMode = "shadow",
evidence_assembly_quality_bias: str = "protect_recall",
evidence_assembly_cost_sensitivity: str = "balanced",
+ prepared_context_diagnostics: PreparedContextDiagnostics | None = None,
return_decision: bool = False,
) -> ContextPackage | tuple[ContextPackage, DecisionRecord]:
resolved_score_map = score_map or {artefact.key: score_artefact(prompt, artefact) for artefact in artefacts}
@@ -49,6 +51,7 @@ def assemble_context_package(
artefacts,
max_tokens=max_tokens,
score_by_key=resolved_score_map,
+ query=prompt,
)
selection_decisions: list[SelectionDecision] = []
context_selections: list[ContextSelection] = []
@@ -104,7 +107,7 @@ def assemble_context_package(
)
answerable = build_answerable_briefing(
prompt,
- [artefact for artefact in artefacts if artefact.key in kept_keys],
+ artefacts,
repo_root=repo_root,
max_tokens=max_tokens,
channels_by_key={
@@ -121,6 +124,12 @@ def assemble_context_package(
)
context_package.answerable_briefing = answerable
context_package.answerability_metadata = answerable.metadata
+ context_package.prepared_context_briefing = answerable.text
+ context_package.prepared_context_mode = _prepared_context_mode(prepared_context_diagnostics, answerable.metadata.answerability)
+ if prepared_context_diagnostics is not None:
+ prepared_context_diagnostics.token_used = used
+ prepared_context_diagnostics.truncation_risk = answerable.metadata.truncation_risk
+ context_package.prepared_context_diagnostics = prepared_context_diagnostics
decision_record = DecisionRecord(
id=context_package.id,
prompt=prompt,
@@ -129,7 +138,29 @@ def assemble_context_package(
token_budget=max_tokens,
token_used=used,
selections=selection_decisions,
+ prepared_context_diagnostics=prepared_context_diagnostics,
)
if return_decision:
return context_package, decision_record
return context_package
+
+
+def _prepared_context_mode(diagnostics: PreparedContextDiagnostics | None, answerability: str) -> str:
+ if diagnostics is None:
+ return "answerable"
+ need = diagnostics.profile.need
+ if need == "implementation_support":
+ return "implementation"
+ if need == "research_mapping":
+ return "research"
+ if need == "creative_grounding":
+ return "writing"
+ if need == "decision_support":
+ return "decision"
+ if need == "conflict_resolution":
+ return "conflict_resolution"
+ if need == "absence_check":
+ return "absence_check"
+ if answerability in {"full", "weak", "conflict"}:
+ return "answerable"
+ return "planning"
diff --git a/src/vaner/broker/compressor.py b/src/vaner/broker/compressor.py
index f78e35f..4a6bb79 100644
--- a/src/vaner/broker/compressor.py
+++ b/src/vaner/broker/compressor.py
@@ -2,23 +2,613 @@
from __future__ import annotations
+import ast
+import re
+from dataclasses import dataclass
+from typing import Literal
+
+from vaner.broker.selector import _prompt_terms
from vaner.models.artefact import Artefact
from vaner.policy.budget import count_tokens
+EvidenceSpanRole = Literal[
+ "direct",
+ "constant_or_default",
+ "schema_or_storage",
+ "caller_or_downstream",
+ "test_or_example",
+ "supporting",
+ "provenance",
+]
+
+
+@dataclass(frozen=True)
+class EvidenceSpan:
+ path: str
+ start_line: int
+ end_line: int
+ role: EvidenceSpanRole
+ symbol: str
+ excerpt: str
+ score: float
+ protected: bool
+ reason: str
+
+
+def _trim_chunk_to_budget(chunk: str, max_tokens: int) -> str:
+ if max_tokens <= 0:
+ return ""
+ if count_tokens(chunk) <= max_tokens:
+ return chunk
+ first_line, _, remainder = chunk.partition("\n")
+ marker = "\n...[trimmed]\n"
+ header = first_line.strip()
+ if count_tokens(header + marker) > max_tokens:
+ max_chars = max(1, max_tokens * 4)
+ return (header + "\n")[:max_chars].rstrip()
+ max_chars = max(120, max_tokens * 4)
+ trimmed = (header + "\n" + remainder[:max_chars]).rstrip()
+ while trimmed and count_tokens(trimmed + marker) > max_tokens:
+ trimmed = trimmed[: max(0, len(trimmed) - 120)].rstrip()
+ return trimmed + marker if trimmed else header + marker
+
+
+def _chunk_for_artefact(artefact: Artefact, *, max_chunk_tokens: int) -> str:
+ header = f"### {artefact.source_path}\n"
+ content = artefact.content or ""
+ if max_chunk_tokens <= 0:
+ return header.rstrip()
+ full = f"{header}{content}\n"
+ if count_tokens(full) <= max_chunk_tokens:
+ return full
+ sections = _important_lines(content)
+ if sections:
+ candidate = f"{header}" + "\n".join(sections) + "\n...[compacted to important implementation anchors]\n"
+ if count_tokens(candidate) <= max_chunk_tokens:
+ return candidate
+ return _trim_chunk_to_budget(full, max_chunk_tokens)
+
+
+def _important_lines(content: str) -> list[str]:
+ important: list[str] = []
+ patterns = (
+ r"^(Classes|Functions|Constants|Limits|Schema):",
+ r"\b(CREATE TABLE|CREATE INDEX|USING fts5|INSERT INTO|SELECT .* FROM)\b",
+ r"\b(class|def|async def)\s+[A-Za-z_][A-Za-z0-9_]*",
+ r"\b(max_|min_|limit|threshold|timeout|ttl|budget|window|top_n|top_k)\w*\b",
+ )
+ for line in content.splitlines():
+ stripped = line.strip()
+ if not stripped:
+ continue
+ if any(re.search(pattern, stripped, flags=re.IGNORECASE) for pattern in patterns):
+ important.append(stripped[:240])
+ if len(important) >= 24:
+ break
+ return important
+
+
+def extract_evidence_spans(
+ artefact: Artefact,
+ query: str,
+ *,
+ base_score: float = 0.0,
+ max_spans_per_file: int = 8,
+) -> list[EvidenceSpan]:
+ """Return query-aware, line-exact spans for one artefact.
+
+ The extractor is deliberately deterministic. It preserves code and config
+ ingredients as copied source spans instead of asking a model to summarize
+ them, which keeps identifiers, constants, table names, and parser contracts
+ exact under tight context budgets.
+ """
+
+ text = artefact.content or ""
+ lines = text.splitlines()
+ if not lines:
+ return []
+ terms = _prompt_terms(query)
+ if not terms:
+ terms = [part for part in re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}", query.lower()) if len(part) > 2]
+ candidates = _structured_candidates(artefact.source_path, lines, terms)
+ candidates.extend(_line_candidates(artefact.source_path, lines, terms))
+ candidates = _merge_candidate_windows(candidates)
+
+ spans: list[EvidenceSpan] = []
+ for start, end, role, symbol, score, reason in candidates:
+ excerpt = _numbered_excerpt(lines, start, end)
+ if not excerpt.strip():
+ continue
+ adjusted = score + (base_score * 0.08)
+ spans.append(
+ EvidenceSpan(
+ path=artefact.source_path,
+ start_line=start,
+ end_line=end,
+ role=role,
+ symbol=symbol,
+ excerpt=excerpt,
+ score=adjusted,
+ protected=role in {"direct", "constant_or_default", "schema_or_storage", "caller_or_downstream", "test_or_example"},
+ reason=reason,
+ )
+ )
+ spans.sort(key=lambda span: (_role_priority(span.role), span.protected, span.score, -span.start_line), reverse=True)
+ return _diverse_spans(spans, max_spans_per_file=max_spans_per_file)
+
+
+def _structured_candidates(
+ path: str,
+ lines: list[str],
+ terms: list[str],
+) -> list[tuple[int, int, EvidenceSpanRole, str, float, str]]:
+ if not path.endswith(".py"):
+ return []
+ text = "\n".join(lines)
+ try:
+ tree = ast.parse(text)
+ except SyntaxError:
+ return []
+
+ candidates: list[tuple[int, int, EvidenceSpanRole, str, float, str]] = []
+ imports = [
+ node.lineno
+ for node in tree.body
+ if isinstance(node, (ast.Import, ast.ImportFrom)) and _node_line_score(lines[node.lineno - 1], terms, path) > 0
+ ]
+ if imports:
+ candidates.append(
+ (max(1, min(imports) - 1), min(len(lines), max(imports) + 1), "provenance", "imports", 4.0, "matched import provenance")
+ )
+
+ for node in ast.walk(tree):
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
+ symbol = node.name
+ start = node.lineno
+ end = min(len(lines), getattr(node, "end_lineno", node.lineno))
+ block_text = "\n".join(lines[start - 1 : end]).lower()
+ score = _symbol_score(symbol, terms) + _text_term_score(block_text, terms)
+ anchor = _block_anchor_score(symbol, block_text)
+ if score <= 0 and anchor <= 0:
+ continue
+ semantic_role = _role_for_block(path, symbol, block_text)
+ role = semantic_role if semantic_role != "supporting" else "direct"
+ max_lines = 3 if isinstance(node, ast.ClassDef) else 42
+ span_start, span_end = _focused_block_window(lines, start, end, terms, max_lines=max_lines)
+ candidates.append((span_start, span_end, role, symbol, score + anchor + 3.0, f"matched {symbol} block"))
+ elif isinstance(node, (ast.Assign, ast.AnnAssign)):
+ target_names = _assignment_targets(node)
+ if not target_names:
+ continue
+ start = node.lineno
+ end = min(len(lines), getattr(node, "end_lineno", node.lineno))
+ line_text = "\n".join(lines[start - 1 : end])
+ score = max((_symbol_score(name, terms) for name in target_names), default=0.0) + _node_line_score(line_text, terms, path)
+ if score <= 0 and not _constantish(line_text):
+ continue
+ candidates.append(
+ (
+ max(1, start - 1),
+ min(len(lines), end + 1),
+ "constant_or_default",
+ ", ".join(target_names[:3]),
+ score + 5.0,
+ "preserved constant/default assignment",
+ )
+ )
+ return candidates
+
+
+def _line_candidates(
+ path: str,
+ lines: list[str],
+ terms: list[str],
+) -> list[tuple[int, int, EvidenceSpanRole, str, float, str]]:
+ candidates: list[tuple[int, int, EvidenceSpanRole, str, float, str]] = []
+ for index, line in enumerate(lines, start=1):
+ stripped = line.strip()
+ if path.endswith(".py") and stripped.startswith("#"):
+ continue
+ score = _node_line_score(line, terms, path)
+ if score <= 0:
+ continue
+ role = _role_for_text(path, line)
+ if path.endswith(".py"):
+ radius = 0 if re.search(r"\b(class|def|async def)\s+[A-Za-z_][A-Za-z0-9_]*", line) else 1
+ else:
+ radius = 1 if role in {"constant_or_default", "schema_or_storage"} else 2
+ symbol = _symbol_from_line(line)
+ candidates.append(
+ (
+ max(1, index - radius),
+ min(len(lines), index + radius),
+ role,
+ symbol,
+ score,
+ f"matched {role.replace('_', ' ')} line",
+ )
+ )
+ return candidates
+
+
+def _node_line_score(text: str, terms: list[str], path: str) -> float:
+ lowered = text.lower()
+ path_lowered = path.lower()
+ score = 0.0
+ content_hit_score = 0.0
+ for term in terms:
+ if len(term) < 3:
+ continue
+ if term in lowered:
+ content_hit_score += 4.0 if "_" not in term else 7.0
+ if term in path_lowered:
+ score += 1.5
+ if content_hit_score <= 0:
+ score = 0.0
+ score += content_hit_score
+ if re.search(r"\b(class|def|async def)\s+[A-Za-z_][A-Za-z0-9_]*", text):
+ score += 2.0
+ if _constantish(text):
+ score += 4.0
+ if re.search(r"\b(CREATE TABLE|CREATE INDEX|USING fts5|ALTER TABLE|INSERT INTO|SELECT\b.+\bFROM)\b", text, re.IGNORECASE):
+ score += 7.0
+ if re.search(r"\b(json\.loads|json\.dumps|JSON_CONTRACT|ranked_files|follow_on|response_format|schema)\b", text):
+ score += 5.0
+ if re.search(r"\b(reward_total|reward_components|cache_tier|quality_lift|judge_score|similarity|raw_reward)\b", text):
+ score += 5.0
+ return score
+
+
+def _text_term_score(text: str, terms: list[str]) -> float:
+ return sum(2.0 if term in text else 0.0 for term in terms if len(term) > 3)
+
+
+def _symbol_score(symbol: str, terms: list[str]) -> float:
+ symbol_terms = set(_identifier_parts(symbol))
+ score = 0.0
+ for term in terms:
+ if term == symbol.lower():
+ score += 10.0
+ elif term in symbol_terms:
+ score += 5.0
+ elif len(term) > 3 and term in symbol.lower():
+ score += 3.0
+ return score
+
+
+def _block_anchor_score(symbol: str, block_text: str) -> float:
+ score = 0.0
+ symbol_lower = symbol.lower()
+ if any(part in symbol_lower for part in ("reward", "feature", "train", "blend", "store", "retrieve", "persist", "schema", "cache")):
+ score += 2.0
+ if any(anchor in block_text for anchor in ("create table", "json.loads", "reward_total", "threshold", "default", "ranked_files")):
+ score += 2.0
+ return score
+
+
+def _assignment_targets(node: ast.Assign | ast.AnnAssign) -> list[str]:
+ targets = node.targets if isinstance(node, ast.Assign) else [node.target]
+ names: list[str] = []
+ for target in targets:
+ if isinstance(target, ast.Name):
+ names.append(target.id)
+ elif isinstance(target, ast.Attribute):
+ names.append(target.attr)
+ elif isinstance(target, (ast.Tuple, ast.List)):
+ names.extend(elt.id for elt in target.elts if isinstance(elt, ast.Name))
+ return names
+
+
+def _constantish(text: str) -> bool:
+ return bool(
+ re.search(
+ r"\b([A-Z][A-Z0-9_]{2,}|default|defaults|threshold|timeout|ttl|limit|weight|weights|ratio|budget|target|schema|contract)\b",
+ text,
+ re.IGNORECASE,
+ )
+ and "=" in text
+ )
+
+
+def _role_for_text(path: str, text: str) -> EvidenceSpanRole:
+ lowered = text.lower()
+ if path.startswith(("tests/", "test/")) or "/tests/" in path or "assert " in lowered:
+ return "test_or_example"
+ if re.search(r"\b(create table|create index|using fts5|alter table|schema|select\b.+\bfrom|insert into)\b", lowered):
+ return "schema_or_storage"
+ if re.search(r"\b(default|threshold|timeout|ttl|limit|weight|weights|ratio|budget|target|contract)\b", lowered) and "=" in text:
+ return "constant_or_default"
+ if re.search(r"\b(call|caller|consumer|downstream|insert|select)\b|retrieve|persist|compute_|load_|save_|train|blend|parse", lowered):
+ return "caller_or_downstream"
+ if re.search(r"\b(class|def|async def)\s+[A-Za-z_][A-Za-z0-9_]*", text):
+ return "direct"
+ return "supporting"
+
+
+def _role_for_block(path: str, symbol: str, block_text: str) -> EvidenceSpanRole:
+ semantic = _role_for_text(path, block_text)
+ if semantic in {"schema_or_storage", "test_or_example"}:
+ return semantic
+ lowered_symbol = symbol.lower()
+ if re.search(r"\b(downstream|consumer|caller|train|target|blend|parse|json\.loads|retrieve|persist|insert|select)\b", block_text):
+ return "caller_or_downstream"
+ if any(part in lowered_symbol for part in ("train", "target", "blend", "parse", "retrieve", "persist")):
+ return "caller_or_downstream"
+ if semantic == "constant_or_default":
+ return semantic
+ return "direct" if block_text.strip() else "supporting"
+
+
+def _role_priority(role: EvidenceSpanRole) -> int:
+ return {
+ "direct": 70,
+ "constant_or_default": 60,
+ "schema_or_storage": 55,
+ "caller_or_downstream": 50,
+ "test_or_example": 45,
+ "supporting": 25,
+ "provenance": 10,
+ }[role]
+
+
+def _focused_block_window(lines: list[str], start: int, end: int, terms: list[str], *, max_lines: int) -> tuple[int, int]:
+ if end - start + 1 <= max_lines:
+ return start, end
+ best_line = start
+ best_score = -1.0
+ for index in range(start, end + 1):
+ score = _node_line_score(lines[index - 1], terms, "")
+ if score > best_score:
+ best_line = index
+ best_score = score
+ half = max_lines // 2
+ window_start = max(start, best_line - half)
+ window_end = min(end, window_start + max_lines - 1)
+ window_start = max(start, window_end - max_lines + 1)
+ return window_start, window_end
+
+
+def _merge_candidate_windows(
+ candidates: list[tuple[int, int, EvidenceSpanRole, str, float, str]],
+) -> list[tuple[int, int, EvidenceSpanRole, str, float, str]]:
+ ordered = sorted(candidates, key=lambda row: (row[0], row[1], -row[4]))
+ merged: list[tuple[int, int, EvidenceSpanRole, str, float, str]] = []
+ for candidate in ordered:
+ start, end, role, symbol, score, reason = candidate
+ if not merged or start > merged[-1][1] + 1:
+ merged.append(candidate)
+ continue
+ prev_start, prev_end, prev_role, prev_symbol, prev_score, prev_reason = merged[-1]
+ best_role = role if _merge_role_priority(role) > _merge_role_priority(prev_role) else prev_role
+ best_symbol = symbol or prev_symbol
+ merged[-1] = (
+ prev_start,
+ max(prev_end, end),
+ best_role,
+ best_symbol,
+ max(prev_score, score),
+ prev_reason if prev_score >= score else reason,
+ )
+ return merged
+
+
+def _merge_role_priority(role: EvidenceSpanRole) -> int:
+ return {
+ "schema_or_storage": 80,
+ "constant_or_default": 75,
+ "caller_or_downstream": 70,
+ "test_or_example": 65,
+ "direct": 55,
+ "supporting": 25,
+ "provenance": 10,
+ }[role]
+
+
+def _diverse_spans(spans: list[EvidenceSpan], *, max_spans_per_file: int) -> list[EvidenceSpan]:
+ kept: list[EvidenceSpan] = []
+ seen_roles: set[str] = set()
+ for span in spans:
+ if span.role in seen_roles and len([item for item in kept if item.role == span.role]) >= 3:
+ continue
+ kept.append(span)
+ seen_roles.add(span.role)
+ if len(kept) >= max_spans_per_file:
+ break
+ kept.sort(key=lambda span: (_role_priority(span.role), span.score), reverse=True)
+ return kept
+
+
+def _numbered_excerpt(lines: list[str], start: int, end: int) -> str:
+ width = len(str(end))
+ return "\n".join(f"L{line_no:0{width}d}: {lines[line_no - 1]}" for line_no in range(start, end + 1))
+
+
+def _symbol_from_line(line: str) -> str:
+ match = re.search(r"\b(?:class|def|async def)\s+([A-Za-z_][A-Za-z0-9_]*)", line)
+ if match:
+ return match.group(1)
+ match = re.search(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*=", line)
+ if match:
+ return match.group(1)
+ return ""
+
+
+def _identifier_parts(identifier: str) -> list[str]:
+ pieces = re.split(r"[^A-Za-z0-9]+", identifier)
+ parts: list[str] = []
+ for piece in pieces:
+ if not piece:
+ continue
+ parts.append(piece.lower())
+ parts.extend(part.lower() for part in re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+", piece))
+ return [part for part in parts if len(part) > 2]
+
+
+def _chunk_for_spans(path: str, spans: list[EvidenceSpan]) -> str:
+ parts = [f"### {path}"]
+ for span in spans:
+ parts.append(_span_entry(span))
+ return "\n".join(parts).rstrip() + "\n"
+
+
+def _span_entry(span: EvidenceSpan) -> str:
+ symbol = f" symbol={span.symbol}" if span.symbol else ""
+ protected = " protected" if span.protected else ""
+ return f"@@ lines {span.start_line}-{span.end_line} role={span.role}{symbol}{protected}\n{span.excerpt}"
+
+
+def _compress_context_with_spans(
+ artefacts: list[Artefact],
+ max_tokens: int,
+ query: str,
+ score_by_key: dict[str, float] | None,
+) -> tuple[str, dict[str, int], int, set[str]]:
+ token_map: dict[str, int] = {}
+ kept_spans_by_key: dict[str, list[EvidenceSpan]] = {artefact.key: [] for artefact in artefacts}
+ spans_by_key: dict[str, list[EvidenceSpan]] = {}
+ ordered_keys = [artefact.key for artefact in artefacts]
+ if score_by_key is not None:
+ ordered_keys.sort(key=lambda key: score_by_key.get(key, 0.0), reverse=True)
+ artefacts_by_key = {artefact.key: artefact for artefact in artefacts}
+ for artefact in artefacts:
+ spans = extract_evidence_spans(artefact, query, base_score=(score_by_key or {}).get(artefact.key, 0.0))
+ if not spans:
+ fallback = _chunk_for_artefact(
+ artefact,
+ max_chunk_tokens=max(96, min(max_tokens, max_tokens // max(1, min(3, len(artefacts) or 1)))),
+ )
+ pseudo_span = EvidenceSpan(
+ path=artefact.source_path,
+ start_line=1,
+ end_line=max(1, len((artefact.content or "").splitlines())),
+ role="supporting",
+ symbol="",
+ excerpt=fallback,
+ score=(score_by_key or {}).get(artefact.key, 0.0),
+ protected=False,
+ reason="fallback compacted artefact",
+ )
+ spans = [pseudo_span] if fallback.strip() else []
+ spans_by_key[artefact.key] = spans
+
+ role_limits = _role_token_limits(max_tokens)
+ role_used = {role: 0 for role in role_limits}
+ role_kept = {role: 0 for role in role_limits}
+ used = 0
+ seen_signatures: set[str] = set()
+ all_spans: list[tuple[str, EvidenceSpan]] = []
+ for key in ordered_keys:
+ for span in spans_by_key.get(key, []):
+ all_spans.append((key, span))
+ all_spans.sort(
+ key=lambda row: (
+ row[1].protected,
+ _role_priority(row[1].role),
+ row[1].score,
+ (score_by_key or {}).get(row[0], 0.0),
+ ),
+ reverse=True,
+ )
+
+ for key, span in all_spans:
+ header_tokens = 0 if kept_spans_by_key[key] else count_tokens(f"### {artefacts_by_key[key].source_path}\n")
+ span_tokens = header_tokens + count_tokens(_span_entry(span))
+ signature = _span_signature(span)
+ if signature in seen_signatures:
+ continue
+ role_limit = role_limits.get(span.role, role_limits["supporting"])
+ if role_used.get(span.role, 0) + span_tokens > role_limit:
+ if not span.protected or role_kept.get(span.role, 0) >= 3:
+ continue
+ if used + span_tokens > max_tokens:
+ if span.protected:
+ trimmed = _trim_span(span, max(48, max_tokens - used - header_tokens))
+ trimmed_tokens = header_tokens + count_tokens(_span_entry(trimmed))
+ if used + trimmed_tokens > max_tokens:
+ continue
+ span = trimmed
+ span_tokens = trimmed_tokens
+ else:
+ continue
+ kept_spans_by_key[key].append(span)
+ seen_signatures.add(signature)
+ role_used[span.role] = role_used.get(span.role, 0) + span_tokens
+ role_kept[span.role] = role_kept.get(span.role, 0) + 1
+ used += span_tokens
+
+ kept_keys = {key for key, spans in kept_spans_by_key.items() if spans}
+ if not kept_keys and ordered_keys and max_tokens > 0:
+ key = ordered_keys[0]
+ chunk = _trim_chunk_to_budget(_chunk_for_artefact(artefacts_by_key[key], max_chunk_tokens=max_tokens), max_tokens)
+ if chunk:
+ token_map[key] = count_tokens(chunk)
+ return chunk, token_map, token_map[key], {key}
+
+ chunks: list[str] = []
+ for key in ordered_keys:
+ spans = kept_spans_by_key[key]
+ if not spans:
+ token_map[key] = count_tokens(_chunk_for_artefact(artefacts_by_key[key], max_chunk_tokens=256))
+ continue
+ spans.sort(key=lambda span: (_role_priority(span.role), span.score), reverse=True)
+ chunk = _chunk_for_spans(artefacts_by_key[key].source_path, spans)
+ token_map[key] = count_tokens(chunk)
+ chunks.append(chunk)
+ used = sum(token_map[key] for key in kept_keys)
+ return "\n".join(chunks).strip(), token_map, used, kept_keys
+
+
+def _role_token_limits(max_tokens: int) -> dict[EvidenceSpanRole, int]:
+ minimum = min(max_tokens, 96)
+ return {
+ "direct": max(minimum, int(max_tokens * 0.48)),
+ "constant_or_default": max(minimum // 2, int(max_tokens * 0.14)),
+ "schema_or_storage": max(minimum // 2, int(max_tokens * 0.12)),
+ "caller_or_downstream": max(minimum // 2, int(max_tokens * 0.12)),
+ "test_or_example": max(minimum // 2, int(max_tokens * 0.08)),
+ "supporting": max(minimum // 2, int(max_tokens * 0.05)),
+ "provenance": max(24, int(max_tokens * 0.01)),
+ }
+
+
+def _span_signature(span: EvidenceSpan) -> str:
+ normalized = re.sub(r"\s+", " ", span.excerpt.lower()).strip()
+ return f"{span.role}:{normalized[:220]}"
+
+
+def _trim_span(span: EvidenceSpan, budget: int) -> EvidenceSpan:
+ excerpt = _trim_chunk_to_budget(span.excerpt, budget)
+ return EvidenceSpan(
+ path=span.path,
+ start_line=span.start_line,
+ end_line=span.end_line,
+ role=span.role,
+ symbol=span.symbol,
+ excerpt=excerpt,
+ score=span.score,
+ protected=span.protected,
+ reason=f"{span.reason}; trimmed to fit transport budget",
+ )
+
def compress_context(
artefacts: list[Artefact],
max_tokens: int,
score_by_key: dict[str, float] | None = None,
+ query: str | None = None,
) -> tuple[str, dict[str, int], int, set[str]]:
+ if query:
+ return _compress_context_with_spans(artefacts, max_tokens, query, score_by_key)
+
token_map: dict[str, int] = {}
chunk_map: dict[str, str] = {}
ordered_keys: list[str] = []
kept_keys: set[str] = set()
used = 0
+ per_chunk_cap = max(512, min(max_tokens, max_tokens // 3 if max_tokens >= 4096 else max_tokens))
for artefact in artefacts:
- chunk = f"### {artefact.source_path}\n{artefact.content}\n"
+ chunk = _chunk_for_artefact(artefact, max_chunk_tokens=per_chunk_cap)
chunk_tokens = count_tokens(chunk)
token_map[artefact.key] = chunk_tokens
chunk_map[artefact.key] = chunk
@@ -29,9 +619,16 @@ def compress_context(
# Greedy knapsack approximation: pack highest-score artefacts first while
# still trying lower-ranked/smaller chunks that fit remaining budget.
- for key in ordered_keys:
+ for index, key in enumerate(ordered_keys):
chunk_tokens = token_map[key]
if used + chunk_tokens > max_tokens:
+ if index == 0 and not kept_keys:
+ trimmed = _trim_chunk_to_budget(chunk_map[key], max_tokens)
+ if trimmed:
+ chunk_map[key] = trimmed
+ token_map[key] = count_tokens(trimmed)
+ kept_keys.add(key)
+ used += token_map[key]
continue
kept_keys.add(key)
used += chunk_tokens
diff --git a/src/vaner/broker/context_preparation.py b/src/vaner/broker/context_preparation.py
new file mode 100644
index 0000000..becb5b9
--- /dev/null
+++ b/src/vaner/broker/context_preparation.py
@@ -0,0 +1,342 @@
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import re
+from collections import Counter
+
+from vaner.models.artefact import Artefact
+from vaner.models.context_preparation import (
+ ContextConstraint,
+ ContextCoverageReport,
+ ContextFacet,
+ ContextPreparationProfile,
+ ContextSourceStats,
+ PreparedContextDiagnostics,
+)
+
+_PATH_RE = re.compile(r"\b(?:[\w.-]+/)+[\w.-]+\b")
+_QUOTED_RE = re.compile(r"[`'\"]([^`'\"]{3,100})[`'\"]")
+_DATE_RE = re.compile(
+ r"\b(?:\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{2,4}|"
+ r"jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|"
+ r"jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b",
+ re.IGNORECASE,
+)
+_RESTRICTIVE_RE = re.compile(
+ r"\b(?:only|except|before|after|latest|current|superseded|newest|oldest|no later than|at least|at most)\b",
+ re.IGNORECASE,
+)
+
+
+def infer_context_preparation_profile(prompt: str) -> ContextPreparationProfile:
+ lowered = prompt.lower()
+ archetype = _infer_archetype(lowered)
+ need = _infer_need(lowered, archetype)
+ constraints = _extract_constraints(prompt)
+ facets = _extract_facets(prompt, constraints)
+ source_hints = _extract_source_hints(lowered)
+ expected_evidence_count = _expected_evidence_count(lowered, need)
+ confidence = 0.55 + min(0.35, 0.05 * len(facets) + 0.06 * len(constraints))
+ notes: list[str] = []
+ if any(constraint.kind == "restrictive_language" for constraint in constraints):
+ notes.append("hard_constraints_detected")
+ if need in {"multi_source_synthesis", "conflict_resolution", "research_mapping"}:
+ notes.append("coverage_sensitive")
+ return ContextPreparationProfile(
+ need=need,
+ archetype=archetype,
+ facets=facets,
+ constraints=constraints,
+ source_hints=source_hints,
+ expected_evidence_count=expected_evidence_count,
+ confidence=min(0.95, confidence),
+ notes=notes,
+ )
+
+
+def query_variants(prompt: str, profile: ContextPreparationProfile, *, max_variants: int = 6) -> list[str]:
+ variants = [prompt]
+ facet_terms = [facet.value for facet in profile.facets if len(facet.value) > 2]
+ constraint_terms = [constraint.value for constraint in profile.constraints if constraint.kind != "restrictive_language"]
+ if facet_terms:
+ variants.append(" ".join(facet_terms[:8]))
+ if constraint_terms:
+ variants.append(" ".join([*constraint_terms[:4], *facet_terms[:6]]))
+ if profile.need in {"multi_source_synthesis", "research_mapping", "decision_support"}:
+ variants.append(" ".join([*facet_terms[:10], "summary status decision evidence"]))
+ if profile.need == "conflict_resolution":
+ variants.append(" ".join([*facet_terms[:8], "current superseded conflict updated latest"]))
+ if profile.need == "implementation_support":
+ variants.append(" ".join([*facet_terms[:10], "implementation caller dependency test"]))
+ if profile.need == "absence_check":
+ variants.append(" ".join([*facet_terms[:8], "source truth reference policy"]))
+ deduped = []
+ for variant in variants:
+ normalized = " ".join(variant.split())
+ if normalized and normalized.lower() not in {item.lower() for item in deduped}:
+ deduped.append(normalized)
+ if len(deduped) >= max(1, max_variants):
+ break
+ return deduped
+
+
+def exact_reference_candidates(prompt: str, available_paths: list[str], *, limit: int = 32) -> list[str]:
+ references = set(_PATH_RE.findall(prompt))
+ references.update(match.group(1).strip() for match in _QUOTED_RE.finditer(prompt))
+ identifier_terms = {
+ token.lower()
+ for token in re.findall(r"\b[A-Za-z_][A-Za-z0-9_]{3,}\b", prompt)
+ if "_" in token or any(char.isupper() for char in token[1:])
+ }
+ ranked: list[tuple[int, str]] = []
+ for path in available_paths:
+ lowered_path = path.lower()
+ score = 0
+ for reference in references:
+ ref = reference.strip().lower()
+ if not ref:
+ continue
+ if ref == lowered_path or lowered_path.endswith(ref):
+ score += 12
+ elif ref in lowered_path:
+ score += 6
+ basename = lowered_path.rsplit("/", 1)[-1]
+ stem = basename.rsplit(".", 1)[0]
+ for term in identifier_terms:
+ if term == stem or term in basename:
+ score += 5
+ elif term in lowered_path:
+ score += 2
+ if score:
+ ranked.append((score, path))
+ ranked.sort(key=lambda item: (-item[0], item[1]))
+ return [path for _, path in ranked[:limit]]
+
+
+def hard_constraints_satisfied(prompt: str, artefact: Artefact, profile: ContextPreparationProfile) -> tuple[bool, list[str], list[str]]:
+ text = f"{artefact.source_path}\n{artefact.content}".lower()
+ satisfied: list[str] = []
+ missing: list[str] = []
+ for constraint in profile.constraints:
+ value = constraint.value.lower()
+ if constraint.kind == "restrictive_language":
+ satisfied.append(constraint.value)
+ continue
+ if value in text:
+ satisfied.append(constraint.value)
+ elif constraint.required:
+ missing.append(constraint.value)
+ if _constraint_terms(prompt) and not satisfied and profile.need in {"direct_reference", "conflict_resolution", "absence_check"}:
+ return False, satisfied, missing
+ return not missing, satisfied, missing
+
+
+def competitive_threshold_multiplier(need: str | None) -> float:
+ if need in {"multi_source_synthesis", "conflict_resolution", "research_mapping", "decision_support"}:
+ return 0.0
+ if need in {"implementation_support", "absence_check", "working_set_extension", "creative_grounding"}:
+ return 0.20
+ if need == "evidence_gathering":
+ return 0.30
+ return 0.45
+
+
+def build_coverage_report(
+ profile: ContextPreparationProfile,
+ selected: list[Artefact],
+ source_by_key: dict[str, set[str]] | None = None,
+) -> ContextCoverageReport:
+ selected_text = "\n".join(f"{artefact.source_path}\n{artefact.content}" for artefact in selected).lower()
+ covered_facets = [facet.value for facet in profile.facets if facet.value.lower() in selected_text]
+ missing_constraints = [
+ constraint.value
+ for constraint in profile.constraints
+ if constraint.kind != "restrictive_language" and constraint.required and constraint.value.lower() not in selected_text
+ ]
+ direct_evidence_count = sum(1 for artefact in selected if _direct_match_count(profile, artefact) > 0)
+ source_sets = [source_by_key.get(artefact.key, set()) for artefact in selected] if source_by_key else []
+ source_agreement = sum(len(sources) for sources in source_sets) / max(1, len(source_sets))
+ conflict_pair_coverage = profile.need != "conflict_resolution" or _has_conflict_pair(selected_text)
+ compactness_risk = "high" if len(selected) > max(8, profile.expected_evidence_count * 3) else "medium" if len(selected) > 6 else "low"
+ gap_flags: list[str] = []
+ if missing_constraints:
+ gap_flags.append("missing_constraints")
+ if len(covered_facets) < min(len(profile.facets), profile.expected_evidence_count):
+ gap_flags.append("facet_coverage_weak")
+ if profile.need == "conflict_resolution" and not conflict_pair_coverage:
+ gap_flags.append("conflict_pair_missing")
+ actionability = "full"
+ if gap_flags:
+ actionability = "weak"
+ if not selected:
+ actionability = "none"
+ if profile.need == "conflict_resolution" and conflict_pair_coverage:
+ actionability = "conflict"
+ return ContextCoverageReport(
+ covered_facets=covered_facets,
+ missing_constraints=missing_constraints,
+ direct_evidence_count=direct_evidence_count,
+ conflict_pair_coverage=conflict_pair_coverage,
+ actionability=actionability,
+ source_agreement=min(1.0, source_agreement / 2.0),
+ weak_expansion_dependency=any(
+ source_by_key and source_by_key.get(artefact.key) == {"generated_query_variants"} for artefact in selected
+ ),
+ compactness_risk=compactness_risk,
+ gap_flags=gap_flags,
+ )
+
+
+def build_prepared_context_diagnostics(
+ profile: ContextPreparationProfile,
+ *,
+ source_by_key: dict[str, set[str]],
+ selected: list[Artefact],
+ fused_candidate_count: int,
+ latency_ms: float,
+) -> PreparedContextDiagnostics:
+ selected_keys = {artefact.key for artefact in selected}
+ source_counter: Counter[str] = Counter()
+ selected_counter: Counter[str] = Counter()
+ for key, sources in source_by_key.items():
+ for source in sources:
+ source_counter[source] += 1
+ if key in selected_keys:
+ selected_counter[source] += 1
+ constraints = [constraint.value for constraint in profile.constraints]
+ coverage = build_coverage_report(profile, selected, source_by_key)
+ return PreparedContextDiagnostics(
+ profile=profile,
+ source_counts=[
+ ContextSourceStats(source=source, candidate_count=count, selected_count=selected_counter.get(source, 0))
+ for source, count in sorted(source_counter.items())
+ ],
+ fused_candidate_count=fused_candidate_count,
+ selected_count=len(selected),
+ hard_constraints_extracted=constraints,
+ hard_constraints_satisfied=[value for value in constraints if value not in coverage.missing_constraints],
+ hard_constraints_missing=coverage.missing_constraints,
+ coverage=coverage,
+ weak_expansion_dependency=coverage.weak_expansion_dependency,
+ truncation_risk=coverage.truncation_risk,
+ latency_ms=latency_ms,
+ compactness_score=1.0 if coverage.compactness_risk == "low" else 0.65 if coverage.compactness_risk == "medium" else 0.35,
+ provenance_coverage=sum(1 for artefact in selected if source_by_key.get(artefact.key)) / max(1, len(selected)),
+ )
+
+
+def _infer_archetype(lowered: str):
+ if re.search(r"\b(file|symbol|function|class|module|bug|test|diff|pr|commit|implementation|dependency|stack trace)\b", lowered):
+ return "developer"
+ if re.search(r"\b(draft|outline|tone|audience|rewrite|section|claim|essay|article|copy)\b", lowered):
+ return "writer"
+ if re.search(r"\b(paper|study|dataset|method|citation|literature|hypothesis|experiment|result)\b", lowered):
+ return "researcher"
+ if re.search(r"\b(incident|runbook|sla|handoff|customer|support|ops|oncall|status|risk)\b", lowered):
+ return "operator"
+ return "general"
+
+
+def _infer_need(lowered: str, archetype: str):
+ if re.search(r"\b(conflict|contradict|superseded|outdated|latest|current|newer|older|reconcile)\b", lowered):
+ return "conflict_resolution"
+ if re.search(r"\b(not found|unavailable|missing|is there|do we have|absence|not available|cannot find)\b", lowered):
+ return "absence_check"
+ if archetype == "developer" and re.search(r"\b(implement|fix|debug|change|affected|callers|tests?|dependency|regression)\b", lowered):
+ return "implementation_support"
+ if archetype == "writer":
+ return "creative_grounding"
+ if archetype == "researcher":
+ return "research_mapping"
+ if re.search(r"\b(all|every|list|compare|across|summarize|synthesize|count|which .* and|what .* and)\b", lowered):
+ return "multi_source_synthesis"
+ if re.search(r"\b(decide|decision|recommend|tradeoff|risk|should we|plan)\b", lowered):
+ return "decision_support"
+ if re.search(r"\b(where|defined|introduced|implemented|exact|which file|what is)\b", lowered):
+ return "direct_reference"
+ if re.search(r"\b(continue|next|finish|carry on|pick up)\b", lowered):
+ return "task_continuation"
+ return "evidence_gathering"
+
+
+def _extract_constraints(prompt: str) -> list[ContextConstraint]:
+ constraints: list[ContextConstraint] = []
+ for path in _PATH_RE.findall(prompt):
+ constraints.append(ContextConstraint(kind="path", value=path))
+ for quoted in _QUOTED_RE.findall(prompt):
+ constraints.append(ContextConstraint(kind="quoted_reference", value=quoted.strip()))
+ for match in _DATE_RE.finditer(prompt):
+ constraints.append(ContextConstraint(kind="date", value=match.group(0)))
+ for match in _RESTRICTIVE_RE.finditer(prompt):
+ constraints.append(ContextConstraint(kind="restrictive_language", value=match.group(0).lower()))
+ return _dedupe_constraints(constraints)
+
+
+def _extract_facets(prompt: str, constraints: list[ContextConstraint]) -> list[ContextFacet]:
+ constrained_values = {constraint.value.lower() for constraint in constraints}
+ facets: list[ContextFacet] = []
+ for token in re.findall(r"\b[A-Za-z][A-Za-z0-9_-]{3,}\b", prompt):
+ lowered = token.lower()
+ if lowered in constrained_values or lowered in {"what", "where", "when", "which", "does", "with", "from", "that", "this", "should"}:
+ continue
+ required = any(char.isupper() for char in token[1:]) or "_" in token or "-" in token
+ facets.append(ContextFacet(name="term", value=token, required=required))
+ return _dedupe_facets(facets[:16])
+
+
+def _extract_source_hints(lowered: str) -> list[str]:
+ hints = []
+ for hint in ("slack", "gmail", "email", "github", "jira", "linear", "confluence", "docs", "meeting", "runbook", "calendar"):
+ if hint in lowered:
+ hints.append(hint)
+ return hints
+
+
+def _expected_evidence_count(lowered: str, need: str) -> int:
+ if need in {"multi_source_synthesis", "research_mapping"}:
+ return 4
+ if need == "conflict_resolution":
+ return 2
+ if re.search(r"\b(all|every|complete|completeness|list)\b", lowered):
+ return 5
+ return 1
+
+
+def _constraint_terms(prompt: str) -> set[str]:
+ return {match.group(0).lower() for match in _RESTRICTIVE_RE.finditer(prompt)}
+
+
+def _direct_match_count(profile: ContextPreparationProfile, artefact: Artefact) -> int:
+ text = f"{artefact.source_path}\n{artefact.content}".lower()
+ return sum(1 for facet in profile.facets if facet.value.lower() in text)
+
+
+def _has_conflict_pair(text: str) -> bool:
+ current = any(term in text for term in ("current", "latest", "updated", "newer", "now"))
+ superseded = any(term in text for term in ("superseded", "old", "older", "deprecated", "previous"))
+ return current and superseded
+
+
+def _dedupe_constraints(constraints: list[ContextConstraint]) -> list[ContextConstraint]:
+ result = []
+ seen = set()
+ for constraint in constraints:
+ key = (constraint.kind, constraint.value.lower())
+ if key in seen:
+ continue
+ seen.add(key)
+ result.append(constraint)
+ return result
+
+
+def _dedupe_facets(facets: list[ContextFacet]) -> list[ContextFacet]:
+ result = []
+ seen = set()
+ for facet in facets:
+ key = facet.value.lower()
+ if key in seen:
+ continue
+ seen.add(key)
+ result.append(facet)
+ return result
diff --git a/src/vaner/broker/selector.py b/src/vaner/broker/selector.py
index 2b0ab33..bfe82c6 100644
--- a/src/vaner/broker/selector.py
+++ b/src/vaner/broker/selector.py
@@ -8,8 +8,18 @@
from collections.abc import Callable
from fnmatch import fnmatch
+from vaner.broker.context_preparation import (
+ build_prepared_context_diagnostics,
+ competitive_threshold_multiplier,
+ exact_reference_candidates,
+ hard_constraints_satisfied,
+ infer_context_preparation_profile,
+ query_variants,
+)
from vaner.models.artefact import Artefact
+from vaner.models.context_preparation import ContextPreparationProfile, PreparedContextDiagnostics
from vaner.models.decision import ScoreFactor
+from vaner.semantic_aliases import engineering_semantic_aliases
def _recency_bonus(artefact: Artefact, decay_half_life_seconds: int = 1800) -> float:
@@ -41,6 +51,7 @@ def _prompt_terms(prompt: str) -> list[str]:
terms.extend(part.lower() for part in raw.split("_") if len(part) > 2)
if lowered not in {"fastapi", "openapi"}:
terms.extend(part.lower() for part in _camel_parts(raw) if len(part) > 2)
+ terms.extend(engineering_semantic_aliases(prompt, terms, stopwords=_COMMON_WORDS))
return list(dict.fromkeys(term for term in terms if len(term) > 2 and term not in _COMMON_WORDS))
@@ -310,12 +321,18 @@ def _doc_domain_bonus(path_text: str, content_text: str, terms: list[str]) -> fl
if path_text in {
"src/vaner/intent/features.py",
"src/vaner/intent/trainer.py",
+ "src/vaner/intent/scorer.py",
"src/vaner/models/artefact.py",
"src/vaner/store/artefacts.py",
"tests/test_intent/test_features_follow_up.py",
"tests/test_intent/test_trainer_v4_rollover.py",
}:
bonus += 14.0
+ if "intent" in term_set and "scorer" in term_set and path_text in {
+ "src/vaner/intent/scorer.py",
+ "src/vaner/intent/features.py",
+ }:
+ bonus += 10.0
if any(
term in content_text
for term in (
@@ -348,6 +365,39 @@ def _doc_domain_bonus(path_text: str, content_text: str, terms: list[str]) -> fl
bonus += 2.0
if any(term in content_text for term in ("full_hit", "partial_hit", "warm_start", "cache_full_hit", "cache_partial_hit")):
bonus += 8.0
+ reward_terms = {"reward", "computation", "compute", "signals", "combine", "final", "value", "quality", "lift", "judge"}
+ if "reward" in term_set and len(term_set & reward_terms) >= 3:
+ if path_text in {
+ "src/vaner/learning/reward.py",
+ "eval/train_policy.py",
+ "src/vaner/intent/trainer.py",
+ "tests/test_learning/test_reward.py",
+ }:
+ bonus += 18.0
+ if any(
+ term in content_text
+ for term in (
+ "compute_reward",
+ "reward_total",
+ "reward_components",
+ "quality_lift",
+ "host_outcome",
+ "judge_score",
+ )
+ ):
+ bonus += 10.0
+ if path_text == "src/vaner/intent/scoring_policy.py":
+ bonus -= 4.0
+ store_schema_terms = {"artefactstore", "artefact", "artefacts", "persist", "retrieve", "database", "schema", "table", "tables"}
+ if len(term_set & store_schema_terms) >= 3:
+ if path_text == "src/vaner/store/artefacts.py":
+ bonus += 24.0
+ if path_text == "src/vaner/models/context.py":
+ bonus += 8.0
+ if "package.json" in path_text:
+ bonus -= 22.0
+ if any(term in content_text for term in ("create table", "from artefacts", "insert into artefacts", "select key")):
+ bonus += 12.0
llm_exploration_terms = {
"llm",
"external",
@@ -441,8 +491,15 @@ def _origin_bonus(prompt: str, content: str) -> float:
def _build_fts_query(prompt: str) -> str:
"""Build a safe FTS5 query string from a natural-language prompt."""
- filtered = _prompt_terms(prompt)[:15]
- return " ".join(filtered)
+ filtered = _prompt_terms(prompt)[:24]
+ return " OR ".join(filtered)
+
+
+def _candidate_limit(prompt: str, top_n: int) -> int:
+ terms = _prompt_terms(prompt)
+ multi_facet = prompt.count("?") + len(re.findall(r"\b(?:and|what|how|when|which)\b", prompt, flags=re.IGNORECASE))
+ requested = max(50, int(top_n) * 12, len(terms) * 8, multi_facet * 12)
+ return max(50, min(240, requested))
async def select_artefacts_fts(
@@ -457,8 +514,16 @@ async def select_artefacts_fts(
path_excludes: list[str] | None = None,
capture_factors: dict[str, list[ScoreFactor]] | None = None,
capture_drop_reasons: dict[str, str] | None = None,
+ candidate_limit: int | None = None,
+ full_scan_limit: int = 2000,
+ context_preparation_mode: str = "balanced",
+ max_query_variants: int = 6,
+ max_candidate_keys: int | None = None,
+ coverage_floor_enabled: bool = True,
+ max_expansion_passes: int = 1,
+ capture_prepared_context_diagnostics: list[PreparedContextDiagnostics] | None = None,
) -> list[Artefact]:
- """Two-phase selection: FTS candidate retrieval, then scorer re-rank.
+ """Multi-source context candidate retrieval, then scorer re-rank.
Falls back to loading all artefacts when the FTS index returns no hits
or when *store* does not expose ``select_artefacts_fts``.
@@ -466,37 +531,132 @@ async def select_artefacts_fts(
from vaner.store.artefacts import ArtefactStore # avoid circular at module level
fts_available = isinstance(store, ArtefactStore)
-
- # Phase 1: FTS candidate retrieval (gracefully skipped if unavailable)
- candidate_keys: set[str] = set()
+ started = time.monotonic()
+ profile = infer_context_preparation_profile(prompt)
+ context_enabled = context_preparation_mode != "legacy"
+
+ source_by_key: dict[str, set[str]] = {}
+ source_rankings: dict[str, list[str]] = {}
+ retrieval_limit = candidate_limit if candidate_limit is not None else _candidate_limit(prompt, top_n)
+ if max_candidate_keys is not None:
+ retrieval_limit = max(50, min(retrieval_limit, max_candidate_keys))
if fts_available:
- fts_query = _build_fts_query(prompt)
- if fts_query:
+ variants = query_variants(prompt, profile, max_variants=max_query_variants) if context_enabled else [prompt]
+ for index, variant in enumerate(variants):
+ fts_query = _build_fts_query(variant)
+ if not fts_query:
+ continue
+ source = "lexical_search" if index == 0 else "generated_query_variants"
+ try:
+ keys = list(await store.select_artefacts_fts(fts_query, limit=retrieval_limit)) # type: ignore[union-attr]
+ except Exception:
+ keys = []
+ _record_source_ranking(source_rankings, source_by_key, source, keys)
+ if context_enabled:
try:
- candidate_keys = set(await store.select_artefacts_fts(fts_query, limit=50)) # type: ignore[union-attr]
+ available_paths = await store.list_source_paths(limit=max(5000, retrieval_limit * 10)) # type: ignore[union-attr]
except Exception:
- candidate_keys = set()
+ available_paths = []
+ exact_paths = set(exact_reference_candidates(prompt, available_paths, limit=min(32, max(8, top_n * 3))))
+ else:
+ exact_paths = set()
+ else:
+ exact_paths = set()
+
+ preferred = preferred_keys or set()
+ preferred_paths_set = preferred_paths or set()
+ if preferred:
+ _record_source_ranking(source_rankings, source_by_key, "working_set", list(preferred))
+
+ fused_keys = _fuse_source_rankings(source_rankings, limit=max_candidate_keys or retrieval_limit)
- # Phase 2: load candidates; fall back to full list on FTS miss
if fts_available:
- all_artefacts: list[Artefact] = await store.list(limit=2000) # type: ignore[union-attr]
+ if fused_keys or exact_paths or preferred:
+ loaded: list[Artefact] = []
+ load_keys = set(fused_keys) | preferred
+ loaded.extend(await store.list_by_keys(load_keys, limit=max(retrieval_limit, len(load_keys)))) # type: ignore[union-attr]
+ path_loads = set(preferred_paths_set) | exact_paths
+ if path_loads:
+ path_loaded = await store.list_by_source_paths(path_loads, limit=max(retrieval_limit, len(path_loads))) # type: ignore[union-attr]
+ loaded.extend(path_loaded)
+ _record_source_ranking(source_rankings, source_by_key, "exact_reference", [artefact.key for artefact in path_loaded])
+ seen: set[str] = set()
+ candidates = []
+ for artefact in loaded:
+ if artefact.key in seen:
+ continue
+ seen.add(artefact.key)
+ candidates.append(artefact)
+ selected = select_artefacts(
+ prompt,
+ candidates,
+ top_n=top_n,
+ preferred_paths=preferred_paths,
+ preferred_keys=preferred_keys,
+ scorer=scorer,
+ exclude_private=exclude_private,
+ path_bonuses=path_bonuses,
+ path_excludes=path_excludes,
+ capture_factors=capture_factors,
+ capture_drop_reasons=capture_drop_reasons,
+ context_need=profile.need if context_enabled else None,
+ context_profile=profile if context_enabled else None,
+ )
+ if context_enabled and coverage_floor_enabled and max_expansion_passes > 0:
+ diagnostics = build_prepared_context_diagnostics(
+ profile,
+ source_by_key=source_by_key,
+ selected=selected,
+ fused_candidate_count=len(fused_keys),
+ latency_ms=(time.monotonic() - started) * 1000.0,
+ )
+ if diagnostics.coverage.gap_flags:
+ recovery_terms = _coverage_recovery_terms(profile, diagnostics)
+ recovery_query = _build_fts_query(recovery_terms)
+ if recovery_query:
+ try:
+ recovery_keys = list(await store.select_artefacts_fts(recovery_query, limit=retrieval_limit)) # type: ignore[union-attr]
+ except Exception:
+ recovery_keys = []
+ new_keys = [key for key in recovery_keys if key not in {candidate.key for candidate in candidates}]
+ if new_keys:
+ _record_source_ranking(source_rankings, source_by_key, "coverage_floor", new_keys)
+ recovered = await store.list_by_keys(set(new_keys), limit=max(retrieval_limit, len(new_keys))) # type: ignore[union-attr]
+ candidates.extend(recovered)
+ selected = select_artefacts(
+ prompt,
+ candidates,
+ top_n=top_n,
+ preferred_paths=preferred_paths,
+ preferred_keys=preferred_keys,
+ scorer=scorer,
+ exclude_private=exclude_private,
+ path_bonuses=path_bonuses,
+ path_excludes=path_excludes,
+ capture_factors=capture_factors,
+ capture_drop_reasons=capture_drop_reasons,
+ context_need=profile.need,
+ context_profile=profile,
+ )
+ _append_source_factors(capture_factors, source_by_key, selected)
+ if capture_prepared_context_diagnostics is not None:
+ capture_prepared_context_diagnostics.append(
+ build_prepared_context_diagnostics(
+ profile,
+ source_by_key=source_by_key,
+ selected=selected,
+ fused_candidate_count=len(fused_keys),
+ latency_ms=(time.monotonic() - started) * 1000.0,
+ )
+ )
+ return selected
+ all_artefacts: list[Artefact] = await store.list(limit=full_scan_limit) # type: ignore[union-attr]
else:
return []
- if candidate_keys:
- candidates = [a for a in all_artefacts if a.key in candidate_keys]
- # Always include preferred items even if not in FTS results
- preferred = preferred_keys or set()
- preferred_paths_set = preferred_paths or set()
- for a in all_artefacts:
- if a.key not in candidate_keys and (a.key in preferred or a.source_path in preferred_paths_set):
- candidates.append(a)
- else:
- candidates = all_artefacts
-
- return select_artefacts(
+ selected = select_artefacts(
prompt,
- candidates,
+ all_artefacts,
top_n=top_n,
preferred_paths=preferred_paths,
preferred_keys=preferred_keys,
@@ -506,7 +666,108 @@ async def select_artefacts_fts(
path_excludes=path_excludes,
capture_factors=capture_factors,
capture_drop_reasons=capture_drop_reasons,
+ context_need=profile.need if context_enabled else None,
+ context_profile=profile if context_enabled else None,
)
+ if capture_prepared_context_diagnostics is not None:
+ capture_prepared_context_diagnostics.append(
+ build_prepared_context_diagnostics(
+ profile,
+ source_by_key=source_by_key,
+ selected=selected,
+ fused_candidate_count=len(all_artefacts),
+ latency_ms=(time.monotonic() - started) * 1000.0,
+ )
+ )
+ return selected
+
+
+def _record_source_ranking(
+ source_rankings: dict[str, list[str]],
+ source_by_key: dict[str, set[str]],
+ source: str,
+ keys: list[str],
+) -> None:
+ deduped = list(dict.fromkeys(key for key in keys if key))
+ if not deduped:
+ return
+ source_rankings.setdefault(source, [])
+ seen = set(source_rankings[source])
+ for key in deduped:
+ source_by_key.setdefault(key, set()).add(source)
+ if key not in seen:
+ source_rankings[source].append(key)
+ seen.add(key)
+
+
+def _fuse_source_rankings(source_rankings: dict[str, list[str]], *, limit: int) -> list[str]:
+ weights = {
+ "exact_reference": 1.45,
+ "prepared_context_cache": 1.25,
+ "working_set": 1.20,
+ "lexical_search": 1.0,
+ "generated_query_variants": 0.72,
+ "relationship_graph": 0.9,
+ "semantic_memory": 0.75,
+ "coverage_floor": 0.85,
+ }
+ scores: dict[str, float] = {}
+ for source, keys in source_rankings.items():
+ weight = weights.get(source, 0.8)
+ for rank, key in enumerate(keys, start=1):
+ scores[key] = scores.get(key, 0.0) + weight / (60.0 + rank)
+ return [
+ key
+ for key, _ in sorted(
+ scores.items(),
+ key=lambda item: (-item[1], item[0]),
+ )[: max(1, limit)]
+ ]
+
+
+def _append_source_factors(
+ capture_factors: dict[str, list[ScoreFactor]] | None,
+ source_by_key: dict[str, set[str]],
+ selected: list[Artefact],
+) -> None:
+ if capture_factors is None:
+ return
+ for artefact in selected:
+ sources = sorted(source_by_key.get(artefact.key, set()))
+ if not sources:
+ continue
+ capture_factors.setdefault(artefact.key, []).append(
+ ScoreFactor(
+ name="context_sources",
+ contribution=min(2.0, 0.25 * len(sources)),
+ detail="candidate appeared in context sources: " + ", ".join(sources),
+ )
+ )
+
+
+def _diversity_bucket(artefact: Artefact, context_need: str | None) -> str:
+ path = artefact.source_path
+ if context_need == "implementation_support":
+ parts = path.split("/")
+ return "/".join(parts[:3]) if len(parts) >= 3 else path
+ source_type = str(artefact.metadata.get("source_type") or artefact.metadata.get("corpus_id") or "")
+ if source_type:
+ return source_type
+ parts = path.split("/")
+ return "/".join(parts[:2]) if len(parts) >= 2 else path
+
+
+def _coverage_recovery_terms(profile: ContextPreparationProfile, diagnostics: PreparedContextDiagnostics) -> str:
+ covered = {item.lower() for item in diagnostics.coverage.covered_facets}
+ missing_facets = [facet.value for facet in profile.facets if facet.value.lower() not in covered]
+ terms = [*diagnostics.coverage.missing_constraints, *missing_facets[:10]]
+ if profile.need == "conflict_resolution":
+ terms.extend(["current", "superseded", "latest", "updated"])
+ elif profile.need == "multi_source_synthesis":
+ terms.extend(["summary", "status", "evidence"])
+ elif profile.need == "implementation_support":
+ terms.extend(["caller", "dependency", "test", "implementation"])
+ return " ".join(term for term in terms if term)
def select_artefacts(
@@ -521,6 +782,8 @@ def select_artefacts(
path_excludes: list[str] | None = None,
capture_factors: dict[str, list[ScoreFactor]] | None = None,
capture_drop_reasons: dict[str, str] | None = None,
+ context_need: str | None = None,
+ context_profile: ContextPreparationProfile | None = None,
) -> list[Artefact]:
preferred_paths = preferred_paths or set()
preferred_keys = preferred_keys or set()
@@ -538,6 +801,13 @@ def select_artefacts(
if capture_drop_reasons is not None:
capture_drop_reasons[artefact.key] = "path_excluded"
continue
+ satisfied_constraints: list[str] = []
+ if context_profile is not None:
+ constraints_ok, satisfied_constraints, missing_constraints = hard_constraints_satisfied(prompt, artefact, context_profile)
+ if not constraints_ok:
+ if capture_drop_reasons is not None:
+ capture_drop_reasons[artefact.key] = "missing_hard_constraints:" + ",".join(missing_constraints[:3])
+ continue
factors: list[ScoreFactor] = []
if scorer is not None:
score = scorer(prompt, artefact)
@@ -605,6 +875,16 @@ def select_artefacts(
)
)
score += pinned_path_bonus
+ if context_profile is not None and satisfied_constraints:
+ constraint_bonus = min(2.0, 0.35 * len(satisfied_constraints))
+ factors.append(
+ ScoreFactor(
+ name="hard_constraint_match",
+ contribution=constraint_bonus,
+ detail="artefact satisfies extracted hard constraints",
+ )
+ )
+ score += constraint_bonus
if capture_factors is not None:
capture_factors[artefact.key] = factors
scored_rows.append((score, artefact))
@@ -612,19 +892,32 @@ def select_artefacts(
ranked = sorted(scored_rows, key=lambda item: item[0], reverse=True)
selected: list[Artefact] = []
seen_corpora: set[str] = set()
- min_competitive_score = ranked[0][0] * 0.45 if ranked else 0.0
+ threshold_multiplier = competitive_threshold_multiplier(context_need)
+ min_competitive_score = ranked[0][0] * threshold_multiplier if ranked else 0.0
+ selected_buckets: set[str] = set()
for score, artefact in ranked:
if score < min_competitive_score:
if capture_drop_reasons is not None:
capture_drop_reasons[artefact.key] = "below_competitive_threshold"
continue
corpus_id = str(artefact.metadata.get("corpus_id", "default"))
+ bucket = _diversity_bucket(artefact, context_need)
+ if (
+ context_need in {"multi_source_synthesis", "conflict_resolution", "research_mapping", "decision_support"}
+ and bucket in selected_buckets
+ and len(selected) < max(1, top_n // 2)
+ ):
+ if capture_drop_reasons is not None:
+ capture_drop_reasons[artefact.key] = "deferred_for_context_diversity"
+ continue
if selected and corpus_id not in seen_corpora:
selected.append(artefact)
seen_corpora.add(corpus_id)
+ selected_buckets.add(bucket)
elif len(selected) < top_n:
selected.append(artefact)
seen_corpora.add(corpus_id)
+ selected_buckets.add(bucket)
if len(selected) >= top_n:
break
if capture_drop_reasons is not None:
diff --git a/src/vaner/cli/commands/config.py b/src/vaner/cli/commands/config.py
index 68e848d..9f85191 100644
--- a/src/vaner/cli/commands/config.py
+++ b/src/vaner/cli/commands/config.py
@@ -166,6 +166,8 @@ def load_config(repo_root: Path) -> VanerConfig:
"exploration_backend": exploration_section.get("backend", "auto"),
"embedding_model": exploration_section.get("embedding_model", "all-MiniLM-L6-v2"),
"embedding_device": exploration_section.get("embedding_device", "cpu"),
+ "runtime_options": exploration_section.get("runtime_options", {}),
+ "sampling_options": exploration_section.get("sampling_options", {}),
}
exploration = ExplorationConfig(**mapped_exploration)
else:
@@ -177,7 +179,7 @@ def load_config(repo_root: Path) -> VanerConfig:
policy = _build_section(PolicyConfig, "policy", policy_section)
max_age_seconds = _coerce_limit(limits_section, "max_age_seconds", 3600)
- max_context_tokens = _coerce_limit(limits_section, "max_context_tokens", 4096)
+ max_context_tokens = _coerce_limit(limits_section, "max_context_tokens", 8192)
store_path = repo_root / ".vaner" / "store.db"
telemetry_path = repo_root / ".vaner" / "telemetry.db"
diff --git a/src/vaner/cli/commands/init.py b/src/vaner/cli/commands/init.py
index cbc0cd0..7aeac5e 100644
--- a/src/vaner/cli/commands/init.py
+++ b/src/vaner/cli/commands/init.py
@@ -121,7 +121,7 @@
[limits]
max_age_seconds = 3600
-max_context_tokens = 4096
+max_context_tokens = 8192
[intent]
enabled = true
diff --git a/src/vaner/cli/commands/setup.py b/src/vaner/cli/commands/setup.py
index 5a50510..5595006 100644
--- a/src/vaner/cli/commands/setup.py
+++ b/src/vaner/cli/commands/setup.py
@@ -1033,7 +1033,8 @@ def catalog_refresh_cmd(
f"{len(skipped)} skipped[/dim])"
)
for entry in skipped:
- _console.print(f" [yellow]skipped[/yellow] {entry['id']} — {entry['reason']}")
+ family = entry.get("family") or entry.get("id") or "unknown"
+ _console.print(f" [yellow]skipped[/yellow] {family} — {entry['reason']}")
@catalog_app.command(
diff --git a/src/vaner/clients/endpoint_pool.py b/src/vaner/clients/endpoint_pool.py
index 380bec9..eed1b1b 100644
--- a/src/vaner/clients/endpoint_pool.py
+++ b/src/vaner/clients/endpoint_pool.py
@@ -138,7 +138,18 @@ def from_endpoints(
if ep.weight <= 0:
continue
if ep.backend == "ollama":
- client: LLMCallable = ollama_llm(model=ep.model, base_url=ep.url, timeout=timeout)
+ options = {
+ str(key): value
+ for source in (getattr(ep, "runtime_options", {}) or {}, getattr(ep, "sampling_options", {}) or {})
+ for key, value in source.items()
+ if value is not None and value != ""
+ }
+ client: LLMCallable = ollama_llm(
+ model=ep.model,
+ base_url=ep.url,
+ timeout=timeout,
+ extra_body={"options": options} if options else None,
+ )
else:
api_key = (_env(ep.api_key_env) if ep.api_key_env else None) or ""
if not api_key and not cls._is_local_url(ep.url):
diff --git a/src/vaner/clients/ollama.py b/src/vaner/clients/ollama.py
index a46b904..287fcd6 100644
--- a/src/vaner/clients/ollama.py
+++ b/src/vaner/clients/ollama.py
@@ -17,6 +17,9 @@ def ollama_llm(
model: str,
base_url: str = "http://127.0.0.1:11434",
timeout: float = 120.0,
+ max_tokens: int | None = None,
+ extra_body: dict | None = None,
+ reasoning_mode: ReasoningMode = "off",
) -> Callable[[str], Awaitable[str]]:
"""Ollama inference client (bare-string return — legacy contract).
@@ -27,7 +30,9 @@ def ollama_llm(
model=model,
base_url=base_url,
timeout=timeout,
- reasoning_mode="off",
+ max_tokens=max_tokens,
+ extra_body=extra_body,
+ reasoning_mode=reasoning_mode,
)
async def _call(prompt: str) -> str:
@@ -79,7 +84,11 @@ async def _call(prompt: str, *, max_tokens: int | None = max_tokens) -> LLMRespo
body = {"model": model, "messages": [{"role": "user", "content": prompt}], "stream": False}
else:
body = {"model": model, "prompt": prompt, "stream": False}
+ merged_extra = dict(extra_body or {})
+ extra_options = merged_extra.pop("options", None)
options: dict = {}
+ if isinstance(extra_options, dict):
+ options.update(extra_options)
if max_tokens is not None and max_tokens > 0:
options["num_predict"] = int(max_tokens)
if options:
@@ -88,7 +97,6 @@ async def _call(prompt: str, *, max_tokens: int | None = max_tokens) -> LLMRespo
rf_type = response_format.get("type")
if rf_type in ("json_object", "json"):
body["format"] = "json"
- merged_extra = dict(extra_body or {})
if reasoning_mode == "off":
# Modern Ollama reasoning models support a top-level ``think``
# switch and return reasoning in a separate ``thinking`` field.
diff --git a/src/vaner/daemon/engine/generator.py b/src/vaner/daemon/engine/generator.py
index ddc1d48..441d1b5 100644
--- a/src/vaner/daemon/engine/generator.py
+++ b/src/vaner/daemon/engine/generator.py
@@ -14,11 +14,14 @@
from vaner.models.artefact import Artefact, ArtefactKind
from vaner.models.config import VanerConfig
+from vaner.policy.internal_llm import EVIDENCE_SUMMARY_POLICY, internal_llm_policy
from vaner.policy.privacy import redact_text
logger = logging.getLogger(__name__)
-FILE_SUMMARY_PROMPT = """You are generating a precise implementation reference for a context system.
+FILE_SUMMARY_PROMPT = (
+ internal_llm_policy(EVIDENCE_SUMMARY_POLICY)
+ + """\n\nYou are generating a precise implementation reference for a context system.
A model will later use this summary to answer exact questions about this file.
Your summary MUST enable correct answers — not just plausible ones.
@@ -42,8 +45,11 @@
{content}
---
Implementation reference:"""
+)
-DIFF_SUMMARY_PROMPT = """Summarize the following workspace change for a context system.
+DIFF_SUMMARY_PROMPT = (
+ internal_llm_policy(EVIDENCE_SUMMARY_POLICY)
+ + """\n\nSummarize the following workspace change for a context system.
State clearly what changed, which modules/functions were affected, and likely intent.
Call out exact limits/guards/conditionals when visible.
Be concise.
@@ -51,10 +57,33 @@
{diff}
---
Summary:"""
+)
+
+
+def _format_python_args(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
+ args: list[str] = []
+ for arg in list(node.args.posonlyargs) + list(node.args.args):
+ item = arg.arg
+ if arg.annotation is not None:
+ item += f": {ast.unparse(arg.annotation)}"
+ args.append(item)
+ if node.args.vararg is not None:
+ args.append(f"*{node.args.vararg.arg}")
+ for arg in node.args.kwonlyargs:
+ item = arg.arg
+ if arg.annotation is not None:
+ item += f": {ast.unparse(arg.annotation)}"
+ args.append(item)
+ if node.args.kwarg is not None:
+ args.append(f"**{node.args.kwarg.arg}")
+ signature = f"{node.name}({', '.join(args)})"
+ if node.returns is not None:
+ signature += f" -> {ast.unparse(node.returns)}"
+ return signature[:180]
def _extract_python_shapes(text: str) -> tuple[list[str], list[str]]:
- """Extract class names, top-level functions, and class method names from Python sources."""
+ """Extract class names, top-level functions, and class method signatures."""
try:
tree = ast.parse(text)
except SyntaxError:
@@ -65,15 +94,15 @@ def _extract_python_shapes(text: str) -> tuple[list[str], list[str]]:
for node in tree.body:
if isinstance(node, ast.ClassDef):
- classes.append(node.name)
+ bases = [ast.unparse(base) for base in node.bases]
+ classes.append(f"{node.name}({', '.join(bases)})" if bases else node.name)
# Include method names so artefact content matches method-specific queries
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not item.name.startswith("_"):
- functions.append(item.name)
+ functions.append(_format_python_args(item))
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
- args = [arg.arg for arg in node.args.args]
- functions.append(f"{node.name}({', '.join(args)})")
+ functions.append(_format_python_args(node))
return classes[:8], list(dict.fromkeys(functions))[:20]
@@ -104,6 +133,30 @@ def _extract_constants(text: str) -> list[str]:
return constants[:12]
+def _extract_sql_schema(text: str) -> list[str]:
+ schema_lines: list[str] = []
+ collecting = False
+ buffer: list[str] = []
+ for raw_line in text.splitlines():
+ stripped = " ".join(raw_line.strip().split())
+ lowered = stripped.lower()
+ if not stripped:
+ continue
+ if any(marker in lowered for marker in ("create table", "create index", "using fts5", "insert into", "select ")):
+ collecting = True
+ buffer = [stripped]
+ elif collecting:
+ buffer.append(stripped)
+ if collecting and (stripped.endswith(")") or stripped.endswith(')"') or stripped.endswith(";") or len(buffer) >= 8):
+ joined = " ".join(buffer)
+ schema_lines.append(joined[:240])
+ collecting = False
+ buffer = []
+ if len(schema_lines) >= 16:
+ break
+ return schema_lines
+
+
def _summarize_text(
text: str,
source_path: Path,
@@ -124,8 +177,9 @@ def _summarize_text(
ast_source = full_text if full_text is not None else text
sections: list[str] = []
- constants = _extract_constants(text)
- limits = _extract_limits(text)
+ constants = _extract_constants(ast_source)
+ limits = _extract_limits(ast_source)
+ sql_schema = _extract_sql_schema(ast_source)
if source_path.suffix == ".py":
classes, functions = _extract_python_shapes(ast_source)
@@ -138,9 +192,11 @@ def _summarize_text(
sections.append("Constants: " + "; ".join(constants[:6]))
if limits:
sections.append("Limits: " + "; ".join(limits[:6]))
+ if sql_schema:
+ sections.append("Schema: " + "; ".join(sql_schema[:8]))
sections.append("Snippet: " + " ".join(lines[:max_lines])[:900])
- return "\n".join(sections)[:1600]
+ return "\n".join(sections)[:2400]
def _build_artefact(
diff --git a/src/vaner/daemon/http.py b/src/vaner/daemon/http.py
index 61ff2d1..f32585f 100644
--- a/src/vaner/daemon/http.py
+++ b/src/vaner/daemon/http.py
@@ -1421,6 +1421,92 @@ async def list_items(kind: str | None = None, limit: int = 10, visibility: str =
)
return JSONResponse({"count": len(rows), "visibility": visibility_mode, "scenarios": [_scenario_payload(row) for row in rows]})
+ async def _heatmap_replay_payload(
+ *,
+ from_ts: float | None = None,
+ to_ts: float | None = None,
+ range_seconds: float | None = None,
+ limit: int = 100,
+ ) -> dict[str, Any]:
+ end_ts = float(to_ts or time.time())
+ default_range = float(range_seconds or 15 * 60)
+ start_ts = float(from_ts if from_ts is not None else end_ts - default_range)
+ if start_ts > end_ts:
+ start_ts, end_ts = end_ts, start_ts
+ capped_limit = max(1, min(int(limit), 200))
+ rows = await _best_effort(
+ scenario_store.list_top(limit=capped_limit, visibility="all"),
+ [],
+ label="heatmap scenarios",
+ timeout=2.0,
+ )
+ scenario_ids = [str(row.id) for row in rows]
+ samples = await _best_effort(
+ scenario_store.list_samples(scenario_ids=scenario_ids, start_ts=start_ts, end_ts=end_ts, limit=50_000),
+ [],
+ label="heatmap samples",
+ timeout=2.0,
+ )
+ live_events = [
+ row
+ for row in read_live_work_events(config.repo_root, limit=1000)
+ if start_ts <= float(row.get("ts") or 0.0) <= end_ts
+ ]
+ return {
+ "from_ts": start_ts,
+ "to_ts": end_ts,
+ "scenarios": [_scenario_payload(row) for row in rows],
+ "samples": [sample.__dict__ for sample in samples],
+ "events": live_events,
+ "metadata": {
+ "sample_source": "scenario_samples",
+ "event_source": "live_work_events",
+ "synthetic": False,
+ "sample_count": len(samples),
+ "event_count": len(live_events),
+ "scenario_count": len(rows),
+ "complete": bool(samples),
+ },
+ }
+
+ @app.get("/heatmap/replay/stream")
+ async def heatmap_replay_stream(
+ from_ts: float | None = None,
+ to_ts: float | None = None,
+ range_seconds: float | None = None,
+ limit: int = 100,
+ ) -> StreamingResponse:
+ async def event_gen() -> AsyncIterator[str]:
+ last_fingerprint = ""
+ last_keepalive = time.monotonic()
+ sent = 0
+ while True:
+ payload = await _heatmap_replay_payload(
+ from_ts=from_ts if to_ts is not None else None,
+ to_ts=to_ts,
+ range_seconds=range_seconds,
+ limit=limit,
+ )
+ serialized = json.dumps(payload, sort_keys=True, default=str)
+ if serialized != last_fingerprint:
+ yield f"event: replay_snapshot\ndata: {serialized}\n\n"
+ last_fingerprint = serialized
+ last_keepalive = time.monotonic()
+ sent += 1
+ if limit is not None and sent >= max(1, int(limit)):
+ return
+ now = time.monotonic()
+ if now - last_keepalive >= 10.0:
+ yield ": keepalive\n\n"
+ last_keepalive = now
+ await asyncio.sleep(1.0)
+
+ return StreamingResponse(event_gen(), media_type="text/event-stream")
+
+ @app.get("/heatmap/replay")
+ async def heatmap_replay(from_ts: float | None = None, to_ts: float | None = None, limit: int = 100) -> JSONResponse:
+ return JSONResponse(await _heatmap_replay_payload(from_ts=from_ts, to_ts=to_ts, limit=limit))
+
@app.get("/scenarios/{scenario_id}")
async def fetch_item(scenario_id: str) -> JSONResponse:
row = await scenario_store.get(scenario_id)
diff --git a/src/vaner/daemon/precompute_worker.py b/src/vaner/daemon/precompute_worker.py
index 1007f29..669de24 100644
--- a/src/vaner/daemon/precompute_worker.py
+++ b/src/vaner/daemon/precompute_worker.py
@@ -367,6 +367,11 @@ async def _run_job(self, job: WorkerJob, *, engine: Any, focus_manager: FocusMan
profile["precompute_ms"] = (time.monotonic() - precompute_start) * 1000.0
profile["total_ms"] = (time.monotonic() - started) * 1000.0
profile["produced"] = float(produced or 0)
+ if hasattr(engine, "get_last_cycle_profile"):
+ try:
+ profile.update(engine.get_last_cycle_profile())
+ except Exception:
+ logger.debug("failed to read engine cycle profile", exc_info=True)
self._write_predictions(engine, cycle_id=cycle_id)
self._status["profile"] = profile
self._set_state("completed", job=job, phase="idle", cycle_id=cycle_id, produced=int(produced or 0))
@@ -533,9 +538,13 @@ def _record_prediction_event(self, event: Any) -> None:
stage = "progress"
status = "running"
if isinstance(payload, dict):
+ tokens_used = int(payload.get("tokens_used") or 0)
+ token_budget = int(payload.get("token_budget") or 0)
+ overage = max(0, tokens_used - token_budget)
+ budget_label = f"{token_budget} token target" if token_budget else "token target"
+ overage_label = f" ({overage} over target)" if overage else ""
summary = (
- f"Progress: {int(payload.get('tokens_used') or 0)}/"
- f"{int(payload.get('token_budget') or 0)} tokens, "
+ f"Progress: {tokens_used} tokens used / {budget_label}{overage_label}, "
f"{int(payload.get('scenarios_complete') or 0)} scenarios complete."
)
elif kind == "prediction.artifact_added":
diff --git a/src/vaner/defaults/catalog_seed.json b/src/vaner/defaults/catalog_seed.json
index 5e740e8..74dce4e 100644
--- a/src/vaner/defaults/catalog_seed.json
+++ b/src/vaner/defaults/catalog_seed.json
@@ -1,99 +1,305 @@
{
"schema_version": 2,
- "generated_at": "2026-05-01",
- "comment": "Family-level seed for the catalog refresher. The refresher (vaner.setup.catalog_refresh) emits one row per family using the family's configured Ollama tag, derives params/size from the manifest's vnd.ollama.image.model layer bytes, and computes memory budgets. The seed carries family-level metadata that doesn't depend on size — display name, workload tags, ranks, sampling defaults, the model's max_context_window — plus the Ollama family name and an optional HF repo template. context_window here is the architectural ceiling; recommend_local_model picks a hardware/archetype-aware effective_context_window at recommend time, floored at 32K.",
+ "generated_at": "2026-05-05",
+ "comment": "Ollama-first, release-curated model recommendation seed. Hugging Face and Ollama public metadata can inform refreshes, but Vaner does not depend on a live recommendation endpoint. The refresher emits one registry row per configured model tag, verifies Ollama tags through registry manifests when online, derives pull size from model layers, and computes memory budgets. context_window is the architectural ceiling; recommend_local_model picks a hardware/archetype-aware effective context window at setup time.",
"quantization_profiles": {
- "Q4_K_M": { "bytes_per_param": 0.55, "label": "Q4_K_M", "ratio_to_fp16": 0.275 },
- "Q5_K_M": { "bytes_per_param": 0.7, "label": "Q5_K_M", "ratio_to_fp16": 0.35 },
- "Q6_K": { "bytes_per_param": 0.84, "label": "Q6_K", "ratio_to_fp16": 0.42 },
- "Q8_0": { "bytes_per_param": 1.06, "label": "Q8_0", "ratio_to_fp16": 0.53 },
- "FP16": { "bytes_per_param": 2.0, "label": "FP16", "ratio_to_fp16": 1.0 }
+ "Q4_K_M": {
+ "bytes_per_param": 0.55,
+ "label": "Q4_K_M",
+ "ratio_to_fp16": 0.275
+ },
+ "Q5_K_M": {
+ "bytes_per_param": 0.7,
+ "label": "Q5_K_M",
+ "ratio_to_fp16": 0.35
+ },
+ "Q6_K": {
+ "bytes_per_param": 0.84,
+ "label": "Q6_K",
+ "ratio_to_fp16": 0.42
+ },
+ "Q8_0": {
+ "bytes_per_param": 1.06,
+ "label": "Q8_0",
+ "ratio_to_fp16": 0.53
+ },
+ "NVFP4": {
+ "bytes_per_param": 0.63,
+ "label": "NVFP4",
+ "ratio_to_fp16": 0.315
+ },
+ "MXFP4": {
+ "bytes_per_param": 0.555,
+ "label": "MXFP4",
+ "ratio_to_fp16": 0.2775
+ },
+ "MXFP8": {
+ "bytes_per_param": 1.1,
+ "label": "MXFP8",
+ "ratio_to_fp16": 0.55
+ },
+ "FP16": {
+ "bytes_per_param": 2.0,
+ "label": "FP16",
+ "ratio_to_fp16": 1.0
+ }
},
"default_quantization": "Q4_K_M",
"families": [
{
- "id": "qwen3.6",
- "display_name": "Qwen 3.6",
+ "id": "gpt-oss-120b",
+ "display_name": "GPT-OSS 120B",
+ "runtime": "ollama",
+ "ollama_family": "gpt-oss",
+ "ollama_tag": "120b",
+ "hf_org": "openai",
+ "hf_repo_template": "openai/gpt-oss-120b",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "reasoning",
+ "agentic",
+ "long_context"
+ ],
+ "stability_rank": 87,
+ "quality_rank": 99,
+ "recency_rank": 98,
+ "default_params_b": 117,
+ "default_download_size_gb": 65,
+ "active_params_b": 5.1,
+ "architecture": "moe",
+ "quantization": "MXFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell",
+ "unified_memory",
+ "dgx_spark",
+ "moe"
+ ],
+ "parameters": {
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "reasoning_effort": "medium",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 1.0,
+ "top_p": 0.95,
+ "top_k": 40,
+ "repeat_penalty": 1.0,
+ "min_p": 0.0
+ }
+ },
+ {
+ "id": "qwen3.6-35b-a3b-coding-nvfp4",
+ "display_name": "Qwen 3.6 35B A3B Coding NVFP4",
"runtime": "ollama",
"ollama_family": "qwen3.6",
- "ollama_tag": "27b",
- "hf_org": "Qwen",
- "hf_repo_template": "Qwen/Qwen3.6-{size}-Instruct",
- "workload_tags": ["general", "coding", "summarization"],
- "stability_rank": 85,
- "quality_rank": 98,
+ "ollama_tag": "35b-a3b-coding-nvfp4",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "long_context"
+ ],
+ "stability_rank": 82,
+ "quality_rank": 99,
+ "recency_rank": 100,
+ "default_params_b": 35,
+ "default_download_size_gb": 22,
+ "active_params_b": 3,
+ "architecture": "moe",
+ "quantization": "NVFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell",
+ "moe"
+ ],
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
+ "top_p": 0.95,
+ "top_k": 20,
+ "repeat_penalty": 1.0,
+ "min_p": 0.0
+ }
+ },
+ {
+ "id": "qwen3.6-27b-coding-nvfp4",
+ "display_name": "Qwen 3.6 27B Coding NVFP4",
+ "runtime": "ollama",
+ "ollama_family": "qwen3.6",
+ "ollama_tag": "27b-coding-nvfp4",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "coding",
+ "agentic",
+ "general",
+ "long_context"
+ ],
+ "stability_rank": 82,
+ "quality_rank": 97,
"recency_rank": 100,
"default_params_b": 27.1,
- "default_download_size_gb": 14.9,
+ "default_download_size_gb": 20,
+ "architecture": "dense",
+ "quantization": "NVFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell"
+ ],
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.7,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
- "min_p": 0.0,
- "repeat_penalty": 1.05
+ "repeat_penalty": 1.0,
+ "min_p": 0.0
}
},
{
- "id": "qwen3.5",
- "display_name": "Qwen 3.5",
+ "id": "qwen3.6-35b-a3b-coding-mxfp8",
+ "display_name": "Qwen 3.6 35B A3B Coding MXFP8",
"runtime": "ollama",
- "ollama_family": "qwen3.5",
- "hf_org": "Qwen",
- "hf_repo_template": "Qwen/Qwen3.5-{size}-Instruct",
- "workload_tags": ["general", "coding", "summarization"],
- "stability_rank": 90,
- "quality_rank": 95,
- "recency_rank": 90,
+ "ollama_family": "qwen3.6",
+ "ollama_tag": "35b-a3b-coding-mxfp8",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "long_context"
+ ],
+ "stability_rank": 84,
+ "quality_rank": 99,
+ "recency_rank": 100,
+ "default_params_b": 35,
+ "default_download_size_gb": 38,
+ "active_params_b": 3,
+ "architecture": "moe",
+ "quantization": "MXFP8",
+ "accelerator_tags": [
+ "apple_silicon",
+ "mlx",
+ "moe"
+ ],
"parameters": {
- "context_window": 131072,
+ "context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.7,
- "top_p": 0.8,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
+ "top_p": 0.95,
"top_k": 20,
- "min_p": 0.0,
- "repeat_penalty": 1.05
+ "repeat_penalty": 1.0,
+ "min_p": 0.0
}
},
{
- "id": "qwen3-coder-next",
- "display_name": "Qwen 3 Coder Next",
+ "id": "qwen3.6-27b-coding-mxfp8",
+ "display_name": "Qwen 3.6 27B Coding MXFP8",
"runtime": "ollama",
- "ollama_family": "qwen3-coder-next",
- "hf_org": "Qwen",
- "hf_repo_template": "Qwen/Qwen3-Coder-Next-{size}",
- "workload_tags": ["coding", "general"],
- "stability_rank": 85,
- "quality_rank": 92,
- "recency_rank": 95,
+ "ollama_family": "qwen3.6",
+ "ollama_tag": "27b-coding-mxfp8",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "coding",
+ "agentic",
+ "general",
+ "long_context"
+ ],
+ "stability_rank": 84,
+ "quality_rank": 97,
+ "recency_rank": 100,
+ "default_params_b": 27.1,
+ "default_download_size_gb": 31,
+ "architecture": "dense",
+ "quantization": "MXFP8",
+ "accelerator_tags": [
+ "apple_silicon",
+ "mlx"
+ ],
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.2,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
- "min_p": 0.0,
- "repeat_penalty": 1.05
+ "repeat_penalty": 1.0,
+ "min_p": 0.0
}
},
{
- "id": "qwen3-next",
- "display_name": "Qwen 3 Next",
+ "id": "qwen3.6-35b",
+ "display_name": "Qwen 3.6 35B A3B",
"runtime": "ollama",
- "ollama_family": "qwen3-next",
- "hf_org": "Qwen",
- "hf_repo_template": "Qwen/Qwen3-Next-{size}",
- "workload_tags": ["general", "coding"],
+ "ollama_family": "qwen3.6",
+ "ollama_tag": "35b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "research",
+ "long_context"
+ ],
"stability_rank": 85,
- "quality_rank": 90,
- "recency_rank": 92,
+ "quality_rank": 98,
+ "recency_rank": 100,
+ "default_params_b": 35,
+ "default_download_size_gb": 24,
+ "active_params_b": 3,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.7,
+ "top_p": 0.95,
+ "top_k": 20,
+ "repeat_penalty": 1.05,
+ "min_p": 0.0
+ }
+ },
+ {
+ "id": "qwen3.6-27b",
+ "display_name": "Qwen 3.6 27B",
+ "runtime": "ollama",
+ "ollama_family": "qwen3.6",
+ "ollama_tag": "27b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "long_context"
+ ],
+ "stability_rank": 86,
+ "quality_rank": 97,
+ "recency_rank": 100,
+ "default_params_b": 27.1,
+ "default_download_size_gb": 17,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
@@ -102,26 +308,37 @@
"temperature": 0.7,
"top_p": 0.95,
"top_k": 20,
- "min_p": 0.0,
- "repeat_penalty": 1.05
+ "repeat_penalty": 1.05,
+ "min_p": 0.0
}
},
{
- "id": "llama4",
- "display_name": "Llama 4",
+ "id": "gemma4-e2b",
+ "display_name": "Gemma 4 E2B",
"runtime": "ollama",
- "ollama_family": "llama4",
- "hf_org": "meta-llama",
- "hf_repo_template": "meta-llama/Llama-4-{size}-Instruct",
- "workload_tags": ["general", "summarization"],
+ "ollama_family": "gemma4",
+ "ollama_tag": "e2b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "lightweight",
+ "vision"
+ ],
"stability_rank": 90,
- "quality_rank": 94,
- "recency_rank": 95,
+ "quality_rank": 84,
+ "recency_rank": 96,
+ "default_params_b": 13,
+ "default_download_size_gb": 7.2,
+ "active_params_b": 2,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
"parameters": {
- "context_window": 1048576,
- "reasoning_mode": "none",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 0,
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 3072,
+ "reasoning_token_budget": 4096,
"temperature": 0.6,
"top_p": 0.9,
"top_k": 50,
@@ -129,71 +346,248 @@
}
},
{
- "id": "gemma4",
- "display_name": "Gemma 4",
+ "id": "gemma4-e4b",
+ "display_name": "Gemma 4 E4B",
"runtime": "ollama",
"ollama_family": "gemma4",
- "hf_org": "google",
- "hf_repo_template": "google/gemma-4-{size}-it",
- "workload_tags": ["general", "summarization", "lightweight"],
- "stability_rank": 88,
- "quality_rank": 78,
- "recency_rank": 92,
+ "ollama_tag": "e4b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "coding",
+ "lightweight",
+ "vision"
+ ],
+ "stability_rank": 90,
+ "quality_rank": 88,
+ "recency_rank": 96,
+ "default_params_b": 17,
+ "default_download_size_gb": 9.6,
+ "active_params_b": 4,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
"parameters": {
"context_window": 131072,
"reasoning_mode": "allowed",
- "max_response_tokens": 3072,
+ "max_response_tokens": 4096,
"reasoning_token_budget": 4096,
- "temperature": 0.8,
- "top_p": 0.95,
- "top_k": 64,
- "repeat_penalty": 1.0
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1
}
},
{
- "id": "deepseek-r2",
- "display_name": "DeepSeek-R2",
+ "id": "gemma4-26b",
+ "display_name": "Gemma 4 26B",
"runtime": "ollama",
- "ollama_family": "deepseek-r2",
- "hf_org": "deepseek-ai",
- "hf_repo_template": "deepseek-ai/DeepSeek-R2-Distill-Qwen-{size}",
- "workload_tags": ["general", "coding", "research"],
- "stability_rank": 80,
- "quality_rank": 96,
+ "ollama_family": "gemma4",
+ "ollama_tag": "26b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "vision",
+ "long_context"
+ ],
+ "stability_rank": 88,
+ "quality_rank": 94,
"recency_rank": 96,
+ "default_params_b": 26,
+ "default_download_size_gb": 18,
+ "active_params_b": 4,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
"parameters": {
- "context_window": 131072,
- "reasoning_mode": "forced",
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
"max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1
+ }
+ },
+ {
+ "id": "gemma4-31b",
+ "display_name": "Gemma 4 31B",
+ "runtime": "ollama",
+ "ollama_family": "gemma4",
+ "ollama_tag": "31b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "vision",
+ "long_context"
+ ],
+ "stability_rank": 89,
+ "quality_rank": 95,
+ "recency_rank": 96,
+ "default_params_b": 31,
+ "default_download_size_gb": 20,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1
+ }
+ },
+ {
+ "id": "nemotron-cascade-2-30b",
+ "display_name": "Nemotron Cascade 2 30B A3B",
+ "runtime": "ollama",
+ "ollama_family": "nemotron-cascade-2",
+ "ollama_tag": "30b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "reasoning",
+ "agentic",
+ "long_context"
+ ],
+ "stability_rank": 80,
+ "quality_rank": 93,
+ "recency_rank": 95,
+ "default_params_b": 30,
+ "default_download_size_gb": 24,
+ "active_params_b": 3,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": [
+ "nvidia",
+ "moe"
+ ],
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
"reasoning_token_budget": 16384,
"temperature": 0.6,
"top_p": 0.95,
"top_k": 40,
- "repeat_penalty": 1.0
+ "repeat_penalty": 1.05
+ }
+ },
+ {
+ "id": "nemotron3-33b",
+ "display_name": "Nemotron 3 Nano Omni 33B",
+ "runtime": "ollama",
+ "ollama_family": "nemotron3",
+ "ollama_tag": "33b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "vision",
+ "agentic"
+ ],
+ "stability_rank": 78,
+ "quality_rank": 88,
+ "recency_rank": 100,
+ "default_params_b": 33,
+ "default_download_size_gb": 28,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": [
+ "nvidia"
+ ],
+ "parameters": {
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 40,
+ "repeat_penalty": 1.05
}
},
{
- "id": "qwen3",
- "display_name": "Qwen 3",
+ "id": "mistral-medium-3.5-128b",
+ "display_name": "Mistral Medium 3.5 128B",
"runtime": "ollama",
- "ollama_family": "qwen3",
- "hf_org": "Qwen",
- "hf_repo_template": "Qwen/Qwen3-{size}-Instruct",
- "workload_tags": ["general", "lightweight"],
- "stability_rank": 95,
- "quality_rank": 75,
- "recency_rank": 80,
+ "ollama_family": "mistral-medium-3.5",
+ "ollama_tag": "128b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "reasoning",
+ "agentic",
+ "vision",
+ "long_context"
+ ],
+ "stability_rank": 82,
+ "quality_rank": 96,
+ "recency_rank": 100,
+ "default_params_b": 128,
+ "default_download_size_gb": 80,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
"parameters": {
- "context_window": 32768,
+ "context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 2048,
- "reasoning_token_budget": 2048,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
"temperature": 0.7,
- "top_p": 0.8,
- "top_k": 20,
- "min_p": 0.0,
+ "top_p": 0.95,
+ "top_k": 40,
"repeat_penalty": 1.05
}
+ },
+ {
+ "id": "llama4-scout",
+ "display_name": "Llama 4 Scout 16x17B",
+ "runtime": "ollama",
+ "ollama_family": "llama4",
+ "ollama_tag": "16x17b",
+ "hf_org": "",
+ "hf_repo_template": "",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "vision",
+ "long_context"
+ ],
+ "stability_rank": 88,
+ "quality_rank": 87,
+ "recency_rank": 65,
+ "default_params_b": 109,
+ "default_download_size_gb": 67,
+ "active_params_b": 17,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "parameters": {
+ "context_window": 10485760,
+ "reasoning_mode": "none",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 0,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1
+ }
}
]
}
diff --git a/src/vaner/defaults/model_registry.json b/src/vaner/defaults/model_registry.json
index ccd167b..0f323a4 100644
--- a/src/vaner/defaults/model_registry.json
+++ b/src/vaner/defaults/model_registry.json
@@ -1,6 +1,6 @@
{
"schema_version": 1,
- "verified_at": "2026-05-01",
+ "verified_at": "2026-05-05",
"generator": "vaner.setup.catalog_refresh",
"online": true,
"sources": [
@@ -9,111 +9,257 @@
],
"models": [
{
- "id": "qwen3.6:27b",
- "display_name": "Qwen 3.6 27B",
+ "id": "gpt-oss:120b",
+ "display_name": "GPT-OSS 120B",
"runtime": "ollama",
"workload_tags": [
"general",
"coding",
- "summarization"
+ "summarization",
+ "reasoning",
+ "agentic",
+ "long_context"
],
- "quality_rank": 98,
- "stability_rank": 85,
+ "quality_rank": 99,
+ "stability_rank": 87,
+ "recency_rank": 98,
+ "download_size_gb": 60.9,
+ "min_effective_memory_gb": 73.8,
+ "recommended_effective_memory_gb": 73.8,
+ "parameters": {
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "reasoning_effort": "medium",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 1.0,
+ "top_p": 0.95,
+ "top_k": 40,
+ "repeat_penalty": 1.0,
+ "min_p": 0.0,
+ "num_ctx": 131072
+ },
+ "family_id": "gpt-oss-120b",
+ "params_b": 109.7,
+ "active_params_b": 5.1,
+ "architecture": "moe",
+ "quantization": "MXFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell",
+ "unified_memory",
+ "dgx_spark",
+ "moe"
+ ]
+ },
+ {
+ "id": "qwen3.6:35b-a3b-coding-nvfp4",
+ "display_name": "Qwen 3.6 35B A3B Coding NVFP4",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "long_context"
+ ],
+ "quality_rank": 99,
+ "stability_rank": 82,
"recency_rank": 100,
- "download_size_gb": 14.9,
- "min_effective_memory_gb": 17.2,
- "recommended_effective_memory_gb": 37.9,
+ "download_size_gb": 22.0,
+ "min_effective_memory_gb": 31.8,
+ "recommended_effective_memory_gb": 32.8,
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.7,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
+ "repeat_penalty": 1.0,
+ "min_p": 0.0,
+ "num_ctx": 262144
+ },
+ "family_id": "qwen3.6-35b-a3b-coding-nvfp4",
+ "params_b": 35.0,
+ "active_params_b": 3.0,
+ "architecture": "moe",
+ "quantization": "NVFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell",
+ "moe"
+ ]
+ },
+ {
+ "id": "qwen3.6:27b-coding-nvfp4",
+ "display_name": "Qwen 3.6 27B Coding NVFP4",
+ "runtime": "ollama",
+ "workload_tags": [
+ "coding",
+ "agentic",
+ "general",
+ "long_context"
+ ],
+ "quality_rank": 97,
+ "stability_rank": 82,
+ "recency_rank": 100,
+ "download_size_gb": 20.0,
+ "min_effective_memory_gb": 19.6,
+ "recommended_effective_memory_gb": 43.2,
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
+ "top_p": 0.95,
+ "top_k": 20,
+ "repeat_penalty": 1.0,
"min_p": 0.0,
- "repeat_penalty": 1.05,
"num_ctx": 262144
},
- "family_id": "qwen3.6",
+ "family_id": "qwen3.6-27b-coding-nvfp4",
"params_b": 27.1,
- "quantization": "Q4_K_M"
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "NVFP4",
+ "accelerator_tags": [
+ "nvidia",
+ "blackwell"
+ ]
},
{
- "id": "qwen3.5:latest",
- "display_name": "Qwen 3.5",
+ "id": "qwen3.6:35b-a3b-coding-mxfp8",
+ "display_name": "Qwen 3.6 35B A3B Coding MXFP8",
"runtime": "ollama",
"workload_tags": [
"general",
"coding",
- "summarization"
+ "summarization",
+ "agentic",
+ "long_context"
],
- "quality_rank": 95,
- "stability_rank": 90,
- "recency_rank": 90,
- "download_size_gb": 6.1,
- "min_effective_memory_gb": 7.4,
- "recommended_effective_memory_gb": 12.1,
+ "quality_rank": 99,
+ "stability_rank": 84,
+ "recency_rank": 100,
+ "download_size_gb": 38.0,
+ "min_effective_memory_gb": 49.6,
+ "recommended_effective_memory_gb": 51.3,
"parameters": {
- "context_window": 131072,
+ "context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.7,
- "top_p": 0.8,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
+ "top_p": 0.95,
"top_k": 20,
+ "repeat_penalty": 1.0,
"min_p": 0.0,
- "repeat_penalty": 1.05,
- "num_ctx": 131072
+ "num_ctx": 262144
},
- "family_id": "qwen3.5",
- "params_b": 11.2,
- "quantization": "Q4_K_M"
+ "family_id": "qwen3.6-35b-a3b-coding-mxfp8",
+ "params_b": 35.0,
+ "active_params_b": 3.0,
+ "architecture": "moe",
+ "quantization": "MXFP8",
+ "accelerator_tags": [
+ "apple_silicon",
+ "mlx",
+ "moe"
+ ]
},
{
- "id": "qwen3-coder-next:latest",
- "display_name": "Qwen 3 Coder Next",
+ "id": "qwen3.6:27b-coding-mxfp8",
+ "display_name": "Qwen 3.6 27B Coding MXFP8",
"runtime": "ollama",
"workload_tags": [
"coding",
- "general"
+ "agentic",
+ "general",
+ "long_context"
],
- "quality_rank": 92,
- "stability_rank": 85,
- "recency_rank": 95,
- "download_size_gb": 48.2,
- "min_effective_memory_gb": 54.5,
- "recommended_effective_memory_gb": 119.1,
+ "quality_rank": 97,
+ "stability_rank": 84,
+ "recency_rank": 100,
+ "download_size_gb": 31.0,
+ "min_effective_memory_gb": 33.9,
+ "recommended_effective_memory_gb": 74.2,
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 8192,
- "temperature": 0.2,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
+ "repeat_penalty": 1.0,
"min_p": 0.0,
- "repeat_penalty": 1.05,
"num_ctx": 262144
},
- "family_id": "qwen3-coder-next",
- "params_b": 87.6,
- "quantization": "Q4_K_M"
+ "family_id": "qwen3.6-27b-coding-mxfp8",
+ "params_b": 27.1,
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "MXFP8",
+ "accelerator_tags": [
+ "apple_silicon",
+ "mlx"
+ ]
},
{
- "id": "qwen3-next:latest",
- "display_name": "Qwen 3 Next",
+ "id": "qwen3.6:35b",
+ "display_name": "Qwen 3.6 35B A3B",
"runtime": "ollama",
"workload_tags": [
"general",
- "coding"
+ "coding",
+ "summarization",
+ "research",
+ "long_context"
],
- "quality_rank": 90,
+ "quality_rank": 98,
"stability_rank": 85,
- "recency_rank": 92,
- "download_size_gb": 46.9,
- "min_effective_memory_gb": 53.0,
- "recommended_effective_memory_gb": 116.0,
+ "recency_rank": 100,
+ "download_size_gb": 22.3,
+ "min_effective_memory_gb": 32.1,
+ "recommended_effective_memory_gb": 32.8,
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.7,
+ "top_p": 0.95,
+ "top_k": 20,
+ "repeat_penalty": 1.05,
+ "min_p": 0.0,
+ "num_ctx": 262144
+ },
+ "family_id": "qwen3.6-35b",
+ "params_b": 40.5,
+ "active_params_b": 3.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
+ },
+ {
+ "id": "qwen3.6:27b",
+ "display_name": "Qwen 3.6 27B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "long_context"
+ ],
+ "quality_rank": 97,
+ "stability_rank": 86,
+ "recency_rank": 100,
+ "download_size_gb": 16.2,
+ "min_effective_memory_gb": 18.7,
+ "recommended_effective_memory_gb": 41.1,
"parameters": {
"context_window": 262144,
"reasoning_mode": "allowed",
@@ -122,108 +268,302 @@
"temperature": 0.7,
"top_p": 0.95,
"top_k": 20,
- "min_p": 0.0,
"repeat_penalty": 1.05,
+ "min_p": 0.0,
"num_ctx": 262144
},
- "family_id": "qwen3-next",
- "params_b": 85.3,
- "quantization": "Q4_K_M"
+ "family_id": "qwen3.6-27b",
+ "params_b": 29.5,
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
},
{
- "id": "llama4:latest",
- "display_name": "Llama 4",
+ "id": "gemma4:e2b",
+ "display_name": "Gemma 4 E2B",
"runtime": "ollama",
"workload_tags": [
"general",
- "summarization"
+ "summarization",
+ "lightweight",
+ "vision"
],
- "quality_rank": 94,
+ "quality_rank": 84,
"stability_rank": 90,
- "recency_rank": 95,
- "download_size_gb": 62.8,
- "min_effective_memory_gb": 70.8,
- "recommended_effective_memory_gb": 426.1,
+ "recency_rank": 96,
+ "download_size_gb": 6.7,
+ "min_effective_memory_gb": 15.2,
+ "recommended_effective_memory_gb": 16.7,
"parameters": {
- "context_window": 1048576,
- "reasoning_mode": "none",
- "max_response_tokens": 4096,
- "reasoning_token_budget": 0,
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 3072,
+ "reasoning_token_budget": 4096,
"temperature": 0.6,
"top_p": 0.9,
"top_k": 50,
"repeat_penalty": 1.1,
- "num_ctx": 1048576
+ "num_ctx": 131072
},
- "family_id": "llama4",
- "params_b": 114.2,
- "quantization": "Q4_K_M"
+ "family_id": "gemma4-e2b",
+ "params_b": 12.1,
+ "active_params_b": 2.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
},
{
- "id": "gemma4:latest",
- "display_name": "Gemma 4",
+ "id": "gemma4:e4b",
+ "display_name": "Gemma 4 E4B",
"runtime": "ollama",
"workload_tags": [
"general",
"summarization",
- "lightweight"
+ "coding",
+ "lightweight",
+ "vision"
],
- "quality_rank": 78,
- "stability_rank": 88,
- "recency_rank": 92,
+ "quality_rank": 88,
+ "stability_rank": 90,
+ "recency_rank": 96,
"download_size_gb": 8.9,
- "min_effective_memory_gb": 10.5,
- "recommended_effective_memory_gb": 16.9,
+ "min_effective_memory_gb": 17.7,
+ "recommended_effective_memory_gb": 19.0,
"parameters": {
"context_window": 131072,
"reasoning_mode": "allowed",
- "max_response_tokens": 3072,
+ "max_response_tokens": 4096,
"reasoning_token_budget": 4096,
- "temperature": 0.8,
- "top_p": 0.95,
- "top_k": 64,
- "repeat_penalty": 1.0,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1,
"num_ctx": 131072
},
- "family_id": "gemma4",
+ "family_id": "gemma4-e4b",
"params_b": 16.3,
- "quantization": "Q4_K_M"
+ "active_params_b": 4.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
+ },
+ {
+ "id": "gemma4:26b",
+ "display_name": "Gemma 4 26B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "vision",
+ "long_context"
+ ],
+ "quality_rank": 94,
+ "stability_rank": 88,
+ "recency_rank": 96,
+ "download_size_gb": 16.8,
+ "min_effective_memory_gb": 26.1,
+ "recommended_effective_memory_gb": 27.9,
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1,
+ "num_ctx": 262144
+ },
+ "family_id": "gemma4-26b",
+ "params_b": 30.5,
+ "active_params_b": 4.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
+ },
+ {
+ "id": "gemma4:31b",
+ "display_name": "Gemma 4 31B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "coding",
+ "summarization",
+ "agentic",
+ "vision",
+ "long_context"
+ ],
+ "quality_rank": 95,
+ "stability_rank": 89,
+ "recency_rank": 96,
+ "download_size_gb": 18.5,
+ "min_effective_memory_gb": 21.2,
+ "recommended_effective_memory_gb": 46.6,
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1,
+ "num_ctx": 262144
+ },
+ "family_id": "gemma4-31b",
+ "params_b": 33.6,
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
+ },
+ {
+ "id": "nemotron-cascade-2:30b",
+ "display_name": "Nemotron Cascade 2 30B A3B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "coding",
+ "reasoning",
+ "agentic",
+ "long_context"
+ ],
+ "quality_rank": 93,
+ "stability_rank": 80,
+ "recency_rank": 95,
+ "download_size_gb": 22.6,
+ "min_effective_memory_gb": 32.4,
+ "recommended_effective_memory_gb": 33.2,
+ "parameters": {
+ "context_window": 262144,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
+ "temperature": 0.6,
+ "top_p": 0.95,
+ "top_k": 40,
+ "repeat_penalty": 1.05,
+ "num_ctx": 262144
+ },
+ "family_id": "nemotron-cascade-2-30b",
+ "params_b": 41.1,
+ "active_params_b": 3.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": [
+ "nvidia",
+ "moe"
+ ]
+ },
+ {
+ "id": "nemotron3:33b",
+ "display_name": "Nemotron 3 Nano Omni 33B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "vision",
+ "agentic"
+ ],
+ "quality_rank": 88,
+ "stability_rank": 78,
+ "recency_rank": 100,
+ "download_size_gb": 25.7,
+ "min_effective_memory_gb": 29.3,
+ "recommended_effective_memory_gb": 45.8,
+ "parameters": {
+ "context_window": 131072,
+ "reasoning_mode": "allowed",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 8192,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 40,
+ "repeat_penalty": 1.05,
+ "num_ctx": 131072
+ },
+ "family_id": "nemotron3-33b",
+ "params_b": 46.8,
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": [
+ "nvidia"
+ ]
},
{
- "id": "qwen3:latest",
- "display_name": "Qwen 3",
+ "id": "mistral-medium-3.5:128b",
+ "display_name": "Mistral Medium 3.5 128B",
"runtime": "ollama",
"workload_tags": [
"general",
- "lightweight"
+ "coding",
+ "summarization",
+ "reasoning",
+ "agentic",
+ "vision",
+ "long_context"
],
- "quality_rank": 75,
- "stability_rank": 95,
- "recency_rank": 80,
- "download_size_gb": 4.9,
- "min_effective_memory_gb": 5.9,
- "recommended_effective_memory_gb": 7.2,
+ "quality_rank": 96,
+ "stability_rank": 82,
+ "recency_rank": 100,
+ "download_size_gb": 74.7,
+ "min_effective_memory_gb": 84.2,
+ "recommended_effective_memory_gb": 183.9,
"parameters": {
- "context_window": 32768,
+ "context_window": 262144,
"reasoning_mode": "allowed",
- "max_response_tokens": 2048,
- "reasoning_token_budget": 2048,
+ "max_response_tokens": 8192,
+ "reasoning_token_budget": 16384,
"temperature": 0.7,
- "top_p": 0.8,
- "top_k": 20,
- "min_p": 0.0,
+ "top_p": 0.95,
+ "top_k": 40,
"repeat_penalty": 1.05,
- "num_ctx": 32768
+ "num_ctx": 262144
},
- "family_id": "qwen3",
- "params_b": 8.8,
- "quantization": "Q4_K_M"
- }
- ],
- "skipped": [
+ "family_id": "mistral-medium-3.5-128b",
+ "params_b": 135.9,
+ "active_params_b": 0.0,
+ "architecture": "dense",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
+ },
{
- "family": "deepseek-r2",
- "reason": "ollama_latest_not_found"
+ "id": "llama4:16x17b",
+ "display_name": "Llama 4 Scout 16x17B",
+ "runtime": "ollama",
+ "workload_tags": [
+ "general",
+ "summarization",
+ "vision",
+ "long_context"
+ ],
+ "quality_rank": 87,
+ "stability_rank": 88,
+ "recency_rank": 65,
+ "download_size_gb": 62.8,
+ "min_effective_memory_gb": 75.8,
+ "recommended_effective_memory_gb": 609.4,
+ "parameters": {
+ "context_window": 10485760,
+ "reasoning_mode": "none",
+ "max_response_tokens": 4096,
+ "reasoning_token_budget": 0,
+ "temperature": 0.6,
+ "top_p": 0.9,
+ "top_k": 50,
+ "repeat_penalty": 1.1,
+ "num_ctx": 10485760
+ },
+ "family_id": "llama4-scout",
+ "params_b": 114.2,
+ "active_params_b": 17.0,
+ "architecture": "moe",
+ "quantization": "Q4_K_M",
+ "accelerator_tags": []
}
]
}
diff --git a/src/vaner/engine.py b/src/vaner/engine.py
index 9422859..fa9d9cb 100644
--- a/src/vaner/engine.py
+++ b/src/vaner/engine.py
@@ -112,6 +112,7 @@
from vaner.models.decision import DecisionRecord, PredictionLink, ScoreFactor
from vaner.models.signal import KIND_COMPOSER_LIFECYCLE, SignalEvent
from vaner.plan_drafts import TERMINAL_PLAN_DRAFT_STATUSES, list_plan_drafts
+from vaner.policy.internal_llm import JSON_CONTRACT_POLICY, PREDICTION_POLICY, internal_llm_policy
from vaner.setup.apply import AppliedPolicy, apply_policy_bundle
from vaner.setup.catalog import bundle_by_id
from vaner.signals.composer import ComposerSignalPump, DraftIntentSnapshot
@@ -334,9 +335,36 @@ def _is_cold_start_intent_text(text: str) -> bool:
),
),
)
+_CONTINUATION_AGENDAS: tuple[tuple[str, tuple[str, ...], float], ...] = (
+ (
+ "normal continuation: review quality, risks, policy, and failure modes",
+ ("review", "risk", "policy", "contract", "error", "failure", "fallback", "timeout", "security"),
+ 0.78,
+ ),
+ (
+ "normal continuation: validate behavior, tests, fixtures, and benchmark evidence",
+ ("test", "spec", "fixture", "benchmark", "eval", "ci", "verify", "check"),
+ 0.76,
+ ),
+ (
+ "normal continuation: harden persistence, cache, config, and schema paths",
+ ("store", "cache", "config", "schema", "sql", "db", "toml", "yaml", "settings"),
+ 0.74,
+ ),
+ (
+ "normal continuation: inspect daemon, worker, queue, signal, and event lifecycle",
+ ("daemon", "worker", "runner", "queue", "signal", "event", "watch", "stream", "background"),
+ 0.72,
+ ),
+ (
+ "normal continuation: prepare documentation, handoff, release, and report context",
+ ("readme", "docs", "guide", "changelog", "release", "report", "handoff", "setup"),
+ 0.70,
+ ),
+)
-def _merge_llm_ranked_with_seed_paths(ranked_files: list[str], seed_paths: list[str]) -> list[str]:
+def _merge_llm_ranked_with_seed_paths(ranked_files: list[str], seed_paths: list[str], *, max_paths: int = 8) -> list[str]:
"""Keep deterministic seed evidence when an LLM rerank is incomplete.
The LLM is useful for reordering and adding adjacent files, but it should
@@ -345,7 +373,12 @@ def _merge_llm_ranked_with_seed_paths(ranked_files: list[str], seed_paths: list[
paths are appended as a backstop and then filtered/deduped.
"""
- return list(dict.fromkeys(filter_evidence_paths([*ranked_files, *seed_paths])))[:8]
+ filtered_ranked = filter_evidence_paths(ranked_files)
+ filtered_seeds = list(dict.fromkeys(filter_evidence_paths(seed_paths)))
+ seed_set = set(filtered_seeds)
+ ranked_without_seeds = [path for path in filtered_ranked if path not in seed_set]
+ ranked_slots = max(0, int(max_paths) - len(filtered_seeds))
+ return list(dict.fromkeys([*ranked_without_seeds[:ranked_slots], *filtered_seeds]))[:max_paths]
def _core_group_matches_recent_query(reason: str, recent_queries: list[str]) -> bool:
@@ -363,6 +396,24 @@ def _core_group_matches_recent_query(reason: str, recent_queries: list[str]) ->
return len(reason_terms & query_terms) >= 2
+def _core_group_paths_for_query(query: str, available_paths: list[str], *, max_paths: int = 12) -> list[str]:
+ """Return core architecture files whose group labels directly match a query."""
+
+ if not query:
+ return []
+ available_path_set = set(available_paths)
+ matched: list[str] = []
+ for reason, candidate_paths in _CORE_ARCHITECTURE_GROUPS:
+ if not _core_group_matches_recent_query(reason, [query]):
+ continue
+ for path in candidate_paths:
+ if path in available_path_set and path not in matched:
+ matched.append(path)
+ if len(matched) >= max_paths:
+ return matched
+ return matched
+
+
# Phase 4 / WS2: richer LLM callable that returns a structured
# ``LLMResponse`` (thinking + content + raw). The engine prefers this when
# available so reasoning-model preambles are captured rather than discarded.
@@ -639,6 +690,9 @@ def __init__(
"hedge_ratio": 0.20,
"invest_ratio": 0.10,
"no_regret_ratio": 0.20,
+ "continuation_agenda_cursor": 0.0,
+ "continuation_rounds_last_cycle": 0.0,
+ "continuation_admitted_last_cycle": 0.0,
}
def _refresh_work_style_adjustments(self) -> None:
@@ -1066,6 +1120,8 @@ async def predict(self, top_k: int = 5) -> list[IntentPrediction]:
async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int = 8) -> ContextPackage:
await self.initialize()
started_at = time.time()
+ context_budget = _effective_context_budget(self.config, requested=max_tokens)
+ selection_top_n = _adaptive_selection_top_n(prompt, requested=top_n, max_context_tokens=context_budget)
self._notify_user_request_start()
# Fold the freshly-arrived prompt into the timing model so
# subsequent precompute cycles size their budgets against the
@@ -1084,18 +1140,18 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
prior_prediction_probs = {
item.category: max(0.0, float(item.confidence)) / total_conf for item in prior_predictions
}
- _quick_artefacts = await self.store.list(limit=2000)
- _available_quick_paths = sorted({artefact.source_path for artefact in _quick_artefacts if artefact.source_path})
+ _available_quick_paths = await self.store.list_source_paths(limit=5000)
+ _quick_selected = await select_artefacts_fts(
+ prompt,
+ self.store,
+ top_n=selection_top_n,
+ exclude_private=self.config.privacy.exclude_private,
+ path_bonuses=self._pinned_focus_paths,
+ path_excludes=self._pinned_avoid_paths,
+ )
_quick_paths = {
artefact.source_path
- for artefact in select_artefacts(
- prompt,
- _quick_artefacts,
- top_n=8,
- exclude_private=self.config.privacy.exclude_private,
- path_bonuses=self._pinned_focus_paths,
- path_excludes=self._pinned_avoid_paths,
- )
+ for artefact in _quick_selected
if artefact.source_path
}
exact_symbol_paths = set(
@@ -1103,7 +1159,7 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
self.config.repo_root,
prompt,
available_paths=_available_quick_paths,
- max_paths=8,
+ max_paths=min(16, max(8, selection_top_n)),
)
)
_quick_paths |= exact_symbol_paths
@@ -1111,7 +1167,7 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
# scoring even when the heuristic selector disagrees. Without this
# union, the bench finds 96% of precompute entries never get consumed
# because their anchor_units don't overlap with the heuristic's picks.
- _quick_paths = _quick_paths | await self._cache.candidate_anchor_units(prompt, top_k=3)
+ _quick_paths = _quick_paths | await self._cache.candidate_anchor_units(prompt, top_k=max(3, min(8, selection_top_n // 2)))
cache_result = await self._cache.match(prompt, relevant_paths=_quick_paths)
observation = self._arc_model.observe_detail(prompt)
category = observation.category
@@ -1246,28 +1302,33 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
# Use the LLM-curated files from the precomputed package as primary selection,
# then fill remaining top_n slots from the heuristic selector.
cache_keys = {s.artefact_key for s in cache_result.package.selections}
- all_artefacts = await self.store.list(limit=2000)
- artefacts_by_key = {a.key: a for a in all_artefacts}
- selected = [artefacts_by_key[k] for k in cache_keys if k in artefacts_by_key]
+ selected = await self.store.list_by_keys(cache_keys, limit=max(selection_top_n, len(cache_keys)))
factor_map: dict[str, list[ScoreFactor]] = {}
drop_reasons: dict[str, str] = {}
- if len(selected) < top_n:
- heuristic_picks = select_artefacts(
+ prepared_context_diagnostics = []
+ if len(selected) < selection_top_n:
+ heuristic_picks = await select_artefacts_fts(
prompt,
- all_artefacts,
- top_n=top_n,
+ self.store,
+ top_n=selection_top_n,
exclude_private=self.config.privacy.exclude_private,
path_bonuses=self._pinned_focus_paths,
path_excludes=self._pinned_avoid_paths,
capture_factors=factor_map,
capture_drop_reasons=drop_reasons,
+ context_preparation_mode=self.config.context_preparation.mode,
+ max_query_variants=self.config.context_preparation.max_query_variants,
+ max_candidate_keys=self.config.context_preparation.max_candidate_keys,
+ coverage_floor_enabled=self.config.context_preparation.coverage_floor_enabled,
+ max_expansion_passes=self.config.context_preparation.max_expansion_passes,
+ capture_prepared_context_diagnostics=prepared_context_diagnostics,
)
seen = {a.key for a in selected}
for pick in heuristic_picks:
if pick.key not in seen:
selected.append(pick)
seen.add(pick.key)
- if len(selected) >= top_n:
+ if len(selected) >= selection_top_n:
break
source_key = selected[0].key if selected else None
features = await extract_hybrid_features(self.store, prompt=prompt, source_key=source_key)
@@ -1281,7 +1342,7 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
package, decision_record = assemble_context_package(
prompt,
selected,
- max_tokens if max_tokens is not None else self.config.max_context_tokens,
+ context_budget,
repo_root=self.config.repo_root,
max_age_seconds=self.config.max_age_seconds,
score_map=score_map,
@@ -1290,6 +1351,7 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
evidence_assembly_mode=self.config.evidence_assembly.mode,
evidence_assembly_quality_bias=self.config.evidence_assembly.quality_bias,
evidence_assembly_cost_sensitivity=self.config.evidence_assembly.cost_sensitivity,
+ prepared_context_diagnostics=prepared_context_diagnostics[-1] if prepared_context_diagnostics else None,
return_decision=True,
)
else:
@@ -1332,8 +1394,8 @@ async def query(self, prompt: str, *, max_tokens: int | None = None, top_n: int
preferred_keys |= set(str(key) for key in cache_result.enrichment.get("relevant_keys", []))
package, selected, decision_record = await self._build_package_for_prompt(
prompt,
- max_tokens=max_tokens,
- top_n=top_n,
+ max_tokens=context_budget,
+ top_n=selection_top_n,
preferred_keys=preferred_keys,
include_working_set_preferences=not cold_start_prompt,
)
@@ -1899,6 +1961,41 @@ async def precompute_cycle(
# newly observed user turn can surface as prepared context immediately.
await self._prime_latest_history_query_prediction(recent_query_text)
+ # Latest-query anchors are stronger than broad periodic coverage. Seed
+ # deterministic exact-symbol paths and matching core groups before the
+ # heuristic, graph, arc, and rotating-core seeders can crowd them out.
+ latest_query_anchor_paths: list[str] = []
+ if recent_query_text:
+ latest_query = recent_query_text[-1]
+ latest_query_anchor_paths.extend(
+ rank_exact_paths(
+ self.config.repo_root,
+ latest_query,
+ available_paths=available_paths,
+ max_paths=12,
+ )
+ )
+ latest_query_anchor_paths.extend(
+ _core_group_paths_for_query(
+ latest_query,
+ available_paths,
+ max_paths=12,
+ )
+ )
+ latest_query_anchor_paths = list(dict.fromkeys(filter_evidence_paths(latest_query_anchor_paths)))
+ if latest_query_anchor_paths:
+ frontier.seed_from_focus_paths(
+ self._rank_paths_for_recent_intent(
+ latest_query_anchor_paths,
+ recent_query_text,
+ focused_paths=set(latest_query_anchor_paths),
+ ),
+ available_paths,
+ reason="latest query exact symbol/core anchor focus",
+ priority_floor=1.18,
+ source="structured_direct",
+ )
+
# Order matters: Jaccard-dedup is first-admitted-wins, and
# structured v2 predictions carry the most concrete evidence targets.
# Seed them before heuristic/core/arc scenarios so direct evidence
@@ -2406,7 +2503,149 @@ async def _process_scenario(scenario: ExplorationScenario) -> None:
# frontier empties we still drain in-flight tasks in case LLM branches
# push new work back onto the queue.
max_inflight = max(2, effective_concurrency * 2)
- while governor.should_continue() and not frontier.is_saturated():
+ continuation_rounds = 0
+ continuation_admitted = 0
+ continuation_chunk_limit = max(1, math.ceil(len(available_paths) / 8))
+ if self._active_deep_run_session is not None:
+ continuation_chunk_limit *= 2
+ # This is a duplicate/finite-work guard, not a short-run policy cap.
+ # Normal background prep should spend the cycle budget when useful work
+ # exists; it just must not spin forever on repeated equivalent chunks.
+ continuation_chunk_limit = max(len(_CONTINUATION_AGENDAS), min(256, continuation_chunk_limit))
+ continuation_cursor = int(self._cycle_policy_state.get("continuation_agenda_cursor", 0.0))
+
+ def _cycle_has_budget_for_continuation() -> bool:
+ if not governor.should_continue():
+ return False
+ if cycle_deadline is None:
+ return True
+ # Keep enough tail room for persistence, cleanup, and snapshot writes.
+ return (cycle_deadline - time.monotonic()) > 2.0
+
+ def _rank_continuation_paths(paths: list[str], keywords: tuple[str, ...]) -> list[str]:
+ changed_set = set(changed_paths) | set(changed_paths_for_horizon)
+ keyword_set = {kw.lower() for kw in keywords}
+
+ def score(path: str) -> tuple[int, str]:
+ lower_path = path.lower()
+ basename = lower_path.rsplit("/", 1)[-1]
+ value = 0
+ if path in changed_set:
+ value += 40
+ if path not in covered_paths:
+ value += 18
+ if lower_path.endswith(_SOURCE_EXTENSIONS):
+ value += 8
+ if lower_path.startswith(("src/", "lib/", "app/", "packages/")):
+ value += 6
+ if lower_path.startswith(("tests/", "test/")):
+ value += 5
+ for keyword in keyword_set:
+ if keyword in basename:
+ value += 12
+ elif keyword in lower_path:
+ value += 5
+ if lower_path.startswith(("docs/", "README".lower())):
+ value += 2
+ return value, path
+
+ return sorted(dict.fromkeys(paths), key=lambda p: (-score(p)[0], score(p)[1]))
+
+ def _seed_continuation_agenda() -> int:
+ nonlocal continuation_rounds, continuation_admitted, continuation_cursor
+ if continuation_rounds >= continuation_chunk_limit or not _cycle_has_budget_for_continuation():
+ return 0
+
+ agenda_count = len(_CONTINUATION_AGENDAS)
+ available_set = set(available_paths)
+ fallback_paths = [
+ path
+ for path in [*core_source_paths, *sorted(core_group_paths), *latest_query_anchor_paths]
+ if path in available_set
+ ]
+ attempts = 0
+ while attempts < agenda_count:
+ agenda_index = (continuation_cursor + attempts) % agenda_count
+ reason, keywords, priority_floor = _CONTINUATION_AGENDAS[agenda_index]
+ matched = [
+ path
+ for path in available_paths
+ if path in available_set and any(keyword in path.lower() for keyword in keywords)
+ ]
+ if not matched:
+ attempts += 1
+ continue
+ ranked = _rank_continuation_paths(matched, keywords)
+ uncovered = [path for path in ranked if path not in covered_paths]
+ candidates = uncovered or ranked
+ stride = 8
+ round_offset = (continuation_rounds // max(1, agenda_count)) * stride
+ if candidates:
+ offset = round_offset % max(1, len(candidates))
+ candidates = [*candidates[offset:], *candidates[:offset]]
+ admitted = frontier.seed_from_focus_paths(
+ candidates[:stride],
+ available_paths,
+ reason=reason,
+ priority_floor=priority_floor,
+ source="horizon",
+ )
+ continuation_cursor = (agenda_index + 1) % agenda_count
+ if admitted:
+ continuation_rounds += 1
+ continuation_admitted += admitted
+ self._emit_live_work_event(
+ {
+ "entity_type": "worker",
+ "entity_id": "precompute-cycle",
+ "stage": "continuation",
+ "status": "queued",
+ "summary": reason,
+ "cycle_id": str(self._precompute_cycles),
+ "targets": candidates[:stride],
+ "metadata": {
+ "round": continuation_rounds,
+ "normal_background": self._active_deep_run_session is None,
+ },
+ }
+ )
+ return admitted
+ attempts += 1
+
+ # If named agendas are exhausted by dedup/coverage, keep a small
+ # architecture slice moving so normal background prep still broadens
+ # without requiring a declared Deep-Run session.
+ fallback_ranked = [path for path in _rank_continuation_paths(fallback_paths, ()) if path not in covered_paths]
+ if fallback_ranked:
+ admitted = frontier.seed_from_focus_paths(
+ fallback_ranked[:8],
+ available_paths,
+ reason="normal continuation: broaden uncovered architecture context",
+ priority_floor=0.68,
+ source="core_architecture",
+ )
+ if admitted:
+ continuation_rounds += 1
+ continuation_admitted += admitted
+ self._emit_live_work_event(
+ {
+ "entity_type": "worker",
+ "entity_id": "precompute-cycle",
+ "stage": "continuation",
+ "status": "queued",
+ "summary": "normal continuation: broaden uncovered architecture context",
+ "cycle_id": str(self._precompute_cycles),
+ "targets": fallback_ranked[:8],
+ "metadata": {
+ "round": continuation_rounds,
+ "normal_background": self._active_deep_run_session is None,
+ },
+ }
+ )
+ return admitted
+ return 0
+
+ while governor.should_continue():
if cycle_deadline is not None and time.monotonic() >= cycle_deadline and self._last_explored_scenarios:
break
@@ -2416,8 +2655,11 @@ async def _process_scenario(scenario: ExplorationScenario) -> None:
if scenario is None:
# No pending work right now. If tasks are in flight they may
# push follow-ons on completion, so wait for one to finish and
- # check again. Otherwise we're done.
+ # check again. Otherwise refill from normal continuation
+ # agendas before ending the cycle.
if not in_flight:
+ if _seed_continuation_agenda():
+ continue
break
done, pending = await asyncio.wait(in_flight, return_when=asyncio.FIRST_COMPLETED)
in_flight = [t for t in pending]
@@ -2459,7 +2701,8 @@ async def _process_scenario(scenario: ExplorationScenario) -> None:
"arc": 4,
"pattern": 5,
"skill": 6,
- "llm_branch": 7,
+ "horizon": 7,
+ "llm_branch": 8,
}
self._last_explored_scenarios.sort(
key=lambda item: (
@@ -2558,6 +2801,9 @@ async def _process_scenario(scenario: ExplorationScenario) -> None:
used_ms=allocation.no_regret_ms * allocation_scale,
bucket="no_regret",
)
+ self._cycle_policy_state["continuation_agenda_cursor"] = float(continuation_cursor)
+ self._cycle_policy_state["continuation_rounds_last_cycle"] = float(continuation_rounds)
+ self._cycle_policy_state["continuation_admitted_last_cycle"] = float(continuation_admitted)
_record_idle_usage_seconds(self.config, cycle_elapsed_s)
if not self._last_explored_scenarios and not self._last_no_scenario_reason:
if cycle_deadline is not None and time.monotonic() >= cycle_deadline:
@@ -2576,6 +2822,15 @@ def get_last_no_scenario_reason(self) -> str:
"""Return the latest best-effort reason a cycle explored no scenarios."""
return self._last_no_scenario_reason
+ def get_last_cycle_profile(self) -> dict[str, float | str]:
+ """Return compact, non-persistent telemetry for the most recent cycle."""
+ return {
+ "continuation_rounds": float(self._cycle_policy_state.get("continuation_rounds_last_cycle", 0.0)),
+ "continuation_admitted": float(self._cycle_policy_state.get("continuation_admitted_last_cycle", 0.0)),
+ "continuation_agenda_cursor": float(self._cycle_policy_state.get("continuation_agenda_cursor", 0.0)),
+ "no_scenario_reason": self._last_no_scenario_reason,
+ }
+
def get_active_predictions(self) -> list[PredictedPrompt]:
"""Return non-terminal PredictedPrompts for the active cycle.
@@ -4121,6 +4376,7 @@ async def _explore_scenario_with_llm(
priority_tag = ""
prompt = (
+ f"{internal_llm_policy(JSON_CONTRACT_POLICY, PREDICTION_POLICY)}\n\n"
f"You are a code-context exploration engine.{priority_tag} Evaluate this scenario and decide "
"which files are most relevant and what adjacent scenarios are worth exploring next.\n\n"
f"Developer context:\n"
@@ -4143,7 +4399,7 @@ async def _explore_scenario_with_llm(
" need this scenario addresses (e.g. 'authentication middleware, JWT validation').\n"
" This is used for matching future queries to this cached context.\n"
"4. Set confidence (0.0-1.0): how likely is this area to be needed next?\n\n"
- "Return JSON only (no markdown fences):\n"
+ "Return JSON only (no markdown fences, no extra keys):\n"
"{\n"
' "ranked_files": ["path/a.py", "path/b.py"],\n'
' "semantic_intent": "...",\n'
@@ -5347,6 +5603,7 @@ async def _run_reasoner_loop_iteration(
# LLM focuses purely on the long-tail predictions they can't make.
graph_covered = "\n".join(sorted(covered_paths)[:20]) or "none"
prompt = (
+ f"{internal_llm_policy(JSON_CONTRACT_POLICY, PREDICTION_POLICY)}\n\n"
"You are Vaner's speculative prediction engine — the long-tail layer.\n"
"The system has ALREADY pre-built context packages for:\n"
" • Dependency-graph neighborhoods (structural, high-confidence)\n"
@@ -5356,7 +5613,8 @@ async def _run_reasoner_loop_iteration(
" – Config or infra files triggered by a specific code change\n"
" – Third-party API surfaces the developer will need to read\n"
" – Novel debugging paths not implied by recent errors\n\n"
- "Return a JSON array with fields: question, file_paths, confidence, rationale.\n"
+ "Return a JSON array only, with fields: question, file_paths, confidence, rationale.\n"
+ "Keep rationale as a short observed-signal evidence note.\n"
"Limit to 3-5 predictions.\n\n"
f"Recent queries:\n{recent_hint or 'none'}\n\n"
f"Feedback summary:\n{feedback_summary}\n\n"
@@ -5495,7 +5753,7 @@ async def _build_package_for_paths(
package, _decision_record = assemble_context_package(
question,
selected[:8],
- self.config.max_context_tokens,
+ _effective_context_budget(self.config),
repo_root=self.config.repo_root,
max_age_seconds=self.config.max_age_seconds,
score_map=score_map,
@@ -5759,6 +6017,7 @@ async def _persist_learning_state(self, *, force: bool = False) -> None:
"draft_evidence_threshold",
"draft_volatility_ceiling",
"draft_budget_min_ms",
+ "continuation_agenda_cursor",
}
cycle_state_to_persist = {k: v for k, v in self._cycle_policy_state.items() if k in persistent_cycle_keys}
await self.store.upsert_learning_state(
@@ -5778,10 +6037,10 @@ async def _build_package_for_prompt(
preferred_keys: set[str] | None = None,
include_working_set_preferences: bool = True,
) -> tuple[ContextPackage, list, DecisionRecord]:
- artefacts = await self.store.list(limit=2000)
- if not artefacts:
+ context_budget = _effective_context_budget(self.config, requested=max_tokens)
+ top_n = _adaptive_selection_top_n(prompt, requested=top_n, max_context_tokens=context_budget)
+ if not await self.store.list(limit=1):
await self.prepare()
- artefacts = await self.store.list(limit=2000)
repo_root = self.config.repo_root
git_state = read_git_state(repo_root)
@@ -5801,6 +6060,7 @@ def _score_with_model(question: str, artefact) -> float:
factor_map: dict[str, list[ScoreFactor]] = {}
drop_reasons: dict[str, str] = {}
+ prepared_context_diagnostics = []
selected = await select_artefacts_fts(
prompt,
self.store,
@@ -5813,6 +6073,12 @@ def _score_with_model(question: str, artefact) -> float:
path_excludes=self._pinned_avoid_paths,
capture_factors=factor_map,
capture_drop_reasons=drop_reasons,
+ context_preparation_mode=self.config.context_preparation.mode,
+ max_query_variants=self.config.context_preparation.max_query_variants,
+ max_candidate_keys=self.config.context_preparation.max_candidate_keys,
+ coverage_floor_enabled=self.config.context_preparation.coverage_floor_enabled,
+ max_expansion_passes=self.config.context_preparation.max_expansion_passes,
+ capture_prepared_context_diagnostics=prepared_context_diagnostics,
)
if source_key is None and selected:
source_key = selected[0].key
@@ -5833,12 +6099,18 @@ def _score_with_source(question: str, artefact) -> float:
path_excludes=self._pinned_avoid_paths,
capture_factors=factor_map,
capture_drop_reasons=drop_reasons,
+ context_preparation_mode=self.config.context_preparation.mode,
+ max_query_variants=self.config.context_preparation.max_query_variants,
+ max_candidate_keys=self.config.context_preparation.max_candidate_keys,
+ coverage_floor_enabled=self.config.context_preparation.coverage_floor_enabled,
+ max_expansion_passes=self.config.context_preparation.max_expansion_passes,
+ capture_prepared_context_diagnostics=prepared_context_diagnostics,
)
score_map = {artefact.key: self._intent_scorer.score(prompt, artefact, features=features) for artefact in selected}
package, decision_record = assemble_context_package(
prompt,
selected,
- max_tokens if max_tokens is not None else self.config.max_context_tokens,
+ context_budget,
repo_root=self.config.repo_root,
max_age_seconds=self.config.max_age_seconds,
score_map=score_map,
@@ -5847,6 +6119,7 @@ def _score_with_source(question: str, artefact) -> float:
evidence_assembly_mode=self.config.evidence_assembly.mode,
evidence_assembly_quality_bias=self.config.evidence_assembly.quality_bias,
evidence_assembly_cost_sensitivity=self.config.evidence_assembly.cost_sensitivity,
+ prepared_context_diagnostics=prepared_context_diagnostics[-1] if prepared_context_diagnostics else None,
return_decision=True,
)
return package, selected, decision_record
@@ -5869,6 +6142,7 @@ def _build_decision_record_from_package(
partial_similarity=partial_similarity,
token_budget=package.token_budget,
token_used=package.token_used,
+ prepared_context_diagnostics=package.prepared_context_diagnostics,
selections=[
{
"artefact_key": selection.artefact_key,
@@ -5929,7 +6203,16 @@ def _resolve_llm(self, llm: LLMCallable | str | None) -> LLMCallable | None:
model = llm.split(":", 1)[1]
if not model:
return None
- return ollama_llm(model=model, timeout=float(self.config.backend.request_timeout_seconds))
+ return ollama_llm(
+ model=model,
+ timeout=float(self.config.backend.request_timeout_seconds),
+ max_tokens=int(self.config.backend.max_response_tokens),
+ extra_body=_ollama_options_extra_body(
+ self.config.backend.runtime_options,
+ self.config.backend.sampling_options,
+ ),
+ reasoning_mode=self.config.backend.reasoning_mode,
+ )
if llm.startswith("vllm:"):
# vllm: or vllm:@:
from vaner.clients.openai import openai_llm
@@ -5993,6 +6276,11 @@ def _resolve_structured_llm(self, llm: str) -> StructuredLLMCallable | None:
model=model,
timeout=timeout,
response_format=response_format,
+ max_tokens=int(self.config.backend.max_response_tokens),
+ extra_body=_ollama_options_extra_body(
+ self.config.backend.runtime_options,
+ self.config.backend.sampling_options,
+ ),
reasoning_mode=reasoning_mode,
)
return None
@@ -6452,6 +6740,81 @@ def _probe_ollama_endpoint(base_url: str, timeout: float = 2.0) -> tuple[bool, l
return False, []
+def _ollama_options_extra_body(runtime_options: dict[str, Any] | None, sampling_options: dict[str, Any] | None) -> dict[str, Any] | None:
+ options = {
+ str(key): value
+ for source in (runtime_options or {}, sampling_options or {})
+ for key, value in source.items()
+ if value is not None and value != ""
+ }
+ return {"options": options} if options else None
+
+
+def _adaptive_selection_top_n(prompt: str, *, requested: int, max_context_tokens: int) -> int:
+ """Expand retrieval breadth for multi-facet questions when context allows."""
+
+ base = max(1, int(requested))
+ if max_context_tokens < 4096:
+ return base
+ lowered = prompt.lower()
+ facet_count = prompt.count("?") + len(re.findall(r"\b(?:and|what|how|when|which|where)\b", lowered))
+ identifier_count = len(re.findall(r"\b[A-Za-z_][A-Za-z0-9_]{3,}\b", prompt))
+ extra = 0
+ if facet_count >= 3:
+ extra += 2
+ if identifier_count >= 8:
+ extra += 2
+ if any(term in lowered for term in ("schema", "pipeline", "flow", "walk me through", "how does")):
+ extra += 2
+ capacity_cap = 16 if max_context_tokens >= 16384 else 12
+ return min(capacity_cap, base + extra)
+
+
+def _configured_context_windows(config: VanerConfig) -> list[int]:
+ windows: list[int] = []
+ for source in (
+ getattr(config.backend, "runtime_options", {}) or {},
+ getattr(config.exploration, "runtime_options", {}) or {},
+ ):
+ if not isinstance(source, dict):
+ continue
+ for key in ("num_ctx", "context_window", "max_context_tokens"):
+ try:
+ value = int(source.get(key) or 0)
+ except (TypeError, ValueError):
+ value = 0
+ if value > 0:
+ windows.append(value)
+ for endpoint in getattr(config.exploration, "endpoints", []) or []:
+ try:
+ value = int(getattr(endpoint, "context_window", 0) or 0)
+ except (TypeError, ValueError):
+ value = 0
+ if value > 0:
+ windows.append(value)
+ return windows
+
+
+def _effective_context_budget(config: VanerConfig, *, requested: int | None = None) -> int:
+ """Return Vaner's evidence-package budget for the current runtime.
+
+ Explicit per-call budgets stay authoritative. Otherwise use the configured
+ package budget as a floor, then expand when the local runtime advertises a
+ large context window. Vaner leaves most of the model window for the user's
+ prompt, final answer, and reasoning overhead.
+ """
+
+ if requested is not None:
+ return max(1, int(requested))
+ configured = max(1, int(getattr(config, "max_context_tokens", 8192) or 8192))
+ windows = _configured_context_windows(config)
+ if not windows:
+ return max(8192, configured)
+ largest_window = max(windows)
+ adaptive = min(262_144, max(16_384, largest_window // 3))
+ return max(configured, adaptive)
+
+
def _build_exploration_llm(ecfg: ExplorationConfig) -> LLMCallable | None:
"""Resolve the exploration LLM from ExplorationConfig, probing endpoints as needed."""
import logging as _logging
@@ -6516,7 +6879,11 @@ def _make_ollama(base_url: str, m: str) -> LLMCallable:
from vaner.clients.ollama import ollama_llm
_log.info("Vaner exploration LLM: Ollama at %s model=%s", base_url, m)
- return ollama_llm(model=m, base_url=base_url)
+ return ollama_llm(
+ model=m,
+ base_url=base_url,
+ extra_body=_ollama_options_extra_body(ecfg.runtime_options, ecfg.sampling_options),
+ )
# ------------------------------------------------------------------
# Explicit endpoint given: probe or trust based on backend hint
diff --git a/src/vaner/intent/drafter.py b/src/vaner/intent/drafter.py
index cdff675..dc7db17 100644
--- a/src/vaner/intent/drafter.py
+++ b/src/vaner/intent/drafter.py
@@ -32,6 +32,7 @@
from vaner.clients.llm_response import approx_tokens
from vaner.intent.briefing import Briefing, BriefingAssembler
from vaner.intent.prediction import PredictedPrompt
+from vaner.policy.internal_llm import DRAFT_POLICY, PREDICTION_POLICY, internal_llm_policy
_log = logging.getLogger(__name__)
@@ -168,6 +169,7 @@ async def draft_for_prediction(
else:
recent_hint = "\n".join(recent_queries[-5:]) or "(no recent queries)"
rewrite_prompt = (
+ f"{internal_llm_policy(PREDICTION_POLICY)}\n\n"
"Rewrite the likely next developer prompt as one concrete sentence.\n"
"Stay semantically equivalent and concise.\n\n"
f"Candidate prompt: {predicted_prompt}\n"
@@ -190,6 +192,7 @@ async def draft_for_prediction(
summaries_text = "\n".join(file_summaries) or "(no artefact summaries available)"
recent_hint = "\n".join(recent_queries[-5:]) or "(no recent queries)"
draft_prompt_text = (
+ f"{internal_llm_policy(DRAFT_POLICY)}\n\n"
"You are Vaner, a context engine drafting a speculative answer for a\n"
"prompt the developer is likely to send next. Stay honest: if the\n"
"evidence below is insufficient to produce a confident draft, say so\n"
diff --git a/src/vaner/intent/evidence_resolver.py b/src/vaner/intent/evidence_resolver.py
index 84c4644..427c886 100644
--- a/src/vaner/intent/evidence_resolver.py
+++ b/src/vaner/intent/evidence_resolver.py
@@ -11,6 +11,7 @@
from vaner.intent.prediction_v2 import StructuredPrediction
from vaner.intent.symbol_index import RELATION_PRIORITY, SymbolCandidate
from vaner.intent.target_normalization import component_terms, normalize_component
+from vaner.semantic_aliases import engineering_semantic_aliases
_STOPWORDS = {
"about",
@@ -131,6 +132,7 @@ def _alias_tokens(text: str) -> set[str]:
if token.endswith(suffix) and len(token) > len(suffix) + 2:
aliases.add(token[: -len(suffix)])
aliases.add(suffix)
+ aliases.update(engineering_semantic_aliases(text, tokens, stopwords=_STOPWORDS))
return aliases
diff --git a/src/vaner/intent/symbol_index.py b/src/vaner/intent/symbol_index.py
index cbdc9da..201b32e 100644
--- a/src/vaner/intent/symbol_index.py
+++ b/src/vaner/intent/symbol_index.py
@@ -180,15 +180,20 @@ def rank_exact_paths(
max_paths: int = 8,
) -> list[str]:
candidates = symbol_candidates_for_text(repo_root, text, available_paths=available_paths)
- ranked: list[str] = []
- seen: set[str] = set()
+ if not candidates:
+ return []
+ by_path: dict[str, float] = {}
+ matched_terms_by_path: dict[str, set[str]] = {}
for candidate in candidates:
- if candidate.path not in seen:
- ranked.append(candidate.path)
- seen.add(candidate.path)
- if len(ranked) >= max_paths:
- break
- return ranked
+ matched_terms_by_path.setdefault(candidate.path, set()).update(candidate.matched_terms)
+ by_path[candidate.path] = by_path.get(candidate.path, 0.0) + _path_candidate_score(candidate)
+ for path, terms in matched_terms_by_path.items():
+ # Multi-term matches are usually better implementation anchors than
+ # single generic word hits, especially for prompts like "reward
+ # computation signals" where many signal modules mention "signal".
+ by_path[path] = by_path.get(path, 0.0) + min(2.5, max(0, len(terms) - 1) * 0.45)
+ ranked = sorted(by_path, key=lambda path: (-by_path[path], path))
+ return ranked[:max_paths]
def _candidate_paths(root: Path, available_paths: list[str] | tuple[str, ...] | None, *, max_files: int) -> list[str]:
@@ -259,6 +264,58 @@ def _dedupe_and_sort(candidates: list[SymbolCandidate]) -> list[SymbolCandidate]
)
+_LOW_SPECIFICITY_TERMS = {
+ "combined",
+ "combine",
+ "computation",
+ "context",
+ "database",
+ "extract",
+ "feature",
+ "final",
+ "model",
+ "package",
+ "persist",
+ "produce",
+ "retrieve",
+ "schema",
+ "signal",
+ "signals",
+ "table",
+ "value",
+}
+
+
+def _path_candidate_score(candidate: SymbolCandidate) -> float:
+ relation_weight = float(RELATION_PRIORITY[candidate.relation]) * 4.0
+ score = relation_weight + candidate.score
+ normalized_path_terms = set(path_component_terms(candidate.path))
+ matched_terms = set(candidate.matched_terms)
+ basename = Path(candidate.path).stem
+ normalized_basename = normalize_component(basename)
+ normalized_symbol = normalize_component(candidate.symbol)
+
+ if matched_terms & normalized_path_terms:
+ score += 5.0
+ if normalized_basename and normalized_basename in matched_terms:
+ score += 8.0
+ if normalized_symbol and normalized_symbol in matched_terms:
+ score += 4.0
+ if candidate.relation == "definition_match" and not candidate.symbol.startswith("_"):
+ score += 3.0
+ if candidate.symbol.startswith("_"):
+ score -= 2.0
+ if matched_terms and matched_terms <= _LOW_SPECIFICITY_TERMS and candidate.relation == "usage_match":
+ score -= 12.0
+ if candidate.relation == "usage_match" and not (matched_terms & normalized_path_terms):
+ score -= 8.0
+ if candidate.path.startswith(("src/", "lib/", "app/", "packages/")):
+ score += 0.5
+ if candidate.path.startswith(("tests/", "test/")):
+ score -= 0.5
+ return score
+
+
def _score_for_relation(relation: SymbolRelation) -> float:
return {
"definition_match": 1.0,
diff --git a/src/vaner/models/__init__.py b/src/vaner/models/__init__.py
index 3038931..7f64f3b 100644
--- a/src/vaner/models/__init__.py
+++ b/src/vaner/models/__init__.py
@@ -12,6 +12,12 @@
from vaner.models.artefact import Artefact, ArtefactKind
from vaner.models.config import VanerConfig
from vaner.models.context import ContextPackage, ContextSelection
+from vaner.models.context_preparation import (
+ ContextCoverageReport,
+ ContextFacet,
+ ContextPreparationProfile,
+ PreparedContextDiagnostics,
+)
from vaner.models.cost import (
CostEstimate,
CostLedgerEntry,
@@ -38,11 +44,15 @@
"EvidenceAssemblyDecision",
"EvidenceAssemblyMetadata",
"ContextPackage",
+ "ContextCoverageReport",
+ "ContextFacet",
+ "ContextPreparationProfile",
"ContextSelection",
"CostEstimate",
"CostLedgerEntry",
"DecisionRecord",
"PredictionLink",
+ "PreparedContextDiagnostics",
"Scenario",
"ScoreFactor",
"SelectionDecision",
diff --git a/src/vaner/models/config.py b/src/vaner/models/config.py
index bd7f2c7..5cced02 100644
--- a/src/vaner/models/config.py
+++ b/src/vaner/models/config.py
@@ -81,6 +81,11 @@ class BackendConfig(BaseModel):
reasoning_token_budget: int = 8192
# Try response_format={"type":"json_object"} before tolerant parsing.
prefer_structured_output: bool = True
+ # Provider/runtime knobs for local model calls. For Ollama these are
+ # forwarded under request ``options`` (for example num_ctx, keep_alive,
+ # temperature, top_p, top_k, min_p, repeat_penalty).
+ runtime_options: dict[str, Any] = Field(default_factory=dict)
+ sampling_options: dict[str, Any] = Field(default_factory=dict)
class GenerationConfig(BaseModel):
@@ -125,6 +130,18 @@ class EvidenceAssemblyConfig(BaseModel):
allow_semantic_compression: bool = False
+class ContextPreparationConfig(BaseModel):
+ mode: Literal["legacy", "balanced", "coverage_plus"] = "balanced"
+ max_query_variants: int = 6
+ max_candidate_keys: int = 640
+ max_expansion_passes: int = 1
+ model_expansion: Literal["off", "local_first", "cloud_allowed"] = "local_first"
+ semantic_memory_enabled: bool = False
+ coverage_floor_enabled: bool = True
+ hard_constraint_validation_enabled: bool = True
+ prepared_briefing_injection_enabled: bool = True
+
+
class GatewayConfig(BaseModel):
passthrough_enabled: bool = False
routes: dict[str, str] = Field(default_factory=dict)
@@ -248,6 +265,8 @@ class ExplorationEndpoint(BaseModel):
latency_p50_ms: float = 800.0
context_window: int = 8192
+ runtime_options: dict[str, Any] = Field(default_factory=dict)
+ sampling_options: dict[str, Any] = Field(default_factory=dict)
reasoning_depth_hint: Literal["low", "medium", "high"] = "medium"
structured_output_reliability: float = 0.7
cost_per_1k_tokens: float = 0.0
@@ -378,6 +397,14 @@ class ExplorationConfig(BaseModel):
fall back to ``"EMPTY"`` for local endpoints.
"""
+ runtime_options: dict[str, Any] = Field(default_factory=dict)
+ sampling_options: dict[str, Any] = Field(default_factory=dict)
+ """Provider/runtime options forwarded to the single exploration endpoint.
+
+ Multi-endpoint routing uses the per-endpoint fields on
+ ``ExplorationEndpoint`` instead.
+ """
+
endpoints: list[ExplorationEndpoint] = Field(default_factory=list)
"""Optional pool of exploration endpoints for multi-endpoint routing.
@@ -735,7 +762,7 @@ class VanerConfig(BaseModel):
store_path: Path
telemetry_path: Path
max_age_seconds: int = 3600
- max_context_tokens: int = 4096
+ max_context_tokens: int = 8192
backend: BackendConfig = Field(default_factory=BackendConfig)
privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
generation: GenerationConfig = Field(default_factory=GenerationConfig)
@@ -752,3 +779,4 @@ class VanerConfig(BaseModel):
policy: PolicyConfig = Field(default_factory=PolicyConfig)
cost: CostConfig = Field(default_factory=CostConfig)
evidence_assembly: EvidenceAssemblyConfig = Field(default_factory=EvidenceAssemblyConfig)
+ context_preparation: ContextPreparationConfig = Field(default_factory=ContextPreparationConfig)
diff --git a/src/vaner/models/context.py b/src/vaner/models/context.py
index fd73f77..3dd5ab8 100644
--- a/src/vaner/models/context.py
+++ b/src/vaner/models/context.py
@@ -5,6 +5,7 @@
from pydantic import BaseModel, Field
from vaner.models.answerable import AnswerabilityMetadata, AnswerableBriefing
+from vaner.models.context_preparation import PreparedContextDiagnostics
class ContextSelection(BaseModel):
@@ -32,6 +33,9 @@ class ContextPackage(BaseModel):
conflict_notes: list[str] = Field(default_factory=list)
answerable_briefing: AnswerableBriefing | None = None
answerability_metadata: AnswerabilityMetadata | None = None
+ prepared_context_briefing: str = ""
+ prepared_context_mode: str = ""
+ prepared_context_diagnostics: PreparedContextDiagnostics | None = None
cache_tier: str = "miss"
"""How this package was sourced: ``"full_hit"`` | ``"partial_hit"`` | ``"miss"``."""
partial_similarity: float = 0.0
diff --git a/src/vaner/models/context_preparation.py b/src/vaner/models/context_preparation.py
new file mode 100644
index 0000000..ba42543
--- /dev/null
+++ b/src/vaner/models/context_preparation.py
@@ -0,0 +1,84 @@
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+ContextNeed = Literal[
+ "direct_reference",
+ "task_continuation",
+ "evidence_gathering",
+ "multi_source_synthesis",
+ "decision_support",
+ "conflict_resolution",
+ "creative_grounding",
+ "research_mapping",
+ "implementation_support",
+ "absence_check",
+ "working_set_extension",
+]
+
+ContextArchetype = Literal["developer", "writer", "researcher", "operator", "general"]
+
+
+class ContextFacet(BaseModel):
+ name: str
+ value: str
+ required: bool = False
+
+
+class ContextConstraint(BaseModel):
+ kind: str
+ value: str
+ required: bool = True
+
+
+class ContextPreparationProfile(BaseModel):
+ need: ContextNeed = "evidence_gathering"
+ archetype: ContextArchetype = "general"
+ facets: list[ContextFacet] = Field(default_factory=list)
+ constraints: list[ContextConstraint] = Field(default_factory=list)
+ source_hints: list[str] = Field(default_factory=list)
+ expected_evidence_count: int = 1
+ confidence: float = 0.5
+ notes: list[str] = Field(default_factory=list)
+
+
+class ContextCoverageReport(BaseModel):
+ covered_facets: list[str] = Field(default_factory=list)
+ missing_constraints: list[str] = Field(default_factory=list)
+ direct_evidence_count: int = 0
+ conflict_pair_coverage: bool = False
+ actionability: Literal["full", "weak", "none", "conflict"] = "none"
+ truncation_risk: Literal["low", "medium", "high"] = "low"
+ source_agreement: float = 0.0
+ weak_expansion_dependency: bool = False
+ compactness_risk: Literal["low", "medium", "high"] = "low"
+ gap_flags: list[str] = Field(default_factory=list)
+
+
+class ContextSourceStats(BaseModel):
+ source: str
+ candidate_count: int = 0
+ selected_count: int = 0
+
+
+class PreparedContextDiagnostics(BaseModel):
+ profile: ContextPreparationProfile = Field(default_factory=ContextPreparationProfile)
+ source_counts: list[ContextSourceStats] = Field(default_factory=list)
+ fused_candidate_count: int = 0
+ selected_count: int = 0
+ hard_constraints_extracted: list[str] = Field(default_factory=list)
+ hard_constraints_satisfied: list[str] = Field(default_factory=list)
+ hard_constraints_missing: list[str] = Field(default_factory=list)
+ coverage: ContextCoverageReport = Field(default_factory=ContextCoverageReport)
+ dropped_direct_evidence: int = 0
+ weak_expansion_dependency: bool = False
+ token_used: int = 0
+ truncation_risk: Literal["low", "medium", "high"] = "low"
+ latency_ms: float = 0.0
+ compactness_score: float = 1.0
+ provenance_coverage: float = 0.0
+
diff --git a/src/vaner/models/decision.py b/src/vaner/models/decision.py
index 675f2fc..5ee05a9 100644
--- a/src/vaner/models/decision.py
+++ b/src/vaner/models/decision.py
@@ -7,6 +7,8 @@
from pydantic import BaseModel, Field
+from vaner.models.context_preparation import PreparedContextDiagnostics
+
class ScoreFactor(BaseModel):
name: str
@@ -45,6 +47,7 @@ class DecisionRecord(BaseModel):
selections: list[SelectionDecision] = Field(default_factory=list)
prediction_links: dict[str, PredictionLink] = Field(default_factory=dict)
notes: list[str] = Field(default_factory=list)
+ prepared_context_diagnostics: PreparedContextDiagnostics | None = None
def to_legacy_markdown(self) -> str:
lines = [
diff --git a/src/vaner/policy/internal_llm.py b/src/vaner/policy/internal_llm.py
new file mode 100644
index 0000000..8bd4b3f
--- /dev/null
+++ b/src/vaner/policy/internal_llm.py
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: Apache-2.0
+"""Internal prompt policy for Vaner's background LLM pipeline.
+
+These blocks are for machine-consumed intermediate calls, not user-facing
+assistant personas. Keep them short so local models spend context on evidence
+and the requested output contract.
+"""
+
+from __future__ import annotations
+
+CORE_POLICY = """Vaner internal LLM policy:
+- Preserve the requested output contract over style.
+- Prefer exact supplied evidence over plausible inference.
+- Preserve implementation anchors exactly: paths, symbols, constants, config keys, routes, database fields, errors, limits, and conditions.
+- Use null, empty arrays, low confidence, or short evidence notes when evidence is missing; do not invent details.
+- Do not expose hidden reasoning unless the output schema explicitly requires it."""
+
+JSON_CONTRACT_POLICY = """JSON contract:
+- Return valid JSON only, with no markdown fences, preamble, commentary, or trailing prose.
+- Use only the requested keys and valid JSON values.
+- Keep rationale/reason fields to short evidence notes, not chain-of-thought."""
+
+EVIDENCE_SUMMARY_POLICY = """Evidence summary policy:
+- Summaries must be grounded only in supplied files, diffs, code, or context.
+- Do not let predictions or likely intent become factual behavior.
+- Prefer exact implementation references over generic descriptions."""
+
+PREDICTION_POLICY = """Prediction policy:
+- Speculate only because this task asks for prediction.
+- Tie predictions to observed signals; label uncertainty through confidence, rationale, evidence, or provenance.
+- Keep predictive claims separate from factual summaries."""
+
+DRAFT_POLICY = """Draft policy:
+- Drafts remain evidence-bound and useful to a downstream AI client.
+- Use tentative wording when source evidence is incomplete.
+- Avoid overstating what Vaner knows."""
+
+
+def internal_llm_policy(*blocks: str) -> str:
+ """Compose the core policy with task-specific overlays."""
+
+ selected = [CORE_POLICY, *[block for block in blocks if block.strip()]]
+ return "\n".join(selected)
diff --git a/src/vaner/router/proxy.py b/src/vaner/router/proxy.py
index 4dcfa81..5ea0ad5 100644
--- a/src/vaner/router/proxy.py
+++ b/src/vaner/router/proxy.py
@@ -47,6 +47,28 @@ def _inject_context(payload: dict[str, Any], context: str) -> dict[str, Any]:
return {**payload, "messages": [system_message, *messages]}
+def _prepared_context_for_injection(context_package: Any, config: VanerConfig) -> str:
+ raw_context = str(getattr(context_package, "injected_context", "") or "")
+ if not config.context_preparation.prepared_briefing_injection_enabled:
+ return raw_context
+ if config.evidence_assembly.mode not in {"safe", "active"}:
+ return raw_context
+ briefing = str(getattr(context_package, "prepared_context_briefing", "") or "")
+ if not briefing:
+ answerable = getattr(context_package, "answerable_briefing", None)
+ briefing = str(getattr(answerable, "text", "") or "") if answerable is not None else ""
+ metadata = getattr(context_package, "answerability_metadata", None)
+ if metadata is None:
+ return raw_context
+ if getattr(metadata, "truncation_risk", "low") == "high":
+ return raw_context
+ if int(getattr(metadata, "dropped_direct_evidence_count", 0) or 0) > 0:
+ return raw_context
+ if getattr(metadata, "answerability", "none") not in {"full", "weak", "conflict"}:
+ return raw_context
+ return briefing or raw_context
+
+
def _normalize_message_content(content: Any) -> str:
if isinstance(content, str):
return content
@@ -536,7 +558,7 @@ async def chat_completions(payload: dict[str, Any], request: Request) -> Any:
metrics.partial_similarity = context_package.partial_similarity
metrics.context_tokens = context_package.token_used
metrics.injected_context_tokens = context_package.token_used
- enriched = _inject_context(payload, context_package.injected_context)
+ enriched = _inject_context(payload, _prepared_context_for_injection(context_package, config))
else:
context_package = type("Package", (), {"cache_tier": "disabled", "partial_similarity": 0.0, "token_used": 0})()
metrics.t1_context_ready = time.monotonic()
diff --git a/src/vaner/semantic_aliases.py b/src/vaner/semantic_aliases.py
new file mode 100644
index 0000000..18a1cca
--- /dev/null
+++ b/src/vaner/semantic_aliases.py
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+from collections.abc import Collection
+
+
+def engineering_semantic_aliases(text: str, tokens: Collection[str], *, stopwords: Collection[str] = ()) -> set[str]:
+ """Expand common engineering paraphrases without model calls.
+
+ These aliases are intentionally broad product vocabulary, not benchmark
+ fixtures. They help Vaner keep exact evidence in view when users describe
+ a concept in plain language while code/docs use implementation terms.
+ """
+
+ lowered = text.lower().replace("_", " ").replace("-", " ")
+ normalized_tokens = {token.lower() for token in tokens}
+ token_text = " ".join(sorted(normalized_tokens))
+ haystack = f"{lowered} {token_text}"
+ aliases: set[str] = set()
+
+ def has_any(*phrases: str) -> bool:
+ return any(phrase in haystack for phrase in phrases)
+
+ if has_any(
+ "dry run",
+ "smoke check",
+ "smoke policy",
+ "full traffic",
+ "candidate release",
+ "staged promote",
+ "staged rollout",
+ ):
+ aliases.update({"rehearse", "rehearsal", "replay", "canary", "escrow", "promote", "promotion", "traffic"})
+ if has_any("rehearse", "rehearsal", "replay", "traffic escrow", "promote", "promotion"):
+ aliases.update({"dry", "run", "smoke", "canary", "candidate", "release", "rollout", "traffic", "gate", "gating"})
+
+ if has_any("low bit", "low precision", "numeric mode", "mixed precision", "quantization", "quantized"):
+ aliases.update(
+ {"precision", "quantization", "quant", "kernel", "stability", "threshold", "annealing", "int4", "int8", "fp16", "fp32"}
+ )
+ if has_any("precision annealing", "kernel stability", "stability threshold", "int4", "int8", "fp16", "fp32"):
+ aliases.update({"low", "bit", "numeric", "mode", "quantization", "safest", "pass", "rate"})
+
+ if has_any("vectorization", "vectorize", "embedding", "embeddings", "embed batch", "batch embeds"):
+ aliases.update({"embedding", "embeddings", "embed", "vector", "vectorize", "vectorization"})
+
+ if has_any("western europe", "europe", "european", "eu west", "eu central"):
+ aliases.update({"eu", "euwest", "eucentral", "europe", "european", "emea"})
+ if has_any("southeast asia", "south east asia", "apac", "ap southeast"):
+ aliases.update({"apac", "asia", "southeast", "apsoutheast"})
+ if has_any("egress", "residency", "cross region", "edge fallback", "routing anomaly", "routed"):
+ aliases.update({"route", "routing", "egress", "residency", "fallback", "failover", "edge", "control", "plane"})
+
+ if has_any("makes things up", "made things up", "hallucination", "hallucinate", "joke", "meme"):
+ aliases.update({"hallucination", "hallucinate", "meme", "memes", "satire", "tagging", "joke", "jokes"})
+
+ stopword_set = {word.lower() for word in stopwords}
+ return {alias for alias in aliases if len(alias) >= 3 and alias not in stopword_set}
diff --git a/src/vaner/setup/catalog_refresh.py b/src/vaner/setup/catalog_refresh.py
index 589d82c..2cf9364 100644
--- a/src/vaner/setup/catalog_refresh.py
+++ b/src/vaner/setup/catalog_refresh.py
@@ -27,6 +27,7 @@
import json
import logging
+import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from importlib import resources
@@ -35,6 +36,7 @@
logger = logging.getLogger(__name__)
OLLAMA_REGISTRY_BASE = "https://registry.ollama.ai/v2/library/{name}/manifests/{tag}"
+OLLAMA_LIBRARY_MODEL_URL = "https://ollama.com/library/{name}%3A{tag}"
HF_API_BASE = "https://huggingface.co/api/models/{repo}"
DEFAULT_HTTP_TIMEOUT = 8.0
OLLAMA_MODEL_LAYER_PREFIX = "application/vnd.ollama.image.model"
@@ -54,6 +56,10 @@ class FamilySeed:
recency_rank: int
default_params_b: float
default_download_size_gb: float
+ active_params_b: float
+ architecture: str
+ quantization: str
+ accelerator_tags: tuple[str, ...]
parameters: dict[str, Any] = field(default_factory=dict)
@@ -81,6 +87,10 @@ def families_from_seed(seed: dict[str, Any]) -> list[FamilySeed]:
recency_rank=int(entry.get("recency_rank", 0)),
default_params_b=float(entry.get("default_params_b", 0.0)),
default_download_size_gb=float(entry.get("default_download_size_gb", 0.0)),
+ active_params_b=float(entry.get("active_params_b", 0.0) or 0.0),
+ architecture=str(entry.get("architecture", "dense") or "dense"),
+ quantization=str(entry.get("quantization", "") or ""),
+ accelerator_tags=tuple(str(t) for t in entry.get("accelerator_tags", []) if isinstance(t, str)),
parameters=dict(entry.get("parameters", {})),
)
)
@@ -95,15 +105,28 @@ def quantization_bytes_per_param(seed: dict[str, Any], quant: str) -> float:
return float(profile.get("bytes_per_param", 0.55))
-def estimate_memory_budget(params_b: float, bytes_per_param: float, context_window: int) -> tuple[float, float]:
+def estimate_memory_budget(
+ params_b: float,
+ bytes_per_param: float,
+ context_window: int,
+ *,
+ active_params_b: float = 0.0,
+ architecture: str = "dense",
+) -> tuple[float, float]:
"""Return (min_effective_gb, recommended_effective_gb)."""
weights_gb = params_b * bytes_per_param
if weights_gb <= 0:
return 0.0, 0.0
- context_overhead = max(0.5, weights_gb * (context_window / 32768) * 0.18)
- min_gb = round(weights_gb * 1.12 + 0.5, 1)
- rec_gb = round(weights_gb + context_overhead + 1.5, 1)
+ if architecture.lower() == "moe" and active_params_b > 0:
+ kv_reference_gb = max(active_params_b * bytes_per_param, weights_gb * 0.08)
+ context_overhead = max(2.0, kv_reference_gb * (context_window / 32768) * 0.18)
+ min_gb = round(weights_gb * 1.08 + 8.0, 1)
+ rec_gb = round(weights_gb + context_overhead + 8.0, 1)
+ else:
+ context_overhead = max(0.5, weights_gb * (context_window / 32768) * 0.18)
+ min_gb = round(weights_gb * 1.12 + 0.5, 1)
+ rec_gb = round(weights_gb + context_overhead + 1.5, 1)
return min_gb, max(rec_gb, min_gb)
@@ -155,6 +178,38 @@ def manifest_weights_bytes(manifest: dict[str, Any]) -> int:
return total
+def fetch_ollama_library_details(family: str, *, tag: str = "latest", timeout: float = DEFAULT_HTTP_TIMEOUT) -> dict[str, Any] | None:
+ """Best-effort model details from the public Ollama library page.
+
+ Some currently published Ollama tags are visible and runnable through
+ the library UI before their registry manifests are readable through the
+ plain OCI endpoint. Treat the page as a weaker verifier: it must show an
+ `ollama run :` command and a concrete local size. Cloud-only
+ rows such as `:cloud` intentionally do not pass this check.
+ """
+
+ import urllib.error
+ import urllib.request
+
+ url = OLLAMA_LIBRARY_MODEL_URL.format(name=family, tag=tag)
+ model_id = f"{family}:{tag}"
+ try:
+ req = urllib.request.Request(url, headers={"User-Agent": "vaner-catalog-refresh/1.0"})
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ html = resp.read().decode("utf-8", errors="replace")
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
+ logger.debug("ollama library page %s failed: %s", model_id, exc)
+ return None
+
+ if f"ollama run {model_id}" not in html:
+ return None
+ size_match = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*GB", html)
+ if not size_match:
+ return None
+ size_gb = float(size_match.group(1))
+ return {"download_size_gb": size_gb, "source": url}
+
+
def fetch_hf_params_b(repo: str, *, timeout: float = DEFAULT_HTTP_TIMEOUT) -> float | None:
"""Best-effort lookup of HF parameter count, in billions."""
@@ -186,6 +241,7 @@ def build_registry_entry_for_family(
quant: str,
online: bool,
manifest_fetcher=fetch_ollama_manifest,
+ library_fetcher=fetch_ollama_library_details,
) -> dict[str, Any] | None:
"""Translate one family into a single ``:`` registry row.
@@ -193,30 +249,43 @@ def build_registry_entry_for_family(
mode) — caller skips it.
"""
- bytes_per_param = quantization_bytes_per_param(seed, quant)
+ effective_quant = family.quantization or quant
+ bytes_per_param = quantization_bytes_per_param(seed, effective_quant)
params_b: float = 0.0
download_gb: float = 0.0
- if online:
+ if online and family.runtime == "ollama":
manifest = manifest_fetcher(family.ollama_family, tag=family.ollama_tag)
- if manifest is None:
- return None
- weights_bytes = manifest_weights_bytes(manifest)
- if weights_bytes <= 0:
- logger.debug("ollama manifest for %s has no model layer", family.id)
- return None
- download_gb = round(weights_bytes / (1024**3), 1)
- # Derive params from on-disk size + quant profile. This is more
- # honest than guessing; the user pulls exactly these bytes.
- params_b = round(weights_bytes / (1024**3) / bytes_per_param, 1)
+ if manifest is not None:
+ weights_bytes = manifest_weights_bytes(manifest)
+ if weights_bytes <= 0:
+ logger.debug("ollama manifest for %s has no model layer", family.id)
+ return None
+ download_gb = round(weights_bytes / (1024**3), 1)
+ # Derive params from on-disk size + quant profile. This is more
+ # honest than guessing; the user pulls exactly these bytes.
+ params_b = round(weights_bytes / (1024**3) / bytes_per_param, 1)
+ else:
+ details = library_fetcher(family.ollama_family, tag=family.ollama_tag)
+ if not details:
+ return None
+ download_gb = float(details["download_size_gb"])
+ params_b = float(family.default_params_b or 0.0)
else:
- # Offline path: use seed sizing when available, otherwise emit a
- # placeholder so the registry has a row per family.
+ # Offline and non-Ollama paths use seed sizing. Non-Ollama runtimes
+ # such as MLX are installed through their own package/model manager,
+ # so the Ollama manifest probe is not the source of truth.
params_b = float(family.default_params_b or 0.0)
download_gb = float(family.default_download_size_gb or 0.0)
context_window = int(family.parameters.get("context_window", 8192))
- min_gb, rec_gb = estimate_memory_budget(params_b, bytes_per_param, context_window)
+ min_gb, rec_gb = estimate_memory_budget(
+ params_b,
+ bytes_per_param,
+ context_window,
+ active_params_b=family.active_params_b,
+ architecture=family.architecture,
+ )
parameters = dict(family.parameters)
parameters.setdefault("num_ctx", context_window)
@@ -237,7 +306,10 @@ def build_registry_entry_for_family(
"parameters": parameters,
"family_id": family.id,
"params_b": params_b,
- "quantization": quant,
+ "active_params_b": family.active_params_b,
+ "architecture": family.architecture,
+ "quantization": effective_quant,
+ "accelerator_tags": list(family.accelerator_tags),
}
@@ -246,6 +318,7 @@ def build_registry(
online: bool = True,
seed: dict[str, Any] | None = None,
manifest_fetcher=fetch_ollama_manifest,
+ library_fetcher=fetch_ollama_library_details,
) -> dict[str, Any]:
"""Produce a full ``model_registry.json`` payload.
@@ -266,6 +339,7 @@ def build_registry(
quant=default_quant,
online=online,
manifest_fetcher=manifest_fetcher,
+ library_fetcher=library_fetcher,
)
except Exception as exc: # pragma: no cover - defensive
logger.warning("catalog: failed for %s (%s)", family.id, exc)
diff --git a/src/vaner/setup/config_io.py b/src/vaner/setup/config_io.py
index f69a233..0fae9fc 100644
--- a/src/vaner/setup/config_io.py
+++ b/src/vaner/setup/config_io.py
@@ -76,6 +76,7 @@ def persist_runtime_recommendation(repo_root: Path, recommendation: dict[str, An
params = selected.get("params") if isinstance(selected.get("params"), dict) else {}
capability = selected.get("capability") if isinstance(selected.get("capability"), dict) else {}
runtime_params = selected.get("runtime_params") if isinstance(selected.get("runtime_params"), dict) else {}
+ sampling_params = selected.get("sampling_params") if isinstance(selected.get("sampling_params"), dict) else {}
reasoning_mode = str(params.get("reasoning_mode") or "allowed")
max_response_tokens = int(params.get("max_response_tokens") or 3072)
reasoning_token_budget = int(params.get("reasoning_token_budget") or 4096)
@@ -83,7 +84,7 @@ def persist_runtime_recommendation(repo_root: Path, recommendation: dict[str, An
# Keep Vaner's own evidence package large enough to use long-context
# models, while leaving most of the window for the user's prompt,
# generated answer, reasoning budget, and runtime overhead.
- max_context_tokens = min(65536, max(8192, context_window // 4))
+ max_context_tokens = min(262144, max(16384, context_window // 3))
hardware = recommendation.get("hardware", {})
memory_source = hardware.get("memory_source") if isinstance(hardware, dict) else None
accelerator_type = hardware.get("accelerator_type") if isinstance(hardware, dict) else None
@@ -107,6 +108,8 @@ def persist_runtime_recommendation(repo_root: Path, recommendation: dict[str, An
"reasoning_mode": reasoning_mode,
"max_response_tokens": max_response_tokens,
"reasoning_token_budget": reasoning_token_budget,
+ "runtime_options": runtime_params,
+ "sampling_options": sampling_params,
},
)
text = update_toml_section(
@@ -116,6 +119,8 @@ def persist_runtime_recommendation(repo_root: Path, recommendation: dict[str, An
"endpoint": base_url.removesuffix("/v1") if runtime == "ollama" else base_url,
"model": model_id,
"backend": "ollama" if runtime == "ollama" else "openai",
+ "runtime_options": runtime_params,
+ "sampling_options": sampling_params,
},
)
text = remove_toml_keys(text, "exploration", {"exploration_endpoint", "exploration_model", "exploration_backend"})
@@ -156,6 +161,12 @@ def toml_literal(value: object) -> str:
else:
items.append(toml_literal(item))
return "[" + ", ".join(items) + "]"
+ if isinstance(value, dict):
+ items = []
+ for key, item in value.items():
+ key_text = str(key).replace("\\", "\\\\").replace('"', '\\"')
+ items.append(f'"{key_text}" = {toml_literal(item)}')
+ return "{ " + ", ".join(items) + " }"
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
diff --git a/src/vaner/setup/hardware.py b/src/vaner/setup/hardware.py
index 4af0b1f..5022355 100644
--- a/src/vaner/setup/hardware.py
+++ b/src/vaner/setup/hardware.py
@@ -42,6 +42,17 @@
MemoryKind = Literal["vram", "unified", "system", "unknown"]
+_NVIDIA_UNIFIED_MEMORY_MARKERS = (
+ "dgx spark",
+ "gb10",
+ "grace blackwell",
+)
+
+
+def _is_nvidia_unified_memory_name(name: str) -> bool:
+ normalized = name.lower()
+ return any(marker in normalized for marker in _NVIDIA_UNIFIED_MEMORY_MARKERS)
+
@dataclass(frozen=True, slots=True)
class GPUDevice:
@@ -95,6 +106,9 @@ class HardwareProfile:
# identify discrete devices — consumers should fall back to the
# ``gpu`` / ``gpu_vram_gb`` summary fields in that case.
gpu_devices: tuple[GPUDevice, ...] = field(default_factory=tuple)
+ # Decimal GB free on the user's home volume. Model setup uses this
+ # to avoid recommending a large local download that cannot fit.
+ disk_free_gb: int = 0
# ---------------------------------------------------------------------------
@@ -140,6 +154,17 @@ def _read_meminfo_gb() -> int | None:
return None
+def _probe_disk_free_gb(path: Path | None = None) -> int:
+ """Best-effort decimal GB free on the volume that will hold model caches."""
+
+ try:
+ usage = shutil.disk_usage(path or Path.home())
+ return max(0, round(usage.free / 1_000_000_000))
+ except Exception:
+ logger.debug("disk free probe failed", exc_info=True)
+ return 0
+
+
def _sysctl_memsize_gb() -> int | None:
"""Run ``sysctl hw.memsize`` (macOS) and convert to GB. Best-effort."""
try:
@@ -288,7 +313,7 @@ def _probe_gpu_devices_nvidia_pynvml() -> tuple[GPUDevice, ...] | None:
kind="nvidia",
memory_total_bytes=total_bytes,
memory_display_gb=vram_gb,
- memory_kind="vram",
+ memory_kind="unified" if _is_nvidia_unified_memory_name(name) else "vram",
)
)
except Exception:
@@ -342,7 +367,7 @@ def _probe_gpu_devices_nvidia_smi() -> tuple[GPUDevice, ...] | None:
kind="nvidia",
memory_total_bytes=total_bytes,
memory_display_gb=vram_gb,
- memory_kind="vram",
+ memory_kind="unified" if _is_nvidia_unified_memory_name(name) else "vram",
)
)
return tuple(devices) if devices else None
@@ -741,6 +766,7 @@ def detect() -> HardwareProfile:
thermal = _probe_thermal()
runtimes = _probe_runtimes()
models = _probe_models(runtimes)
+ disk_free_gb = _probe_disk_free_gb()
# When the OS probe fails entirely we still need a literal value for the
# frozen dataclass; fall back to "linux" but force the tier to "unknown"
@@ -778,6 +804,7 @@ def detect() -> HardwareProfile:
memory_is_unified=memory_is_unified,
tier="unknown",
gpu_devices=devices,
+ disk_free_gb=disk_free_gb,
)
final_tier: HardwareTier = "unknown" if os_kind is None else tier_for(profile)
return HardwareProfile(
@@ -795,6 +822,7 @@ def detect() -> HardwareProfile:
memory_is_unified=memory_is_unified,
tier=final_tier,
gpu_devices=devices,
+ disk_free_gb=disk_free_gb,
)
diff --git a/src/vaner/setup/model_recommendation.py b/src/vaner/setup/model_recommendation.py
index 3d8e600..04d41a5 100644
--- a/src/vaner/setup/model_recommendation.py
+++ b/src/vaner/setup/model_recommendation.py
@@ -29,6 +29,12 @@ class RecommendedModel:
min_effective_memory_gb: float
recommended_effective_memory_gb: float
parameters: dict[str, Any]
+ family_id: str = ""
+ params_b: float = 0.0
+ active_params_b: float = 0.0
+ architecture: str = "dense"
+ quantization: str = ""
+ accelerator_tags: tuple[str, ...] = ()
@classmethod
def from_raw(cls, raw: dict[str, Any]) -> RecommendedModel:
@@ -44,6 +50,12 @@ def from_raw(cls, raw: dict[str, Any]) -> RecommendedModel:
min_effective_memory_gb=float(raw.get("min_effective_memory_gb", 0)),
recommended_effective_memory_gb=float(raw.get("recommended_effective_memory_gb", raw.get("min_effective_memory_gb", 0))),
parameters=dict(raw.get("parameters", {})),
+ family_id=str(raw.get("family_id", raw.get("id", ""))),
+ params_b=float(raw.get("params_b", 0) or 0),
+ active_params_b=float(raw.get("active_params_b", 0) or 0),
+ architecture=str(raw.get("architecture", "dense") or "dense"),
+ quantization=str(raw.get("quantization", "") or ""),
+ accelerator_tags=tuple(str(v) for v in raw.get("accelerator_tags", []) if isinstance(v, str)),
)
@@ -112,6 +124,10 @@ def _fallback_registry(warning: str) -> ModelRegistry:
"max_response_tokens": 2048,
"reasoning_token_budget": 2048,
},
+ family_id="qwen3",
+ params_b=4.0,
+ architecture="dense",
+ quantization="Q4_K_M",
),
),
)
@@ -137,7 +153,19 @@ def recommend_local_model(
for model in reg.models:
fit = _fit_status(model, effective_memory_gb)
installed_match = (model.runtime, model.id) in installed
+ disk_status = _disk_status(model, hw)
+ if disk_status["status"] == "insufficient" and not installed_match:
+ rejected.append(
+ {
+ "model_id": model.id,
+ "runtime": model.runtime,
+ "reason": "insufficient_disk",
+ **disk_status,
+ }
+ )
+ continue
runtime_available = model.runtime in available_runtimes
+ runtime_installable = _runtime_installable(model.runtime, hw)
if fit == "too_large":
rejected.append(
{
@@ -149,10 +177,20 @@ def recommend_local_model(
}
)
continue
- if model.runtime != "ollama" and not runtime_available:
+ if not runtime_available and not runtime_installable:
rejected.append({"model_id": model.id, "runtime": model.runtime, "reason": "runtime_unavailable"})
continue
- score = _score_model(model, workload_tags, installed_match, runtime_available, fit)
+ score = _score_model(
+ model,
+ workload_tags,
+ installed_match,
+ runtime_available,
+ fit,
+ effective_memory_gb=effective_memory_gb,
+ answers=answers,
+ hardware=hw,
+ disk_status=disk_status["status"],
+ )
candidates.append(
(
score,
@@ -161,12 +199,14 @@ def recommend_local_model(
"fit": fit,
"already_installed": installed_match,
"runtime_available": runtime_available,
+ "runtime_installable": runtime_installable,
+ "disk": disk_status,
},
)
)
if not candidates:
- fallback = min(reg.models, key=lambda m: m.min_effective_memory_gb)
+ fallback = min(reg.models, key=lambda m: (m.download_size_gb or 0.0, m.min_effective_memory_gb))
candidates.append(
(
0,
@@ -175,6 +215,7 @@ def recommend_local_model(
"fit": "fallback_cpu",
"already_installed": (fallback.runtime, fallback.id) in installed,
"runtime_available": fallback.runtime in available_runtimes,
+ "disk": _disk_status(fallback, hw),
},
)
)
@@ -183,10 +224,10 @@ def recommend_local_model(
score, selected, selected_diag = candidates[0]
runtime_available = selected.runtime in available_runtimes
already_installed = (selected.runtime, selected.id) in installed
- needs_runtime_install = selected.runtime == "ollama" and not runtime_available
+ needs_runtime_install = not runtime_available
needs_model_download = not already_installed
user_explanation = _plain_explanation(hw, selected, effective_memory_gb, memory_source, already_installed)
- install_plan = _install_plan(selected, needs_runtime_install, needs_model_download)
+ install_plan = _install_plan(selected, needs_runtime_install, needs_model_download, disk_status=selected_diag.get("disk"))
runtime = _runtime_payload(selected.runtime)
work_styles_tuple: tuple[str, ...] = tuple(answers.work_styles) if answers else ()
selected_payload = _selected_payload(
@@ -222,6 +263,9 @@ def recommend_local_model(
"accelerator_label": _accelerator_label(hw),
"effective_gb_q4": effective_memory_gb,
"memory_source": memory_source,
+ "disk_free_gb": hw.disk_free_gb,
+ "gpu_count": _gpu_count(hw),
+ "gpu_total_memory_gb": _gpu_total_memory_gb(hw),
"notes": [],
},
"selected": selected_payload,
@@ -251,6 +295,7 @@ def recommend_local_model(
"raw_hardware": {
"memory_total_bytes": hw.memory_total_bytes,
"memory_is_unified": hw.memory_is_unified,
+ "disk_free_gb": hw.disk_free_gb,
"gpu_devices": [
{
"name": d.name,
@@ -266,8 +311,11 @@ def recommend_local_model(
}
-def _effective_memory_gb(hw: HardwareProfile) -> tuple[float, Literal["vram", "unified", "system", "cpu"]]:
- if hw.memory_is_unified and hw.memory_display_gb:
+def _effective_memory_gb(hw: HardwareProfile) -> tuple[float, Literal["vram", "unified", "inferred_gpu", "system", "cpu"]]:
+ if _looks_like_nvidia_unified_memory(hw) and hw.memory_display_gb:
+ reserve = 8 if hw.memory_display_gb >= 24 else 4
+ return max(2.0, float(hw.memory_display_gb - reserve)), "unified"
+ if hw.memory_is_unified and hw.gpu == "apple_silicon" and hw.memory_display_gb:
reserve = 8 if hw.memory_display_gb >= 24 else 4
return max(2.0, float(hw.memory_display_gb - reserve)), "unified"
gpu_memories = [d.memory_display_gb for d in hw.gpu_devices if d.memory_kind == "vram" and d.memory_display_gb]
@@ -276,17 +324,43 @@ def _effective_memory_gb(hw: HardwareProfile) -> tuple[float, Literal["vram", "u
return max(2.0, float(max(gpu_memories) - 2)), "vram"
if hw.gpu_vram_gb:
return max(2.0, float(hw.gpu_vram_gb - 2)), "vram"
+ if hw.gpu == "nvidia" and hw.memory_display_gb >= 96:
+ # NVIDIA + large host memory + missing VRAM telemetry is common on
+ # new developer-class systems where NVML/nvidia-smi reporting may be
+ # incomplete or unified-memory platforms are not named clearly. Do
+ # not treat host RAM as fully usable GPU memory, but do avoid a tiny
+ # CPU-class default.
+ return 30.0, "inferred_gpu"
if hw.gpu in {"nvidia", "amd"}:
- # A discrete GPU without readable VRAM is not enough evidence for
- # a large-model recommendation. Stay conservative until diagnostics
- # can read the actual accelerator memory.
- return min(8.0, max(2.0, float((hw.memory_display_gb or hw.ram_gb) - 8))), "system"
+ # A discrete GPU without readable VRAM is not enough evidence for a
+ # large local-model recommendation. System RAM is useful for the OS
+ # and caches, not for fast Vaner inference.
+ return 2.0, "system"
if hw.memory_display_gb:
- reserve = 6 if hw.memory_display_gb >= 16 else 3
- return max(2.0, float(hw.memory_display_gb - reserve)), "system"
+ return 2.0, "system"
return 2.0, "cpu"
+def _looks_like_nvidia_unified_memory(hw: HardwareProfile) -> bool:
+ if hw.gpu != "nvidia":
+ return False
+ if hw.memory_is_unified:
+ return True
+ names = " ".join(device.name.lower() for device in hw.gpu_devices)
+ return any(marker in names for marker in ("dgx spark", "gb10", "grace blackwell"))
+
+
+def _gpu_count(hw: HardwareProfile) -> int:
+ return len([d for d in hw.gpu_devices if d.kind not in {"cpu", "integrated"}]) or (1 if hw.gpu in {"nvidia", "amd"} else 0)
+
+
+def _gpu_total_memory_gb(hw: HardwareProfile) -> int:
+ values = [int(d.memory_display_gb or 0) for d in hw.gpu_devices if d.memory_display_gb and d.memory_kind in {"vram", "unified"}]
+ if values:
+ return sum(values)
+ return int(hw.gpu_vram_gb or 0)
+
+
def _workload_tags(answers: SetupAnswers | None) -> set[str]:
tags = {"general", "summarization"}
if answers is None:
@@ -313,23 +387,50 @@ def _fit_status(model: RecommendedModel, effective_memory_gb: float) -> Literal[
hardware that can clearly run the larger one. The strict
"recommended" tier remains the upper bound.
"""
- # `min + 4` GB ≈ weights + a sensible 32K-ish KV-cache budget, which
- # is what `compute_effective_context_window` actually picks at
- # runtime on a card sized at the model's `min_effective_memory_gb`.
- relaxed_recommended = model.min_effective_memory_gb + 4.0
+ # The registry budgets are calculated against the model's architectural
+ # max context. Setup picks a runtime-effective context later, so fit
+ # should test the practical floor-context load too: weights + runtime
+ # reserve + one 32K KV slice. This is especially important for current
+ # MoE models where active params make context cheaper than total params
+ # imply, while weights still need to fit.
+ kv_reference_gb = _kv_reference_gb(model) or model.download_size_gb
+ practical_min = (model.download_size_gb or 0.0) + 3.0 + max(0.5, kv_reference_gb * 0.16)
+ relaxed_recommended = max(practical_min + 4.0, model.min_effective_memory_gb)
if effective_memory_gb >= min(model.recommended_effective_memory_gb, relaxed_recommended):
return "recommended"
- if effective_memory_gb >= model.min_effective_memory_gb:
+ if effective_memory_gb >= min(model.min_effective_memory_gb, practical_min):
return "fits"
return "too_large"
+def _disk_status(model: RecommendedModel, hw: HardwareProfile) -> dict[str, Any]:
+ free_gb = int(getattr(hw, "disk_free_gb", 0) or 0)
+ download_gb = max(0.0, float(model.download_size_gb or 0.0))
+ # Keep room for the compressed download, expanded cache/metadata, and a
+ # little operational headroom. Installed models still report a need here;
+ # `already_installed` gets scored separately and the install plan can skip
+ # the download step.
+ required_gb = round(download_gb * 1.15 + 8.0, 1) if download_gb > 0 else 0.0
+ if free_gb <= 0 or required_gb <= 0:
+ return {"status": "unknown", "free_gb": free_gb, "required_gb": required_gb}
+ if free_gb < required_gb:
+ return {"status": "insufficient", "free_gb": free_gb, "required_gb": required_gb}
+ if free_gb < required_gb + 25.0:
+ return {"status": "tight", "free_gb": free_gb, "required_gb": required_gb}
+ return {"status": "enough", "free_gb": free_gb, "required_gb": required_gb}
+
+
def _score_model(
model: RecommendedModel,
workload_tags: set[str],
installed_match: bool,
runtime_available: bool,
fit: str,
+ *,
+ effective_memory_gb: float,
+ answers: SetupAnswers | None,
+ hardware: HardwareProfile,
+ disk_status: str = "unknown",
) -> float:
"""Score a candidate against the user's hardware + workload tags.
@@ -347,6 +448,12 @@ def _score_model(
score += tag_overlap * 35.0
score += max(0.0, 30.0 - model.download_size_gb) * 0.5
score += model.recency_rank * 1.0
+ score += _context_score(model)
+ score += _hardware_utilization_score(model, effective_memory_gb, answers)
+ score += _runtime_affinity_score(model, hardware, runtime_available)
+ score += _architecture_score(model, hardware)
+ if model.recency_rank < 80:
+ score -= (80 - model.recency_rank) * 8.0
if fit == "recommended":
score += 75
elif fit == "fits":
@@ -355,6 +462,111 @@ def _score_model(
score += 120
elif runtime_available:
score += 30
+ if disk_status == "tight":
+ score -= 50
+ if answers is not None:
+ if answers.priority in {"speed", "low_resource"} or answers.compute_posture == "light":
+ score -= max(0.0, model.download_size_gb - 30.0) * 2.1
+ if answers.priority == "quality":
+ score += model.quality_rank * 0.35
+ if answers.compute_posture == "available_power":
+ score += min(90.0, model.recommended_effective_memory_gb * 0.35)
+ if answers.background_posture == "deep_run_aggressive":
+ score += min(80.0, _max_context_window(model) / 131072.0 * 10.0)
+ return score
+
+
+def _runtime_installable(runtime: Runtime, hw: HardwareProfile) -> bool:
+ if runtime == "ollama":
+ return True
+ if runtime == "mlx":
+ return hw.os == "darwin" and hw.gpu == "apple_silicon"
+ if runtime == "vllm":
+ return hw.os == "linux" and hw.gpu == "nvidia"
+ return False
+
+
+def _max_context_window(model: RecommendedModel) -> int:
+ try:
+ return int(model.parameters.get("context_window", _CONTEXT_WINDOW_FLOOR))
+ except (TypeError, ValueError):
+ return _CONTEXT_WINDOW_FLOOR
+
+
+def _context_score(model: RecommendedModel) -> float:
+ # Reward native long-context models without letting context alone beat
+ # model quality. 32K => 0, 262K => about 45, 1M => about 75.
+ window = max(_CONTEXT_WINDOW_FLOOR, _max_context_window(model))
+ multiples = max(1.0, window / _CONTEXT_WINDOW_FLOOR)
+ import math
+
+ return min(90.0, math.log2(multiples) * 15.0)
+
+
+def _hardware_utilization_score(model: RecommendedModel, effective_memory_gb: float, answers: SetupAnswers | None) -> float:
+ if effective_memory_gb <= 0 or model.recommended_effective_memory_gb <= 0:
+ return 0.0
+ usage = min(1.0, model.recommended_effective_memory_gb / effective_memory_gb)
+ # Balanced users on very large boxes should not get a tiny-model default.
+ # Speed / low-resource explicitly opts back toward smaller models.
+ if answers and (answers.priority in {"speed", "low_resource"} or answers.compute_posture == "light"):
+ return -40.0 * usage
+ return min(160.0, 180.0 * (usage**0.5))
+
+
+def _runtime_affinity_score(model: RecommendedModel, hw: HardwareProfile, runtime_available: bool) -> float:
+ score = 0.0
+ tags = set(model.accelerator_tags)
+ if hw.gpu == "apple_silicon":
+ if model.runtime == "mlx":
+ score += 125.0
+ elif model.runtime == "ollama":
+ score += 20.0
+ if "apple_silicon" in tags or "unified_memory" in tags:
+ score += 35.0
+ if model.quantization.upper() in {"MXFP8", "MLX"} or "mlx" in tags:
+ score += 55.0
+ if "blackwell" in tags or model.quantization.upper() == "NVFP4":
+ score -= 70.0
+ elif hw.gpu == "nvidia":
+ if model.runtime == "vllm":
+ score += 90.0
+ elif model.runtime == "ollama":
+ score += 35.0
+ if "cuda" in tags or "nvidia" in tags:
+ score += 30.0
+ if _looks_like_nvidia_unified_memory(hw):
+ if "dgx_spark" in tags or "unified_memory" in tags:
+ score += 90.0
+ elif "dgx_spark" in tags:
+ score -= 55.0
+ if "blackwell" in tags and _has_blackwell_gpu(hw):
+ score += 65.0
+ if model.quantization.upper() in {"MXFP8", "MLX"}:
+ score -= 35.0
+ elif model.runtime == "ollama":
+ score += 25.0
+ if not runtime_available and model.runtime != "ollama":
+ score -= 25.0
+ return score
+
+
+def _has_blackwell_gpu(hw: HardwareProfile) -> bool:
+ names = " ".join(device.name.lower() for device in hw.gpu_devices)
+ return any(marker in names for marker in ("rtx 50", "5090", "5080", "5070", "5060", "blackwell", "pro 6000", "dgx spark", "gb10"))
+
+
+def _architecture_score(model: RecommendedModel, hw: HardwareProfile) -> float:
+ if model.architecture.lower() != "moe":
+ return 0.0
+ active = model.active_params_b or model.params_b
+ total = model.params_b or active
+ if active <= 0 or total <= 0:
+ return 25.0
+ sparse_ratio = max(0.0, min(1.0, 1.0 - (active / total)))
+ score = 35.0 + sparse_ratio * 45.0
+ if (hw.memory_is_unified or _looks_like_nvidia_unified_memory(hw)) and hw.memory_display_gb >= 128:
+ score += 30.0
return score
@@ -367,6 +579,22 @@ def _runtime_payload(runtime: Runtime) -> dict[str, Any]:
"native_endpoint": OLLAMA_NATIVE_ENDPOINT,
"install_managed": True,
}
+ if runtime == "mlx":
+ return {
+ "id": "mlx",
+ "label": "MLX",
+ "base_url": "http://127.0.0.1:8080/v1",
+ "native_endpoint": "http://127.0.0.1:8080",
+ "install_managed": True,
+ }
+ if runtime == "vllm":
+ return {
+ "id": "vllm",
+ "label": "vLLM",
+ "base_url": "http://127.0.0.1:8000/v1",
+ "native_endpoint": "http://127.0.0.1:8000",
+ "install_managed": True,
+ }
return {"id": runtime, "label": runtime, "base_url": "", "install_managed": False}
@@ -430,6 +658,7 @@ def compute_effective_context_window(
effective_memory_gb: float,
work_styles: tuple[str, ...] = (),
runtime: str = "ollama",
+ kv_reference_gb: float | None = None,
) -> int:
"""Pick a runtime-effective context window.
@@ -441,14 +670,14 @@ def compute_effective_context_window(
from the hardware probe.
- ``work_styles``: the wizard's archetype answers (coding,
research, …) — long-context archetypes get a higher target.
- - ``runtime``: today only ``ollama`` is wired; left as an input so
- future runtimes (vLLM, llama.cpp w/ flash-attn) can override.
+ - ``runtime``: runtime family identifier; local runtimes can tune
+ context/KV behavior as support evolves.
Returns the chosen window, clamped to [floor, max].
Heuristic — KV-cache scales roughly linearly with both context
length and weight size; for Q4 + GQA models a 32K context costs
- around 18% of weight memory. We turn that around: pick the largest
+ around 16% of weight memory. We turn that around: pick the largest
multiple of 32K that fits in the headroom we have after weights and
a small safety reserve.
"""
@@ -469,10 +698,11 @@ def compute_effective_context_window(
# extra reserve keeps long-context defaults from pinning VRAM at the edge.
runtime_reserve_gb = 3.0
headroom_gb = max(0.0, effective_memory_gb - weights_gb - runtime_reserve_gb)
- # Cost of KV at the family's 32K reference. 0.18 is the rough Q4+GQA
+ # Cost of KV at the family's 32K reference. 0.16 is the rough Q4+GQA
# constant; flash-attn / paged-attention runtimes get more headroom
# implicitly because they pack the cache more tightly.
- cost_per_32k = max(0.5, weights_gb * 0.18)
+ kv_gb = kv_reference_gb if kv_reference_gb and kv_reference_gb > 0 else weights_gb
+ cost_per_32k = max(0.5, kv_gb * 0.16)
if cost_per_32k <= 0:
return min(cap, max(floor, target))
@@ -483,11 +713,47 @@ def compute_effective_context_window(
return min(cap, floor)
affordable = floor * max(1, multiples)
chosen = min(cap, max(target, affordable))
+ chosen = min(chosen, _default_context_tier_cap(effective_memory_gb, weights_gb, runtime))
# Round down to the nearest multiple of 8K so num_ctx is friendly.
chosen = (chosen // 8192) * 8192
return min(cap, max(floor, chosen))
+def _default_context_tier_cap(effective_memory_gb: float, weights_gb: float, runtime: str) -> int:
+ """Cap first-run context by memory tier.
+
+ This is intentionally more conservative than "what might fit". Vaner's
+ default runner is Ollama, and onboarding must avoid accidental CPU
+ offload / KV pressure. Larger windows remain available through advanced
+ or custom profiles; this function chooses the safe first-run default.
+ """
+
+ if effective_memory_gb <= 0:
+ return _CONTEXT_WINDOW_FLOOR
+ if effective_memory_gb < 12:
+ return 32768
+ if effective_memory_gb < 18:
+ return 65536
+ if effective_memory_gb < 28:
+ return 131072
+ if effective_memory_gb < 44:
+ return 65536 if weights_gb >= 20 else 131072
+ if effective_memory_gb < 96:
+ return 262144
+ if runtime == "ollama":
+ return 262144
+ return 524288
+
+
+def _kv_reference_gb(model: RecommendedModel) -> float:
+ if model.architecture.lower() == "moe" and model.active_params_b > 0:
+ # MoE models load all weights, but the active expert set gives a
+ # better default proxy for KV/context scaling than total parameters.
+ active_weight_gb = model.active_params_b * 0.55
+ return max(active_weight_gb, model.download_size_gb * 0.08)
+ return model.download_size_gb or 0.0
+
+
def _split_parameters(parameters: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
"""Split a registry ``parameters`` block into capability / runtime / sampling.
@@ -539,6 +805,7 @@ def _selected_payload(
effective_memory_gb=float(effective_memory_gb or 0.0),
work_styles=work_styles,
runtime=model.runtime,
+ kv_reference_gb=_kv_reference_gb(model),
)
capability["context_window"] = effective_ctx
capability["max_context_window"] = max_ctx
@@ -557,8 +824,15 @@ def _selected_payload(
"download_size_gb": model.download_size_gb,
"min_effective_memory_gb": model.min_effective_memory_gb,
"recommended_effective_memory_gb": model.recommended_effective_memory_gb,
+ "family_id": model.family_id,
+ "params_b": model.params_b,
+ "active_params_b": model.active_params_b,
+ "architecture": model.architecture,
+ "quantization": model.quantization,
+ "accelerator_tags": list(model.accelerator_tags),
"already_installed": bool(diag.get("already_installed")),
"fit": diag.get("fit", "unknown"),
+ "disk": diag.get("disk", {"status": "unknown"}),
"params": combined,
"runtime_params": runtime_params,
"model_params": combined,
@@ -567,19 +841,59 @@ def _selected_payload(
}
-def _install_plan(model: RecommendedModel, needs_runtime_install: bool, needs_model_download: bool) -> list[dict[str, Any]]:
+def _install_plan(
+ model: RecommendedModel,
+ needs_runtime_install: bool,
+ needs_model_download: bool,
+ *,
+ disk_status: dict[str, Any] | None = None,
+) -> list[dict[str, Any]]:
steps = [{"id": "save_config", "label": "Save Vaner settings", "required": True}]
+ if disk_status and disk_status.get("status") in {"tight", "insufficient"}:
+ steps.append(
+ {
+ "id": "confirm_disk_space",
+ "label": "Free disk space" if disk_status.get("status") == "insufficient" else "Confirm disk space",
+ "required": True,
+ "free_gb": disk_status.get("free_gb"),
+ "required_gb": disk_status.get("required_gb"),
+ }
+ )
if needs_runtime_install:
- steps.append({"id": "install_runtime", "label": "Install Ollama", "required": True})
+ if model.runtime == "mlx":
+ steps.append(
+ {
+ "id": "install_runtime",
+ "label": "Install MLX",
+ "required": True,
+ "command": ["python", "-m", "pip", "install", "mlx-lm"],
+ }
+ )
+ elif model.runtime == "vllm":
+ steps.append(
+ {
+ "id": "install_runtime",
+ "label": "Install vLLM",
+ "required": True,
+ "command": ["python", "-m", "pip", "install", "vllm"],
+ }
+ )
+ else:
+ steps.append({"id": "install_runtime", "label": "Install Ollama", "required": True})
else:
steps.append({"id": "check_runtime", "label": "Check local model runner", "required": True})
if needs_model_download:
+ command: list[str] = []
+ if model.runtime == "ollama":
+ command = ["ollama", "pull", model.id]
+ elif model.runtime == "mlx":
+ command = ["mlx_lm.server", "--model", model.id, "--port", "8080"]
steps.append(
{
"id": "download_model",
"label": f"Download {model.display_name}",
"required": True,
- "command": ["ollama", "pull", model.id] if model.runtime == "ollama" else [],
+ "command": command,
}
)
else:
@@ -632,7 +946,12 @@ def _plain_explanation(
if memory_source == "vram":
return f"Vaner found enough GPU memory for {model.display_name} with headroom for normal desktop use.{installed}"
if memory_source == "unified":
- return f"Vaner found unified memory and chose {model.display_name} with safe headroom for macOS and the model runner.{installed}"
+ return f"Vaner found unified accelerator memory and chose {model.display_name} with safe headroom for the desktop and model runner.{installed}"
+ if memory_source == "inferred_gpu":
+ return (
+ f"Vaner found an NVIDIA GPU but could not read exact GPU memory, so it chose {model.display_name} "
+ f"as a cautious GPU-class default instead of a tiny CPU model.{installed}"
+ )
if hw.gpu == "none":
return (
f"Vaner did not find a dedicated GPU, so it chose {model.display_name} as a safer local setup. "
@@ -647,8 +966,11 @@ def _hardware_summary(hw: HardwareProfile, effective_memory_gb: float, memory_so
"accelerator_type": hw.gpu,
"effective_memory_gb": effective_memory_gb,
"memory_source": memory_source,
+ "gpu_count": _gpu_count(hw),
+ "gpu_total_memory_gb": _gpu_total_memory_gb(hw),
"system_memory_gb": hw.memory_display_gb or hw.ram_gb,
- "is_unified_memory": hw.memory_is_unified,
+ "is_unified_memory": hw.memory_is_unified or _looks_like_nvidia_unified_memory(hw),
+ "disk_free_gb": hw.disk_free_gb,
"tier": hw.tier,
}
diff --git a/src/vaner/setup/serializers.py b/src/vaner/setup/serializers.py
index ad743bb..44163e7 100644
--- a/src/vaner/setup/serializers.py
+++ b/src/vaner/setup/serializers.py
@@ -104,6 +104,7 @@ def hardware_to_dict(hw: HardwareProfile) -> dict[str, Any]:
"thermal_constrained": hw.thermal_constrained,
"detected_runtimes": list(hw.detected_runtimes),
"detected_models": [list(row) for row in hw.detected_models],
+ "disk_free_gb": hw.disk_free_gb,
"tier": hw.tier,
}
diff --git a/src/vaner/store/artefacts.py b/src/vaner/store/artefacts.py
index e9981e5..60a4a3e 100644
--- a/src/vaner/store/artefacts.py
+++ b/src/vaner/store/artefacts.py
@@ -787,26 +787,74 @@ async def list(
cursor = await db.execute(query, tuple(params))
rows = await cursor.fetchall()
- artefacts: list[Artefact] = []
+ return [self._artefact_from_row(row) for row in rows]
+
+ async def list_by_keys(self, keys: set[str] | list[str] | tuple[str, ...], *, limit: int = 200) -> list[Artefact]:
+ ordered = list(dict.fromkeys(str(key) for key in keys if str(key)))
+ if not ordered:
+ return []
+ capped = ordered[: max(1, int(limit))]
+ placeholders = ",".join("?" for _ in capped)
+ query = (
+ "SELECT key, kind, source_path, source_mtime, generated_at, model, content, "
+ "metadata_json, relevance_score, access_count, last_accessed, signal_id "
+ f"FROM artefacts WHERE key IN ({placeholders})"
+ )
+ async with self._connect() as db:
+ cursor = await db.execute(query, tuple(capped))
+ rows = await cursor.fetchall()
+ by_key = {row[0]: self._artefact_from_row(row) for row in rows}
+ return [by_key[key] for key in capped if key in by_key]
+
+ async def list_by_source_paths(self, paths: set[str] | list[str] | tuple[str, ...], *, limit: int = 200) -> list[Artefact]:
+ ordered = list(dict.fromkeys(str(path) for path in paths if str(path)))
+ if not ordered:
+ return []
+ capped = ordered[: max(1, int(limit))]
+ placeholders = ",".join("?" for _ in capped)
+ query = (
+ "SELECT key, kind, source_path, source_mtime, generated_at, model, content, "
+ "metadata_json, relevance_score, access_count, last_accessed, signal_id "
+ f"FROM artefacts WHERE source_path IN ({placeholders})"
+ )
+ async with self._connect() as db:
+ cursor = await db.execute(query, tuple(capped))
+ rows = await cursor.fetchall()
+ by_path: dict[str, list[Artefact]] = {}
for row in rows:
- artefacts.append(
- Artefact(
- key=row[0],
- kind=ArtefactKind(row[1]),
- source_path=row[2],
- source_mtime=row[3],
- generated_at=row[4],
- model=row[5],
- content=row[6],
- metadata=json.loads(row[7]),
- relevance_score=row[8],
- access_count=row[9],
- last_accessed=row[10],
- signal_id=row[11],
- )
- )
+ artefact = self._artefact_from_row(row)
+ by_path.setdefault(artefact.source_path, []).append(artefact)
+ artefacts: list[Artefact] = []
+ for path in capped:
+ artefacts.extend(by_path.get(path, []))
return artefacts
+ async def list_source_paths(self, *, limit: int = 2000) -> list[str]:
+ async with self._connect() as db:
+ cursor = await db.execute(
+ "SELECT DISTINCT source_path FROM artefacts WHERE source_path != '' ORDER BY source_path LIMIT ?",
+ (max(1, int(limit)),),
+ )
+ rows = await cursor.fetchall()
+ return [str(row[0]) for row in rows if row[0]]
+
+ @staticmethod
+ def _artefact_from_row(row: tuple[object, ...]) -> Artefact:
+ return Artefact(
+ key=row[0],
+ kind=ArtefactKind(row[1]),
+ source_path=row[2],
+ source_mtime=row[3],
+ generated_at=row[4],
+ model=row[5],
+ content=row[6],
+ metadata=json.loads(row[7]),
+ relevance_score=row[8],
+ access_count=row[9],
+ last_accessed=row[10],
+ signal_id=row[11],
+ )
+
async def mark_accessed(self, key: str) -> None:
async with self._connect() as db:
await db.execute(
diff --git a/src/vaner/store/scenarios/sqlite.py b/src/vaner/store/scenarios/sqlite.py
index 2358245..ce7c482 100644
--- a/src/vaner/store/scenarios/sqlite.py
+++ b/src/vaner/store/scenarios/sqlite.py
@@ -4,6 +4,7 @@
import re
import time
from collections import defaultdict
+from dataclasses import dataclass
from pathlib import Path
from typing import cast
@@ -25,6 +26,28 @@
ScenarioVisibility,
)
+SCENARIO_SAMPLE_MIN_INTERVAL_SECONDS = 15.0
+SCENARIO_SAMPLE_KEEP_ROWS = 50_000
+
+
+@dataclass(frozen=True)
+class ScenarioSample:
+ ts: float
+ scenario_id: str
+ relevance: float
+ readiness: str
+ confidence: float
+ freshness: str
+ visible_priority: float
+ visibility: str
+ lifecycle_motion: str
+ status: str
+ pinned: bool
+ active: bool
+ cycle_id: str | None = None
+ job_id: str | None = None
+ source_event_id: str | None = None
+
class ScenarioStore:
def __init__(self, db_path: Path) -> None:
@@ -77,11 +100,36 @@ async def initialize(self) -> None:
)
"""
)
+ await db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS scenario_samples (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ ts REAL NOT NULL,
+ scenario_id TEXT NOT NULL,
+ relevance REAL NOT NULL,
+ readiness TEXT NOT NULL,
+ confidence REAL NOT NULL,
+ freshness TEXT NOT NULL,
+ visible_priority REAL NOT NULL,
+ visibility TEXT NOT NULL,
+ lifecycle_motion TEXT NOT NULL,
+ status TEXT NOT NULL,
+ pinned INTEGER NOT NULL DEFAULT 0,
+ active INTEGER NOT NULL DEFAULT 0,
+ cycle_id TEXT,
+ job_id TEXT,
+ source_event_id TEXT,
+ FOREIGN KEY (scenario_id) REFERENCES scenarios(id) ON DELETE CASCADE
+ )
+ """
+ )
await db.execute("CREATE INDEX IF NOT EXISTS idx_scenarios_kind ON scenarios(kind)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_scenarios_score ON scenarios(score DESC)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_scenarios_freshness ON scenarios(freshness)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_scenario_evidence_sid ON scenario_evidence(scenario_id)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_prompt_macro_clusters_centroid ON prompt_macro_clusters(centroid_label)")
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_scenario_samples_sid_ts ON scenario_samples(scenario_id, ts)")
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_scenario_samples_ts ON scenario_samples(ts)")
await self._add_column_if_missing(db, "ALTER TABLE scenarios ADD COLUMN context_envelope_json TEXT NOT NULL DEFAULT '{}'")
await self._add_column_if_missing(db, "ALTER TABLE scenarios ADD COLUMN memory_state TEXT NOT NULL DEFAULT 'candidate'")
await self._add_column_if_missing(db, "ALTER TABLE scenarios ADD COLUMN memory_confidence REAL NOT NULL DEFAULT 0.0")
@@ -259,6 +307,37 @@ async def list_top(self, *, kind: str | None = None, limit: int = 10, visibility
evidence_map = await self._load_evidence_for_scenarios(db, [str(row["id"]) for row in rows])
return [self._row_to_scenario(row, evidence_map.get(str(row["id"]), [])) for row in rows]
+ async def list_samples(
+ self,
+ *,
+ scenario_ids: list[str] | None = None,
+ start_ts: float | None = None,
+ end_ts: float | None = None,
+ limit: int = 20_000,
+ ) -> list[ScenarioSample]:
+ query = "SELECT * FROM scenario_samples"
+ predicates: list[str] = []
+ params: list[object] = []
+ if scenario_ids:
+ placeholders = ", ".join("?" for _ in scenario_ids)
+ predicates.append(f"scenario_id IN ({placeholders})")
+ params.extend(scenario_ids)
+ if start_ts is not None:
+ predicates.append("ts >= ?")
+ params.append(float(start_ts))
+ if end_ts is not None:
+ predicates.append("ts <= ?")
+ params.append(float(end_ts))
+ if predicates:
+ query += " WHERE " + " AND ".join(predicates)
+ query += " ORDER BY ts ASC LIMIT ?"
+ params.append(max(1, min(100_000, int(limit))))
+ async with aiosqlite.connect(self.db_path) as db:
+ db.row_factory = aiosqlite.Row
+ cur = await db.execute(query, params)
+ rows = await cur.fetchall()
+ return [self._sample_from_row(row) for row in rows]
+
async def get(self, scenario_id: str) -> Scenario | None:
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
@@ -658,6 +737,7 @@ def _row_to_scenario(self, row: aiosqlite.Row, evidence_rows: list[aiosqlite.Row
async def _refresh_lifecycle_for_ids(self, db: aiosqlite.Connection, scenario_ids: list[str], *, now: float | None = None) -> None:
if not scenario_ids:
return
+ sample_ts = time.time() if now is None else now
placeholders = ", ".join("?" for _ in scenario_ids)
db.row_factory = aiosqlite.Row
cur = await db.execute(f"SELECT * FROM scenarios WHERE id IN ({placeholders})", scenario_ids)
@@ -686,6 +766,98 @@ async def _refresh_lifecycle_for_ids(self, db: aiosqlite.Connection, scenario_id
refreshed.id,
),
)
+ await self._record_sample_if_changed(db, refreshed, ts=sample_ts)
+
+ async def _record_sample_if_changed(self, db: aiosqlite.Connection, scenario: Scenario, *, ts: float) -> None:
+ cur = await db.execute(
+ """
+ SELECT ts, relevance, readiness, confidence, freshness, visible_priority,
+ visibility, lifecycle_motion, status, pinned, active
+ FROM scenario_samples
+ WHERE scenario_id = ?
+ ORDER BY ts DESC
+ LIMIT 1
+ """,
+ (scenario.id,),
+ )
+ latest = await cur.fetchone()
+ status = _scenario_status(scenario)
+ active = 1 if status == "active" else 0
+ pinned = 1 if int(scenario.pinned) else 0
+ if latest is not None:
+ age = ts - float(latest[0])
+ numeric_delta = max(
+ abs(float(latest[1]) - float(scenario.relevance)),
+ abs(float(latest[3]) - float(scenario.confidence)),
+ abs(float(latest[5]) - float(scenario.visible_priority)),
+ )
+ categorical_same = (
+ str(latest[2]) == scenario.readiness
+ and str(latest[4]) == scenario.freshness
+ and str(latest[6]) == scenario.visibility
+ and str(latest[7]) == scenario.lifecycle_motion
+ and str(latest[8]) == status
+ and int(latest[9]) == pinned
+ and int(latest[10]) == active
+ )
+ if categorical_same and age < SCENARIO_SAMPLE_MIN_INTERVAL_SECONDS:
+ return
+ if categorical_same and numeric_delta < 0.002 and age < 300:
+ return
+ await db.execute(
+ """
+ INSERT INTO scenario_samples (
+ ts, scenario_id, relevance, readiness, confidence, freshness,
+ visible_priority, visibility, lifecycle_motion, status, pinned,
+ active, cycle_id, job_id, source_event_id
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)
+ """,
+ (
+ float(ts),
+ scenario.id,
+ float(scenario.relevance),
+ scenario.readiness,
+ float(scenario.confidence),
+ scenario.freshness,
+ float(scenario.visible_priority),
+ scenario.visibility,
+ scenario.lifecycle_motion,
+ status,
+ pinned,
+ active,
+ ),
+ )
+ await db.execute(
+ """
+ DELETE FROM scenario_samples
+ WHERE id IN (
+ SELECT id FROM scenario_samples
+ ORDER BY ts DESC
+ LIMIT -1 OFFSET ?
+ )
+ """,
+ (SCENARIO_SAMPLE_KEEP_ROWS,),
+ )
+
+ def _sample_from_row(self, row: aiosqlite.Row) -> ScenarioSample:
+ return ScenarioSample(
+ ts=float(row["ts"]),
+ scenario_id=str(row["scenario_id"]),
+ relevance=float(row["relevance"]),
+ readiness=str(row["readiness"]),
+ confidence=float(row["confidence"]),
+ freshness=str(row["freshness"]),
+ visible_priority=float(row["visible_priority"]),
+ visibility=str(row["visibility"]),
+ lifecycle_motion=str(row["lifecycle_motion"]),
+ status=str(row["status"]),
+ pinned=bool(row["pinned"]),
+ active=bool(row["active"]),
+ cycle_id=str(row["cycle_id"]) if row["cycle_id"] is not None else None,
+ job_id=str(row["job_id"]) if row["job_id"] is not None else None,
+ source_event_id=str(row["source_event_id"]) if row["source_event_id"] is not None else None,
+ )
async def _add_column_if_missing(self, db: aiosqlite.Connection, ddl: str) -> None:
try:
@@ -693,3 +865,17 @@ async def _add_column_if_missing(self, db: aiosqlite.Connection, ddl: str) -> No
except aiosqlite.OperationalError as exc:
if "duplicate column" not in str(exc).lower():
raise
+
+
+def _scenario_status(scenario: Scenario) -> str:
+ if scenario.last_outcome == "useful":
+ return "completed"
+ if scenario.last_outcome in {"irrelevant", "wrong"}:
+ return "rejected"
+ if scenario.visibility == "archived" or scenario.freshness == "stale":
+ return "stale"
+ if scenario.readiness == "ready":
+ return "ready"
+ if scenario.readiness == "cooling":
+ return "cooling"
+ return "prep"
diff --git a/tests/test_broker/test_answerable.py b/tests/test_broker/test_answerable.py
index 575e955..89f8b14 100644
--- a/tests/test_broker/test_answerable.py
+++ b/tests/test_broker/test_answerable.py
@@ -147,3 +147,68 @@ def test_evidence_assembly_safe_labels_transport_limited_context():
assert assembly.items_transport_limited >= 1
assert any(decision.decision == "transport_limited" for decision in assembly.decisions)
assert briefing.sections[0].items
+
+
+def test_answerable_briefing_keeps_multiple_evidence_roles_from_one_file():
+ content = "\n".join(
+ [
+ "CACHE_HIT_THRESHOLD = 0.87",
+ "RAW_REWARD_WEIGHT = 0.15",
+ *[f"# filler {index}" for index in range(35)],
+ "def compute_reward(inputs):",
+ " raw_reward = inputs.cache_tier + inputs.quality_lift",
+ " return {'reward_total': raw_reward}",
+ *[f"# more filler {index}" for index in range(35)],
+ "def apply_cache_threshold(similarity):",
+ " return similarity >= CACHE_HIT_THRESHOLD",
+ ]
+ )
+
+ briefing = build_answerable_briefing(
+ "Explain cache thresholds and the raw reward flow.",
+ [_artefact("src/vaner/learning/reward.py", content)],
+ max_tokens=260,
+ assembly_mode="safe",
+ )
+
+ text = briefing.text
+ assert "CACHE_HIT_THRESHOLD" in text
+ assert "RAW_REWARD_WEIGHT" in text
+ assert "raw_reward" in text
+ assert "apply_cache_threshold" in text
+ assert "role=constant_or_default" in text
+ assert "L" in briefing.sections[0].items[0].excerpt
+ assert briefing.metadata.evidence_assembly.items_protected >= 1
+
+
+def test_answerable_briefing_preserves_llm_exploration_json_contract_and_parser():
+ content = "\n".join(
+ [
+ 'JSON_CONTRACT_POLICY = """Return JSON with ranked_files and follow_on."""',
+ *[f"# prompt filler {index}" for index in range(30)],
+ "def parse_exploration_response(text):",
+ " payload = json.loads(text)",
+ " ranked_files = payload['ranked_files']",
+ " follow_on = payload.get('follow_on', [])",
+ " return ranked_files, follow_on",
+ *[f"# behavior filler {index}" for index in range(30)],
+ "def enqueue_follow_on_scenarios(frontier, follow_on):",
+ " for scenario in follow_on:",
+ " frontier.add(scenario)",
+ ]
+ )
+
+ briefing = build_answerable_briefing(
+ "Explain the LLM exploration JSON contract, parser behavior, and follow-on behavior.",
+ [_artefact("src/vaner/policy/internal_llm.py", content)],
+ max_tokens=280,
+ assembly_mode="safe",
+ )
+
+ text = briefing.text
+ assert "JSON_CONTRACT_POLICY" in text
+ assert "json.loads" in text
+ assert "ranked_files" in text
+ assert "follow_on" in text
+ assert "enqueue_follow_on_scenarios" in text
+ assert briefing.metadata.evidence_assembly.items_protected >= 1
diff --git a/tests/test_broker/test_compressor.py b/tests/test_broker/test_compressor.py
index 53ed591..85a6576 100644
--- a/tests/test_broker/test_compressor.py
+++ b/tests/test_broker/test_compressor.py
@@ -32,10 +32,24 @@ def test_compressor_empty_input():
def test_compressor_single_chunk_over_budget():
artefacts = [_artefact("a", "a.py", "word " * 400)]
context, token_map, used, kept = compress_context(artefacts, max_tokens=10)
- assert context == ""
- assert token_map["a"] > 10
- assert used == 0
- assert kept == set()
+ assert "a.py" in context
+ assert "[trimmed]" in context
+ assert token_map["a"] <= 10
+ assert used == token_map["a"]
+ assert kept == {"a"}
+
+
+def test_compressor_trims_top_scored_chunk_instead_of_dropping_it():
+ artefacts = [_artefact("low", "low.py", "tiny"), _artefact("high", "high.py", "alpha " * 500)]
+ context, token_map, used, kept = compress_context(
+ artefacts,
+ max_tokens=24,
+ score_by_key={"high": 10.0, "low": 1.0},
+ )
+ assert "high.py" in context
+ assert context.index("high.py") < context.find("low.py") if "low.py" in context else True
+ assert "high" in kept
+ assert used == sum(token_map[key] for key in kept)
def test_compressor_counts_tokens_once():
@@ -56,3 +70,119 @@ def test_compressor_prefers_high_score_when_order_unsorted():
)
assert kept == {"high", "low"}
assert context.index("high.py") < context.index("low.py")
+
+
+def test_compressor_compacts_large_chunks_to_implementation_anchors():
+ content = "\n".join(
+ [
+ "intro " * 300,
+ "Schema: CREATE TABLE artefacts (key TEXT PRIMARY KEY, source_path TEXT NOT NULL)",
+ "Functions: list_by_keys(keys: list[str]) -> list[Artefact]",
+ "filler " * 300,
+ ]
+ )
+ artefacts = [_artefact("store", "src/vaner/store/artefacts.py", content)]
+ context, _, used, kept = compress_context(artefacts, max_tokens=180)
+
+ assert kept == {"store"}
+ assert used <= 180
+ assert "CREATE TABLE artefacts" in context
+ assert "list_by_keys" in context
+ assert "compacted to important implementation anchors" in context
+
+
+def test_query_aware_compressor_preserves_dispersed_reward_ingredients():
+ content = "\n".join(
+ [
+ "from dataclasses import dataclass",
+ "DEFAULT_REWARD_WEIGHTS = {'cache_tier': 0.25, 'quality_lift': 0.35, 'raw_reward': 0.10}",
+ *[f"# filler {index}" for index in range(40)],
+ "def compute_reward(inputs):",
+ " raw_reward = inputs.cache_tier + inputs.quality_lift",
+ " return {'reward_total': raw_reward, 'reward_components': DEFAULT_REWARD_WEIGHTS}",
+ *[f"# gap {index}" for index in range(40)],
+ "def update_replay_priority(outcome):",
+ " downstream_consumer = outcome.reward_total",
+ " return 1.0 + downstream_consumer",
+ ]
+ )
+ artefacts = [_artefact("reward", "src/vaner/learning/reward.py", content)]
+
+ context, token_map, used, kept = compress_context(
+ artefacts,
+ max_tokens=170,
+ score_by_key={"reward": 30.0},
+ query="How does reward computation use default weights and downstream consumers?",
+ )
+
+ assert kept == {"reward"}
+ assert used <= 170
+ assert token_map["reward"] == used
+ assert "DEFAULT_REWARD_WEIGHTS" in context
+ assert "reward_total" in context
+ assert "downstream_consumer" in context
+ assert "@@ lines" in context
+
+
+def test_query_aware_compressor_keeps_non_redundant_bridging_file():
+ direct = _artefact(
+ "direct",
+ "src/vaner/intent/scorer.py",
+ "\n".join(
+ [
+ "def feature_vector_for_artefact(item):",
+ " return [item.path_score, item.content_score]",
+ *["# scorer filler" for _ in range(80)],
+ ]
+ ),
+ )
+ bridge = _artefact(
+ "bridge",
+ "src/vaner/intent/trainer.py",
+ "def train_target_from_reward(payload):\n"
+ " target = (payload['reward_total'] + 1.0) * 0.5\n"
+ " return target\n",
+ )
+
+ context, _, used, kept = compress_context(
+ [direct, bridge],
+ max_tokens=150,
+ score_by_key={"direct": 50.0, "bridge": 5.0},
+ query="How does the IntentScorer use GBDT feature groups, training target, and blending logic?",
+ )
+
+ assert used <= 150
+ assert {"direct", "bridge"} <= kept
+ assert "feature_vector_for_artefact" in context
+ assert "train_target_from_reward" in context
+ assert "reward_total" in context
+
+
+def test_query_aware_compressor_preserves_schema_and_methods_under_budget():
+ content = "\n".join(
+ [
+ "class ArtefactStore:",
+ " SCHEMA = '''CREATE TABLE context_packages (id TEXT PRIMARY KEY, injected_context TEXT)'''",
+ *[" # storage filler" for _ in range(50)],
+ " def persist_context_package(self, package):",
+ " self.conn.execute('INSERT INTO context_packages VALUES (?, ?)', (package.id, package.injected_context))",
+ *[" # retrieval filler" for _ in range(50)],
+ " def get_context_package(self, package_id):",
+ " return self.conn.execute('SELECT id, injected_context FROM context_packages WHERE id=?', (package_id,)).fetchone()",
+ ]
+ )
+ artefacts = [_artefact("store", "src/vaner/store/artefacts.py", content)]
+
+ context, _, used, kept = compress_context(
+ artefacts,
+ max_tokens=190,
+ score_by_key={"store": 40.0},
+ query="How does the ArtefactStore persist and retrieve context packages? What database schema does it use?",
+ )
+
+ assert kept == {"store"}
+ assert used <= 190
+ assert "CREATE TABLE context_packages" in context
+ assert "persist_context_package" in context
+ assert "get_context_package" in context
+ assert "SELECT id, injected_context" in context
diff --git a/tests/test_broker/test_selector.py b/tests/test_broker/test_selector.py
index d0c01ac..172f994 100644
--- a/tests/test_broker/test_selector.py
+++ b/tests/test_broker/test_selector.py
@@ -4,6 +4,7 @@
import time
+from vaner.broker.context_preparation import infer_context_preparation_profile
from vaner.broker.selector import select_artefacts
from vaner.models.artefact import Artefact, ArtefactKind
@@ -379,6 +380,210 @@ def test_select_artefacts_prioritizes_llm_exploration_flow_files():
]
+def test_select_artefacts_resolves_rollout_rehearsal_paraphrase():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/runtime/release_notes.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/runtime/release_notes.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="Release notes formatter for changelog entries and deployment announcements.",
+ ),
+ Artefact(
+ key="file_summary:src/runtime/traffic_escrow.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/runtime/traffic_escrow.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content=(
+ "TrafficEscrow controller coordinates rehearse_proxy replay runs, "
+ "smoke policy checks, and staged promote gates."
+ ),
+ ),
+ ]
+
+ selected = select_artefacts(
+ "What prevents a candidate release from getting full traffic until replay and smoke checks pass?",
+ artefacts,
+ top_n=1,
+ )
+
+ assert selected[0].source_path == "src/runtime/traffic_escrow.py"
+
+
+def test_select_artefacts_resolves_vectorization_region_paraphrase():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/incidents/general_latency.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/incidents/general_latency.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="Generic latency incident notes for customer-facing status updates.",
+ ),
+ Artefact(
+ key="file_summary:src/incidents/eu_apac_embedding_egress.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/incidents/eu_apac_embedding_egress.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content=(
+ "Embedding batch incident: eu-west residency stamp lag caused "
+ "ap-southeast edge fallback and cross-region egress."
+ ),
+ ),
+ ]
+
+ selected = select_artefacts(
+ "Why did a Western Europe tenant get routed to a Southeast Asia edge during a vectorization spike?",
+ artefacts,
+ top_n=1,
+ )
+
+ assert selected[0].source_path == "src/incidents/eu_apac_embedding_egress.py"
+
+
+def test_select_artefacts_resolves_low_precision_numeric_mode_paraphrase():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/runtime/model_limits.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/runtime/model_limits.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="Model admission limits and request queue accounting.",
+ ),
+ Artefact(
+ key="file_summary:src/runtime/precision_annealing.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/runtime/precision_annealing.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="kernel_stability_threshold precision annealing pass rate checks before stepping from fp32 to int8.",
+ ),
+ ]
+
+ selected = select_artefacts(
+ "What default pass rate is required before stepping down from the safest numeric mode in low bit inference?",
+ artefacts,
+ top_n=1,
+ )
+
+ assert selected[0].source_path == "src/runtime/precision_annealing.py"
+
+
+def test_select_artefacts_prefers_reward_computation_source_over_generic_policy():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/vaner/intent/scoring_policy.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/intent/scoring_policy.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="score weights priority scenario policy multiplicative nudges",
+ ),
+ Artefact(
+ key="file_summary:src/vaner/learning/reward.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/learning/reward.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="compute_reward RewardInput cache_tier similarity quality_lift host_outcome judge_score reward_total reward_components",
+ ),
+ ]
+
+ selected = select_artefacts(
+ "How does the reward computation work? What signals does it combine to produce the final reward value?",
+ artefacts,
+ top_n=1,
+ )
+
+ assert selected[0].source_path == "src/vaner/learning/reward.py"
+
+
+def test_select_artefacts_keeps_intent_scorer_and_features_together():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/vaner/intent/scorer.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/intent/scorer.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="class IntentScorer HistGradientBoosting model score predict feature vector",
+ ),
+ Artefact(
+ key="file_summary:src/vaner/intent/features.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/intent/features.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content=(
+ "extract_hybrid_features feature_vector_for_artefact active signal flags replay priority "
+ "reward target component weights"
+ ),
+ ),
+ Artefact(
+ key="file_summary:src/vaner/router/proxy.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/router/proxy.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="proxy routing request response",
+ ),
+ ]
+
+ selected = select_artefacts(
+ "How does the IntentScorer use GBDT models? What features does it extract and how are they combined?",
+ artefacts,
+ top_n=2,
+ )
+
+ assert {item.source_path for item in selected} == {"src/vaner/intent/scorer.py", "src/vaner/intent/features.py"}
+
+
+def test_select_artefacts_prefers_artefact_store_schema_over_package_metadata():
+ artefacts = [
+ Artefact(
+ key="file_summary:src/vaner/store/artefacts.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="src/vaner/store/artefacts.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="class ArtefactStore CREATE TABLE artefacts SELECT key INSERT INTO artefacts context packages persist retrieve schema",
+ ),
+ Artefact(
+ key="file_summary:ui/cockpit/package.json",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="ui/cockpit/package.json",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="package scripts dependencies",
+ ),
+ ]
+
+ selected = select_artefacts(
+ "How does the ArtefactStore persist and retrieve context packages? What database schema does it use?",
+ artefacts,
+ top_n=1,
+ )
+
+ assert selected[0].source_path == "src/vaner/store/artefacts.py"
+
+
def test_select_artefacts_custom_scorer_changes_ranking():
artefacts = [
Artefact(
@@ -535,3 +740,53 @@ def custom_scorer(_: str, artefact: Artefact) -> float:
selected = select_artefacts("anything", artefacts, top_n=2, scorer=custom_scorer)
assert [a.key for a in selected] == ["file_summary:top.py"]
+
+
+def test_context_profile_infers_multi_source_synthesis_without_benchmark_labels():
+ profile = infer_context_preparation_profile(
+ "List every customer escalation across Slack and Jira after March 2026 and summarize the common risk."
+ )
+
+ assert profile.need == "multi_source_synthesis"
+ assert profile.archetype in {"general", "operator"}
+ assert "slack" in profile.source_hints
+ assert "jira" in profile.source_hints
+ assert any(constraint.kind == "restrictive_language" and constraint.value == "after" for constraint in profile.constraints)
+
+
+def test_select_artefacts_disables_global_gate_for_multi_source_context_need():
+ artefacts = [
+ Artefact(
+ key="file_summary:top.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="top.py",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="top result",
+ metadata={"corpus_id": "repo"},
+ ),
+ Artefact(
+ key="file_summary:low_notes.md",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="low_notes.md",
+ source_mtime=time.time(),
+ generated_at=time.time(),
+ model="test",
+ content="low result",
+ metadata={"corpus_id": "notes"},
+ ),
+ ]
+
+ def custom_scorer(_: str, artefact: Artefact) -> float:
+ return 10.0 if artefact.key.endswith("top.py") else 2.0
+
+ selected = select_artefacts(
+ "summarize all related evidence",
+ artefacts,
+ top_n=2,
+ scorer=custom_scorer,
+ context_need="multi_source_synthesis",
+ )
+
+ assert [a.key for a in selected] == ["file_summary:top.py", "file_summary:low_notes.md"]
diff --git a/tests/test_cli/test_config.py b/tests/test_cli/test_config.py
index d7ea552..c2aa744 100644
--- a/tests/test_cli/test_config.py
+++ b/tests/test_cli/test_config.py
@@ -274,7 +274,7 @@ def test_load_config_warns_and_defaults_invalid_semantic_sections(
assert config.policy.bundle_overrides == {}
assert config.integrations.guidance_variant == "canonical"
assert config.max_age_seconds == 3600
- assert config.max_context_tokens == 4096
+ assert config.max_context_tokens == 8192
assert "Ignoring invalid Vaner config section [setup]" in caplog.text
assert "Ignoring invalid Vaner config section [policy]" in caplog.text
assert "Ignoring invalid Vaner config section [integrations]" in caplog.text
diff --git a/tests/test_clients/test_structured_output.py b/tests/test_clients/test_structured_output.py
index cfc91ec..bc541d2 100644
--- a/tests/test_clients/test_structured_output.py
+++ b/tests/test_clients/test_structured_output.py
@@ -141,6 +141,31 @@ def _handler(req: httpx.Request) -> httpx.Response:
assert captured["body"]["options"]["num_predict"] == 128
+@pytest.mark.asyncio
+async def test_ollama_extra_options_are_merged_with_num_predict(monkeypatch):
+ captured: dict[str, dict] = {}
+
+ def _handler(req: httpx.Request) -> httpx.Response:
+ captured["body"] = json.loads(req.content or b"{}")
+ return httpx.Response(200, json={"response": "{}"})
+
+ monkeypatch.setattr(httpx, "AsyncClient", _stub_async_client(_handler))
+
+ call = ollama_llm_structured(
+ model="qwen3.6:27b",
+ base_url="http://localhost:11434",
+ max_tokens=256,
+ extra_body={"options": {"num_ctx": 131072, "temperature": 0.7, "top_k": 20}},
+ )
+ await call("hi")
+ assert captured["body"]["options"] == {
+ "num_ctx": 131072,
+ "temperature": 0.7,
+ "top_k": 20,
+ "num_predict": 256,
+ }
+
+
@pytest.mark.asyncio
async def test_ollama_response_format_translates_to_format_json(monkeypatch):
captured: dict[str, dict] = {}
diff --git a/tests/test_daemon/test_generator.py b/tests/test_daemon/test_generator.py
index 189883c..e517a1d 100644
--- a/tests/test_daemon/test_generator.py
+++ b/tests/test_daemon/test_generator.py
@@ -8,7 +8,14 @@
import httpx
from vaner.daemon.engine import generator as generator_mod
-from vaner.daemon.engine.generator import _llm_summarize, agenerate_file_summary, generate_artefact, generate_diff_summary
+from vaner.daemon.engine.generator import (
+ DIFF_SUMMARY_PROMPT,
+ FILE_SUMMARY_PROMPT,
+ _llm_summarize,
+ agenerate_file_summary,
+ generate_artefact,
+ generate_diff_summary,
+)
from vaner.models.config import BackendConfig, GenerationConfig, VanerConfig
_real_async_client = httpx.AsyncClient
@@ -30,10 +37,42 @@ def test_generate_artefact_for_normal_file(temp_repo):
artefact = generate_artefact(source, temp_repo)
assert artefact.source_path == "normal.py"
assert "Functions:" in artefact.content
+ assert "f(limit: int)" in artefact.content
assert "Constants:" in artefact.content
assert "Limits:" in artefact.content
+def test_generate_artefact_preserves_sql_schema_anchors(temp_repo):
+ source = temp_repo / "store.py"
+ source.write_text(
+ """
+import aiosqlite
+
+class Store:
+ async def initialize(self) -> None:
+ await self.db.execute(\"\"\"
+ CREATE TABLE artefacts (
+ key TEXT PRIMARY KEY,
+ source_path TEXT NOT NULL,
+ metadata_json TEXT NOT NULL
+ )
+ \"\"\")
+ await self.db.execute("CREATE INDEX idx_artefacts_source_path ON artefacts(source_path)")
+
+ async def list_by_keys(self, keys: list[str]) -> list[str]:
+ return keys
+""",
+ encoding="utf-8",
+ )
+
+ artefact = generate_artefact(source, temp_repo)
+
+ assert "Schema:" in artefact.content
+ assert "CREATE TABLE artefacts" in artefact.content
+ assert "CREATE INDEX idx_artefacts_source_path" in artefact.content
+ assert "list_by_keys(self, keys: list[str]) -> list[str]" in artefact.content
+
+
def test_generate_artefact_for_empty_file(temp_repo):
source = temp_repo / "empty.py"
source.write_text("\n", encoding="utf-8")
@@ -59,6 +98,14 @@ def test_generate_diff_summary_redacts_patterns(temp_repo):
assert "REDACTED" in artefact.content
+def test_llm_summary_prompts_include_internal_evidence_policy():
+ for prompt in (FILE_SUMMARY_PROMPT, DIFF_SUMMARY_PROMPT):
+ assert "Vaner internal LLM policy" in prompt
+ assert "Evidence summary policy" in prompt
+ assert "Preserve implementation anchors" in prompt
+ assert "Do not let predictions or likely intent become factual behavior" in prompt
+
+
def test_agenerate_file_summary_uses_llm_when_enabled(temp_repo, monkeypatch):
source = temp_repo / "llm.py"
source.write_text("def x():\n return 1\n", encoding="utf-8")
diff --git a/tests/test_daemon/test_http.py b/tests/test_daemon/test_http.py
index 7c2403b..cb7cdd6 100644
--- a/tests/test_daemon/test_http.py
+++ b/tests/test_daemon/test_http.py
@@ -184,6 +184,83 @@ async def _seed() -> None:
assert history.json()["scenarios"][0]["visibility"] == "archived"
+def test_heatmap_replay_endpoint_returns_persisted_samples(temp_repo) -> None:
+ now = time.time()
+
+ async def _seed() -> None:
+ store = ScenarioStore(temp_repo / ".vaner" / "scenarios.db")
+ await store.initialize()
+ await store.upsert(
+ Scenario(
+ id="scn_heatmap",
+ kind="debug",
+ score=0.9,
+ confidence=0.8,
+ entities=["src/main.py"],
+ prepared_context="ctx",
+ freshness="fresh",
+ created_at=now,
+ last_refreshed_at=now,
+ )
+ )
+
+ asyncio.run(_seed())
+
+ config = VanerConfig(
+ repo_root=temp_repo,
+ store_path=temp_repo / ".vaner" / "store.db",
+ telemetry_path=temp_repo / ".vaner" / "telemetry.db",
+ )
+ app = create_daemon_http_app(config)
+ with TestClient(app) as client:
+ response = client.get(f"/heatmap/replay?from_ts={now - 5}&to_ts={now + 5}&limit=10")
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["metadata"]["synthetic"] is False
+ assert [item["id"] for item in payload["scenarios"]] == ["scn_heatmap"]
+ assert payload["samples"]
+ assert payload["samples"][0]["scenario_id"] == "scn_heatmap"
+
+
+def test_heatmap_replay_stream_emits_real_snapshot(temp_repo) -> None:
+ now = time.time()
+
+ async def _seed() -> None:
+ store = ScenarioStore(temp_repo / ".vaner" / "scenarios.db")
+ await store.initialize()
+ await store.upsert(
+ Scenario(
+ id="scn_heatmap_stream",
+ kind="debug",
+ score=0.9,
+ confidence=0.8,
+ entities=["src/main.py"],
+ prepared_context="ctx",
+ freshness="fresh",
+ created_at=now,
+ last_refreshed_at=now,
+ )
+ )
+
+ asyncio.run(_seed())
+
+ config = VanerConfig(
+ repo_root=temp_repo,
+ store_path=temp_repo / ".vaner" / "store.db",
+ telemetry_path=temp_repo / ".vaner" / "telemetry.db",
+ )
+ app = create_daemon_http_app(config)
+ with TestClient(app) as client:
+ with client.stream("GET", "/heatmap/replay/stream?range_seconds=60&limit=1") as response:
+ text = "".join(response.iter_text())
+
+ assert response.status_code == 200
+ assert "event: replay_snapshot" in text
+ assert '"synthetic": false' in text
+ assert "scn_heatmap_stream" in text
+
+
def test_scenario_stream_route_not_shadowed_by_id_route(temp_repo) -> None:
config = VanerConfig(
repo_root=temp_repo,
diff --git a/tests/test_engine/test_context_budget.py b/tests/test_engine/test_context_budget.py
new file mode 100644
index 0000000..10f066f
--- /dev/null
+++ b/tests/test_engine/test_context_budget.py
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+from vaner.engine import _adaptive_selection_top_n, _effective_context_budget
+from vaner.models.config import BackendConfig, VanerConfig
+
+
+def _config(tmp_path, *, max_context_tokens: int = 8192, num_ctx: int | None = None) -> VanerConfig:
+ runtime_options = {"num_ctx": num_ctx} if num_ctx is not None else {}
+ return VanerConfig(
+ repo_root=tmp_path,
+ store_path=tmp_path / ".vaner" / "store.db",
+ telemetry_path=tmp_path / ".vaner" / "telemetry.db",
+ max_context_tokens=max_context_tokens,
+ backend=BackendConfig(name="ollama", runtime_options=runtime_options),
+ )
+
+
+def test_effective_context_budget_uses_explicit_request(tmp_path):
+ assert _effective_context_budget(_config(tmp_path, num_ctx=131_072), requested=2048) == 2048
+
+
+def test_effective_context_budget_expands_for_large_local_context(tmp_path):
+ assert _effective_context_budget(_config(tmp_path, max_context_tokens=4096, num_ctx=131_072)) == 43_690
+
+
+def test_effective_context_budget_expands_for_million_token_runtime(tmp_path):
+ assert _effective_context_budget(_config(tmp_path, max_context_tokens=4096, num_ctx=1_048_576)) == 262_144
+
+
+def test_adaptive_selection_top_n_expands_for_multifacet_large_context():
+ prompt = "How does the ArtefactStore persist packages? What schema does it use and how are rows retrieved?"
+ assert _adaptive_selection_top_n(prompt, requested=8, max_context_tokens=43_690) > 8
diff --git a/tests/test_engine/test_deep_drill.py b/tests/test_engine/test_deep_drill.py
index f7e6dfc..254501e 100644
--- a/tests/test_engine/test_deep_drill.py
+++ b/tests/test_engine/test_deep_drill.py
@@ -70,6 +70,10 @@ async def _capturing_llm(prompt: str) -> str:
assert ranked == []
assert len(follow_on) == 5, f"expected 5 follow-ons, got {len(follow_on)}"
+ assert "Vaner internal LLM policy" in captured["prompt"]
+ assert "JSON contract" in captured["prompt"]
+ assert "Prediction policy" in captured["prompt"]
+ assert "Return JSON only (no markdown fences, no extra keys)" in captured["prompt"]
assert "HIGH-PRIORITY" in captured["prompt"]
assert "0-5" in captured["prompt"]
diff --git a/tests/test_engine/test_exploration_parallelism.py b/tests/test_engine/test_exploration_parallelism.py
index c8a6f8f..0ac08ac 100644
--- a/tests/test_engine/test_exploration_parallelism.py
+++ b/tests/test_engine/test_exploration_parallelism.py
@@ -24,6 +24,7 @@
_merge_llm_ranked_with_seed_paths,
)
from vaner.intent.adapter import CodeRepoAdapter
+from vaner.intent.governor import PredictionGovernor
from vaner.models.config import ComputeConfig, ExplorationConfig
# ---------------------------------------------------------------------------
@@ -114,6 +115,17 @@ def test_llm_rerank_preserves_deterministic_seed_paths() -> None:
]
+def test_llm_rerank_reserves_space_for_seed_paths_when_llm_list_is_full() -> None:
+ merged = _merge_llm_ranked_with_seed_paths(
+ [f"src/vaner/noisy_{idx}.py" for idx in range(8)],
+ ["src/vaner/store/artefacts.py", "src/vaner/learning/reward.py"],
+ )
+
+ assert len(merged) == 8
+ assert "src/vaner/store/artefacts.py" in merged
+ assert "src/vaner/learning/reward.py" in merged
+
+
def test_core_group_matching_boosts_specific_mechanism_query() -> None:
assert _core_group_matches_recent_query(
"external LLM exploration flow ranked_files file ranking follow_on follow-on scenario proposal",
@@ -249,3 +261,33 @@ async def flaky_llm(prompt: str) -> str:
# all, the flaky first call must not crash the surrounding cycle.
await engine.precompute_cycle()
assert call_count >= 0 # tautologically true — proof is in the no-raise above
+
+
+@pytest.mark.asyncio
+async def test_normal_background_refills_empty_frontier_with_continuation_agenda(temp_repo):
+ """Normal precompute should keep preparing useful adjacent work after the first frontier drains."""
+
+ extra_files = {
+ "src/example/cache.py": "CACHE_LIMIT = 3\n",
+ "src/example/worker.py": "def run_worker():\n return 'ok'\n",
+ "tests/test_cache_behavior.py": "def test_cache_limit():\n assert True\n",
+ "docs/release-notes.md": "# Release notes\n",
+ }
+ for rel_path, content in extra_files.items():
+ path = temp_repo / rel_path
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+
+ adapter = CodeRepoAdapter(temp_repo)
+ engine = VanerEngine(adapter=adapter, llm=None)
+ engine.config.compute.idle_only = False
+ engine.config.compute.max_cycle_seconds = 20
+ engine.config.compute.adaptive_cycle_budget = False
+ engine.config.exploration.llm_gate = "none"
+ await engine.prepare()
+
+ await engine.precompute_cycle(governor=PredictionGovernor(mode=PredictionGovernor.Mode.BUDGET, budget_units=20))
+
+ assert engine._cycle_policy_state["continuation_rounds_last_cycle"] > 0 # noqa: SLF001
+ explored = engine.get_explored_scenarios()
+ assert any("normal continuation" in scenario.reason for scenario in explored)
diff --git a/tests/test_engine/test_structured_evidence_seeding.py b/tests/test_engine/test_structured_evidence_seeding.py
index e35fc62..065144e 100644
--- a/tests/test_engine/test_structured_evidence_seeding.py
+++ b/tests/test_engine/test_structured_evidence_seeding.py
@@ -43,3 +43,83 @@ async def test_precompute_seeds_structured_direct_scenarios_before_broad_arc(tem
assert explored[0].source == "structured_direct"
assert "src/vaner/intent/frontier.py" in explored[0].unit_ids
assert all(".mypy_cache" not in path for scenario in explored for path in scenario.unit_ids)
+
+
+@pytest.mark.asyncio
+async def test_precompute_latest_query_exact_anchor_beats_generic_signal_paths(temp_repo) -> None:
+ (temp_repo / "src" / "vaner" / "learning").mkdir(parents=True)
+ (temp_repo / "src" / "vaner" / "signals").mkdir(parents=True)
+ (temp_repo / "src" / "vaner" / "intent").mkdir(parents=True)
+ (temp_repo / "src" / "vaner" / "learning" / "reward.py").write_text(
+ "class RewardInput:\n pass\n\n"
+ "def compute_reward(inputs):\n"
+ " return {'reward_total': 0.0, 'reward_components': {}}\n",
+ encoding="utf-8",
+ )
+ (temp_repo / "src" / "vaner" / "signals" / "schema.py").write_text(
+ "class SignalEvent:\n pass\n",
+ encoding="utf-8",
+ )
+ (temp_repo / "src" / "vaner" / "intent" / "scoring_policy.py").write_text(
+ "class ScoringPolicy:\n pass\n",
+ encoding="utf-8",
+ )
+
+ engine = VanerEngine(adapter=CodeRepoAdapter(temp_repo))
+ engine.config.compute.idle_only = False
+ engine.config.exploration.llm_gate = "none"
+ await engine.initialize()
+ query = "How does the reward computation work? What signals does it combine to produce the final reward value?"
+ engine._arc_model.observe(query)
+ await engine.store.insert_query_history(
+ session_id="s",
+ query_text=query,
+ selected_paths=[],
+ hit_precomputed=False,
+ token_used=0,
+ )
+
+ await engine.precompute_cycle()
+
+ explored = engine.get_explored_scenarios()
+ assert explored
+ assert explored[0].source == "structured_direct"
+ assert "src/vaner/learning/reward.py" in explored[0].unit_ids
+
+
+@pytest.mark.asyncio
+async def test_precompute_latest_query_exact_anchor_keeps_store_schema_source(temp_repo) -> None:
+ (temp_repo / "src" / "vaner" / "store").mkdir(parents=True)
+ (temp_repo / "src" / "vaner" / "daemon").mkdir(parents=True)
+ (temp_repo / "src" / "vaner" / "store" / "artefacts.py").write_text(
+ "class ArtefactStore:\n"
+ " def initialize(self):\n"
+ " return 'CREATE TABLE artefacts key content metadata SELECT INSERT'\n",
+ encoding="utf-8",
+ )
+ (temp_repo / "src" / "vaner" / "daemon" / "http.py").write_text(
+ "def _artefact_store():\n"
+ " return 'route factory for context package database schema'\n",
+ encoding="utf-8",
+ )
+
+ engine = VanerEngine(adapter=CodeRepoAdapter(temp_repo))
+ engine.config.compute.idle_only = False
+ engine.config.exploration.llm_gate = "none"
+ await engine.initialize()
+ query = "How does the ArtefactStore persist and retrieve context packages? What database schema does it use?"
+ engine._arc_model.observe(query)
+ await engine.store.insert_query_history(
+ session_id="s",
+ query_text=query,
+ selected_paths=[],
+ hit_precomputed=False,
+ token_used=0,
+ )
+
+ await engine.precompute_cycle()
+
+ explored = engine.get_explored_scenarios()
+ assert explored
+ assert explored[0].source == "structured_direct"
+ assert "src/vaner/store/artefacts.py" in explored[0].unit_ids
diff --git a/tests/test_intent/test_drafter.py b/tests/test_intent/test_drafter.py
index 94abea6..45db0f9 100644
--- a/tests/test_intent/test_drafter.py
+++ b/tests/test_intent/test_drafter.py
@@ -155,6 +155,10 @@ async def _llm(prompt: str) -> str:
)
assert result is not None
assert len(calls) == 2 # rewrite + draft
+ assert "Vaner internal LLM policy" in calls[0]
+ assert "Prediction policy" in calls[0]
+ assert "Draft policy" in calls[1]
+ assert "Use tentative wording when source evidence is incomplete" in calls[1]
assert result.predicted_prompt == "canonicalised prompt"
assert result.draft_answer == "draft body"
diff --git a/tests/test_intent/test_evidence_resolver.py b/tests/test_intent/test_evidence_resolver.py
index 4e1c42f..9a7523e 100644
--- a/tests/test_intent/test_evidence_resolver.py
+++ b/tests/test_intent/test_evidence_resolver.py
@@ -198,3 +198,59 @@ def test_weak_component_match_stays_evidence_ready_only() -> None:
assert not ready
assert reason == "weak_component_match"
+
+
+def test_semantic_aliases_resolve_rollout_rehearsal_terms() -> None:
+ structured = structured_from_prediction_fields(
+ label="Explain release dry-run gate",
+ description="What prevents a candidate release from getting full traffic until replay and smoke checks pass?",
+ anchor="candidate release full traffic dry run",
+ confidence=0.8,
+ )
+
+ targets = resolve_evidence_targets(
+ structured,
+ available_paths=[
+ "src/runtime/traffic_escrow.py",
+ "src/runtime/release_notes.py",
+ "docs/rollout.md",
+ ],
+ artefacts_by_key={
+ "file_summary:src/runtime/traffic_escrow.py": _Artefact(
+ "TrafficEscrow controller coordinates rehearse_proxy replay runs, smoke policy checks, and staged promote gates."
+ ),
+ "file_summary:src/runtime/release_notes.py": _Artefact("Release notes formatter for changelog entries."),
+ "file_summary:docs/rollout.md": _Artefact("Rollout overview for staged canaries."),
+ },
+ )
+
+ assert targets
+ assert targets[0].path == "src/runtime/traffic_escrow.py"
+
+
+def test_semantic_aliases_resolve_vectorization_routing_terms() -> None:
+ structured = structured_from_prediction_fields(
+ label="Explain vectorization routing issue",
+ description="Why did a Western Europe tenant get routed to a Southeast Asia edge during a vectorization load spike?",
+ anchor="western europe vectorization routed southeast asia",
+ confidence=0.8,
+ )
+
+ targets = resolve_evidence_targets(
+ structured,
+ available_paths=[
+ "src/incidents/eu_apac_embedding_egress.py",
+ "src/incidents/general_latency.py",
+ "docs/regions.md",
+ ],
+ artefacts_by_key={
+ "file_summary:src/incidents/eu_apac_embedding_egress.py": _Artefact(
+ "Embedding batch incident: eu-west residency stamp lag caused ap-southeast edge fallback and cross-region egress."
+ ),
+ "file_summary:src/incidents/general_latency.py": _Artefact("Generic latency incident notes."),
+ "file_summary:docs/regions.md": _Artefact("Region naming conventions."),
+ },
+ )
+
+ assert targets
+ assert targets[0].path == "src/incidents/eu_apac_embedding_egress.py"
diff --git a/tests/test_intent/test_symbol_index.py b/tests/test_intent/test_symbol_index.py
index 319edf1..2516d85 100644
--- a/tests/test_intent/test_symbol_index.py
+++ b/tests/test_intent/test_symbol_index.py
@@ -87,3 +87,50 @@ def test_generic_lowercase_methods_do_not_outrank_component_paths(tmp_path: Path
)
assert paths[0] == "src/vaner/intent/cache.py"
+
+
+def test_exact_path_ranking_prefers_reward_module_over_generic_signal_paths(tmp_path: Path) -> None:
+ (tmp_path / "src" / "vaner" / "learning").mkdir(parents=True)
+ (tmp_path / "src" / "vaner" / "signals").mkdir(parents=True)
+ (tmp_path / "src" / "vaner" / "learning" / "reward.py").write_text(
+ "class RewardInput:\n pass\n\n"
+ "def compute_reward(inputs):\n"
+ " return {'reward_total': 0.0, 'reward_components': {}}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "src" / "vaner" / "signals" / "schema.py").write_text(
+ "class SignalEvent:\n pass\n",
+ encoding="utf-8",
+ )
+
+ paths = rank_exact_paths(
+ tmp_path,
+ "How does the reward computation work? What signals does it combine to produce the final reward value?",
+ max_paths=3,
+ )
+
+ assert paths[0] == "src/vaner/learning/reward.py"
+
+
+def test_exact_path_ranking_prefers_public_store_definition_over_private_factory(tmp_path: Path) -> None:
+ (tmp_path / "src" / "vaner" / "store").mkdir(parents=True)
+ (tmp_path / "src" / "vaner" / "daemon").mkdir(parents=True)
+ (tmp_path / "src" / "vaner" / "store" / "artefacts.py").write_text(
+ "class ArtefactStore:\n"
+ " def persist(self):\n"
+ " return 'CREATE TABLE artefacts SELECT key INSERT INTO artefacts'\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "src" / "vaner" / "daemon" / "http.py").write_text(
+ "def _artefact_store():\n"
+ " return 'factory for context package database schema route'\n",
+ encoding="utf-8",
+ )
+
+ paths = rank_exact_paths(
+ tmp_path,
+ "How does the ArtefactStore persist and retrieve context packages? What database schema does it use?",
+ max_paths=3,
+ )
+
+ assert paths[0] == "src/vaner/store/artefacts.py"
diff --git a/tests/test_policy/test_internal_llm.py b/tests/test_policy/test_internal_llm.py
new file mode 100644
index 0000000..f129d1a
--- /dev/null
+++ b/tests/test_policy/test_internal_llm.py
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+from vaner.policy.internal_llm import (
+ CORE_POLICY,
+ DRAFT_POLICY,
+ EVIDENCE_SUMMARY_POLICY,
+ JSON_CONTRACT_POLICY,
+ PREDICTION_POLICY,
+ internal_llm_policy,
+)
+
+
+def test_core_policy_is_contract_preserving_not_persona() -> None:
+ assert "Preserve the requested output contract over style" in CORE_POLICY
+ assert "paths, symbols, constants" in CORE_POLICY
+ assert "assistant persona" not in CORE_POLICY.lower()
+
+
+def test_json_policy_bans_common_parser_breakers() -> None:
+ policy = internal_llm_policy(JSON_CONTRACT_POLICY)
+ assert "valid JSON only" in policy
+ assert "no markdown fences" in policy
+ assert "no markdown fences, preamble, commentary, or trailing prose" in policy
+ assert "not chain-of-thought" in policy
+
+
+def test_task_overlays_are_short_for_local_models() -> None:
+ for policy in [
+ internal_llm_policy(EVIDENCE_SUMMARY_POLICY),
+ internal_llm_policy(JSON_CONTRACT_POLICY, PREDICTION_POLICY),
+ internal_llm_policy(DRAFT_POLICY),
+ ]:
+ assert len(policy.split()) <= 140
diff --git a/tests/test_setup/test_catalog_refresh.py b/tests/test_setup/test_catalog_refresh.py
index 7b2ea15..5dc2479 100644
--- a/tests/test_setup/test_catalog_refresh.py
+++ b/tests/test_setup/test_catalog_refresh.py
@@ -14,6 +14,7 @@
build_registry_entry_for_family,
estimate_memory_budget,
families_from_seed,
+ fetch_ollama_library_details,
load_catalog_seed,
manifest_weights_bytes,
quantization_bytes_per_param,
@@ -37,6 +38,10 @@ def _fake_manifest(weight_bytes: int) -> dict[str, Any]:
}
+def _first_ollama_family():
+ return next(f for f in families_from_seed(_seed()) if f.runtime == "ollama")
+
+
def test_seed_loads_with_family_metadata() -> None:
seed = _seed()
assert "quantization_profiles" in seed
@@ -44,7 +49,10 @@ def test_seed_loads_with_family_metadata() -> None:
families = seed.get("families", [])
assert families, "seed must list at least one family"
for family in families:
- assert family.get("ollama_family"), f"family {family['id']} missing ollama_family"
+ if family.get("runtime", "ollama") == "ollama":
+ assert family.get("ollama_family"), f"family {family['id']} missing ollama_family"
+ else:
+ assert family.get("default_download_size_gb", 0) > 0, f"family {family['id']} missing non-Ollama sizing"
params = family["parameters"]
assert "context_window" in params
assert "temperature" in params, f"family {family['id']} missing sampling defaults"
@@ -73,6 +81,9 @@ def test_estimate_memory_budget_recommended_exceeds_min() -> None:
_, rec_short = estimate_memory_budget(7.0, 0.55, 8192)
_, rec_long = estimate_memory_budget(7.0, 0.55, 131072)
assert rec_long > rec_short
+ _, moe_rec = estimate_memory_budget(284.0, 0.55, 1048576, active_params_b=13.0, architecture="moe")
+ _, dense_rec = estimate_memory_budget(284.0, 0.55, 1048576)
+ assert moe_rec < dense_rec
def test_manifest_weights_bytes_sums_model_layers() -> None:
@@ -93,8 +104,7 @@ def test_manifest_weights_bytes_zero_when_no_model_layer() -> None:
def test_build_entry_online_uses_manifest_size() -> None:
seed = _seed()
- families = families_from_seed(seed)
- family = families[0]
+ family = _first_ollama_family()
# ~24 GB on disk → at Q4_K_M (0.55 GB/B) ≈ 43.6 B params.
fake_bytes = int(24 * 1024**3)
@@ -119,17 +129,51 @@ def test_build_entry_online_uses_manifest_size() -> None:
def test_build_entry_online_skips_when_manifest_missing() -> None:
seed = _seed()
- families = families_from_seed(seed)
entry = build_registry_entry_for_family(
seed,
- families[0],
+ _first_ollama_family(),
quant="Q4_K_M",
online=True,
manifest_fetcher=lambda _f, *, tag="latest": None,
+ library_fetcher=lambda _f, *, tag="latest": None,
)
assert entry is None
+def test_build_entry_online_can_use_ollama_library_page_when_manifest_missing() -> None:
+ seed = _seed()
+ family = _first_ollama_family()
+ entry = build_registry_entry_for_family(
+ seed,
+ family,
+ quant="Q4_K_M",
+ online=True,
+ manifest_fetcher=lambda _f, *, tag="latest": None,
+ library_fetcher=lambda _f, *, tag="latest": {"download_size_gb": 22.0, "source": "https://ollama.com/library/example"},
+ )
+ assert entry is not None
+ assert entry["id"] == f"{family.ollama_family}:{family.ollama_tag}"
+ assert entry["download_size_gb"] == 22.0
+ assert entry["params_b"] == family.default_params_b
+
+
+def test_ollama_library_details_require_run_command_and_size(monkeypatch) -> None:
+ class _Response:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_args):
+ return None
+
+ def read(self) -> bytes:
+ return b"ollama run qwen3.6:35b-a3b-coding-nvfp4 cd2692a833e6 22GB"
+
+ monkeypatch.setattr("urllib.request.urlopen", lambda _req, timeout=0: _Response())
+ details = fetch_ollama_library_details("qwen3.6", tag="35b-a3b-coding-nvfp4")
+ assert details is not None
+ assert details["download_size_gb"] == 22.0
+
+
def test_build_registry_offline_emits_one_per_family() -> None:
payload = build_registry(online=False)
seed_families = len(_seed()["families"])
@@ -203,7 +247,7 @@ def fake_fetcher(family_name: str, *, tag: str = "latest"):
return _fake_manifest(int(8 * 1024**3))
return None
- payload = build_registry(online=True, seed=seed, manifest_fetcher=fake_fetcher)
+ payload = build_registry(online=True, seed=seed, manifest_fetcher=fake_fetcher, library_fetcher=lambda _f, *, tag="latest": None)
assert len(payload["models"]) == 1
assert payload["models"][0]["family_id"] == families[0].id
skipped = payload.get("skipped", [])
diff --git a/tests/test_setup/test_hardware.py b/tests/test_setup/test_hardware.py
index 82f8a04..ca443f3 100644
--- a/tests/test_setup/test_hardware.py
+++ b/tests/test_setup/test_hardware.py
@@ -196,6 +196,14 @@ def test_models_lmstudio_parses_ids() -> None:
]
+def test_disk_free_probe_reports_decimal_gb() -> None:
+ class Usage:
+ free = 123_400_000_000
+
+ with patch.object(hw.shutil, "disk_usage", return_value=Usage()):
+ assert hw._probe_disk_free_gb() == 123
+
+
def test_thermal_probe_returns_false_off_linux() -> None:
with patch.object(hw.sys, "platform", "darwin"):
assert hw._probe_thermal() is False
@@ -224,6 +232,39 @@ def test_gpu_probe_handles_subprocess_failure() -> None:
assert vram is None
+def test_nvidia_smi_marks_dgx_spark_memory_as_unified() -> None:
+ class Result:
+ returncode = 0
+ stdout = "NVIDIA DGX Spark GB10, 131072\n"
+
+ with (
+ patch.object(hw.shutil, "which", return_value="/usr/bin/nvidia-smi"),
+ patch.object(hw.subprocess, "run", return_value=Result()),
+ ):
+ devices = hw._probe_gpu_devices_nvidia_smi()
+
+ assert devices is not None
+ assert devices[0].name == "NVIDIA DGX Spark GB10"
+ assert devices[0].memory_display_gb == 128
+ assert devices[0].memory_kind == "unified"
+
+
+def test_nvidia_smi_keeps_regular_rtx_memory_as_vram() -> None:
+ class Result:
+ returncode = 0
+ stdout = "NVIDIA GeForce RTX 5090, 32768\n"
+
+ with (
+ patch.object(hw.shutil, "which", return_value="/usr/bin/nvidia-smi"),
+ patch.object(hw.subprocess, "run", return_value=Result()),
+ ):
+ devices = hw._probe_gpu_devices_nvidia_smi()
+
+ assert devices is not None
+ assert devices[0].memory_display_gb == 32
+ assert devices[0].memory_kind == "vram"
+
+
# ---------------------------------------------------------------------------
# detect() composition
# ---------------------------------------------------------------------------
diff --git a/tests/test_setup/test_model_recommendation.py b/tests/test_setup/test_model_recommendation.py
index 6d81b93..13f6c1f 100644
--- a/tests/test_setup/test_model_recommendation.py
+++ b/tests/test_setup/test_model_recommendation.py
@@ -76,16 +76,105 @@ def test_recommendation_user_layer_hides_diagnostics() -> None:
assert "diagnostics" not in payload["user"]
+def test_system_ram_does_not_expand_default_local_model() -> None:
+ payload = recommend_local_model(
+ answers=_answers("mixed"),
+ hardware=_profile(
+ ram_gb=64,
+ memory_total_bytes=64 * 1024**3,
+ memory_display_gb=64,
+ memory_is_unified=False,
+ gpu="none",
+ gpu_vram_gb=None,
+ gpu_devices=(),
+ ),
+ )
+ assert payload["hardware"]["memory_source"] == "system"
+ assert payload["hardware"]["effective_memory_gb"] == 2.0
+ assert payload["selected"]["model_id"] == "gemma4:e2b"
+ assert payload["selected"]["capability"]["context_window"] == 32768
+
+
+def test_unknown_high_memory_nvidia_uses_cautious_gpu_class_recommendation() -> None:
+ payload = recommend_local_model(
+ answers=_answers("mixed"),
+ hardware=_profile(
+ ram_gb=128,
+ memory_total_bytes=128 * 1024**3,
+ memory_display_gb=128,
+ memory_is_unified=False,
+ gpu="nvidia",
+ gpu_vram_gb=None,
+ gpu_devices=(),
+ ),
+ )
+ assert payload["hardware"]["memory_source"] == "inferred_gpu"
+ assert payload["hardware"]["effective_memory_gb"] == 30.0
+ assert payload["selected"]["model_id"] != "gemma4:e2b"
+ assert payload["selected"]["download_size_gb"] >= 16
+
+
+def test_unknown_low_memory_nvidia_stays_conservative_without_vram_signal() -> None:
+ payload = recommend_local_model(
+ answers=_answers("mixed"),
+ hardware=_profile(
+ ram_gb=32,
+ memory_total_bytes=32 * 1024**3,
+ memory_display_gb=32,
+ memory_is_unified=False,
+ gpu="nvidia",
+ gpu_vram_gb=None,
+ gpu_devices=(),
+ ),
+ )
+ assert payload["hardware"]["memory_source"] == "system"
+ assert payload["selected"]["model_id"] == "gemma4:e2b"
+
+
+def test_dgx_spark_uses_unified_memory_and_gpt_oss_120b() -> None:
+ payload = recommend_local_model(
+ answers=_answers("coding", "research"),
+ hardware=_profile(
+ ram_gb=128,
+ memory_total_bytes=128 * 1024**3,
+ memory_display_gb=128,
+ memory_is_unified=False,
+ disk_free_gb=500,
+ gpu="nvidia",
+ gpu_vram_gb=None,
+ gpu_devices=(
+ GPUDevice(
+ name="NVIDIA DGX Spark GB10",
+ vendor="NVIDIA",
+ kind="nvidia",
+ memory_total_bytes=None,
+ memory_display_gb=None,
+ memory_kind="unknown",
+ ),
+ ),
+ ),
+ )
+ selected = payload["selected"]
+ assert payload["hardware"]["memory_source"] == "unified"
+ assert payload["hardware"]["effective_memory_gb"] == 120.0
+ assert payload["hardware"]["is_unified_memory"] is True
+ assert selected["model_id"] == "gpt-oss:120b"
+ assert selected["quantization"] == "MXFP4"
+ assert selected["params_b"] >= 100
+ assert selected["active_params_b"] == 5.1
+ assert selected["capability"]["context_window"] == 131072
+
+
def test_recommendation_prefers_installed_compatible_model() -> None:
# Pick whichever bundled model has a compatible installed match.
payload = recommend_local_model(
- hardware=_profile(detected_models=(("ollama", "qwen3.5:latest", "10GB"),)),
+ hardware=_profile(detected_models=(("ollama", "gemma4:e4b", "9GB"),)),
)
- assert payload["selected"]["model_id"] == "qwen3.5:latest"
+ assert payload["selected"]["model_id"] == "gemma4:e4b"
assert payload["user"]["needs_model_download"] is False
-def test_recommendation_prefers_qwen36_27b_on_32gb_vram_over_installed_35b() -> None:
+def test_recommendation_prefers_current_fit_on_32gb_vram_over_legacy_installed_model() -> None:
payload = recommend_local_model(
answers=_answers("coding", "research"),
hardware=_profile(
@@ -104,12 +193,112 @@ def test_recommendation_prefers_qwen36_27b_on_32gb_vram_over_installed_35b() ->
),
)
selected = payload["selected"]
- assert selected["model_id"] == "qwen3.6:27b"
- assert selected["capability"]["context_window"] == 131072
- assert selected["runtime_params"]["num_ctx"] == 131072
+ assert selected["model_id"] == "qwen3.6:35b-a3b-coding-nvfp4"
+ assert selected["capability"]["context_window"] == 65536
+ assert selected["runtime_params"]["num_ctx"] == 65536
assert payload["user"]["needs_model_download"] is True
+def test_recommendation_uses_current_ollama_moe_on_huge_apple_unified_memory() -> None:
+ payload = recommend_local_model(
+ answers=_answers("mixed"),
+ hardware=_profile(
+ os="darwin",
+ ram_gb=512,
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_is_unified=True,
+ disk_free_gb=300,
+ gpu="apple_silicon",
+ gpu_vram_gb=None,
+ gpu_devices=(
+ GPUDevice(
+ name="Apple M3 Ultra",
+ vendor="Apple",
+ kind="apple_silicon",
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_kind="unified",
+ ),
+ ),
+ detected_runtimes=("ollama",),
+ ),
+ )
+ selected = payload["selected"]
+ assert selected["model_id"] == "qwen3.6:35b-a3b-coding-mxfp8"
+ assert selected["runtime"] == "ollama"
+ assert selected["architecture"] == "moe"
+ assert selected["quantization"] == "MXFP8"
+ assert selected["capability"]["context_window"] == 262144
+ assert selected["runtime_params"]["num_ctx"] == 262144
+ assert payload["user"]["needs_runtime_install"] is False
+
+
+def test_speed_posture_avoids_oversized_stale_context_model_on_huge_apple() -> None:
+ payload = recommend_local_model(
+ answers=SetupAnswers(
+ work_styles=("mixed",),
+ priority="speed",
+ compute_posture="balanced",
+ cloud_posture="ask_first",
+ background_posture="normal",
+ ),
+ hardware=_profile(
+ os="darwin",
+ ram_gb=512,
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_is_unified=True,
+ disk_free_gb=300,
+ gpu="apple_silicon",
+ gpu_vram_gb=None,
+ gpu_devices=(
+ GPUDevice(
+ name="Apple M3 Ultra",
+ vendor="Apple",
+ kind="apple_silicon",
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_kind="unified",
+ ),
+ ),
+ detected_runtimes=("ollama",),
+ ),
+ )
+ assert payload["selected"]["model_id"] != "llama4:16x17b"
+
+
+def test_disk_space_filters_large_downloads() -> None:
+ payload = recommend_local_model(
+ answers=_answers("mixed"),
+ hardware=_profile(
+ os="darwin",
+ ram_gb=512,
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_is_unified=True,
+ disk_free_gb=25,
+ gpu="apple_silicon",
+ gpu_vram_gb=None,
+ gpu_devices=(
+ GPUDevice(
+ name="Apple M3 Ultra",
+ vendor="Apple",
+ kind="apple_silicon",
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_kind="unified",
+ ),
+ ),
+ detected_runtimes=("ollama",),
+ ),
+ )
+ assert payload["selected"]["model_id"] == "gemma4:e4b"
+ assert payload["selected"]["disk"]["status"] in {"enough", "tight"}
+ rejected = payload["diagnostics"]["rejected_models"]
+ assert any(row["model_id"] == "qwen3.6:35b-a3b-coding-nvfp4" and row["reason"] == "insufficient_disk" for row in rejected)
+
+
def test_qwen36_27b_context_keeps_headroom_on_32gb_vram() -> None:
chosen = compute_effective_context_window(
max_context_window=262144,
@@ -121,6 +310,30 @@ def test_qwen36_27b_context_keeps_headroom_on_32gb_vram() -> None:
assert chosen == 131072
+def test_heavy_qwen_context_caps_at_64k_on_32gb_vram() -> None:
+ chosen = compute_effective_context_window(
+ max_context_window=262144,
+ weights_gb=22.0,
+ effective_memory_gb=30.0,
+ work_styles=("coding", "research"),
+ runtime="ollama",
+ kv_reference_gb=1.8,
+ )
+ assert chosen == 65536
+
+
+def test_heavy_qwen_context_uses_256k_on_48gb_plus_vram() -> None:
+ chosen = compute_effective_context_window(
+ max_context_window=262144,
+ weights_gb=22.0,
+ effective_memory_gb=46.0,
+ work_styles=("coding", "research"),
+ runtime="ollama",
+ kv_reference_gb=1.8,
+ )
+ assert chosen == 262144
+
+
def test_persist_runtime_recommendation_writes_backend(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
@@ -131,10 +344,53 @@ def test_persist_runtime_recommendation_writes_backend(tmp_path: Path) -> None:
parsed = tomllib.loads((repo / ".vaner" / "config.toml").read_text(encoding="utf-8"))
assert parsed["backend"]["name"] == "ollama"
assert parsed["backend"]["model"] == payload["selected"]["model_id"]
+ assert parsed["backend"]["runtime_options"]["num_ctx"] == payload["selected"]["runtime_params"]["num_ctx"]
+ assert parsed["backend"]["sampling_options"] == payload["selected"]["sampling_params"]
assert parsed["exploration"]["model"] == payload["selected"]["model_id"]
+ assert parsed["exploration"]["runtime_options"]["num_ctx"] == payload["selected"]["runtime_params"]["num_ctx"]
+ assert parsed["exploration"]["sampling_options"] == payload["selected"]["sampling_params"]
assert "exploration_model" not in parsed["exploration"]
assert parsed["compute"]["device"] == "cuda"
- assert parsed["limits"]["max_context_tokens"] == payload["selected"]["capability"]["context_window"] // 4
+ assert parsed["limits"]["max_context_tokens"] == payload["selected"]["capability"]["context_window"] // 3
+
+
+def test_persist_huge_apple_recommendation_keeps_ollama_backend(tmp_path: Path) -> None:
+ repo = tmp_path / "repo"
+ repo.mkdir()
+ answers = _answers("mixed")
+ persist_setup_and_policy(repo, answers, "local_balanced")
+ payload = recommend_local_model(
+ answers=answers,
+ hardware=_profile(
+ os="darwin",
+ ram_gb=512,
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_is_unified=True,
+ disk_free_gb=300,
+ gpu="apple_silicon",
+ gpu_vram_gb=None,
+ gpu_devices=(
+ GPUDevice(
+ name="Apple M3 Ultra",
+ vendor="Apple",
+ kind="apple_silicon",
+ memory_total_bytes=512 * 1000**3,
+ memory_display_gb=512,
+ memory_kind="unified",
+ ),
+ ),
+ detected_runtimes=("ollama",),
+ ),
+ )
+ persist_runtime_recommendation(repo, payload)
+ parsed = tomllib.loads((repo / ".vaner" / "config.toml").read_text(encoding="utf-8"))
+ assert parsed["backend"]["name"] == "ollama"
+ assert parsed["backend"]["base_url"] == "http://127.0.0.1:11434/v1"
+ assert parsed["exploration"]["backend"] == "ollama"
+ assert parsed["exploration"]["runtime_options"]["num_ctx"] == payload["selected"]["runtime_params"]["num_ctx"]
+ assert parsed["compute"]["device"] == "mps"
+ assert parsed["limits"]["max_context_tokens"] == payload["selected"]["capability"]["context_window"] // 3
def test_models_recommended_cli(monkeypatch) -> None:
diff --git a/tests/test_store/test_artefacts.py b/tests/test_store/test_artefacts.py
index 92a5405..7c1892b 100644
--- a/tests/test_store/test_artefacts.py
+++ b/tests/test_store/test_artefacts.py
@@ -30,6 +30,43 @@ async def test_store_upsert_and_list(tmp_path):
assert rows[0].key == artefact.key
+@pytest.mark.asyncio
+async def test_store_lists_artefacts_by_keys_and_source_paths(tmp_path):
+ store = ArtefactStore(tmp_path / "store.db")
+ await store.initialize()
+ now = time.time()
+ artefacts = [
+ Artefact(
+ key="file_summary:a.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="a.py",
+ source_mtime=now,
+ generated_at=now,
+ model="test",
+ content="alpha",
+ ),
+ Artefact(
+ key="file_summary:b.py",
+ kind=ArtefactKind.FILE_SUMMARY,
+ source_path="b.py",
+ source_mtime=now,
+ generated_at=now,
+ model="test",
+ content="beta",
+ ),
+ ]
+ for artefact in artefacts:
+ await store.upsert(artefact)
+
+ by_key = await store.list_by_keys(["file_summary:b.py", "missing", "file_summary:a.py"])
+ by_path = await store.list_by_source_paths(["b.py", "a.py"])
+ paths = await store.list_source_paths()
+
+ assert [row.key for row in by_key] == ["file_summary:b.py", "file_summary:a.py"]
+ assert [row.source_path for row in by_path] == ["b.py", "a.py"]
+ assert paths == ["a.py", "b.py"]
+
+
@pytest.mark.asyncio
async def test_store_initialize_migrates_synthetic_v6_database(tmp_path):
db_path = tmp_path / "store.db"
diff --git a/tests/test_store/test_store_staleness.py b/tests/test_store/test_store_staleness.py
index f4aae2f..fb56775 100644
--- a/tests/test_store/test_store_staleness.py
+++ b/tests/test_store/test_store_staleness.py
@@ -8,6 +8,7 @@
import pytest
from vaner.models.artefact import Artefact, ArtefactKind
+from vaner.models.scenario import Scenario
from vaner.store.artefacts import ArtefactStore
from vaner.store.scenarios.sqlite import ScenarioStore
@@ -110,3 +111,52 @@ async def test_scenario_store_initialize_migrates_legacy_scenarios_table(tmp_pat
assert "memory_confidence" in columns
assert "memory_evidence_hashes_json" in columns
assert "prior_successes" in columns
+
+
+@pytest.mark.asyncio
+async def test_scenario_store_records_real_heatmap_samples(tmp_path):
+ store = ScenarioStore(tmp_path / "scenarios.db")
+ await store.initialize()
+ await store.upsert(
+ Scenario(
+ id="scn_sample",
+ kind="change",
+ score=0.8,
+ confidence=0.7,
+ entities=["src/app.py"],
+ prepared_context="ready",
+ freshness="fresh",
+ created_at=100.0,
+ last_refreshed_at=100.0,
+ )
+ )
+
+ samples = await store.list_samples(scenario_ids=["scn_sample"], start_ts=0.0, end_ts=time.time())
+
+ assert len(samples) == 1
+ assert samples[0].scenario_id == "scn_sample"
+ assert samples[0].readiness == "ready"
+ assert samples[0].status == "ready"
+
+
+@pytest.mark.asyncio
+async def test_scenario_store_samples_change_on_feedback(tmp_path):
+ store = ScenarioStore(tmp_path / "scenarios.db")
+ await store.initialize()
+ await store.upsert(
+ Scenario(
+ id="scn_feedback",
+ kind="debug",
+ score=0.6,
+ confidence=0.6,
+ prepared_context="ready",
+ freshness="recent",
+ )
+ )
+ await store.record_outcome("scn_feedback", "useful")
+
+ samples = await store.list_samples(scenario_ids=["scn_feedback"], start_ts=0.0, end_ts=time.time())
+
+ assert samples
+ assert samples[-1].status == "completed"
+ assert samples[-1].freshness == "fresh"
diff --git a/ui/cockpit/src/App.tsx b/ui/cockpit/src/App.tsx
index 3cb33c4..cd19452 100644
--- a/ui/cockpit/src/App.tsx
+++ b/ui/cockpit/src/App.tsx
@@ -27,7 +27,7 @@ import { useActiveWork } from './api/useActiveWork'
import { useEvents } from './api/useEvents'
import { useFocusRuntime } from './api/useFocusRuntime'
import { useLiveWork, type LiveWorkSelection } from './api/useLiveWork'
-import { usePipelineEvents } from './api/usePipelineEvents'
+import { usePipelineEvents, type PipelineEvent } from './api/usePipelineEvents'
import { usePreparedWork } from './api/usePreparedWork'
import { useScenarios } from './api/useScenarios'
import {
@@ -49,6 +49,7 @@ import {
type CommandItem,
} from './components/chrome'
import { EventStreamPanel } from './components/EventStreamPanel'
+import { HeatmapReplayView } from './components/HeatmapReplayView'
import { Inspector } from './components/Inspector'
import { LearningPanel } from './components/LearningPanel'
import { LiveWorkInspector } from './components/LiveWorkInspector'
@@ -65,6 +66,7 @@ import type {
ImpactSummary,
LatestInvalidationSignal,
LimitSettings,
+ LiveWorkEvent,
MCPSettings,
PredictionsByState,
PredictionSummary,
@@ -86,6 +88,7 @@ const COCKPIT_BUILD_SHA = (import.meta as unknown as { env?: { VITE_COCKPIT_SHA?
type ScenarioScope = 'live' | 'session' | 'focus' | 'history'
const PREDICTION_STATE_ORDER = ['ready', 'drafting', 'evidence_gathering', 'grounding', 'queued', 'stale']
+const LIVE_STALE_GRACE_SECONDS = 30 * 60
function predictionReadiness(prediction: PredictionSummary): string {
return String(prediction.readiness ?? prediction.run?.readiness ?? 'queued')
@@ -185,6 +188,8 @@ function scenarioFromPrediction(prediction: PredictionSummary): UIScenario {
readiness: ready ? 'ready' : readiness === 'stale' ? 'cooling' : 'warming',
visibility: ready ? 'prominent' : readiness === 'stale' ? 'cooling' : 'warming',
lifecycleMotion: ready ? 'rising' : readiness === 'stale' ? 'falling' : 'stable',
+ createdAt: prediction.created_at ?? null,
+ lastRefreshedAt: prediction.updated_at ?? prediction.created_at ?? null,
lastReinforcedAt: prediction.updated_at ?? prediction.created_at ?? null,
archivedAt: null,
visibilityReason: ready ? 'prepared prediction is ready' : 'background prediction is warming',
@@ -203,6 +208,165 @@ function isPredictionScenarioId(id: string): boolean {
return id.startsWith('prediction:')
}
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function liveWorkEventsFromPipeline(events: PipelineEvent[]): LiveWorkEvent[] {
+ const rows: LiveWorkEvent[] = []
+ for (const event of events) {
+ if (event.kind !== 'work.snapshot') {
+ continue
+ }
+ const items = Array.isArray(event.payload.items) ? event.payload.items : []
+ for (const item of items) {
+ if (!isRecord(item)) {
+ continue
+ }
+ const entityType = String(item.entity_type ?? '')
+ const entityId = String(item.entity_id ?? '')
+ const eventId = String(item.event_id ?? '')
+ if (!entityType || !entityId || !eventId) {
+ continue
+ }
+ rows.push(item as unknown as LiveWorkEvent)
+ }
+ }
+ return rows.sort((a, b) => Number(b.ts ?? 0) - Number(a.ts ?? 0))
+}
+
+function isActiveLiveWorkEvent(event: LiveWorkEvent): boolean {
+ return (
+ ['queued', 'running', 'grounding', 'evidence_gathering', 'drafting'].includes(event.status) ||
+ ['model', 'progress', 'prediction_precompute', 'queued'].includes(event.stage)
+ )
+}
+
+function scenarioCandidatesForLiveWork(event: LiveWorkEvent): string[] {
+ const candidates: string[] = []
+ if (event.entity_type === 'prediction' && event.entity_id) {
+ candidates.push(`prediction:${event.entity_id}`)
+ }
+ if (event.entity_type === 'scenario' && event.entity_id) {
+ candidates.push(event.entity_id)
+ }
+ if (event.scenario_id) {
+ candidates.push(event.scenario_id)
+ }
+ return candidates
+}
+
+function predictionScenarioIdFromLiveWork(event: LiveWorkEvent): string | null {
+ return event.entity_type === 'prediction' && event.entity_id ? `prediction:${event.entity_id}` : null
+}
+
+function activeScenarioIdFromLiveWork(events: PipelineEvent[], scenarios: UIScenario[]): string | null {
+ const scenarioIds = new Set(scenarios.map((scenario) => scenario.id))
+ const workEvents = liveWorkEventsFromPipeline(events)
+ const activeEvents = workEvents.filter(isActiveLiveWorkEvent)
+ for (const event of activeEvents.length ? activeEvents : workEvents) {
+ const candidates = event.scenario_id ? [event.scenario_id, ...scenarioCandidatesForLiveWork(event)] : scenarioCandidatesForLiveWork(event)
+ for (const candidate of candidates) {
+ if (scenarioIds.has(candidate)) {
+ return candidate
+ }
+ }
+ }
+ return null
+}
+
+function activeScenarioCandidateFromPipelineEvent(event: PipelineEvent | undefined): string | null {
+ if (!event) {
+ return null
+ }
+ if (event.scn) {
+ return event.scn
+ }
+ const activeEvent = liveWorkEventsFromPipeline([event]).find(isActiveLiveWorkEvent)
+ return activeEvent ? activeEvent.scenario_id ?? scenarioCandidatesForLiveWork(activeEvent)[0] ?? null : null
+}
+
+function parentScenarioFromLiveWorkEvent(event: LiveWorkEvent): UIScenario | null {
+ const id = predictionScenarioIdFromLiveWork(event) ?? (event.entity_type === 'scenario' ? event.entity_id : null)
+ if (!id) return null
+ const targets = Array.isArray(event.targets) ? event.targets.filter((target): target is string => typeof target === 'string' && target.length > 0) : []
+ const active = isActiveLiveWorkEvent(event)
+ const summary = event.summary || `${event.entity_type} ${event.entity_id}`
+ return {
+ id,
+ kind: event.stage === 'model' ? 'research' : 'change',
+ title: active ? `Working: ${summary}` : summary,
+ score: active ? 0.98 : 0.78,
+ relevance: active ? 0.98 : 0.76,
+ confidence: active ? 0.92 : 0.7,
+ visiblePriority: active ? 0.99 : 0.76,
+ freshness: active ? 'fresh' : 'recent',
+ readiness: active ? 'warming' : 'ready',
+ visibility: active ? 'prominent' : 'warming',
+ lifecycleMotion: active ? 'rising' : 'stable',
+ createdAt: Number(event.ts ?? Date.now() / 1000),
+ lastRefreshedAt: Number(event.ts ?? Date.now() / 1000),
+ lastReinforcedAt: Number(event.ts ?? Date.now() / 1000),
+ archivedAt: null,
+ visibilityReason: active ? 'active background work' : 'recent background work',
+ depth: 0,
+ parent: null,
+ path: targets[0] ?? event.scenario_id ?? event.entity_id,
+ skill: null,
+ decisionState: active ? 'active' : 'pending',
+ reason: summary,
+ entities: targets.slice(0, 10),
+ pinned: active,
+ }
+}
+
+function childScenarioFromLiveWorkEvent(event: LiveWorkEvent): UIScenario | null {
+ if (!event.scenario_id || event.scenario_id === event.entity_id) {
+ return null
+ }
+ const parent = predictionScenarioIdFromLiveWork(event)
+ const targets = Array.isArray(event.targets) ? event.targets.filter((target): target is string => typeof target === 'string' && target.length > 0) : []
+ const active = isActiveLiveWorkEvent(event)
+ return {
+ id: event.scenario_id,
+ kind: event.stage === 'model' ? 'research' : 'change',
+ title: targets[0] ? `Exploring ${targets[0]}` : event.summary || `Scenario ${event.scenario_id.slice(0, 8)}`,
+ score: active ? 0.94 : 0.7,
+ relevance: active ? 0.92 : 0.68,
+ confidence: active ? 0.82 : 0.65,
+ visiblePriority: active ? 0.92 : 0.68,
+ freshness: active ? 'fresh' : 'recent',
+ readiness: active ? 'warming' : 'ready',
+ visibility: active ? 'prominent' : 'warming',
+ lifecycleMotion: active ? 'rising' : 'stable',
+ createdAt: Number(event.ts ?? Date.now() / 1000),
+ lastRefreshedAt: Number(event.ts ?? Date.now() / 1000),
+ lastReinforcedAt: Number(event.ts ?? Date.now() / 1000),
+ archivedAt: null,
+ visibilityReason: active ? 'active scenario under prepared work' : 'recent scenario under prepared work',
+ depth: parent ? 1 : 0,
+ parent,
+ path: targets[0] ?? event.scenario_id,
+ skill: null,
+ decisionState: active ? 'active' : 'pending',
+ reason: event.summary,
+ entities: targets.slice(0, 10),
+ pinned: false,
+ }
+}
+
+function scenariosFromLiveWorkEvent(event: LiveWorkEvent): UIScenario[] {
+ return [parentScenarioFromLiveWorkEvent(event), childScenarioFromLiveWorkEvent(event)].filter((scenario): scenario is UIScenario => Boolean(scenario))
+}
+
+function scenarioBelongsOnActiveMap(scenario: UIScenario, nowSeconds = Date.now() / 1000): boolean {
+ if (scenario.pinned || scenario.visibility === 'prominent' || scenario.freshness !== 'stale') {
+ return true
+ }
+ const reinforcedAt = Number(scenario.lastReinforcedAt ?? 0)
+ return reinforcedAt > 0 && nowSeconds - reinforcedAt <= LIVE_STALE_GRACE_SECONDS
+}
+
function formatDecisionTime(assembledAt: number): string {
return new Date(assembledAt * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
}
@@ -258,6 +422,7 @@ function App() {
const [pinnedFacts, setPinnedFacts] = useState([])
const [selectedId, setSelectedId] = useState(null)
const [liveSelection, setLiveSelection] = useState(null)
+ const [scenarioAutoFocus, setScenarioAutoFocus] = useState(false)
const [selectedDecisionId, setSelectedDecisionId] = useState(null)
const [paletteOpen, setPaletteOpen] = useState(false)
const [drawerOpen, setDrawerOpen] = useState(false)
@@ -303,6 +468,8 @@ function App() {
readiness: 'ready',
visibility: 'prominent',
lifecycleMotion: 'rising',
+ createdAt: draft.created_at ?? null,
+ lastRefreshedAt: draft.updated_at ?? draft.created_at ?? null,
lastReinforcedAt: Date.now() / 1000,
archivedAt: null,
visibilityReason: 'active draft plan',
@@ -328,12 +495,28 @@ function App() {
[predictions],
)
+ const liveWorkScenarios = useMemo(() => {
+ const byId = new Map()
+ for (const event of liveWorkEventsFromPipeline(pipeline.events).filter(isActiveLiveWorkEvent).slice(0, 8)) {
+ for (const scenario of scenariosFromLiveWorkEvent(event)) {
+ if (!byId.has(scenario.id)) {
+ byId.set(scenario.id, scenario)
+ }
+ }
+ }
+ return [...byId.values()]
+ }, [pipeline.events])
+
const scenarios = useMemo(() => {
const withLive = (rows: UIScenario[]) => {
if (scenarioScope === 'history') {
return rows
}
- const combined = [...predictionScenarios, ...rows.filter((row) => !predictionScenarios.some((prediction) => prediction.id === row.id))]
+ const activeRows = scenarioScope === 'live' || scenarioScope === 'focus'
+ ? rows.filter((row) => scenarioBelongsOnActiveMap(row))
+ : rows
+ const seeded = [...predictionScenarios, ...liveWorkScenarios.filter((live) => !predictionScenarios.some((prediction) => prediction.id === live.id))]
+ const combined = [...seeded, ...activeRows.filter((row) => !seeded.some((seed) => seed.id === row.id))]
return livePlanScenario ? [livePlanScenario, ...combined.filter((row) => row.id !== livePlanScenario.id)] : combined
}
if (scenarioScope === 'focus' && !focusMatches) {
@@ -349,7 +532,7 @@ function App() {
const refreshedAt = scenarioMap[scenario.id]?.last_refreshed_at ?? scenarioMap[scenario.id]?.created_at
return typeof refreshedAt === 'number' && refreshedAt >= bootstrapPayload.daemon_started_at!
}))
- }, [bootstrapPayload?.daemon_started_at, focusMatches, livePlanScenario, predictionScenarios, scenarioMap, scenarioResult.scenarios, scenarioScope])
+ }, [bootstrapPayload?.daemon_started_at, focusMatches, livePlanScenario, liveWorkScenarios, predictionScenarios, scenarioMap, scenarioResult.scenarios, scenarioScope])
const preparedCards = useMemo(() => {
const freshnessRank = (label: string) => {
@@ -409,7 +592,7 @@ function App() {
}, [cockpit.accent])
useEffect(() => {
- const pulseTarget = pipeline.events[0]?.scn
+ const pulseTarget = activeScenarioCandidateFromPipelineEvent(pipeline.events[0])
if (!pulseTarget) {
return
}
@@ -433,6 +616,19 @@ function App() {
}
}, [scenarios, selectedId])
+ const scenarioAutoFocusTargetId = useMemo(
+ () => activeScenarioIdFromLiveWork(pipeline.events, scenarios),
+ [pipeline.events, scenarios],
+ )
+ const scenarioAutoFocusTarget = scenarios.find((scenario) => scenario.id === scenarioAutoFocusTargetId) ?? null
+
+ useEffect(() => {
+ if (!scenarioAutoFocus || view !== 'scenario-map' || !scenarioAutoFocusTargetId || selectedId === scenarioAutoFocusTargetId) {
+ return
+ }
+ selectScenario(scenarioAutoFocusTargetId)
+ }, [scenarioAutoFocus, scenarioAutoFocusTargetId, selectedId, view])
+
useEffect(() => {
if (!preparedCards.length) {
return
@@ -552,6 +748,13 @@ function App() {
setSelectedId(id)
if (id && isPredictionScenarioId(id)) {
setLiveSelection({ entityType: 'prediction', entityId: id.replace(/^prediction:/, '') })
+ } else if (id) {
+ const scenario = scenarios.find((item) => item.id === id)
+ if (scenario?.parent && isPredictionScenarioId(scenario.parent)) {
+ setLiveSelection({ entityType: 'prediction', entityId: scenario.parent.replace(/^prediction:/, '') })
+ return
+ }
+ setLiveSelection(null)
} else {
setLiveSelection(null)
}
@@ -711,9 +914,10 @@ function App() {
'2': 'prepared-work',
'3': 'now',
'4': 'scenario-map',
- '5': 'timeline',
- '6': 'board',
- '7': 'evidence',
+ '5': 'heatmap',
+ '6': 'timeline',
+ '7': 'board',
+ '8': 'evidence',
}
const nextView = shortcutView[event.key]
if (nextView) {
@@ -826,9 +1030,10 @@ function App() {
{ id: 'view-prepared-work', kind: 'view', label: 'View Prepared Work', hint: '2', run: () => setView('prepared-work') },
{ id: 'view-now', kind: 'view', label: 'View Now', hint: '3', run: () => setView('now') },
{ id: 'view-scenario-map', kind: 'view', label: 'View Scenario Map', hint: '4', run: () => setView('scenario-map') },
- { id: 'view-timeline', kind: 'view', label: 'View Timeline', hint: '5', run: () => setView('timeline') },
- { id: 'view-board', kind: 'view', label: 'View Board', hint: '6', run: () => setView('board') },
- { id: 'view-evidence', kind: 'view', label: 'View Evidence', hint: '7', run: () => setView('evidence') },
+ { id: 'view-heatmap', kind: 'view', label: 'View Heatmap Replay', hint: '5', run: () => setView('heatmap') },
+ { id: 'view-timeline', kind: 'view', label: 'View Timeline', hint: '6', run: () => setView('timeline') },
+ { id: 'view-board', kind: 'view', label: 'View Board', hint: '7', run: () => setView('board') },
+ { id: 'view-evidence', kind: 'view', label: 'View Evidence', hint: '8', run: () => setView('evidence') },
{
id: 'clear-events',
kind: 'action',
@@ -898,9 +1103,10 @@ function App() {
: scenarioScope === 'history'
? 'No archived suggestions yet. Cooling scenarios will move here instead of staying on the live map.'
: 'No suggestions have been created in this cockpit session yet. Switch to History to inspect older suggestions.'
+ const embeddedInspector = view === 'heatmap' && mode !== 'proxy'
return (
-