feat(research): wire tools + research workflow graph (research-workflow §2/§3)

Makes the research feature runnable end-to-end, off by default.

- config: [tools.research] (enabled, searxng_url, max_results, max_fetch_bytes).
- registration: web_search/web_fetch are built into BOTH the default and per-workspace
  tool registries when research.enabled, sharing one HTTP client threaded from Main
  (none built on the static path). Egress stays harness-enforced: web_fetch is T2
  (operator-approved) and the existing NetworkHostRule still applies.
- workflow: examples/workflows/research.toml — decompose → gather → report, with the
  three artifact schemas and prompts. Fan-out (search per sub-question, fetch per source)
  runs as repeated tool calls inside the gather stage (Correx has no parallel agents);
  per-source synthesis into the dossier is the compression step, so the report stage
  consumes summaries, never raw pages. ResearchWorkflowTest validates the graph contract.

To run: set [tools.research].enabled, register the 3 [[artifacts]], copy research.toml +
prompts + schemas into the workflows dir, start SearXNG. Launch like any workflow (the
T2 fetch approval surfaces as an approval card; the report opens in the artifact viewer).

Follow-ups (noted, not blocking): batch fetch-approval at the source-list level (§3),
a dedicated SourceFetched/LowQualityExtraction event (quality + content hash are already
in tool-result metadata), dynamic per-session egress allowlist, and the web approval client (§6).
This commit is contained in:
2026-06-13 23:18:19 +04:00
parent ad2d38ce46
commit 7a0d4d0ee2
11 changed files with 321 additions and 1 deletions
@@ -0,0 +1,20 @@
You are the **Research Planner** — the first stage of a deep-research workflow.
Your job is to turn one research question into a concrete plan to answer it. You do not search
or browse yet; you decompose.
Steps:
1. Restate the research question in your own words so intent is unambiguous.
2. Break it into a small set of sub-questions that can each be answered independently. Cover the
whole question; avoid overlap. Prefer 36 sub-questions over a long list.
3. For each sub-question, write one or more concrete web search queries — the actual strings you
would type into a search engine, specific enough to surface authoritative sources.
The decision history above (steering, approvals) is ground truth — honour it.
Emit your result as the `research_plan` artifact (JSON, schema provided):
- `question`: the research question, restated.
- `sub_questions`: the decomposed sub-questions, one per item.
- `search_queries`: concrete search queries to run, one per item.
Do not answer the question here. Plan only.
@@ -0,0 +1,21 @@
You are the **Research Gatherer** — you run the plan and build a dossier of summarized sources.
You have two tools:
- `web_search(query)` — searches the local SearXNG instance; returns result titles, URLs, snippets.
- `web_fetch(url)` — fetches a URL and returns its main content as clean markdown. Fetches are
approved by the operator before they run, so choose sources deliberately — quality over quantity.
Steps:
1. For each search query in the `research_plan` above, call `web_search`.
2. From the results, pick the most promising, authoritative sources. Skip duplicates, SEO spam,
and pages unlikely to contain primary information.
3. `web_fetch` each chosen source. If a fetch comes back empty or clearly low-quality (a paywall
or JS-only page), drop it and move on — do not retry it.
4. **Summarize each fetched source on its own**, in your own words: what it contributes to the
question and which sub-question(s) it bears on. This is the most important step — the next
stage sees only your summaries, never the raw pages, so a faithful summary is the whole product.
Never copy raw page text into a summary. Cite each source by its exact URL.
Emit your result as the `source_dossier` artifact (JSON, schema provided):
- `sources`: one entry per fetched source, each with `url`, `title`, `summary`, and `relevance`.
@@ -0,0 +1,19 @@
You are the **Research Synthesizer** — you write the final report from the source dossier.
You see only the `source_dossier` above: per-source summaries with their URLs. You do not have the
raw pages, and you do not search or fetch. Work from the summaries.
Steps:
1. Read across all source summaries and find where they agree, disagree, or leave gaps.
2. Answer the research question directly and concisely in the `summary`.
3. State the key findings, each supported by at least one source. Where sources conflict, say so
rather than picking silently.
4. Cite the sources you actually relied on by their exact URLs.
Do not invent facts that no source supports. If the dossier is insufficient to answer the
question, say what is missing in the `summary`.
Emit your result as the `research_report` artifact (JSON, schema provided):
- `summary`: the executive answer to the question.
- `findings`: the key findings, one per item, each grounded in a cited source.
- `sources`: the source URLs you cited.
+79
View File
@@ -0,0 +1,79 @@
# Deep research workflow: decompose -> gather (search + fetch) -> report
#
# A native Correx workflow (research-workflow-spec): one graph, two network tools, no new
# orchestration machinery. The fan-out the spec describes (search per sub-question, fetch per
# source) happens *inside* the gather stage as repeated tool calls — Correx runs stages linearly
# (no parallel agents), so the model drives the fan-out itself.
#
# Per-source synthesis is the compression mechanism, not a nicety: the gather stage summarizes
# each fetched page into the source_dossier and the raw page text is discarded. The report stage
# consumes only those summaries, so research survives the token budget.
#
# Requires (in ~/.config/correx/config.toml):
# [tools.research]
# enabled = true
# searxng_url = "http://localhost:8888" # your self-hosted SearXNG (format=json enabled)
# [[artifacts]]
# id = "research_plan"; schema_path = "schemas/research_plan.json"; llm_emitted = true
# [[artifacts]]
# id = "source_dossier"; schema_path = "schemas/source_dossier.json"; llm_emitted = true
# [[artifacts]]
# id = "research_report"; schema_path = "schemas/research_report.json"; llm_emitted = true
#
# web_fetch is T2: the operator approves fetches before they leave the machine. web_search is T1
# (it only ever touches the configured SearXNG instance).
id = "research"
start = "decompose"
description = "Deep research: decompose a question, search + fetch sources, synthesize a cited report."
# 1. Question -> sub-questions + search queries. Reuses the planning discipline; read-only.
[[stages]]
id = "decompose"
prompt = "prompts/research_decompose.md"
produces = [{ name = "research_plan", kind = "research_plan" }]
token_budget = 8192
max_retries = 2
# 2. Run the searches, fetch promising sources, and summarize each one (per-source synthesis).
# web_search (T1) hits only SearXNG; web_fetch (T2) is operator-approved per source.
[[stages]]
id = "gather"
prompt = "prompts/research_gather.md"
needs = ["research_plan"]
produces = [{ name = "source_dossier", kind = "source_dossier" }]
allowed_tools = ["web_search", "web_fetch"]
token_budget = 32768
max_retries = 2
# 3. Cross-source synthesis into a cited report. Consumes the dossier summaries only — never raw pages.
[[stages]]
id = "report"
prompt = "prompts/research_report.md"
needs = ["source_dossier"]
produces = [{ name = "research_report", kind = "research_report" }]
token_budget = 16384
max_retries = 2
# --- forward edges ---
[[transitions]]
id = "decompose-to-gather"
from = "decompose"
to = "gather"
condition_type = "artifact_validated"
condition_artifact_id = "research_plan"
[[transitions]]
id = "gather-to-report"
from = "gather"
to = "report"
condition_type = "artifact_validated"
condition_artifact_id = "source_dossier"
[[transitions]]
id = "report-done"
from = "report"
to = "done"
condition_type = "artifact_validated"
condition_artifact_id = "research_report"