deep-research
A deep web research agent, built as a Coyote graph agent. It plans an investigation, decomposes it into sub-questions researched in parallel, grounds the work in a local knowledge corpus, vets the credibility of cited sources, runs a reflexion self-critique loop to revise weak findings, delegates the final write-up to a focused sub-agent, checks that the cited sources are reachable, and gates the result behind human approval.
Unlike a regular agent (which takes a goal and improvises the steps),
this agent runs a fixed graph: every request goes through the same
plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve
pipeline.
This agent is also the canonical reference for the Coyote graph
system: it exercises every node type (script, llm, rag, map,
agent, input, approval, end) and both static fan-out and
dynamic map fan-out. If you are learning how to build a graph
agent, this is the file to read alongside the
Graph-Agents wiki.
Workflow
17 nodes. Solid arrows are static next / routes edges declared in
graph.yaml; script nodes can also route dynamically via _next (shown as
labeled branches out of the diamond). Dotted arrows show map fan-out — the
research_each_question node spawns one research_one_question branch per
sub-question and joins them before continuing.
flowchart TD
parse_request{"parse_request<br/>script"}
parse_request -->|"topic given"| bootstrap_research
parse_request -->|"no topic"| ask_topic
ask_topic[/"ask_topic<br/>input"/] --> bootstrap_research
bootstrap_research{"bootstrap_research<br/>script"}
bootstrap_research --> plan
bootstrap_research --> knowledge_lookup
plan["plan<br/>llm + output_schema"] --> research_each_question
knowledge_lookup[("knowledge_lookup<br/>rag")] --> research_each_question
research_each_question[\research_each_question<br/>map/]
research_each_question -. "spawns × N" .-> research_one_question["research_one_question<br/>llm + web tools"]
research_each_question --> combine_findings
combine_findings{"combine_findings<br/>script"} --> vet_sources
vet_sources["vet_sources<br/>llm + classify_source"] --> critique
critique["critique<br/>llm"] --> reflexion_gate
reflexion_gate{"reflexion_gate<br/>script"}
reflexion_gate -->|"PASS"| synthesize
reflexion_gate -->|"REVISE (budget left)"| research_each_question
reflexion_gate -->|"REVISE (budget spent)"| synthesize
synthesize[["synthesize<br/>agent → report-writer"]] --> verify_sources
verify_sources{"verify_sources<br/>script"} --> approve
approve{{"approve<br/>approval"}}
approve -->|"accept"| end_accepted
approve -->|"reject"| end_rejected
approve -->|"other (free-form feedback)"| incorporate_feedback
incorporate_feedback{"incorporate_feedback<br/>script"} --> research_each_question
end_accepted(["end_accepted<br/>report"])
end_rejected(["end_rejected"])
Node-type breakdown
| Type | Nodes |
|---|---|
script (Python) |
parse_request, bootstrap_research, combine_findings, reflexion_gate, verify_sources, incorporate_feedback |
llm (tools: []) |
plan, critique |
llm (with tool whitelist) |
research_one_question, vet_sources |
rag |
knowledge_lookup — local corpus retrieval |
map |
research_each_question — dynamic fan-out per sub-question |
agent |
synthesize — spawns the report-writer sub-agent |
input |
ask_topic |
approval |
approve |
end |
end_accepted, end_rejected |
Parallel execution
The graph has two parallel super-steps where Coyote's BSP scheduler runs branches concurrently.
1. Context loading (plan ‖ knowledge_lookup) — after
bootstrap_research, the LLM planner (which decomposes the topic into
sub-questions) and the RAG retrieval over the local knowledge/
corpus run side by side. They write disjoint state keys (plan writes
research_plan and questions; knowledge_lookup writes
local_context and local_sources) so no reducer is needed.
2. Per-question research (research_each_question map) — the
plan emits a questions array (3-5 entries, enforced by its
output_schema). The map node spawns one parallel branch per
question (max_concurrency: 3). Each branch is an isolated
research_one_question LLM invocation with web tools, instructed to
investigate exactly its assigned question. Outputs collect into
question_findings in input order, then combine_findings joins
them into a single findings Markdown document for downstream nodes.
settings.max_concurrency: 4 is the graph-wide cap; the per-map
override (max_concurrency: 3 on research_each_question) is
deliberately lower to leave headroom for the planner's tool calls
running alongside RAG.
Local knowledge corpus
knowledge_lookup is a rag node — it runs hybrid (vector + keyword)
retrieval over every file in knowledge/. The directory ships with a
small research-style-notes.md so the RAG node has something to
retrieve against on a clean install; drop your own Markdown notes,
PDFs, or text files into knowledge/ to bias the research toward
your local context.
The knowledge base is built once, at agent-load time, into
~/.config/coyote/agents/deep-research/knowledge_lookup.yaml. Because
the node fully specifies its build config (embedding_model,
chunk_size, chunk_overlap), the build is non-interactive. Delete
that cached file after adding or changing knowledge to force a
rebuild.
Sub-agent: report-writer
The synthesize node is an agent node that spawns the
report-writer sub-agent (assets/agents/report-writer/). This is
the agent-as-tool pattern: the orchestrating graph delegates the
writing phase to a focused sub-agent dedicated to coherent prose,
while the research phase uses different (typically cheaper) LLM nodes
for fast-and-many-question investigation.
The report-writer sub-agent has no tools — it cannot access the
web, cannot search, and cannot invent facts. It reads only the
findings it is given and produces a final Markdown report preserving
every inline citation. See assets/agents/report-writer/README.md
for details.
Tools and tool scoping
This agent demonstrates Coyote's three tool sources and how an llm
node's tools: whitelist scopes them per node.
The agent's full tool universe, declared in graph.yaml:
- Global tools (
global_tools):web_search_coyote,fetch_url_via_curl,search_arxiv- Coyote's built-in tool scripts. - MCP server (
mcp_servers):ddg-search- a DuckDuckGo web search MCP server. Referenced in a whitelist asmcp:ddg-search. - Custom agent tool (
tools.sh):classify_source- a deterministic source-credibility classifier shipped with this agent.
No node receives all of these. Each llm node's tools: whitelist
narrows the universe to exactly what that step needs:
| Node | tools: whitelist |
Draws from |
|---|---|---|
plan, critique |
[] |
nothing - pure reasoning |
research_one_question |
web_search_coyote, fetch_url_via_curl, search_arxiv, mcp:ddg-search |
global tools + MCP |
vet_sources |
classify_source |
the custom tool only |
research_one_question (each parallel branch of the map) can search
and fetch but cannot classify sources; vet_sources can classify
sources but cannot touch the web. That separation is the point of the
tools: whitelist: a node gets only the tools its job calls for,
never the agent's full set.
The classify_source custom tool (tools.sh) takes a URL and returns
a credibility tier (government, academic, preprint, organization,
unverified) derived from the host and top-level domain. It is
deterministic - exactly the kind of logic a tool should own rather than
the LLM guessing.
Web search may require API-key configuration; see the
Tools docs.
fetch_url_via_curl, search_arxiv, and classify_source work
without a key.
Setup
research_one_question (each parallel branch of the map) uses the
ddg-search MCP server via mcp:ddg-search. It is one of Coyote's
default MCP servers; make sure it is registered in
~/.config/coyote/mcp.json (run coyote --install mcp_config to restore
the default template if it is missing). If ddg-search is unavailable,
the branches still have their global web-search tools to fall back on.
The synthesize node spawns the report-writer sub-agent. Both
agents ship with coyote agents install; if you install one manually,
install both so the agent reference resolves.
Reflexion
The agent has two loops, both built with script nodes that route via
_next. The engine allows back-edges at runtime; the validator only
rejects cycles built from static next / routes edges, so script
_next loops are always allowed.
Automated reflexion loop. After the parallel research map and
vet_sources, the critique node reviews the merged findings
against the research plan and the source credibility assessment, and
emits VERDICT: PASS or VERDICT: REVISE with specific feedback.
reflexion_gate.py then:
PASS-> continue tosynthesize.REVISE, budget remaining -> loop back toresearch_each_question, with the critique injected asresearch_feedbackso every parallel branch sees it on the retry.REVISE, budget spent -> continue tosynthesizeanyway (the human approval step is the final backstop).
The budget is MAX_REFLEXION_REVISIONS in reflexion_gate.py
(default 2, so the research map runs at most 3 times per pass).
Human-feedback loop. At approve the user answers accept,
reject, or types their own feedback. A free-form answer routes via
the approval node's on_other to incorporate_feedback.py, which
folds that text into research_feedback and loops back to
research_each_question for another parallel pass.
settings.max_loop_iterations (40) is the engine's infinite-loop
backstop: it caps the total visits to any single node.
Running
coyote agents install # ships deep-research
coyote -a deep-research "How does HTTP/3 differ from HTTP/2?"
coyote -a deep-research "Recent advances in solid-state batteries"
coyote -a deep-research # no prompt -> triggers ask_topic
Anti-hallucination
research_one_question(each map branch) is instructed to back every claim with a real retrieved source and never to fabricate URLs, titles, or DOIs.vet_sourcesclassifies every cited source so weak sources are visible to the critique step.critiqueindependently reviews the merged findings and sends weak or uncited work back for another parallel research pass.synthesize(thereport-writersub-agent) is grounded: it may use only the gathered findings and must keep each claim's inline source. It has no tools and cannot browse the web.verify_sourcesprobes every cited URL / DOI with an HTTP HEAD request and reports which are unreachable, so the human reviewer sees broken citations before approving.
Customizing
- Loop budget.
MAX_REFLEXION_REVISIONSinreflexion_gate.py. - Map concurrency. The
research_each_questionnode'smax_concurrency: 3caps simultaneous web-research branches. Raise to investigate more questions in parallel; lower to be gentle on rate-limited providers. - Per-node model. Add
model: anthropic:...to anyllmnode. Cheap models work well forplan/critique/vet_sources; the heavy intelligence is needed inresearch_one_questionand thereport-writersub-agent. - Tool scope. Narrow the
research_one_questionnode'stools:list to constrain where each branch looks (for example, dropweb_search_coyoteandmcp:ddg-searchto force arXiv-only research). - Local knowledge. Drop files into
knowledge/to bias every research branch toward your local context (see the Local knowledge corpus section above). - Different writer. Replace
agent: report-writeron thesynthesizenode with the name of any other agent. The orchestrator does not care what kind of agent the writer is. - Skip approval. Point both
approveroutes atend_accepted, or wireverify_sourcesstraight to anendnode.
Files
assets/agents/deep-research/
graph.yaml - agent config + 17-node workflow
tools.sh - classify_source custom tool
README.md - this file
knowledge/
README.md - corpus-format notes
research-style-notes.md - starter knowledge file (replace with your notes)
scripts/
parse_request.py - _next: bootstrap_research, or ask_topic if no topic
bootstrap_research.py - fan-out source: next [plan, knowledge_lookup]
combine_findings.py - joins map output (question_findings) into findings
reflexion_gate.py - _next: research_each_question (revise) or synthesize
verify_sources.py - HTTP HEAD on cited URLs / DOIs
incorporate_feedback.py - _next: research_each_question, with user feedback
See also assets/agents/report-writer/ — the sub-agent the
synthesize node spawns.