ielab/skim-search-agent

A composable library for building and evaluating deep-research agents over your own corpus.

Python

14

79 commits

updated Oct 9, 2026

See the code

README

SkimSearchAgent

A research framework for deep-search agents. Change one component, keep the rest of the experiment fixed.

CI Python 3.10+ License: Apache-2.0

Project page · Leaderboard · Paper · The library · Install · Run an experiment · Change one thing · Python API · Strategies · Train a retriever · Docs

Corpora, models, strategies, retrievers and evaluators around one agent loop

A deep-search agent answers a question by searching a collection over several steps. Which retriever it uses, what a search result shows, how it reads a document, which agent backbone drives it, and how the answer is scored are separate decisions. SkimSearchAgent makes each one a component with a fixed interface and runs every combination through the same evaluation, so two experiments differ only where you changed them. Every run writes the same record: the full trajectory, the configuration, and the metrics.

The library

One package, agent_search, one folder per kind of component. Each folder is a base class plus one file per concrete class, so a new method is one new file. Read them in the order a query travels:

modulewhat it holdsthe files
corpus/documents and functions as units; an on-disk store for corpora too large for memory; fingerprints so a persisted index is never served against a corpus it was not built forunits.py, docstore.py, fingerprint.py, code_repo.py
retrievers/engines that rank ids for a query: Lucene BM25, dense encoders (one file per family), the Boolean method BQL, Indri; and two compositions, a hybrid (any retrievers fused by one method) and a reranked retriever (one retriever's pool reordered by a reranker)lexical/, dense/, bql/, indri/, lucene/, fusion/, hybrid.py, rerankers/, reranked.py, engines.py, backend.py
snippets/how one hit is excerpted in a listing: the opening line, the best window for the query terms, or nothing; widths in model tokensopening.py, term_window.py, none.py
tools/the actions the agent backbone can call, one folder each with its declaration, code and manual; a tool names its engine kind and its snippetsearch_bm25/, search_dense/, search_hybrid/, search_reranked/, search_bql/, search_indri/, search_dedup/, search_bm25_dci/, visit/, fetch/, fetch_code/, get_document/, bash/, read/, grep/
tasks/what the backbone is asked to produce: the prompt template and the answer protocolresearch/, research_dedup/, codefix/, codefix_patch/
strategies/the named combinations a run selects: tools with their options and a harness, or a retrieval-only floor; conditions.py pairs a strategy with a tasksearch_visit.py, search_fetch.py, autoread.py, sieve.py, indri.py, dci.py, dedup.py, codefix.py, rag.py, teams.py, retrieval_only.py
harness/how the backbone is put to work on a condition, one file each: ReAct (the default loop, the backbone picks each step), one-shot RAG (rank once, one call), plan-and-search (a team: a planner, one member agent per sub-question, a synthesizer)react.py, rag.py, plan_and_search.py
agent/the machinery a harness is built from: the backbone providers, the policies, the step loop, the forced answer, the Agents-SDK and Responses drivers, the run recordbackbone/, policies.py, loop.py, forced_answer.py, sdk_driver.py, responses_driver.py, record.py
evaluation/the evaluation: datasets, the runner, the metrics, the judge, the run record and its identity, the index prebuilddatasets/, runner.py, run_eval.py, llm_judge.py, identity.py, build_indexes.py
training/the ITER recipe: trajectories to triples, retriever training and evaluationbuild_triples.py, retriever.py, retriever_eval.py

Document corpora rank on Lucene (BM25 through Pyserini, BQL and Indri on a fielded Lucene index); a code repository is indexed in memory and re-indexed only for the files that change. Every length limit is a token count. docs/ARCHITECTURE.md explains each module; docs/EXTENDING.md shows how to add one of each.

What ships with the library

Each name below is a file in the library and a value in an experiment file. A row on the leaderboard is one experiment file under configs/baselines/, and docs/BASELINES.md has the commands.

partsupported
Lexical retrieversBM25 on Lucene (Pyserini), the Boolean method BQL, Indri query language, grep
Dense retrieversBGE (small, base, large), Qwen3-Embedding (0.6B, 4B, 8B), RepLLaMA, LRAT (Qwen3 and multilingual-e5), Agentic-R, AgentIR, the ITER checkpoints, CodeRankEmbed for code, and any checkpoint trained with skimsearchagent-train-retriever
Learned sparse and multi-vector retrieversSPLADE++, ColBERTv2, Agent-ModernColBERT, DiffRetriever (dense and sparse)
Compositionshybrid fusion of two rankings (rrf, interpolation), a reranker over a retriever's pool
Rerankerscross_encoder (bge-reranker-v2-m3), qwen3_reranker (Qwen3-Reranker), monot5 (monoT5), laya (the Laya decision models)
Agent backbonesany model behind an OpenAI-compatible server (vLLM), vLLM in-process, the OpenAI chat and reasoning models, Gemini. Experiment files ship for Tongyi-DeepResearch-30B, Qwen3.5 (4B, 9B, 27B), Qwen3.6-27B, Qwen3.8-27B, gpt-oss (20b, 120b), Qwen-AgentWorld-35B, OpenResearcher-30B, OpenSeeker-v2-30B, MiroThinker-1.7-mini and QUEST-35B. Those last four run in their own interface: the tools, prompt and history format of each agent's released code, with a 100-turn budget
Tool-call formatsthe JSON <tool_call> format by default; glm, mcp, lenient_json and qwen_xml for backbones that write calls their own way (model.tool_call_format)
Judgesthe BrowseComp-Plus judge prompt with gpt-4o-mini or a served model (Qwen3-32B), DIVER's judge template (Qwen3-30B-A3B-Thinking-2507), exact match and F1
DatasetsBrowseComp-Plus, HotpotQA, MuSiQue, 2WikiMultihopQA, InfoSeek, SWE-bench Lite and Verified, LocBench, and your own corpus as a BEIR-style directory

docs/EXTENDING.md shows how to add one of each.

Install

git clone https://github.com/ielab/skim-search-agent.git && cd skim-search-agent
python -m pip install -e .                 # core: PyYAML only
python -m pip install -e ".[api]"          # OpenAI, Gemini, OpenAI-compatible servers
python -m pip install -e ".[retrieval]"    # Lucene (BM25, BQL, Indri), dense and hybrid retrieval (Java 21+, torch)

Other extras: eval (dataset staging, statistics), serve (vLLM), demo-live, dev (tests), train (retriever training, separate environment), all. Pins match requirements.txt, the environment used for the paper.

Every document run needs the retrieval extra: document corpora rank on Lucene only (BM25 through Pyserini, and the structured index behind BQL and Indri). The core install runs code repositories, which are indexed in memory. Point JAVA_HOME at a JDK 21 or newer before running anything that touches Lucene, the test suite included. Pyserini starts a JVM on import, and an older Java aborts the process without a message.

Run an experiment

One YAML file is one complete setting. The file lists every knob its strategy reads: dataset, agent backbone, token budgets, listing depths, retrieval engines, scoring, output. A search_visit file has no dense-model keys; a sieve file has no listing depths.

skimsearchagent run configs/fixtures/smoke_doc_fixture_sieve_bm25.yaml        # scripted policy, no keys
export OPENAI_API_KEY=...
skimsearchagent run configs/fixtures/doc_fixture_sieve_bm25_gpt4omini.yaml    # a real backbone
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml     # the paper's setting, scripted policy (no backbone named)
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml model.name=gpt-4o output.runs_dir=runs/gpt4o   # the same with a backbone
skimsearchagent validate configs/sieve/hotpotqa_structured_sieve.yaml   # what it needs, what it will run
skimsearchagent template paper sieve > configs/ablation/mine.yaml              # a complete file for one strategy, to edit

The run directory holds config.json (the file's content plus every resolved knob), rows.jsonl (one full trajectory per question) and results.json (metrics). Rerunning the same command resumes. Rerunning a different setting into the same directory is refused.

For one-off runs every knob is also a key=value argument:

skimsearchagent dataset=doc_fixture strategy=search_visit model=gpt-4o-mini snippet_tokens=64

Change one thing

Each block changes one component and leaves the rest of the experiment as it was. The blocks follow the module table above: the strategy, the agent backbone, the harness, the retriever, the snippet, the tool, the task, the corpus, the dataset.

The strategy (one word in the experiment file, or on the command line):

strategy: search_visit        # sieve, sieve_bm25, search_fetch, autoread, dci, indri, ... (see Strategies)

The agent backbone, the language model that drives the agent. The experiment file's section is called model; it takes any OpenAI-compatible server, OpenAI, Gemini, or in-process vLLM:

model:
  name: Alibaba-NLP/Tongyi-DeepResearch-30B-A3B
  backend: api                # a served backbone
  api_base: http://localhost:8000/v1

From Python the backbone is any callable that maps chat messages to text:

def my_backbone(messages: list[dict]) -> str:
    return my_client.chat(messages)          # returns the generation, tool calls included

research(question, docs, strategy="sieve_bm25", generate=my_backbone)

The harness, how the backbone is put to work. ReAct is the default; a strategy names another one, such as a team whose members are conditions:

from agent_search.harness.plan_and_search import PlanAndSearch
from agent_search.strategies.base import Strategy, register_strategy

register_strategy(Strategy(name="plan_and_search_visit_dense", description="a planner, dense search-visit agents, a synthesizer",
                           harness=PlanAndSearch(searcher="search_visit_dense")))
# run it: strategy=plan_and_search_visit_dense

The retriever, as a new engine or as a retrieval-only floor:

from agent_search.retrievers.base import Retriever
from agent_search.retrievers.registry import register

class MyRetriever(Retriever):
    name = "my_method"
    def index(self, units, key=None): self._ids = [u.doc_id for u in units]; return self
    def search(self, query, k): return self._ids[:k]

register("my_method")(lambda cfg, name: (lambda: MyRetriever()))
# run it: strategy=my_method (retrieval-only), or give it an engine kind so a tool can use it

A hybrid is any retrievers fused by one method (retrieval.hybrid_retrievers, retrieval.hybrid_fusion); a reranked retriever is one retriever's pool reordered by a reranker (retrieval.rerank_base, retrieval.rerank_model).

The dense retriever, one setting for Sieve's ranker, its fallback, and every dense baseline:

retrieval:
  dense_model: models/my-retriever     # a hub id or a checkpoint trained below
  dense_query_style: null              # how the query is written (null = the model's own: ITER's trained format for ITER)

The snippet, what a listing shows under each hit. A tool takes a method from agent_search/snippets/; widths are model tokens:

from agent_search.snippets import TermWindow
from agent_search.tools.search_bm25.tool import SearchBm25

SearchBm25(name="bm25q_search", snippet=TermWindow())      # the best window for the query terms

A tool (its declaration and its code in one class), a strategy (which tools, under which names, with which options) and a condition (a task with that strategy):

from agent_search.tools.base import Tool
from agent_search.strategies.base import Strategy, register_strategy
from agent_search.strategies.conditions import condition

class TitleLookup(Tool):
    name = "title_lookup"
    description = "Documents whose title contains the words."
    parameters = {"type": "object", "properties": {"words": {"type": "string"}}, "required": ["words"]}
    def run(self, args):                          # self.units, self.ubyid, self.state, self.engine[...]
        hits = [u for u in self.units if all(w in (u.title or "").lower() for w in args["words"].lower().split())]
        self.state.seen.update(u.doc_id for u in hits)   # first-seen order = the agent's ranking
        return "\n".join(f"{u.doc_id}  {u.title}: {u.body}" for u in hits) or "no match"

register_strategy(Strategy(name="title_only", description="look up by title", tools=(TitleLookup(name="title_lookup"),)))
condition("title_agent", task="research", strategy="title_only")
# run it: strategy=title_agent

The task, what the backbone is asked to produce: edit agent_search/tasks/research/prompt.md (the default prompt; the Sieve paper's own prompt is research_paper/), or write a task with your own template and pair it with an existing strategy:

from agent_search.tasks.base import Task, register_task
from agent_search.strategies.conditions import condition

@register_task
class MyTask(Task):
    name, domain, terminal = "my_task", "general", "answer"
    prompt_file = "/path/to/my_task.md"        # front matter + body with {{tools}} and {{tool_manuals}}

condition("my_sieve", task="my_task", strategy="sieve_bm25")
# run it: strategy=my_sieve

The corpus, from Python, with your own documents:

from agent_search import research

docs = [{"_id": "d1", "title": "Treaty of Guadalupe Hidalgo",
         "text": "The Treaty of Guadalupe Hidalgo ended the Mexican-American War in 1848."}]
result = research("Which treaty ended the Mexican-American War?", docs,
                  strategy="sieve_bm25", model="gpt-4o-mini")
print(result.answer, result.ranking, result.usage)

A dataset:

from agent_search.evaluation.datasets import Instance, register_dataset

@register_dataset("my_qa", domain="general")
def load_my_qa(limit=None, corpus_limit=None):
    return [Instance(instance_id="my_qa__1", repo="local/my_qa", base_commit="0" * 40,
                     problem_statement="Which treaty ended the Mexican-American War?", patch="",
                     docs=docs, gold_doc_ids={"d1"}, answer="Treaty of Guadalupe Hidalgo", corpus_id="my_qa")]
# run it: dataset=my_qa

Registrations in an installed package load through the skimsearchagent.plugins entry-point group or SKIMSEARCHAGENT_PLUGINS=my_module. docs/EXTENDING.md has the complete version of each block, including metrics and judges.

Python API

from agent_search import research, build_agent

result = research(question, docs, strategy="sieve_bm25", model="gpt-4o-mini")
result.answer        # the answer span
result.ranking       # documents surfaced, in first-seen order
result.steps         # per step: tool, arguments, full observation, tokens, hit_ids, read_ids
result.usage         # llm_calls, prompt, completion and cached tokens

agent = build_agent("sieve_bm25", model="gpt-4o-mini")   # reuse one index for many questions
agent.index(units, key="my_corpus")
agent.search(question, k=10)

Strategies

familystrategy=what the agent does
Retrieval-onlybm25, dense, bql, grep, hybrid, rerankedrank once, no agent loop, no backbone; reranked reorders the pool of the retriever named in retrieval.rerank_base with the reranker in retrieval.rerank_model; hybrid fuses the retrievers named in retrieval.hybrid_retrievers with retrieval.hybrid_fusion (rrf or interpolation)
One-shot RAGrag_bm25, rag_dense, rag_hybridrank once, put the top five documents in one prompt, one backbone call
Search–Visitsearch_visit, search_visit_dense, search_visit_hybrid, search_visit_reranked, search_visit_snippetsread a result list, open whole documents
Search–AutoReadautoread, autoread_dense, autoread_hybridevery search returns full text
Direct corpus interactiondci, bounded_dcishell commands over exported files, optionally within a BM25 working set
Search–Fetchsearch_fetch, search_fetch_dense, search_fetch_hybrid, search_fetch_bm25_plain, search_fetch_dense_plainresult cards with snippets (or, for the plain arms, without), then named sections
Sievesieve, sieve_bm25, sieve_dense, sieve_nosnip, sieve_card, sieve_nomanual, sieve_reference_howto, sieve_reference_hops, sieve_reference_mistakes, sieve_reference_howto_hops_mistakes, sieve_reference_howto_hops_mistakes_noconstruct, sieve_plain, sieve_v2, sieve_visit, sieve_visit_fused, sieve_visit_denseBQL candidate filtering, one ranking model, result cards, section fetch (or whole documents)
Structured controlindri, indri_plain, indri_visitIndri-QL retrieval with cards and section fetch (or whole documents)
Code localizationcodefix, codefix_grep, codefix_patchsearch or grep a repository, read functions, propose a fix (dataset=code_fixture)
ITER searchiter_dense, iter_bm25, iter_reranked, iter_splade, iter_colbert, iter_diffretriever, dedup_bm25, dedup_densea top-ten search and a whole-document read; iter_* puts one retriever behind the search (the leaderboard's setting), dedup_* also drops documents already listed; see docs/ITER.md
Multi-agentplan_and_search, plan_and_search_visita planner splits the question, one agent per sub-question (Sieve, or search-visit), a synthesizer answers; every member episode is recorded

Every index is built once per dataset, before any run. Build the dense embedding cache with skimsearchagent-build-indexes --dataset <name> --retriever dense, the Lucene BM25 index with --retriever bm25_pyserini, and the Lucene structured index behind BQL and Indri with --retriever search_lucene. A run builds what it needs in its first step; a missing dense cache stops the run before the first episode and prints the build command. A code repository is indexed in memory and re-indexed only for the files that change.

A corpus too large for memory is served from disk and searched through prebuilt indexes named in the file; nothing is encoded during a run. See docs/ITER.md.

Train a retriever

Every run is a trajectory, so it is also training data. Triples with tiered negatives come out of any run directory. A patched FlagEmbedding trainer fits a dense retriever on them. The checkpoint plugs back in as the dense model of any strategy, served with the query style, instruction and precision it was trained with:

skimsearchagent-build-triples --runs runs/... --dataset hotpotqa_structured --out train_data/hotpotqa_i2.jsonl --query-style i2 --labeller oracle
skimsearchagent-train-retriever template > train.yaml
sbatch --export=ALL,TRAIN=train.yaml,TRAIN_ENV=$PWD/envs-train scripts/slurm/train_retriever.sbatch
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml retrieval.dense_model=models/my-retriever retrieval.dense_query_style=i2

docs/TRAINING.md is the recipe; docs/ITER.md is the paper it comes from, with its backbones, released retrievers, datasets and results.

Reproducibility

python -m pip install -e ".[dev]"
python -m pytest -q                            # no GPU or API key needed; the document tests need the retrieval extra and Java 21, and skip without them

Demo

pip install -e ".[demo-live]"
python demo/server.py          # http://localhost:8008/

Ask a question and watch Sieve and Search–Visit answer it side by side, over a 250-document subsample of BrowseComp-Plus, with every token metered. Your OpenAI key stays in the browser's session storage and is sent per request to OpenAI; the server does not store or log it. See demo/.

Documentation

documentcontent
docs/EXTENDING.mdevery extension point with a complete code example
docs/CONFIGURATION.mdthe experiment-file schema, every flag and knob
docs/ARCHITECTURE.mdmodules, contracts, one episode end to end
docs/TRAINING.mdretriever training from run records
docs/BASELINES.mdretriever, reranker and agent-backbone baselines on BrowseComp-Plus: how to run each one, and its results on three judges; the same rows are on the leaderboard
docs/SIEVE.mdthe Sieve paper: the method, step-by-step reproduction from a fresh clone, and every result
docs/ITER.mdthe ITER paper: step-by-step reproduction, its search tools, retrievers, datasets, training and results
CONTRIBUTING.mddevelopment setup and how to add components

Papers

Two papers run on this library. Each has its own page; the README only points at them.

Sieve

docs/SIEVE.md. A Boolean-filtered search, inspect, fetch strategy: fielded candidate selection (BQL), one ranking model, compact result cards with query-biased snippets, and section-level reading. On BrowseComp-Plus, HotpotQA and MuSiQue it matched or improved accuracy while reading 30 to 51% fewer tokens than Search-Visit.

@misc{wang2026sieve,
  title         = {Search, Inspect, Fetch: Exploiting Boolean Retrieval for Deep-Research Agents},
  author        = {Wang, Shuai and Chen, Haodong and Yin, Yu and Zhuang, Shengyao and
                   Koopman, Bevan and Zuccon, Guido},
  year          = {2026},
  eprint        = {2608.02751},
  archivePrefix = {arXiv},
  primaryClass  = {cs.IR},
  url           = {https://arxiv.org/abs/2608.02751}
}

ITER

docs/ITER.md, https://github.com/ielab/ITER. Interaction-aware retrieval for agentic search: a dense retriever trained from search-agent trajectories, conditioned on the agent's earlier searches and trained to return documents it has not read yet. The library carries its search tools, its training recipe, its released checkpoints and its evaluation sets.

@misc{chen2026iter,
  title         = {ITER: Interaction-Aware Retrieval for Agentic Search},
  author        = {Chen, Haodong and Wang, Shuai and Yin, Yu and Zhuang, Shengyao and
                   Zuccon, Guido and Leelanupab, Teerapong},
  year          = {2026},
  eprint        = {2608.27912},
  archivePrefix = {arXiv},
  primaryClass  = {cs.IR},
  url           = {https://arxiv.org/abs/2608.27912}
}

ielab/skim-search-agent

A composable library for building and evaluating deep-research agents over your own corpus.

Python

14

79 commits

updated Oct 9, 2026

See the code

README

SkimSearchAgent

A research framework for deep-search agents. Change one component, keep the rest of the experiment fixed.

CI Python 3.10+ License: Apache-2.0

Project page · Leaderboard · Paper · The library · Install · Run an experiment · Change one thing · Python API · Strategies · Train a retriever · Docs

Corpora, models, strategies, retrievers and evaluators around one agent loop

A deep-search agent answers a question by searching a collection over several steps. Which retriever it uses, what a search result shows, how it reads a document, which agent backbone drives it, and how the answer is scored are separate decisions. SkimSearchAgent makes each one a component with a fixed interface and runs every combination through the same evaluation, so two experiments differ only where you changed them. Every run writes the same record: the full trajectory, the configuration, and the metrics.

The library

One package, agent_search, one folder per kind of component. Each folder is a base class plus one file per concrete class, so a new method is one new file. Read them in the order a query travels:

modulewhat it holdsthe files
corpus/documents and functions as units; an on-disk store for corpora too large for memory; fingerprints so a persisted index is never served against a corpus it was not built forunits.py, docstore.py, fingerprint.py, code_repo.py
retrievers/engines that rank ids for a query: Lucene BM25, dense encoders (one file per family), the Boolean method BQL, Indri; and two compositions, a hybrid (any retrievers fused by one method) and a reranked retriever (one retriever's pool reordered by a reranker)lexical/, dense/, bql/, indri/, lucene/, fusion/, hybrid.py, rerankers/, reranked.py, engines.py, backend.py
snippets/how one hit is excerpted in a listing: the opening line, the best window for the query terms, or nothing; widths in model tokensopening.py, term_window.py, none.py
tools/the actions the agent backbone can call, one folder each with its declaration, code and manual; a tool names its engine kind and its snippetsearch_bm25/, search_dense/, search_hybrid/, search_reranked/, search_bql/, search_indri/, search_dedup/, search_bm25_dci/, visit/, fetch/, fetch_code/, get_document/, bash/, read/, grep/
tasks/what the backbone is asked to produce: the prompt template and the answer protocolresearch/, research_dedup/, codefix/, codefix_patch/
strategies/the named combinations a run selects: tools with their options and a harness, or a retrieval-only floor; conditions.py pairs a strategy with a tasksearch_visit.py, search_fetch.py, autoread.py, sieve.py, indri.py, dci.py, dedup.py, codefix.py, rag.py, teams.py, retrieval_only.py
harness/how the backbone is put to work on a condition, one file each: ReAct (the default loop, the backbone picks each step), one-shot RAG (rank once, one call), plan-and-search (a team: a planner, one member agent per sub-question, a synthesizer)react.py, rag.py, plan_and_search.py
agent/the machinery a harness is built from: the backbone providers, the policies, the step loop, the forced answer, the Agents-SDK and Responses drivers, the run recordbackbone/, policies.py, loop.py, forced_answer.py, sdk_driver.py, responses_driver.py, record.py
evaluation/the evaluation: datasets, the runner, the metrics, the judge, the run record and its identity, the index prebuilddatasets/, runner.py, run_eval.py, llm_judge.py, identity.py, build_indexes.py
training/the ITER recipe: trajectories to triples, retriever training and evaluationbuild_triples.py, retriever.py, retriever_eval.py

Document corpora rank on Lucene (BM25 through Pyserini, BQL and Indri on a fielded Lucene index); a code repository is indexed in memory and re-indexed only for the files that change. Every length limit is a token count. docs/ARCHITECTURE.md explains each module; docs/EXTENDING.md shows how to add one of each.

What ships with the library

Each name below is a file in the library and a value in an experiment file. A row on the leaderboard is one experiment file under configs/baselines/, and docs/BASELINES.md has the commands.

partsupported
Lexical retrieversBM25 on Lucene (Pyserini), the Boolean method BQL, Indri query language, grep
Dense retrieversBGE (small, base, large), Qwen3-Embedding (0.6B, 4B, 8B), RepLLaMA, LRAT (Qwen3 and multilingual-e5), Agentic-R, AgentIR, the ITER checkpoints, CodeRankEmbed for code, and any checkpoint trained with skimsearchagent-train-retriever
Learned sparse and multi-vector retrieversSPLADE++, ColBERTv2, Agent-ModernColBERT, DiffRetriever (dense and sparse)
Compositionshybrid fusion of two rankings (rrf, interpolation), a reranker over a retriever's pool
Rerankerscross_encoder (bge-reranker-v2-m3), qwen3_reranker (Qwen3-Reranker), monot5 (monoT5), laya (the Laya decision models)
Agent backbonesany model behind an OpenAI-compatible server (vLLM), vLLM in-process, the OpenAI chat and reasoning models, Gemini. Experiment files ship for Tongyi-DeepResearch-30B, Qwen3.5 (4B, 9B, 27B), Qwen3.6-27B, Qwen3.8-27B, gpt-oss (20b, 120b), Qwen-AgentWorld-35B, OpenResearcher-30B, OpenSeeker-v2-30B, MiroThinker-1.7-mini and QUEST-35B. Those last four run in their own interface: the tools, prompt and history format of each agent's released code, with a 100-turn budget
Tool-call formatsthe JSON <tool_call> format by default; glm, mcp, lenient_json and qwen_xml for backbones that write calls their own way (model.tool_call_format)
Judgesthe BrowseComp-Plus judge prompt with gpt-4o-mini or a served model (Qwen3-32B), DIVER's judge template (Qwen3-30B-A3B-Thinking-2507), exact match and F1
DatasetsBrowseComp-Plus, HotpotQA, MuSiQue, 2WikiMultihopQA, InfoSeek, SWE-bench Lite and Verified, LocBench, and your own corpus as a BEIR-style directory

docs/EXTENDING.md shows how to add one of each.

Install

git clone https://github.com/ielab/skim-search-agent.git && cd skim-search-agent
python -m pip install -e .                 # core: PyYAML only
python -m pip install -e ".[api]"          # OpenAI, Gemini, OpenAI-compatible servers
python -m pip install -e ".[retrieval]"    # Lucene (BM25, BQL, Indri), dense and hybrid retrieval (Java 21+, torch)

Other extras: eval (dataset staging, statistics), serve (vLLM), demo-live, dev (tests), train (retriever training, separate environment), all. Pins match requirements.txt, the environment used for the paper.

Every document run needs the retrieval extra: document corpora rank on Lucene only (BM25 through Pyserini, and the structured index behind BQL and Indri). The core install runs code repositories, which are indexed in memory. Point JAVA_HOME at a JDK 21 or newer before running anything that touches Lucene, the test suite included. Pyserini starts a JVM on import, and an older Java aborts the process without a message.

Run an experiment

One YAML file is one complete setting. The file lists every knob its strategy reads: dataset, agent backbone, token budgets, listing depths, retrieval engines, scoring, output. A search_visit file has no dense-model keys; a sieve file has no listing depths.

skimsearchagent run configs/fixtures/smoke_doc_fixture_sieve_bm25.yaml        # scripted policy, no keys
export OPENAI_API_KEY=...
skimsearchagent run configs/fixtures/doc_fixture_sieve_bm25_gpt4omini.yaml    # a real backbone
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml     # the paper's setting, scripted policy (no backbone named)
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml model.name=gpt-4o output.runs_dir=runs/gpt4o   # the same with a backbone
skimsearchagent validate configs/sieve/hotpotqa_structured_sieve.yaml   # what it needs, what it will run
skimsearchagent template paper sieve > configs/ablation/mine.yaml              # a complete file for one strategy, to edit

The run directory holds config.json (the file's content plus every resolved knob), rows.jsonl (one full trajectory per question) and results.json (metrics). Rerunning the same command resumes. Rerunning a different setting into the same directory is refused.

For one-off runs every knob is also a key=value argument:

skimsearchagent dataset=doc_fixture strategy=search_visit model=gpt-4o-mini snippet_tokens=64

Change one thing

Each block changes one component and leaves the rest of the experiment as it was. The blocks follow the module table above: the strategy, the agent backbone, the harness, the retriever, the snippet, the tool, the task, the corpus, the dataset.

The strategy (one word in the experiment file, or on the command line):

strategy: search_visit        # sieve, sieve_bm25, search_fetch, autoread, dci, indri, ... (see Strategies)

The agent backbone, the language model that drives the agent. The experiment file's section is called model; it takes any OpenAI-compatible server, OpenAI, Gemini, or in-process vLLM:

model:
  name: Alibaba-NLP/Tongyi-DeepResearch-30B-A3B
  backend: api                # a served backbone
  api_base: http://localhost:8000/v1

From Python the backbone is any callable that maps chat messages to text:

def my_backbone(messages: list[dict]) -> str:
    return my_client.chat(messages)          # returns the generation, tool calls included

research(question, docs, strategy="sieve_bm25", generate=my_backbone)

The harness, how the backbone is put to work. ReAct is the default; a strategy names another one, such as a team whose members are conditions:

from agent_search.harness.plan_and_search import PlanAndSearch
from agent_search.strategies.base import Strategy, register_strategy

register_strategy(Strategy(name="plan_and_search_visit_dense", description="a planner, dense search-visit agents, a synthesizer",
                           harness=PlanAndSearch(searcher="search_visit_dense")))
# run it: strategy=plan_and_search_visit_dense

The retriever, as a new engine or as a retrieval-only floor:

from agent_search.retrievers.base import Retriever
from agent_search.retrievers.registry import register

class MyRetriever(Retriever):
    name = "my_method"
    def index(self, units, key=None): self._ids = [u.doc_id for u in units]; return self
    def search(self, query, k): return self._ids[:k]

register("my_method")(lambda cfg, name: (lambda: MyRetriever()))
# run it: strategy=my_method (retrieval-only), or give it an engine kind so a tool can use it

A hybrid is any retrievers fused by one method (retrieval.hybrid_retrievers, retrieval.hybrid_fusion); a reranked retriever is one retriever's pool reordered by a reranker (retrieval.rerank_base, retrieval.rerank_model).

The dense retriever, one setting for Sieve's ranker, its fallback, and every dense baseline:

retrieval:
  dense_model: models/my-retriever     # a hub id or a checkpoint trained below
  dense_query_style: null              # how the query is written (null = the model's own: ITER's trained format for ITER)

The snippet, what a listing shows under each hit. A tool takes a method from agent_search/snippets/; widths are model tokens:

from agent_search.snippets import TermWindow
from agent_search.tools.search_bm25.tool import SearchBm25

SearchBm25(name="bm25q_search", snippet=TermWindow())      # the best window for the query terms

A tool (its declaration and its code in one class), a strategy (which tools, under which names, with which options) and a condition (a task with that strategy):

from agent_search.tools.base import Tool
from agent_search.strategies.base import Strategy, register_strategy
from agent_search.strategies.conditions import condition

class TitleLookup(Tool):
    name = "title_lookup"
    description = "Documents whose title contains the words."
    parameters = {"type": "object", "properties": {"words": {"type": "string"}}, "required": ["words"]}
    def run(self, args):                          # self.units, self.ubyid, self.state, self.engine[...]
        hits = [u for u in self.units if all(w in (u.title or "").lower() for w in args["words"].lower().split())]
        self.state.seen.update(u.doc_id for u in hits)   # first-seen order = the agent's ranking
        return "\n".join(f"{u.doc_id}  {u.title}: {u.body}" for u in hits) or "no match"

register_strategy(Strategy(name="title_only", description="look up by title", tools=(TitleLookup(name="title_lookup"),)))
condition("title_agent", task="research", strategy="title_only")
# run it: strategy=title_agent

The task, what the backbone is asked to produce: edit agent_search/tasks/research/prompt.md (the default prompt; the Sieve paper's own prompt is research_paper/), or write a task with your own template and pair it with an existing strategy:

from agent_search.tasks.base import Task, register_task
from agent_search.strategies.conditions import condition

@register_task
class MyTask(Task):
    name, domain, terminal = "my_task", "general", "answer"
    prompt_file = "/path/to/my_task.md"        # front matter + body with {{tools}} and {{tool_manuals}}

condition("my_sieve", task="my_task", strategy="sieve_bm25")
# run it: strategy=my_sieve

The corpus, from Python, with your own documents:

from agent_search import research

docs = [{"_id": "d1", "title": "Treaty of Guadalupe Hidalgo",
         "text": "The Treaty of Guadalupe Hidalgo ended the Mexican-American War in 1848."}]
result = research("Which treaty ended the Mexican-American War?", docs,
                  strategy="sieve_bm25", model="gpt-4o-mini")
print(result.answer, result.ranking, result.usage)

A dataset:

from agent_search.evaluation.datasets import Instance, register_dataset

@register_dataset("my_qa", domain="general")
def load_my_qa(limit=None, corpus_limit=None):
    return [Instance(instance_id="my_qa__1", repo="local/my_qa", base_commit="0" * 40,
                     problem_statement="Which treaty ended the Mexican-American War?", patch="",
                     docs=docs, gold_doc_ids={"d1"}, answer="Treaty of Guadalupe Hidalgo", corpus_id="my_qa")]
# run it: dataset=my_qa

Registrations in an installed package load through the skimsearchagent.plugins entry-point group or SKIMSEARCHAGENT_PLUGINS=my_module. docs/EXTENDING.md has the complete version of each block, including metrics and judges.

Python API

from agent_search import research, build_agent

result = research(question, docs, strategy="sieve_bm25", model="gpt-4o-mini")
result.answer        # the answer span
result.ranking       # documents surfaced, in first-seen order
result.steps         # per step: tool, arguments, full observation, tokens, hit_ids, read_ids
result.usage         # llm_calls, prompt, completion and cached tokens

agent = build_agent("sieve_bm25", model="gpt-4o-mini")   # reuse one index for many questions
agent.index(units, key="my_corpus")
agent.search(question, k=10)

Strategies

familystrategy=what the agent does
Retrieval-onlybm25, dense, bql, grep, hybrid, rerankedrank once, no agent loop, no backbone; reranked reorders the pool of the retriever named in retrieval.rerank_base with the reranker in retrieval.rerank_model; hybrid fuses the retrievers named in retrieval.hybrid_retrievers with retrieval.hybrid_fusion (rrf or interpolation)
One-shot RAGrag_bm25, rag_dense, rag_hybridrank once, put the top five documents in one prompt, one backbone call
Search–Visitsearch_visit, search_visit_dense, search_visit_hybrid, search_visit_reranked, search_visit_snippetsread a result list, open whole documents
Search–AutoReadautoread, autoread_dense, autoread_hybridevery search returns full text
Direct corpus interactiondci, bounded_dcishell commands over exported files, optionally within a BM25 working set
Search–Fetchsearch_fetch, search_fetch_dense, search_fetch_hybrid, search_fetch_bm25_plain, search_fetch_dense_plainresult cards with snippets (or, for the plain arms, without), then named sections
Sievesieve, sieve_bm25, sieve_dense, sieve_nosnip, sieve_card, sieve_nomanual, sieve_reference_howto, sieve_reference_hops, sieve_reference_mistakes, sieve_reference_howto_hops_mistakes, sieve_reference_howto_hops_mistakes_noconstruct, sieve_plain, sieve_v2, sieve_visit, sieve_visit_fused, sieve_visit_denseBQL candidate filtering, one ranking model, result cards, section fetch (or whole documents)
Structured controlindri, indri_plain, indri_visitIndri-QL retrieval with cards and section fetch (or whole documents)
Code localizationcodefix, codefix_grep, codefix_patchsearch or grep a repository, read functions, propose a fix (dataset=code_fixture)
ITER searchiter_dense, iter_bm25, iter_reranked, iter_splade, iter_colbert, iter_diffretriever, dedup_bm25, dedup_densea top-ten search and a whole-document read; iter_* puts one retriever behind the search (the leaderboard's setting), dedup_* also drops documents already listed; see docs/ITER.md
Multi-agentplan_and_search, plan_and_search_visita planner splits the question, one agent per sub-question (Sieve, or search-visit), a synthesizer answers; every member episode is recorded

Every index is built once per dataset, before any run. Build the dense embedding cache with skimsearchagent-build-indexes --dataset <name> --retriever dense, the Lucene BM25 index with --retriever bm25_pyserini, and the Lucene structured index behind BQL and Indri with --retriever search_lucene. A run builds what it needs in its first step; a missing dense cache stops the run before the first episode and prints the build command. A code repository is indexed in memory and re-indexed only for the files that change.

A corpus too large for memory is served from disk and searched through prebuilt indexes named in the file; nothing is encoded during a run. See docs/ITER.md.

Train a retriever

Every run is a trajectory, so it is also training data. Triples with tiered negatives come out of any run directory. A patched FlagEmbedding trainer fits a dense retriever on them. The checkpoint plugs back in as the dense model of any strategy, served with the query style, instruction and precision it was trained with:

skimsearchagent-build-triples --runs runs/... --dataset hotpotqa_structured --out train_data/hotpotqa_i2.jsonl --query-style i2 --labeller oracle
skimsearchagent-train-retriever template > train.yaml
sbatch --export=ALL,TRAIN=train.yaml,TRAIN_ENV=$PWD/envs-train scripts/slurm/train_retriever.sbatch
skimsearchagent run configs/sieve/hotpotqa_structured_sieve.yaml retrieval.dense_model=models/my-retriever retrieval.dense_query_style=i2

docs/TRAINING.md is the recipe; docs/ITER.md is the paper it comes from, with its backbones, released retrievers, datasets and results.

Reproducibility

python -m pip install -e ".[dev]"
python -m pytest -q                            # no GPU or API key needed; the document tests need the retrieval extra and Java 21, and skip without them

Demo

pip install -e ".[demo-live]"
python demo/server.py          # http://localhost:8008/

Ask a question and watch Sieve and Search–Visit answer it side by side, over a 250-document subsample of BrowseComp-Plus, with every token metered. Your OpenAI key stays in the browser's session storage and is sent per request to OpenAI; the server does not store or log it. See demo/.

Documentation

documentcontent
docs/EXTENDING.mdevery extension point with a complete code example
docs/CONFIGURATION.mdthe experiment-file schema, every flag and knob
docs/ARCHITECTURE.mdmodules, contracts, one episode end to end
docs/TRAINING.mdretriever training from run records
docs/BASELINES.mdretriever, reranker and agent-backbone baselines on BrowseComp-Plus: how to run each one, and its results on three judges; the same rows are on the leaderboard
docs/SIEVE.mdthe Sieve paper: the method, step-by-step reproduction from a fresh clone, and every result
docs/ITER.mdthe ITER paper: step-by-step reproduction, its search tools, retrievers, datasets, training and results
CONTRIBUTING.mddevelopment setup and how to add components

Papers

Two papers run on this library. Each has its own page; the README only points at them.

Sieve

docs/SIEVE.md. A Boolean-filtered search, inspect, fetch strategy: fielded candidate selection (BQL), one ranking model, compact result cards with query-biased snippets, and section-level reading. On BrowseComp-Plus, HotpotQA and MuSiQue it matched or improved accuracy while reading 30 to 51% fewer tokens than Search-Visit.

@misc{wang2026sieve,
  title         = {Search, Inspect, Fetch: Exploiting Boolean Retrieval for Deep-Research Agents},
  author        = {Wang, Shuai and Chen, Haodong and Yin, Yu and Zhuang, Shengyao and
                   Koopman, Bevan and Zuccon, Guido},
  year          = {2026},
  eprint        = {2608.02751},
  archivePrefix = {arXiv},
  primaryClass  = {cs.IR},
  url           = {https://arxiv.org/abs/2608.02751}
}

ITER

docs/ITER.md, https://github.com/ielab/ITER. Interaction-aware retrieval for agentic search: a dense retriever trained from search-agent trajectories, conditioned on the agent's earlier searches and trained to return documents it has not read yet. The library carries its search tools, its training recipe, its released checkpoints and its evaluation sets.

@misc{chen2026iter,
  title         = {ITER: Interaction-Aware Retrieval for Agentic Search},
  author        = {Chen, Haodong and Wang, Shuai and Yin, Yu and Zhuang, Shengyao and
                   Zuccon, Guido and Leelanupab, Teerapong},
  year          = {2026},
  eprint        = {2608.27912},
  archivePrefix = {arXiv},
  primaryClass  = {cs.IR},
  url           = {https://arxiv.org/abs/2608.27912}
}

Significant stargazers

felix-wang

222 followers · starred Aug 2026

Shengyao Zhuang

103 followers · starred Aug 2026