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
@@ -5,6 +5,9 @@ import com.correx.infrastructure.tools.filesystem.FileEditTool
import com.correx.infrastructure.tools.filesystem.FileReadTool
import com.correx.infrastructure.tools.filesystem.FileWriteTool
import com.correx.infrastructure.tools.shell.ShellTool
import com.correx.infrastructure.tools.web.WebFetchTool
import com.correx.infrastructure.tools.web.WebSearchTool
import io.ktor.client.HttpClient
import java.nio.file.Path
data class ToolConfig(
@@ -12,6 +15,20 @@ data class ToolConfig(
val fileRead: FileReadConfig = FileReadConfig(),
val fileWrite: FileWriteConfig = FileWriteConfig(),
val fileEdit: FileEditConfig = FileEditConfig(),
val research: ResearchToolConfig = ResearchToolConfig(),
)
/**
* Research tool registration. [httpClient] is the single shared client threaded in from the
* composition root (one per process, not per registry); when null the tools are not built even
* if [enabled], so tests and the static path never spin up a network client.
*/
class ResearchToolConfig(
val enabled: Boolean = false,
val searxngUrl: String = "http://localhost:8888",
val maxResults: Int = 8,
val maxFetchBytes: Long = 10_000_000,
val httpClient: HttpClient? = null,
)
data class FileReadConfig(
@@ -73,4 +90,8 @@ fun ToolConfig.buildTools(): List<Tool> = buildList {
),
)
}
research.httpClient?.takeIf { research.enabled }?.let { client ->
add(WebSearchTool(client, searxngBaseUrl = research.searxngUrl, maxResults = research.maxResults))
add(WebFetchTool(client, maxBytes = research.maxFetchBytes))
}
}