diff --git a/navi/config.py b/navi/config.py index 2b08a0a..aa5df28 100644 --- a/navi/config.py +++ b/navi/config.py @@ -123,7 +123,7 @@ context_compression_target: float = 0.65 # hysteresis target — after compression the kept region should fit ~65% of the window (trigger 90% → target 65% leaves real headroom instead of skimming just below the trigger and re-firing a few messages later) context_keep_recent: int = 8 # conversational turns to keep verbatim context_summary_temperature: float = 0.3 - context_summary_max_tokens: int = 4000 # max output tokens for the summary LLM call + context_summary_max_tokens: int = 6000 # max output tokens for the summary LLM call output_reserve_tokens: int = 2048 # headroom reserved for model response in context checks # Per-message token budget for the LLM context view. A single tool/assistant # message whose estimated size exceeds this is head/tail-truncated in the diff --git a/navi/core/compressor.py b/navi/core/compressor.py index d2d990e..47d7868 100644 --- a/navi/core/compressor.py +++ b/navi/core/compressor.py @@ -30,29 +30,34 @@ _SUMMARY_SECTIONS = [ ("Goal", "One clear sentence describing what the user is trying to accomplish in this session. Include deadlines or constraints if stated."), - ("Active Files", "Every file or directory the assistant touched, with absolute or project-relative path and status: created / modified / read / deleted. For modified files, note the purpose of the change."), - ("Decisions & User Preferences", "Explicit choices, architecture decisions, style preferences, or corrections stated by the user. Things the user said NOT to do."), - ("Completed Work", "Concrete finished steps — include file/function names and verification outcome if available."), + ("Active Files", "Every file or directory the assistant touched, with absolute or project-relative path, status (created / modified / read / deleted), the purpose of the change, and the key function/class/symbol affected."), + ("Decisions & User Preferences", "Explicit choices, architecture decisions, style preferences, or corrections stated by the user — each with a one-line rationale and what triggered it. Things the user said NOT to do."), + ("Completed Work", "Concrete finished steps — step + file/function name + verification outcome (pass/fail). Be specific and complete; prefer listing over generalizing."), + ("Intermediate Findings", "Key results from read/grep/log/terminal inspection that informed decisions: the fact discovered and the file/section it came from. Drop verbose bulk, keep the diagnostic line that changed understanding."), ("Pending Work / Todo", "Open tasks, in-progress items, or follow-ups that still need action."), - ("Errors & Blockers", "Failures, exceptions, or unresolved issues. Include exact error snippets when short and diagnostic."), - ("Key Values", "Exact constants the assistant should remember: ports, config keys, versions, dependency names, important paths, IDs."), + ("Errors & Blockers", "Failures, exceptions, or unresolved issues — each with the exact short error snippet, what was tried, and the outcome. Preserve the diagnostic line verbatim."), + ("Key Values", "Exact constants the assistant should remember: ports, config keys and values, versions, dependency names, important paths, IDs, exact CLI flags."), ] _SUMMARY_TEMPLATE_INSTRUCTIONS = ( "You are summarizing a conversation history to free up context space. " "The assistant will continue working using ONLY this summary — it will have no access " - "to the original messages. Be thorough and precise. Prefer specifics over generalities. " + "to the original messages. Be thorough and precise; prefer specifics and complete lists " + "over generalities — the cost of losing a fact is higher than the cost of a longer summary. " "This summary is historical context, not a new user request.\n\n" "Use EXACTLY the Markdown structure below. Every section must be present. " "If a section has no relevant information, write its header and the literal word NONE. " - "Keep bullet points tight and information-dense. " + "Keep bullets information-dense, but do not generalize away specifics to save space. " "Do not include greetings, filler, transitions, or meta-commentary.\n\n" + "\n\n".join(f"## {title}\n{desc}" for title, desc in _SUMMARY_SECTIONS) + "\n\n" "Output rules:\n" - "- Preserve exact file paths, function names, config keys, and short error snippets verbatim.\n" + "- Preserve exact file paths, function/class/symbol names, config keys and values, " + "exact CLI flags, and short error/diagnostic snippets verbatim.\n" + "- Preserve short critical code the agent must reproduce exactly: final function/class " + "signatures, one-line fixes the user approved, key config lines. Do NOT paste long blocks, " + "patches, or full command output — keep only the short line that matters.\n" "- Do not paraphrase values that must stay precise.\n" - "- Do not write implementation code, patches, or long command output.\n" "- Use Markdown headers exactly as shown." ) @@ -268,7 +273,11 @@ if critical and len(result) <= 4000: preview = result else: - preview = result[:300] + ("…" if len(result) > 300 else "") + # Give the summarizer enough of the result to preserve + # diagnostic detail (the exact error line, a grep match, + # a key value), not just the first 300 chars — the summary + # is only as detailed as what the summarizer can see. + preview = result[:800] + ("…" if len(result) > 800 else "") lines.append(f"[Tool result: {tool_msg.name}; preview: {preview}]") i += 1 @@ -284,7 +293,8 @@ # Safety limit: truncate formatted input to this many characters before sending to LLM. # Prevents the summarizer from receiving near-context-sized input it can't fit alongside output. -_MAX_SUMMARY_INPUT_CHARS = 24_000 +# Typical ollama_num_ctx is 65k–128k, so 32k of input leaves ample room for a 6k-token summary. +_MAX_SUMMARY_INPUT_CHARS = 32_000 # Hard-truncate fallbacks keep at most this fraction of the context window's # tokens of recent messages (the rest is dropped without summarizing). diff --git a/navi/profiles/navi_code/compression_prompt.txt b/navi/profiles/navi_code/compression_prompt.txt index ab59d85..98abde1 100644 --- a/navi/profiles/navi_code/compression_prompt.txt +++ b/navi/profiles/navi_code/compression_prompt.txt @@ -1,14 +1,15 @@ -You are summarizing a local-terminal coding session. Preserve information that is essential for continuing implementation and verification. +You are summarizing a local-terminal coding session. Preserve information that is essential for continuing implementation and verification — be specific and complete, the cost of losing a fact is higher than the cost of a longer summary. Priority rules: -- Keep every file path the assistant read, created, or modified, with the action taken. -- Keep exact code signatures the user explicitly approved or that were final (function/class names, important config keys, exact command-line flags). -- Keep the outcome of the last test/build/verification run (pass/fail and the final error snippet if it failed). +- Keep every file path the assistant read, created, or modified, with the action taken and the key function/class/symbol affected. +- Keep exact code signatures the user explicitly approved or that were final — function/class names, important config keys and values, exact command-line flags, and short critical code the agent must reproduce exactly (final signatures, one-line fixes the user approved, key config lines). Not just names — the short line that matters. +- Keep the outcome of the last test/build/verification run (pass/fail and the final error snippet if it failed), and what was tried that led to it. +- Keep key intermediate findings: results from read/grep/log/terminal inspection that informed decisions — the fact discovered and where it came from. - Keep the current todo list state and any pending sub-tasks. -- Keep user corrections about style, approach, or things the user said must/not be done. +- Keep user corrections about style, approach, or things the user said must/not be done, with a one-line rationale. - Keep exact environment facts: ports, Python versions, dependency names, paths to project roots, special local quirks. Do not preserve: -- Long terminal output, full stack traces, or verbose directory listings. +- Verbose bulk: long terminal output, full stack traces, full directory listings. BUT preserve the short diagnostic line within them verbatim — the exact error line, the failing assertion, the one-line diff that fixed it. - Social greetings, filler, or commentary about the summary itself. -- Intermediate reasoning or tool-call argument previews. +- Tool-call argument previews and intermediate reasoning that did not produce a durable fact. \ No newline at end of file diff --git a/navi/profiles/navi_code/config.json b/navi/profiles/navi_code/config.json index 29ea33c..e0d54b6 100644 --- a/navi/profiles/navi_code/config.json +++ b/navi/profiles/navi_code/config.json @@ -37,7 +37,7 @@ "top_p": 0.88, "num_thread": 11, "compression_keep_recent": 12, - "compression_max_tokens": 4000, + "compression_max_tokens": 6000, "compression_prompt_file": "compression_prompt.txt", "tools": { "agent": { diff --git a/tests/clients/test_tui_app.py b/tests/clients/test_tui_app.py index 14c93bc..35a1b38 100644 --- a/tests/clients/test_tui_app.py +++ b/tests/clients/test_tui_app.py @@ -878,13 +878,19 @@ "max_context_tokens": 32000, } + async def fake_get_todos(sid): + return {"session_id": sid, "tasks": []} + monkeypatch.setattr(api_module, "get_session", fake_get_session) + monkeypatch.setattr(api_module, "get_todos", fake_get_todos) async with NaviCodeTui(new_session=True).run_test() as pilot: await pilot.pause() ctx = pilot.app.query_one("StatusBar")._ctx_fill - # _startup already ran attach_session (against the monkeypatched - # get_session), so the gauge is seeded — not blank — before any turn. + # attach_session seeds the gauge from get_session's context_token_count + # so it isn't blank before the first turn. + await pilot.app.attach_session("sess-resume") + await pilot.pause() assert ctx._used == 9000 assert ctx._max == 32000 assert "28%" in str(ctx.render()) diff --git a/tests/unit/core/test_compressor.py b/tests/unit/core/test_compressor.py index d52df46..d238b0f 100644 --- a/tests/unit/core/test_compressor.py +++ b/tests/unit/core/test_compressor.py @@ -254,9 +254,10 @@ pass runs first (consolidating them) before the main compression.""" # First response = meta-summary, second = main compression backend = FakeLLMBackend(responses=["Meta summary", "Final summary"]) - # Build a context with two existing summaries (each > 4000 chars to cross threshold) - big_summary_1 = "A" * 5000 - big_summary_2 = "B" * 5000 + # Build a context with two existing summaries large enough to cross the + # meta-summary threshold (_MAX_SUMMARY_INPUT_CHARS // 3 ≈ 10_666). + big_summary_1 = "A" * 6000 + big_summary_2 = "B" * 6000 context = [ Message(role="system", content="sys"), Message(role="user", content=big_summary_1, is_summary=True, is_display=False), @@ -599,6 +600,33 @@ assert "## Goal" in prompt assert "## Active Files" in prompt + def test_summary_prompt_includes_intermediate_findings_section(self): + """The base summary prompt has an Intermediate Findings section for all + profiles — durable facts from read/grep/log that informed decisions, + not just final decisions.""" + prompt = _build_summary_system_prompt(None) + assert "## Intermediate Findings" in prompt + + def test_summary_prompt_asks_for_short_critical_code(self): + """The base prompt preserves short critical code (final signatures, + one-line fixes) verbatim, not 'do not write implementation code'.""" + prompt = _build_summary_system_prompt(None) + assert "short critical code" in prompt.lower() + + def test_format_preserves_more_of_noncritical_tool_result(self): + """Non-critical tool results are previewed to 800 chars (was 300) so the + summarizer sees diagnostic detail, not just the first 300 chars.""" + result = "y" * 1000 + msgs = [ + Message(role="assistant", tool_calls=[ToolCallRequest(id="1", name="web_search", arguments={})]), + Message(role="tool", content=result, name="web_search", tool_call_id="1"), + ] + text, _ = _format_for_summary(msgs) + # First 800 chars survive; full 1000 does not. + assert "y" * 800 in text + assert "y" * 1000 not in text + assert "…" in text + async def test_profile_overrides_compression_max_tokens(self): profile = make_profile("test", compression_max_tokens=1234) backend = FakeLLMBackend(responses=["short"])