Search mode in the window¶
The demo window (demo/ui_server.py, the page at demo/ui/index.html) runs ONE agent loop
over the Limma MCP server in one of three modes, chosen in the header: answer (the
analyst's agent), search (the search product: closed sets with counts) and people
(a brief about people, a niche included: the ordinary conditions go to the search verb
and the niche is read from profiles). The mode changes only the agent's system prompt —
the same loop, the same tools, the same store.
This page says what search mode does today, from the code; the operating record of each
workspace's tested questions is demo/RUNBOOK.md.
from demo.agent_demo import MODES, SEARCH_SYSTEM, SYSTEM, _system_for
print("modes:", MODES, "| search prompt is its own:", _system_for("search") is SEARCH_SYSTEM
and SEARCH_SYSTEM is not SYSTEM)
rule_0 = SEARCH_SYSTEM.split("\n0. ", 1)[1].split("\n1. ", 1)[0]
print("0. " + " ".join(rule_0.split()))
modes: ('answer', 'search', 'people') | search prompt is its own: True
0. A brief (conditions on people or records): call limma_search FIRST with brief= the request and read the closed set from its reply instead of assembling it from limma_facts calls — `matches` (who is in the set, and `why`: each condition's value and where it is from), `matched.total` (the whole set: page with offset= `matched.next_offset` until it is null), `others` (`ruled_out` is the one "no"; the rest are not recorded or cannot be placed) and `complete`. What `incomplete_because` says was dropped, not compiled or not bound was NOT searched; use rules 1-5 only for that part, and for requests limma_search cannot express. A row's receipt is fetched with the call `more.entity` describes.
What the agent does with a request¶
- A brief goes to the search verb first. A request naming conditions on people or
records ("every expert who held a VP-level Sales role in DACH and passed a compliance
screening") is sent as
limma_search(brief=...), and the agent reads the closed set from the warrant — each match with its rows' receipts, the counts, None and Not recorded — rather than assembling it from its ownlimma_factscalls. Reading that warrant is search. - What the warrant says was not searched stays not searched by the verb. The prompt
names
gaps—dropped,uncompiled,unbound— as the parts of the request the verb did not search, and the agent falls back to its address rules (below) only for that part, and for requests the search verb cannot express. - Everything else is addressed. For a request over numeric facts the agent explores
to learn the store's names, enumerates ONE
limma_factscall per address the request implies, gives every row its receipt marker[p<page>:<char_start>-<char_end>](or "no receipt"), reads a row label withlimma_regiononly when rows at one address cannot otherwise be told apart, recomputes any value computed across the set with onelimma_verifycall, and applies a condition in the request AFTER enumerating. - The answer is a result set, not prose: a first line N results at K addresses (or N of M when a condition was applied, or 0 results); a table of every row with its receipt; a Count line per address; Computed when a value was asked for; None for an address bound and empty; Not recorded for any part of the request no address covers. No narration and no raw field names.
The output format in rule 6 of the prompt is written over addresses (exist at attribute
· period · entity); the prompt does not give a separate table layout for a limma_search
result set.
When the API has no model for the compile¶
The brief is compiled by the API, not by the window's agent. An API running without a
model (EPISTEM_DISABLE_LLM=1, or no provider key) answers the brief with 503, and the
MCP tool hands the agent the refusal as a tool error. The window's loop keeps the error flag
and gives the model the result's text (_mcp_dispatch / _mcp_result_text in
demo/agent_demo.py) — the verb's own body as JSON behind the MCP library's prefix. Here
is that text, from a real tool call over an MCP client session
(call_tool):
from demo.agent_demo import _mcp_result_text
from docs.examples.over_mcp import call_tool
from docs.examples.roster import roster
result = call_tool(client, "limma_search", {"workspace_id": roster(client).workspace_id,
"brief": "VP-level Sales leaders in DACH."})
print("is_error:", bool(result.isError))
print(_mcp_result_text(result))
is_error: True
Error executing tool limma_search: {"status": 503, "path": "/v1/public/search", "detail": {"type": "compile_unavailable", "reason": "compile requires a model; pass constraints"}}
The tool's own description tells the agent to send constraints then; the search-mode
prompt itself does not mention constraints. What the agent does next is the model's
choice, visible in the tool calls on the page.
The chips and the page¶
- In search mode the page offers the workspace's search suggestions: the
searchlist of the--hintsJSON the server was started with ({"answer": [...], "search": [...]}), or the page's built-in three when the server has none. The built-ins are the finance workspace's search-mode beats recorded indemo/RUNBOOK.md— requests over numeric addresses, not briefs over relations. The mode is remembered in the browser (localStoragekeylimma.mode). - A run is
POST /api/runwith{question, mode, compare, k}; the agent's turn is labelled Limma search.compareruns the RAG baseline beside it in the same run record. - Recording, replay and
--no-recordbehave identically in both modes (operator: data handling).
How to replay¶
Every recorded run is listed in the page's replay menu and served from
$LIMMA_DATA_DIR/runs with no network and no spend; a window started with --no-record
lists none.