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
+23
View File
@@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "the research question, restated in your own words"
},
"sub_questions": {
"type": "array",
"description": "the question decomposed into concrete, independently-answerable sub-questions",
"minItems": 1,
"items": { "type": "string" }
},
"search_queries": {
"type": "array",
"description": "concrete web search queries to run, one or more per sub-question",
"minItems": 1,
"items": { "type": "string" }
}
},
"required": ["question", "sub_questions", "search_queries"],
"additionalProperties": true
}
+23
View File
@@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "the executive answer to the research question, synthesized across all sources"
},
"findings": {
"type": "array",
"description": "the key findings, one per item; each should be supported by at least one cited source",
"minItems": 1,
"items": { "type": "string" }
},
"sources": {
"type": "array",
"description": "the source URLs cited in the report (citations into the source dossier)",
"minItems": 1,
"items": { "type": "string" }
}
},
"required": ["summary", "findings", "sources"],
"additionalProperties": true
}
+29
View File
@@ -0,0 +1,29 @@
{
"type": "object",
"properties": {
"sources": {
"type": "array",
"description": "one entry per fetched source — each summarized on its own (per-source synthesis). This is the compression step: raw page text must never be copied here, only what the source contributes.",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"url": { "type": "string", "description": "the fetched source URL (a citation handle)" },
"title": { "type": "string" },
"summary": {
"type": "string",
"description": "what this source contributes to the question, in your own words — not a copy of the page"
},
"relevance": {
"type": "string",
"description": "which sub-question(s) this source bears on, and how strongly"
}
},
"required": ["url", "summary"],
"additionalProperties": true
}
}
},
"required": ["sources"],
"additionalProperties": true
}