Skip to content

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

  1. 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 own limma_facts calls. Reading that warrant is search.
  2. 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.
  3. Everything else is addressed. For a request over numeric facts the agent explores to learn the store's names, enumerates ONE limma_facts call per address the request implies, gives every row its receipt marker [p<page>:<char_start>-<char_end>] (or "no receipt"), reads a row label with limma_region only when rows at one address cannot otherwise be told apart, recomputes any value computed across the set with one limma_verify call, and applies a condition in the request AFTER enumerating.
  4. 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 search list of the --hints JSON 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 in demo/RUNBOOK.md — requests over numeric addresses, not briefs over relations. The mode is remembered in the browser (localStorage key limma.mode).
  • A run is POST /api/run with {question, mode, compare, k}; the agent's turn is labelled Limma search. compare runs the RAG baseline beside it in the same run record.
  • Recording, replay and --no-record behave 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.