Files
coyote/assets/agents/deep-research/README.md
T

18 KiB
Raw Blame History

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

23 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 — and fallback edges (labeled "fallback"), taken when a node fails outright (see Fault paths below).

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
    plan -. "fallback" .-> plan_fault{"plan_fault<br/>script"}
    plan_fault --> end_fault
    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_one_question -. "fallback" .-> question_fault{"question_fault<br/>script"}
    research_each_question --> combine_findings
    combine_findings{"combine_findings<br/>script"} --> vet_sources
    vet_sources["vet_sources<br/>llm + classify_source"] --> critique
    vet_sources -. "fallback" .-> vet_fault{"vet_fault<br/>script"}
    vet_fault --> critique
    critique["critique<br/>llm"] --> reflexion_gate
    critique -. "fallback" .-> critique_fault{"critique_fault<br/>script"}
    critique_fault --> 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
    synthesize -. "fallback" .-> synth_fault{"synth_fault<br/>script"}
    synth_fault --> end_fault
    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"])
    end_fault(["end_fault<br/>DEEP_RESEARCH FAILED"])

Node-type breakdown

Type Nodes
script (Python) parse_request, bootstrap_research, combine_findings, reflexion_gate, verify_sources, incorporate_feedback, plus the fault markers plan_fault, question_fault, vet_fault, critique_fault, synth_fault
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, end_fault

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 as mcp: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 to synthesize.
  • REVISE, budget remaining -> loop back to research_each_question, with the critique injected as research_feedback so every parallel branch sees it on the retry.
  • REVISE, budget spent -> continue to synthesize anyway (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.

Fault paths

Every stage that can die outright has a fallback: edge to a small fault-marker script — the dashed edges in the diagram. A fallback fires after a node's retries are exhausted (research_one_question and synthesize each get two attempts via max_attempts: 2); the engine writes its failure text into the node's state key before routing, and the marker normalizes that text into a PIPELINE-FAULT: ... entry in pipeline_faults. The five routes:

  • plan -> plan_fault -> end_fault — fail-closed: no plan means nothing to research.
  • research_one_question -> question_fault — branch-local, inside the map branch: one dead research lane becomes a fault finding instead of sinking the whole map.
  • vet_sources -> vet_fault -> critique — degrade-visibly: vetting is a quality lane, not the product; source_assessment becomes a neutral "unvetted" note.
  • critique -> critique_fault -> reflexion_gate — degrade-visibly: the rewritten critique deliberately has no VERDICT: line, so the gate's malformed-critique PASS default proceeds to synthesis.
  • synthesize -> synth_fault -> end_fault — fail-closed: no report means no product.

On the fail-closed routes, end_fault renders the DEEP_RESEARCH FAILED — PIPELINE-FAULT: ... sentinel. On the degrade-visibly routes, verify_sources folds the accumulated faults into a ## Pipeline notes section shown at the approval step and appended to the accepted report ({{report}}{{pipeline_notes}}; empty when there were no faults). Degraded output is never a silent pass — every fault is visible to the reviewer or the caller — and some end node always renders its sentinel, so the graph degrades instead of dying.

All 11 scripts also carry a top-level crash guard (R3): a script that throws records a fault and fails forward where the flow allows it (a crashed reflexion_gate proceeds to synthesize; a crashed verify_sources reports that sources were NOT checked) instead of killing the run.

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_sources classifies every cited source so weak sources are visible to the critique step.
  • critique independently reviews the merged findings and sends weak or uncited work back for another parallel research pass.
  • synthesize (the report-writer sub-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_sources probes every cited URL / DOI with an HTTP HEAD request and reports which are unreachable, so the human reviewer sees broken citations before approving.
  • Fault paths degrade visibly, never silently: a PIPELINE-FAULT can only ever surface as a failure sentinel or a ## Pipeline notes section, never as a clean report.

Customizing

  • Loop budget. MAX_REFLEXION_REVISIONS in reflexion_gate.py.
  • Map concurrency. The research_each_question node's max_concurrency: 3 caps 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 any llm node. Cheap models work well for plan / critique / vet_sources; the heavy intelligence is needed in research_one_question and the report-writer sub-agent.
  • Tool scope. Narrow the research_one_question node's tools: list to constrain where each branch looks (for example, drop web_search_coyote and mcp:ddg-search to 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-writer on the synthesize node with the name of any other agent. The orchestrator does not care what kind of agent the writer is.
  • Skip approval. Point both approve routes at end_accepted, or wire verify_sources straight to an end node.

Files

assets/agents/deep-research/
  graph.yaml                    - agent config + 23-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]
    plan_fault.py               - fail-closed fault marker: dead plan -> end_fault
    question_fault.py           - branch-local fault marker for a dead research lane
    combine_findings.py         - joins map output (question_findings) into findings
    vet_fault.py                - degrade-visibly fault marker: unvetted sources
    critique_fault.py           - degrade-visibly fault marker: skipped critique
    reflexion_gate.py           - _next: research_each_question (revise) or synthesize
    synth_fault.py              - fail-closed fault marker: no report -> end_fault
    verify_sources.py           - HTTP HEAD on cited URLs / DOIs; folds pipeline_faults
                                  into the report's "## Pipeline notes" section
    incorporate_feedback.py     - _next: research_each_question, with user feedback

See also assets/agents/report-writer/ — the sub-agent the synthesize node spawns.