From 60a3530444f65c2cdcd4e5b983e4aa380ed651c7 Mon Sep 17 00:00:00 2001 From: blunkjamie-dev <235017879+blunkjamie-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:00:29 -0500 Subject: [PATCH 1/4] perf(session-search): adapt discovery result hydration --- tests/tools/test_session_search.py | 89 ++++++++++++++++++++++-- tools/session_search_tool.py | 106 +++++++++++++++++++++-------- 2 files changed, 161 insertions(+), 34 deletions(-) diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index c5c64635de377..0cf05f01c76cf 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -1,12 +1,14 @@ """Tests for the single-shape session_search tool. -Three calling shapes: - 1. DISCOVERY — pass query → FTS5 + anchored window + bookends per hit +Four calling shapes: + 1. DISCOVERY — pass query → FTS5 + adaptive/full hydration 2. SCROLL — pass session_id + around_message_id → just the window - 3. BROWSE — no args → recent sessions chronologically + 3. READ — pass session_id → whole or head/tail-truncated session + 4. BROWSE — no args → recent sessions chronologically All run zero LLM calls. """ +import inspect import json import time @@ -72,6 +74,8 @@ def test_schema_params_cover_every_shape(self): assert "query" in params assert "limit" in params assert params["sort"]["enum"] == ["newest", "oldest"] + assert params["detail"]["enum"] == ["adaptive", "full"] + assert params["detail"]["default"] == "adaptive" # Scroll shape assert "session_id" in params assert "around_message_id" in params @@ -81,6 +85,10 @@ def test_schema_params_cover_every_shape(self): # Mode is inferred from which args are set — no explicit mode param assert "mode" not in params + def test_detail_parameter_is_appended_for_positional_compatibility(self): + parameters = list(inspect.signature(session_search).parameters) + assert parameters[-1] == "detail" + class TestFormatTimestamp: def test_formats_unix_and_passes_through_the_rest(self): @@ -132,17 +140,22 @@ def search_spy(*args, **kwargs): assert "context" not in requested_fields assert len(result["results"]) == 1 hit = result["results"][0] + assert hit["detail"] == "full" assert "bookend_start" in hit assert hit["messages"] assert "bookend_end" in hit - def test_discovery_result_has_bookends_and_window(self, db): + def test_full_detail_returns_bookends_and_window_for_every_hit(self, db): _seed_modpack_sessions(db) - result = json.loads(session_search(query="modpack", limit=3, db=db)) + result = json.loads(session_search( + query="modpack", limit=3, detail="full", db=db + )) assert result["success"] is True assert result["mode"] == "discover" + assert result["detail"] == "full" assert result["count"] >= 1 for hit in result["results"]: + assert hit["detail"] == "full" assert "bookend_start" in hit assert "messages" in hit assert "bookend_end" in hit @@ -151,6 +164,72 @@ def test_discovery_result_has_bookends_and_window(self, db): assert "messages_before" in hit assert "messages_after" in hit + def test_default_discovery_keeps_top_full_and_compacts_lower_hits(self, db): + _seed_modpack_sessions(db) + + result = json.loads(session_search(query="modpack", limit=3, db=db)) + + assert result["success"] is True + assert result["detail"] == "adaptive" + assert len(result["results"]) == 3 + + top, *lower = result["results"] + assert top["detail"] == "full" + assert "bookend_start" in top + assert len(top["messages"]) > 1 + assert "bookend_end" in top + + for hit in lower: + assert hit["detail"] == "compact" + assert hit["bookend_start"] == [] + assert len(hit["messages"]) == 1 + assert hit["messages"][0]["id"] == hit["match_message_id"] + assert hit["messages"][0]["anchor"] is True + assert hit["bookend_end"] == [] + + def test_adaptive_detail_preserves_ranking_and_reduces_payload(self, db): + now = int(time.time()) + for session_index in range(3): + session_id = f"payload_{session_index}" + db.create_session(session_id, source="cli") + db._conn.execute( + "UPDATE sessions SET started_at = ? WHERE id = ?", + (now - session_index, session_id), + ) + for message_index in range(8): + db.append_message( + session_id, + role="user" if message_index % 2 == 0 else "assistant", + content=f"opening {session_index}-{message_index} " + "o" * 2500, + ) + db.append_message( + session_id, + role="user", + content=f"payloadneedle anchor {session_index} " + "a" * 3500, + ) + for message_index in range(8): + db.append_message( + session_id, + role="assistant" if message_index % 2 == 0 else "user", + content=f"closing {session_index}-{message_index} " + "c" * 2500, + ) + db._conn.commit() + + adaptive_json = session_search(query="payloadneedle", limit=3, db=db) + full_json = session_search( + query="payloadneedle", limit=3, detail="full", db=db + ) + adaptive = json.loads(adaptive_json) + full = json.loads(full_json) + + assert [r["session_id"] for r in adaptive["results"]] == [ + r["session_id"] for r in full["results"] + ] + assert [r["match_message_id"] for r in adaptive["results"]] == [ + r["match_message_id"] for r in full["results"] + ] + assert len(adaptive_json.encode("utf-8")) < len(full_json.encode("utf-8")) * 0.6 + def test_current_session_filtered_out(self, db): _seed_modpack_sessions(db) diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 1c15aca5bf4db..5c1abf3a41533 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -2,23 +2,27 @@ """ Session Search Tool - Long-Term Conversation Recall -Single-shape tool with three calling modes (inferred from args, no explicit +Single-shape tool with four calling modes (inferred from args, no explicit mode parameter): - 1. DISCOVERY — pass ``query``. Runs FTS5, dedupes hits by session lineage, - returns top N sessions each with: snippet, ±5 message window around the - match, plus bookend_start (first 3 user+assistant msgs of session) and - bookend_end (last 3). Zero LLM cost. + 1. DISCOVERY — pass ``query``. Runs FTS5 and dedupes hits by session lineage. + Adaptive detail (the default) fully hydrates the top result with a ±5 + message window and bookends, while lower-ranked results keep the exact + anchor message plus metadata. Pass ``detail="full"`` to fully hydrate + every result. Zero LLM cost. 2. SCROLL — pass ``session_id`` + ``around_message_id``. Returns a window of ±window messages centered on the anchor, no FTS5, no bookends. To scroll forward / backward, re-anchor on the last / first message id of the returned window. - 3. BROWSE — no args. Returns recent sessions chronologically (titles, + 3. READ — pass ``session_id`` without an anchor. Returns the whole session, + or a bounded head/tail view for large sessions. + + 4. BROWSE — no args. Returns recent sessions chronologically (titles, previews, timestamps). -All three modes operate on the SQLite session DB via the FTS5 index and +All four modes operate on the SQLite session DB via the FTS5 index and the get_anchored_view / get_messages_around primitives in hermes_state. No LLM calls anywhere — every shape returns actual messages from the DB. @@ -680,6 +684,7 @@ def _title_match_result( "bookend_end": [_shape_message(m) for m in (view.get("bookend_end") or messages[-3:])], "messages_before": view.get("messages_before", 0), "messages_after": view.get("messages_after", max(len(messages) - 5, 0)), + "detail": "full", "_lineage_root": lineage_root, } if lineage_root and lineage_root != session_id: @@ -693,10 +698,11 @@ def _discover( role_filter: Optional[List[str]], limit: int, sort: Optional[str], + detail: str, current_session_id: str = None, link_profile: str = None, ) -> str: - """Discovery shape: FTS5 + anchored window + bookends per hit. Single call.""" + """Discovery shape: FTS5 plus adaptive or full result hydration.""" role_list = role_filter if role_filter else ["user", "assistant"] current_lineage_root = _resolve_lineage(db, current_session_id) if current_session_id else None title_result = _title_match_result(db, query, current_lineage_root) @@ -728,6 +734,7 @@ def _discover( "success": True, "mode": "discover", "query": query, + "detail": detail, "results": [], "count": 0, "message": "No matching sessions found.", @@ -801,6 +808,11 @@ def _discover( except Exception: session_meta = {} + result_detail = "full" if detail == "full" or not results else "compact" + window_messages = view.get("window") or [] + if result_detail == "compact": + window_messages = [m for m in window_messages if m.get("id") == msg_id] + entry = { "session_id": hit_sid, "when": _format_timestamp( @@ -812,19 +824,31 @@ def _discover( "matched_role": match_info.get("role"), "match_message_id": msg_id, "snippet": match_info.get("snippet") or "", - "bookend_start": [ - _shape_message(m, max_content_len=1200) - for m in (view.get("bookend_start") or []) - if not _is_compaction_summary(m.get("content", "")) - ], - "messages": [_shape_message(m, anchor_id=msg_id, max_content_len=4000) for m in (view.get("window") or [])], - "bookend_end": [ - _shape_message(m, max_content_len=1200) - for m in (view.get("bookend_end") or []) - if not _is_compaction_summary(m.get("content", "")) + "bookend_start": ( + [ + _shape_message(m, max_content_len=1200) + for m in (view.get("bookend_start") or []) + if not _is_compaction_summary(m.get("content", "")) + ] + if result_detail == "full" + else [] + ), + "messages": [ + _shape_message(m, anchor_id=msg_id, max_content_len=4000) + for m in window_messages ], + "bookend_end": ( + [ + _shape_message(m, max_content_len=1200) + for m in (view.get("bookend_end") or []) + if not _is_compaction_summary(m.get("content", "")) + ] + if result_detail == "full" + else [] + ), "messages_before": view.get("messages_before", 0), "messages_after": view.get("messages_after", 0), + "detail": result_detail, } if lineage_root and lineage_root != hit_sid: entry["parent_session_id"] = lineage_root @@ -837,6 +861,7 @@ def _discover( "success": True, "mode": "discover", "query": query, + "detail": detail, "results": results, "count": len(results), "sessions_searched": len(seen_sessions), @@ -859,10 +884,12 @@ def session_search( sort: str = None, # Cross-profile (any shape) profile: str = None, + # Discovery result shaping (appended to preserve positional compatibility) + detail: str = "adaptive", ) -> str: """Single-shape tool. Mode inferred from which args are set. - Discovery: pass ``query``. + Discovery: pass ``query``; ``detail="full"`` hydrates every result. Scroll: pass ``session_id`` + ``around_message_id``. Read: pass ``session_id`` (no anchor) — dumps the whole session. Browse: pass nothing. @@ -959,12 +986,19 @@ def session_search( if candidate in ("newest", "oldest"): sort_norm = candidate + detail_norm = ( + "full" + if isinstance(detail, str) and detail.strip().lower() == "full" + else "adaptive" + ) + return _discover( db=db, query=query.strip(), role_filter=role_list, limit=limit, sort=sort_norm, + detail=detail_norm, current_session_id=current_session_id, link_profile=profile, ) @@ -999,19 +1033,21 @@ def check_session_search_requirements() -> bool: "FOUR CALLING SHAPES\n\n" " 1) DISCOVERY — pass `query`:\n" " session_search(query=\"auth refactor\", limit=3)\n" - " Runs FTS5, dedupes hits by session lineage, returns the top N sessions. " - "Each result carries:\n" + " Runs FTS5, dedupes hits by session lineage, and returns the top N " + "sessions. Adaptive detail is the default: the top-ranked result carries " + "full context, while lower-ranked results stay compact. Pass `detail=\"full\"` " + "to fully hydrate every result. Every result carries:\n" " - session_id, title, when, source\n" " - snippet: FTS5-highlighted match excerpt\n" - " - bookend_start: first 3 user+assistant messages of the session " - "(the goal / kickoff)\n" - " - messages: ±5 messages around the FTS5 match, with the anchor message " - "flagged (the hit in context)\n" - " - bookend_end: last 3 user+assistant messages of the session " - "(the resolution / decisions)\n" + " - detail: `full` or `compact`\n" + " - bookend_start/bookend_end: the first/last 3 user+assistant messages " + "for full results; empty lists for compact results\n" + " - messages: ±5 messages around the FTS5 match for full results; only " + "the flagged anchor message for compact results\n" " - match_message_id, messages_before, messages_after\n" - " Bookends + window together let you reconstruct goal → match → resolution " - "without paying for the whole transcript.\n\n" + " The top result's bookends + window let you reconstruct goal → match → " + "resolution immediately. Scroll a compact result when another session looks " + "more promising.\n\n" " 2) SCROLL — pass `session_id` + `around_message_id`:\n" " session_search(session_id=\"...\", around_message_id=12345, window=10)\n" " Returns a window of ±`window` messages centered on the anchor. No FTS5, " @@ -1088,6 +1124,17 @@ def check_session_search_requirements() -> bool: "and browse shapes." ), }, + "detail": { + "type": "string", + "enum": ["adaptive", "full"], + "description": ( + "Discovery shape only. 'adaptive' (default) fully hydrates the " + "top-ranked result and returns only the exact anchor message for " + "lower-ranked results. 'full' returns bookends and the complete " + "anchored window for every result." + ), + "default": "adaptive", + }, "session_id": { "type": "string", "description": ( @@ -1152,6 +1199,7 @@ def check_session_search_requirements() -> bool: around_message_id=args.get("around_message_id"), window=args.get("window", 5), sort=args.get("sort"), + detail=args.get("detail", "adaptive"), profile=args.get("profile"), db=kw.get("db"), current_session_id=kw.get("current_session_id"), From 5f6de984f170ef470c7fbbd7662484bfaffc821d Mon Sep 17 00:00:00 2001 From: blunkjamie-dev <235017879+blunkjamie-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:10:15 -0500 Subject: [PATCH 2/4] fix(session-search): forward detail through agent paths --- agent/agent_runtime_helpers.py | 1 + agent/tool_executor.py | 1 + .../test_token_persistence_non_cli.py | 42 ++++++++++++++++++- website/docs/user-guide/sessions.md | 27 ++++++++---- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index e64fd9ad72309..05f29f5f16aae 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -2932,6 +2932,7 @@ def _execute(next_args: dict) -> Any: around_message_id=next_args.get("around_message_id"), window=next_args.get("window", 5), sort=next_args.get("sort"), + detail=next_args.get("detail", "adaptive"), db=session_db, current_session_id=agent.session_id, ), diff --git a/agent/tool_executor.py b/agent/tool_executor.py index 9645199ef012d..c4065b730e878 100644 --- a/agent/tool_executor.py +++ b/agent/tool_executor.py @@ -1761,6 +1761,7 @@ def _execute(next_args: dict) -> Any: around_message_id=next_args.get("around_message_id"), window=next_args.get("window", 5), sort=next_args.get("sort"), + detail=next_args.get("detail", "adaptive"), db=session_db, current_session_id=agent.session_id, ) diff --git a/tests/run_agent/test_token_persistence_non_cli.py b/tests/run_agent/test_token_persistence_non_cli.py index dd82395d237bf..7479c2af85cae 100644 --- a/tests/run_agent/test_token_persistence_non_cli.py +++ b/tests/run_agent/test_token_persistence_non_cli.py @@ -80,9 +80,49 @@ def fake_session_search(**kwargs): monkeypatch.setitem(sys.modules, "tools.session_search_tool", session_search_mod) agent = _make_agent(None, platform="acp") - result = json.loads(agent._invoke_tool("session_search", {"query": "Hermes"}, "task-id")) + result = json.loads(agent._invoke_tool( + "session_search", + {"query": "Hermes", "detail": "full"}, + "task-id", + )) assert result["success"] is True assert captured["db"] is sentinel_db assert captured["query"] == "Hermes" + assert captured["detail"] == "full" assert agent._session_db is sentinel_db + + +def test_sequential_session_search_forwards_detail(monkeypatch): + session_db = MagicMock() + captured = {} + + session_search_mod = ModuleType("tools.session_search_tool") + + def fake_session_search(**kwargs): + captured.update(kwargs) + return json.dumps({"success": True, "results": []}) + + session_search_mod.session_search = fake_session_search + monkeypatch.setitem(sys.modules, "tools.session_search_tool", session_search_mod) + + agent = _make_agent(session_db, platform="acp") + tool_call = SimpleNamespace( + id="search-1", + function=SimpleNamespace( + name="session_search", + arguments=json.dumps({"query": "Hermes", "detail": "full"}), + ), + ) + assistant_message = SimpleNamespace(tool_calls=[tool_call]) + messages = [] + + agent._execute_tool_calls_sequential( + assistant_message, + messages, + "task-id", + ) + + assert captured["db"] is session_db + assert captured["query"] == "Hermes" + assert captured["detail"] == "full" diff --git a/website/docs/user-guide/sessions.md b/website/docs/user-guide/sessions.md index 20d4ed4951940..7f094e756aeb8 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -569,9 +569,9 @@ For deeper analytics — token usage, cost estimates, tool breakdown, and activi ## Session Search Tool -The agent has a built-in `session_search` tool that performs full-text search across all past conversations using SQLite's FTS5 engine — and lets the agent scroll through any session it finds. No LLM calls, no summarization, no truncation. Every shape returns actual messages from the DB. +The agent has a built-in `session_search` tool that performs full-text search across all past conversations using SQLite's FTS5 engine — and lets the agent scroll through any session it finds. It makes no LLM calls and returns bounded views of actual messages from the DB rather than generated summaries. -### Three calling shapes +### Four calling shapes The tool infers what you want from which arguments you set. There's no `mode` parameter. @@ -581,16 +581,18 @@ The tool infers what you want from which arguments you set. There's no `mode` pa session_search(query="auth refactor", limit=3) ``` -Runs FTS5, dedupes hits by session lineage, returns the top N sessions. Each result carries: +Runs FTS5, dedupes hits by session lineage, and returns the top N sessions. Discovery uses adaptive detail by default: the highest-ranked result includes its full context window and bookends, while lower-ranked results stay compact. Pass `detail="full"` to fully hydrate every result. + +Each result carries: - `session_id`, `title`, `when`, `source` - `snippet` — FTS5-highlighted match excerpt -- `bookend_start` — first 3 user+assistant messages of the session (the goal/kickoff) -- `messages` — ±5 messages around the FTS5 match, with the anchor message flagged (the hit in context) -- `bookend_end` — last 3 user+assistant messages of the session (the resolution/decisions) +- `detail` — `full` or `compact` +- `bookend_start` / `bookend_end` — first/last 3 user+assistant messages for full results; empty lists for compact results +- `messages` — ±5 messages around the FTS5 match for full results; only the flagged anchor message for compact results - `match_message_id`, `messages_before`, `messages_after` -Bookends + window together reconstruct goal → match → resolution without paying for the whole transcript. Typical wall time: 15–50ms on a real session DB. +The top result reconstructs goal → match → resolution immediately. If another compact result looks more promising, use its session and message IDs with the scroll shape. Typical wall time is tens of milliseconds on a real session DB. **2. Scroll — pass `session_id` + `around_message_id`:** @@ -607,7 +609,15 @@ Returns a window of ±`window` messages centered on the anchor. No FTS5, no book Typical wall time: 1–2ms per scroll call. -**3. Browse — no args:** +**3. Read — pass `session_id` without an anchor:** + +```python +session_search(session_id="20260510_174648_805cc2") +``` + +Returns the whole session, or a bounded head/tail view for large sessions. This shape is also used to resolve an `@session:/` link. + +**4. Browse — no args:** ```python session_search() @@ -627,6 +637,7 @@ The keyword mode supports standard FTS5 query syntax: ### Optional parameters - `sort` — `newest` or `oldest`, on top of FTS5 ranking. Omit for relevance-only ordering (the default; suitable for exploratory recall). Use `newest` for "where did we leave X" questions, `oldest` for "how did X start" questions. +- `detail` — `adaptive` (default) fully hydrates only the top discovery result; `full` hydrates every discovery result. - `role_filter` — comma-separated roles to include. Discovery defaults to `user,assistant` (tool output is usually noise). Pass `user,assistant,tool` to include tool output (debugging tool behaviour) or `tool` to search tool output only. ### When It's Used From c9b1286be40651a3d5d2a0877a06be9bf34d7294 Mon Sep 17 00:00:00 2001 From: blunkjamie-dev <235017879+blunkjamie-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:18:51 -0500 Subject: [PATCH 3/4] docs(sessions): clarify actual-message retrieval --- website/docs/user-guide/sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/user-guide/sessions.md b/website/docs/user-guide/sessions.md index 7f094e756aeb8..6f73ba65c2feb 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -569,7 +569,7 @@ For deeper analytics — token usage, cost estimates, tool breakdown, and activi ## Session Search Tool -The agent has a built-in `session_search` tool that performs full-text search across all past conversations using SQLite's FTS5 engine — and lets the agent scroll through any session it finds. It makes no LLM calls and returns bounded views of actual messages from the DB rather than generated summaries. +The agent has a built-in `session_search` tool that performs full-text search across all past conversations using SQLite's FTS5 engine — and lets the agent scroll through any session it finds. It makes no LLM calls and returns views of actual messages from the DB rather than generating summaries. ### Four calling shapes From 73592200c69a4f0b6d7c290ce45832847df608e2 Mon Sep 17 00:00:00 2001 From: blunkjamie-dev <235017879+blunkjamie-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:29:52 -0500 Subject: [PATCH 4/4] test(session-search): lock positional parameter prefix --- tests/tools/test_session_search.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index 0cf05f01c76cf..d997787a717bd 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -87,7 +87,19 @@ def test_schema_params_cover_every_shape(self): def test_detail_parameter_is_appended_for_positional_compatibility(self): parameters = list(inspect.signature(session_search).parameters) - assert parameters[-1] == "detail" + historical_prefix = [ + "query", + "role_filter", + "limit", + "db", + "current_session_id", + "session_id", + "around_message_id", + "window", + "sort", + "profile", + ] + assert parameters == [*historical_prefix, "detail"] class TestFormatTimestamp: