1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4pub const DEEP_RESEARCH_COMMAND_NAME: &str = "deep-research";
5
6pub fn deep_research_workflow_source() -> &'static str {
7 DEEP_RESEARCH_WORKFLOW_SOURCE
8}
9
10pub fn deep_research_arguments(
11 question: impl Into<String>,
12 provider: Option<&dyn DeepResearchSearchProvider>,
13) -> Value {
14 let question = question.into();
15 let seed_results = provider
16 .map(|provider| provider.search(&question))
17 .unwrap_or_default();
18 json!({
19 "question": question,
20 "seedResults": seed_results
21 })
22}
23
24pub trait DeepResearchSearchProvider {
25 fn search(&self, query: &str) -> Vec<DeepResearchSearchResult>;
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "camelCase")]
30pub struct DeepResearchSearchResult {
31 pub title: String,
32 pub url: String,
33 pub snippet: String,
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase")]
38pub struct DeepResearchFixtureSearchProvider {
39 pub results: Vec<DeepResearchSearchResult>,
40}
41
42impl DeepResearchFixtureSearchProvider {
43 pub fn from_json_str(json: &str) -> serde_json::Result<Self> {
44 serde_json::from_str(json)
45 }
46}
47
48impl DeepResearchSearchProvider for DeepResearchFixtureSearchProvider {
49 fn search(&self, query: &str) -> Vec<DeepResearchSearchResult> {
50 let query_terms = normalized_terms(query);
51 let mut scored = self
52 .results
53 .iter()
54 .map(|result| (fixture_score(result, &query_terms), result))
55 .filter(|(score, _)| *score > 0)
56 .collect::<Vec<_>>();
57 scored.sort_by(|left, right| right.0.cmp(&left.0).then(left.1.title.cmp(&right.1.title)));
58 if scored.is_empty() {
59 return self.results.iter().take(4).cloned().collect();
60 }
61 scored
62 .into_iter()
63 .take(4)
64 .map(|(_, result)| result.clone())
65 .collect()
66 }
67}
68
69fn normalized_terms(value: &str) -> Vec<String> {
70 value
71 .split(|ch: char| !ch.is_ascii_alphanumeric())
72 .map(str::to_ascii_lowercase)
73 .filter(|term| term.len() > 2)
74 .collect()
75}
76
77fn fixture_score(result: &DeepResearchSearchResult, query_terms: &[String]) -> usize {
78 let text = format!("{} {}", result.title, result.snippet).to_ascii_lowercase();
79 query_terms
80 .iter()
81 .filter(|term| text.contains(term.as_str()))
82 .count()
83}
84
85const DEEP_RESEARCH_WORKFLOW_SOURCE: &str = r###"
86workflow.define({
87 name: "deep-research",
88 description: "Run a multi-agent deep research workflow with web search when available.",
89 hostApiVersion: 1,
90 argumentsSchema: {
91 type: "object",
92 additionalProperties: false,
93 required: ["question"],
94 properties: {
95 question: { type: "string", minLength: 1 },
96 queries: {
97 type: "array",
98 items: { type: "string" },
99 maxItems: 12
100 },
101 seedResults: {
102 type: "array",
103 items: {
104 type: "object",
105 required: ["title", "url", "snippet"],
106 properties: {
107 title: { type: "string" },
108 url: { type: "string" },
109 snippet: { type: "string" }
110 }
111 }
112 },
113 webSearchAvailable: {
114 type: "boolean"
115 }
116 }
117 },
118 phases: ["scope", "parallel-research", "synthesis", "verification"],
119 limits: {
120 maxConcurrentAgents: 8,
121 maxAgentsPerRun: 64,
122 defaultAgentTimeoutSeconds: 900,
123 defaultRunTimeoutSeconds: 7200,
124 maxReportBytes: 65536
125 }
126}, async (ctx) => {
127 const args = ctx.run.arguments || {};
128 const question = String(args.question || args.query || args.arguments || "").trim();
129 if (!question) {
130 throw new Error("deep-research requires a non-empty question");
131 }
132 const seedResults = Array.isArray(args.seedResults) ? args.seedResults : [];
133 const webSearchAvailable = args.webSearchAvailable !== false;
134 if (!webSearchAvailable && seedResults.length === 0) {
135 throw new Error("deep-research requires web-search capability or fixture seedResults");
136 }
137 const requestedQueries = Array.isArray(args.queries) && args.queries.length
138 ? args.queries.map((query) => String(query)).slice(0, 12)
139 : [
140 question,
141 `${question} current evidence`,
142 `${question} opposing evidence`,
143 `${question} implementation details`,
144 `${question} risks limitations`
145 ];
146
147 ctx.phase.start("scope");
148 const scope = await ctx.agents.run("research-lead", {
149 lane: "planning",
150 description: "scope the research question and source strategy",
151 prompt: `Scope this deep research question, identify likely source classes, and define acceptance criteria.\n\nQuestion: ${question}`,
152 output: `scope:${question}`
153 });
154 ctx.checkpoint.save("scope", {
155 question,
156 queryCount: requestedQueries.length,
157 seedResultCount: seedResults.length
158 });
159
160 ctx.phase.start("parallel-research");
161 const researchers = await ctx.agents.map("researcher", requestedQueries, (query, index) => ({
162 lane: "research",
163 description: `research query ${index + 1}`,
164 prompt: `Research this query for the deep research workflow.\n\nQuestion: ${question}\nQuery: ${query}\n\nUse Roder's canonical web-search tools when available. Prefer primary sources, quote sparingly, record URLs, and call out uncertainty. If fixture seed results are present, use them as offline evidence before adding live sources.\n\nFixture seed results:\n${JSON.stringify(seedResults)}`,
165 output: `research:${index + 1}:${query}`
166 }));
167 ctx.checkpoint.save("research", researchers.map((agent) => agent.output));
168
169 ctx.phase.start("synthesis");
170 const synthesis = await ctx.agents.run("synthesizer", {
171 lane: "synthesis",
172 description: "merge findings into a concise research answer",
173 prompt: `Synthesize the research outputs into a sourced answer.\n\nQuestion: ${question}\nScope: ${scope.output}\nResearch outputs:\n${researchers.map((agent) => agent.output).join("\n")}`,
174 output: `synthesis:${question}`
175 });
176
177 ctx.phase.start("verification");
178 const verification = await ctx.agents.run("verifier", {
179 lane: "verification",
180 description: "challenge the synthesis and check source quality",
181 prompt: `Verify this synthesis. Check for stale facts, unsupported claims, missing counterarguments, and source-quality issues.\n\nQuestion: ${question}\nSynthesis: ${synthesis.output}`,
182 output: `verification:${question}`
183 });
184
185 return ctx.report.markdown([
186 `# Deep research: ${question}`,
187 "",
188 `Scope: ${scope.output}`,
189 "",
190 "## Research lanes",
191 researchers.map((agent) => `- ${agent.output}`).join("\n"),
192 "",
193 `Synthesis: ${synthesis.output}`,
194 `Verification: ${verification.output}`
195 ]);
196});
197"###;