name: deep-research description: | Deep web research workflow. 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 or incomplete 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. A reviewer's free-form feedback at the approval step feeds back into another research pass. Each sub-question is researched by a spawned librarian running at deep budgets. Tunable via --variables: reflexion_budget (default 2) caps the automated critique->re-research passes - raise it to let the run churn until the critique passes; autonomy=autonomous skips the human approval gate (REQUIRED when another agent spawns this one). This is the canonical Coyote graph-agent reference: it exercises every node type (script, llm, rag, map, agent, input, approval, end) and both static fan-out and dynamic map fan-out. version: '1.0' global_tools: - web_search_coyote.sh - fetch_url_via_curl.sh - search_arxiv.sh mcp_servers: - ddg-search conversation_starters: - 'How does HTTP/3 differ from HTTP/2?' - 'Summarize recent advances in solid-state battery chemistry' variables: - name: reflexion_budget description: "Max automated critique->re-research passes before synthesis proceeds anyway. Default 2; raise it (4-8) for exhaustive runs that should churn until the critique passes." default: '2' - name: autonomy description: "supervised (default): the finished report stops at the human approval gate. autonomous: skip the gate and return the report directly - REQUIRED when another agent spawns this one (a human gate would block the run)." default: 'supervised' settings: max_loop_iterations: 120 log_state_snapshots: false validate_before_run: true max_concurrency: 4 initial_state: research_feedback: '' research_attempts: 0 local_context: '' local_sources: '' # Fault-path defaults. `questions: []` also keeps the # knowledge_lookup lane alive for one super-step when `plan` dies inside the # static fan-out: research_each_question maps over zero items while the # plan_fault → end_fault route renders the failure sentinel. pipeline_faults: [] pipeline_notes: '' fault_text: '' questions: [] start: parse_request nodes: parse_request: id: parse_request type: script script: scripts/parse_request.py next: bootstrap_research ask_topic: id: ask_topic type: input question: 'What would you like me to research?' validation: 'len(input) > 0' state_updates: topic: '{{input}}' next: bootstrap_research bootstrap_research: id: bootstrap_research type: script script: scripts/bootstrap_research.py next: [plan, knowledge_lookup] plan: id: plan type: llm instructions: | You are a research planner. Given a topic, produce a focused research plan and decompose it into 3-5 specific sub-questions that can each be researched independently in parallel. The plan is a short narrative naming the key questions and the kinds of sources that would be authoritative. The sub-questions are precise, self-contained queries (each one is sent on its own to a separate research worker, so they must be answerable without each other's context). prompt: 'Research topic: {{topic}}' tools: [] output_schema: type: object properties: research_plan: type: string description: A short plan narrative. questions: type: array items: { type: string } minItems: 1 maxItems: 6 description: 3-5 specific, self-contained sub-questions. required: [research_plan, questions] # On success plan_failure holds the node's structured JSON output; on # failure the engine writes "LLM node ...failed: " into it BEFORE # routing to the fallback (schema properties are only merged on success). state_updates: plan_failure: '{{output}}' fallback: plan_fault next: research_each_question plan_fault: id: plan_fault type: script description: Fail-closed marker for a dead planning stage (reached only via plan's fallback) — normalizes the failure text captured in plan_failure into a PIPELINE-FAULT and continues to end_fault; no plan means nothing to research, so the DEEP_RESEARCH FAILED sentinel always renders. The sibling knowledge_lookup lane survives one more super-step by mapping over the empty initial_state `questions` list. script: scripts/plan_fault.py fallback: end_fault next: end_fault knowledge_lookup: id: knowledge_lookup type: rag documents: - ./knowledge/ query: '{{topic}}' top_k: 6 chunk_size: 1000 chunk_overlap: 100 state_updates: local_context: '{{output.context}}' local_sources: '{{output.sources}}' next: research_each_question research_each_question: id: research_each_question type: map over: '{{questions}}' as: question branch: research_one_question collect_into: question_findings output_key: finding max_concurrency: 3 next: combine_findings research_one_question: id: research_one_question type: agent agent: librarian # The leaf research primitive: triage -> doc+OSS search -> cited # synthesis. `inputs:` overlays the librarian graph's initial state, # pinning research_depth to deep for every sub-question lane. prompt: | Research question: {{question}} Local context that may help: {{local_context}} {{research_feedback}} inputs: research_depth: deep timeout: 1900 max_attempts: 2 state_updates: finding: '{{output}}' # On failure the engine writes "Agent node failed: " into `finding` # via the state_updates above BEFORE routing to the fallback. The marker # is branch-local: a map branch's fallback must stay inside the branch. fallback: question_fault question_fault: id: question_fault type: script description: Fail-closed marker for a dead research lane (reached only via research_one_question's fallback route inside the map branch). Normalizes the engine's failure text in `finding` into a PIPELINE-FAULT finding the map collects — one dead lane never sinks the whole map. combine_findings later lifts PIPELINE-FAULT findings into pipeline_faults for the report's Pipeline notes. script: scripts/question_fault.py combine_findings: id: combine_findings type: script script: scripts/combine_findings.py next: vet_sources vet_sources: id: vet_sources type: llm instructions: | You assess the credibility of the sources cited in a set of research findings. For every distinct source URL in the findings, call the `classify_source` tool to get its credibility tier. Then summarize: which claims rest on HIGH-credibility sources, and which rest on PREPRINT or UNVERIFIED sources and so need corroboration. Do NOT do any new research -- assess only what is already cited. prompt: | Findings to assess: {{findings}} tools: - classify_source max_iterations: 15 state_updates: source_assessment: '{{output}}' fallback: vet_fault next: critique vet_fault: id: vet_fault type: script description: Degrade-visibly marker for a dead vetting stage (reached only via vet_sources' fallback) — vetting is a quality lane, not the product. Records a PIPELINE-FAULT, rewrites source_assessment to a neutral "unvetted" note, and continues to critique, the stage vet_sources' own next pointed to. script: scripts/vet_fault.py fallback: critique next: critique critique: id: critique type: llm instructions: | You are a meticulous research reviewer. Judge whether the findings below are good enough to synthesize a complete, well-supported report that answers the research plan. Mark the findings REVISE if ANY of these hold: - A research-plan question is unanswered or only weakly addressed. - A factual claim has no source, or cites a source that looks fabricated. - The findings lean on a single source where corroboration is needed. - A key claim rests only on a PREPRINT or UNVERIFIED source, per the source credibility assessment below. - An obvious counter-perspective or recent development is missing. Otherwise mark them PASS. Respond in EXACTLY this format, nothing else: VERDICT: FEEDBACK: prompt: | Research plan: {{research_plan}} Findings under review: {{findings}} Source credibility assessment: {{source_assessment}} tools: [] state_updates: critique: '{{output}}' fallback: critique_fault next: reflexion_gate critique_fault: id: critique_fault type: script description: Degrade-visibly marker for a dead critique stage (reached only via critique's fallback) — critique is a quality lane, not the product. Records a PIPELINE-FAULT and rewrites `critique` to a neutral note with deliberately NO "VERDICT:" line, so reflexion_gate's malformed-critique PASS default proceeds to synthesis; then continues to reflexion_gate, the stage critique's own next pointed to. script: scripts/critique_fault.py fallback: reflexion_gate next: reflexion_gate reflexion_gate: id: reflexion_gate type: script script: scripts/reflexion_gate.py next: synthesize synthesize: id: synthesize type: agent agent: report-writer prompt: | Research topic: {{topic}} Findings (organized by sub-question, with inline citations): {{findings}} Source credibility assessment: {{source_assessment}} Produce the final report following your instructions. timeout: 300 max_attempts: 2 state_updates: report: '{{output}}' # On failure the engine writes "Agent node failed: " into `report` # via the state_updates above BEFORE routing to the fallback. fallback: synth_fault next: verify_sources synth_fault: id: synth_fault type: script description: Fail-closed marker for a dead synthesis stage (reached only via synthesize's fallback) — no report means no product. Normalizes the "Agent node failed:" text captured in `report` into a PIPELINE-FAULT and continues to end_fault, which renders the DEEP_RESEARCH FAILED sentinel. script: scripts/synth_fault.py fallback: end_fault next: end_fault verify_sources: id: verify_sources type: script script: scripts/verify_sources.py next: approval_gate approval_gate: id: approval_gate type: script description: Autonomy router - autonomous runs (spawned by another agent) skip the human approval gate and end accepted with the source check + pipeline notes attached; supervised (default) proceeds to the approval node. A crash fails forward via `next` to approve, the safe human-gated direction. script: scripts/approval_gate.py next: approve approve: id: approve type: approval question: | Research report on: {{topic}} {{report}} ---- {{source_check}}{{pipeline_notes}} ---- Accept this report? Pick "accept" or "reject", or type specific feedback to send the research back for another pass. options: - 'accept' - 'reject' routes: 'accept': end_accepted 'reject': end_rejected on_other: incorporate_feedback state_updates: decision: '{{choice}}' incorporate_feedback: id: incorporate_feedback type: script script: scripts/incorporate_feedback.py end_accepted: id: end_accepted type: end output: '{{report}}{{pipeline_notes}}' end_autonomous: id: end_autonomous type: end output: | {{report}} ---- {{source_check}}{{pipeline_notes}} end_rejected: id: end_rejected type: end output: "Research on '{{topic}}' was rejected and discarded." end_fault: id: end_fault type: end output: 'DEEP_RESEARCH FAILED — PIPELINE-FAULT: {{fault_text}}'