Skip to main content

agent_engine/tools/subagent/
oneshot.rs

1use super::super::{resolve_agent_prompt, Tool, ToolContext, NEXT_SUBAGENT_ID};
2pub use crate::runtime::subagent::SubagentResult;
3use crate::{AgentEvent, LlmEvent, Result, RuntimeError, SessionEvent};
4use serde_json::{json, Value};
5use std::sync::atomic::Ordering;
6use std::time::Duration;
7
8pub struct SubagentTool;
9
10#[async_trait::async_trait]
11impl Tool for SubagentTool {
12    fn origin(&self) -> crate::tools::ToolOrigin {
13        crate::tools::ToolOrigin::Builtin
14    }
15
16    fn name(&self) -> &str {
17        "subagent"
18    }
19
20    /// Each blocking subagent is its own actor: several dispatched in one
21    /// batch run side by side (the parent waits for all of them either way)
22    /// instead of one after another in the shared serial lane.
23    fn runs_independently(&self) -> bool {
24        true
25    }
26
27    fn description(&self) -> &str {
28        "Dispatch a one-shot subagent with a specific system prompt to perform a task. The subagent gets its own tool suite (bash, read, write, edit, grep, find, ls) and runs autonomously until done. Use this when you need the result before continuing. Blocks until done. For parallel work, use subagent_start instead. Provide either an agent name (resolves from ~/.synaps-cli/agents/<name>.md) or a system_prompt string directly; give an inline subagent a short name so it is recognisable in the subagent panel."
29    }
30
31    fn parameters(&self) -> Value {
32        json!({
33            "type": "object",
34            "properties": {
35                "agent": {
36                    "type": "string",
37                    "description": "Agent name — resolves to ~/.synaps-cli/agents/<name>.md. Mutually exclusive with system_prompt."
38                },
39                "system_prompt": {
40                    "type": "string",
41                    "description": "Inline system prompt for the subagent. Use when you don't have a named agent file."
42                },
43                "name": {
44                    "type": "string",
45                    "description": "Short display name for an inline (system_prompt) subagent, e.g. \"gif-recorder\". Shown in the subagent panel and in status/collect output. Letters, digits, - and _ only, max 32. Ignored when agent is set; defaults to \"inline\"."
46                },
47                "task": {
48                    "type": "string",
49                    "description": "The task/prompt to send to the subagent."
50                },
51                "model": {"type": "string", "description": "Omit to inherit the session foreground qualified identity. Explicit values must be one of subagent_models' listed exact choices."},
52                "role": {"type": "string", "enum": ["planner", "implementer", "tester", "reviewer", "researcher", "debugger"]},
53                "write_policy": {
54                    "oneOf": [
55                        {"type": "object", "properties": {"mode": {"const": "read_only"}}, "required": ["mode"]},
56                        {"type": "object", "properties": {"mode": {"const": "isolated_worktree"}}, "required": ["mode"]},
57                        {"type": "object", "properties": {"mode": {"const": "non_overlapping_paths"}, "scopes": {"type": "array", "items": {"type": "string"}}}, "required": ["mode", "scopes"]}
58                    ]
59                },
60                "thinking": {
61                    "type": "string",
62                    "enum": ["low", "medium", "high", "xhigh", "max"],
63                    "description": "Worker effort override. Omit for subagent_thinking (default xhigh), independent of foreground thinking. Unsupported model levels are refused."
64                },
65                "timeout": {
66                    "type": "integer",
67                    "description": "Wall-clock limit in seconds. Omit for the session default (config `subagent_timeout`); 0 = no limit."
68                }
69            },
70            "required": ["task"]
71        })
72    }
73
74    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
75        // One config snapshot for the whole spawn (`spawn_config`).
76        let config = super::spawn_config();
77        let thinking = super::resolve_subagent_thinking(&params, None, &config)?;
78        let task = params["task"]
79            .as_str()
80            .ok_or_else(|| RuntimeError::Tool("Missing 'task' parameter".to_string()))?
81            .to_string();
82
83        // Treat blank / whitespace / control-char strings as absent — see
84        // subagent_start.rs for full rationale. Models occasionally pass `agent: ""`
85        // (or "\u{0}", or " ") alongside a real `system_prompt`; without this filter
86        // we'd try to resolve an empty agent name and fail, instead of falling
87        // through to the inline prompt.
88        let is_blank = |s: &String| s.chars().all(|c| c.is_whitespace() || c.is_control());
89        let agent_name = params["agent"]
90            .as_str()
91            .map(|s| s.to_string())
92            .filter(|s| !is_blank(s));
93        let inline_prompt = params["system_prompt"]
94            .as_str()
95            .map(|s| s.to_string())
96            .filter(|s| !is_blank(s));
97        let requested_model = params["model"].as_str();
98        let system_prompt = match (&agent_name, &inline_prompt) {
99            (Some(name), _) => resolve_agent_prompt(name).map_err(RuntimeError::Tool)?,
100            (None, Some(prompt)) => prompt.clone(),
101            (None, None) => {
102                return Err(RuntimeError::Tool(
103                    "Must provide either 'agent' (name) or 'system_prompt' (inline). Got neither."
104                        .to_string(),
105                ));
106            }
107        };
108        let subagent_id = NEXT_SUBAGENT_ID.fetch_add(1, Ordering::Relaxed);
109        let orchestration_id = format!("sa_{}", subagent_id);
110        let decision = ctx
111            .capabilities
112            .orchestration
113            .as_ref()
114            .ok_or_else(|| RuntimeError::Tool("delegation policy unavailable".into()))?
115            .resolve_and_authorize(&orchestration_id, requested_model)
116            .map_err(|error| RuntimeError::Tool(error.to_string()))?;
117        let model = decision.model.as_str().to_owned();
118        let codex_parent_plan = ctx.capabilities.codex_parent_plan.clone();
119        let memory_backend = ctx.capabilities.memory_backend.clone();
120        let session_allow_all = ctx.capabilities.session_allow_all.clone();
121        let telemetry = ctx.capabilities.telemetry.clone();
122        let workspace = super::WorkerWorkspace::from_capabilities(&ctx.capabilities);
123        let timeout_secs = params["timeout"]
124            .as_u64()
125            .unwrap_or(ctx.limits.subagent_timeout);
126
127        let label = super::subagent_label(agent_name.as_deref(), params["name"].as_str());
128        let telemetry = telemetry.map(|t| t.for_worker(&label, &orchestration_id));
129        let task_preview: String = task.chars().take(80).collect();
130
131        tracing::info!(
132            "Dispatching subagent '{}' (id={}) with model {}",
133            label,
134            subagent_id,
135            model
136        );
137
138        if let Some(ref tx) = ctx.channels.tx_events {
139            let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentStart {
140                subagent_id,
141                agent_name: label.clone(),
142                task_preview: task_preview.clone(),
143            }));
144        }
145
146        let start_time = std::time::Instant::now();
147
148        let (result_tx, result_rx) =
149            tokio::sync::oneshot::channel::<std::result::Result<SubagentResult, String>>();
150        let label_inner = label.clone();
151        let model_inner = model.clone();
152        let tx_events_inner = ctx.channels.tx_events.clone();
153
154        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
155        let orchestration = ctx.capabilities.orchestration.as_ref().unwrap();
156        if let Err(error) = orchestration.mark_starting(&orchestration_id) {
157            orchestration.rollback(&orchestration_id);
158            return Err(RuntimeError::Tool(error));
159        }
160
161        // Names this worker's rolled-over context windows (`worker_context`).
162        let transcript_stem = orchestration_id.clone();
163        let thread_handle = match std::thread::Builder::new().spawn(move || {
164            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
165                let rt = match tokio::runtime::Builder::new_current_thread()
166                    .enable_all()
167                    .build()
168                {
169                    Ok(rt) => rt,
170                    Err(_) => {
171                        let _ = result_tx.send(Err("runtime initialization failed".into()));
172                        return;
173                    }
174                };
175
176                let result = rt.block_on(async move {
177                    use futures::StreamExt;
178
179                    // Host-built worker (shared client/creds/token cache, cached
180                    // registry) or the legacy fresh runtime — see `spawn_runtime`.
181                    let mut runtime = match super::spawn_runtime().await {
182                        Ok(r) => r,
183                        Err(_) => return Err("subagent runtime initialization failed".into()),
184                    };
185
186                    // Apply subagent spawn policy: worker role, 5m cache TTL, worker
187                    // turn budget. Subagents are short-lived one-shots — paying the 1h
188                    // write premium (~2× input price) on them is unrecoverable waste
189                    // (~$0.23 per 10-spawn fan-out). (#110)
190                    super::apply_subagent_runtime_policy(&mut runtime, &config, memory_backend.as_ref(), session_allow_all.as_ref(), telemetry.as_ref(), &workspace);
191                    runtime.set_system_prompt(super::compose_system_prompt(system_prompt, runtime.memory_backend_is_axel()));
192                    runtime.set_model(model);
193                    super::fit_subagent_context_window(&mut runtime);
194                    super::apply_codex_worker_reasoning(&mut runtime, codex_parent_plan.as_ref());
195                    super::apply_subagent_thinking(&mut runtime, thinking)?;
196                    // Rolled-over context goes to the worker archive directory.
197                    super::attach_worker_transcripts(
198                        &mut runtime,
199                        super::archive::process_dir(),
200                        &transcript_stem,
201                        None,
202                        &[],
203                        tx_events_inner.clone(),
204                        subagent_id,
205                        &label_inner,
206                    );
207
208                    let cancel = crate::CancellationToken::new();
209                    let cancel_inner = cancel.clone();
210
211                    tokio::spawn(async move {
212                        let _ = shutdown_rx.await;
213                        cancel_inner.cancel();
214                    });
215
216                    let mut stream = runtime.run_stream(task, cancel).await;
217
218                let mut final_text = String::new();
219                let mut tool_count = 0u32;
220                let mut tool_log: Vec<String> = Vec::new();
221                let mut response_baseline: (usize, usize, u32) = (0, 0, 0);
222                let mut total_input_tokens = 0u64;
223                let mut total_output_tokens = 0u64;
224                let mut total_cache_read = 0u64;
225                let mut total_cache_creation = 0u64;
226                // TTL split: None only if no turn ever reported one; otherwise summed.
227                let mut total_cache_5m: Option<u64> = None;
228                let mut total_cache_1h: Option<u64> = None;
229
230                // 0 = no limit (tokio clamps Duration::MAX to its far-future instant).
231                let timeout_fut = tokio::time::sleep(if timeout_secs == 0 {
232                    Duration::MAX
233                } else {
234                    Duration::from_secs(timeout_secs)
235                });
236                tokio::pin!(timeout_fut);
237
238                loop {
239                    tokio::select! {
240                        event = stream.next() => {
241                            let Some(event) = event else { break };
242                            crate::runtime::openai::codex_wire::forward_worker_web_search(
243                                &label_inner,
244                                &event,
245                                tx_events_inner.as_ref(),
246                            );
247                            match event {
248                                crate::StreamEvent::Llm(LlmEvent::Thinking(_)) => {
249                                    if let Some(ref tx) = tx_events_inner {
250                                        let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
251                                            subagent_id,
252                                            agent_name: label_inner.clone(),
253                                            status: "💭 thinking...".to_string(),
254                                        }));
255                                    }
256                                }
257                                crate::StreamEvent::Llm(LlmEvent::ResponseStart) => {
258                                    response_baseline = (final_text.len(), tool_log.len(), tool_count);
259                                }
260                                crate::StreamEvent::Llm(LlmEvent::ResponseReset) => {
261                                    final_text.truncate(response_baseline.0);
262                                    tool_log.truncate(response_baseline.1);
263                                    tool_count = response_baseline.2;
264                                }
265                                crate::StreamEvent::Llm(LlmEvent::Text(text)) => {
266                                    final_text.push_str(&text);
267                                }
268                                crate::StreamEvent::Llm(LlmEvent::ToolUseStart { tool_name: name, .. }) => {
269                                    tool_count += 1;
270                                    if let Some(ref tx) = tx_events_inner {
271                                        let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
272                                            subagent_id,
273                                            agent_name: label_inner.clone(),
274                                            status: format!("⚙ {} (tool #{})", name, tool_count),
275                                        }));
276                                    }
277                                }
278                                crate::StreamEvent::Llm(LlmEvent::ToolUse { tool_name, input, .. }) => {
279                                    let input_str = input.to_string();
280                                    let input_preview: String = input_str.chars().take(200).collect();
281                                    tool_log.push(format!("[tool_use]: {} — {}", tool_name, input_preview));
282                                    // Build a rich status from the tool input
283                                    let detail = match tool_name.as_str() {
284                                        "bash" => {
285                                            let cmd = input["command"].as_str().unwrap_or("");
286                                            let preview: String = cmd.chars().take(60).collect();
287                                            format!("$ {}", preview)
288                                        }
289                                        "read" => {
290                                            let path = input["path"].as_str().unwrap_or("?");
291                                            let short = path.rsplit('/').next().unwrap_or(path);
292                                            format!("reading {}", short)
293                                        }
294                                        "write" => {
295                                            let path = input["path"].as_str().unwrap_or("?");
296                                            let short = path.rsplit('/').next().unwrap_or(path);
297                                            format!("writing {}", short)
298                                        }
299                                        "edit" => {
300                                            let path = input["path"].as_str().unwrap_or("?");
301                                            let short = path.rsplit('/').next().unwrap_or(path);
302                                            format!("editing {}", short)
303                                        }
304                                        "multi_edit" => crate::tools::multi_edit_label(&input),
305                                        "grep" => {
306                                            let pat = input["pattern"].as_str().unwrap_or("?");
307                                            let preview: String = pat.chars().take(30).collect();
308                                            format!("grep /{}/", preview)
309                                        }
310                                        "find" => {
311                                            let pat = input["pattern"].as_str().unwrap_or("?");
312                                            format!("find {}", pat)
313                                        }
314                                        "ls" => {
315                                            let path = input["path"].as_str().unwrap_or(".");
316                                            let short = path.rsplit('/').next().unwrap_or(path);
317                                            format!("ls {}", short)
318                                        }
319                                        "subagent" => {
320                                            let agent = input["agent"].as_str()
321                                                .filter(|s| !s.trim().is_empty());
322                                            let name = super::subagent_label(agent, input["name"].as_str());
323                                            format!("spawning {}", name)
324                                        }
325                                        other => {
326                                            // MCP or unknown tools — show tool name + first param
327                                            let short_name = if other.starts_with("ext__") {
328                                                other.splitn(3, "__").last().unwrap_or(other)
329                                            } else {
330                                                other
331                                            };
332                                            short_name.to_string()
333                                        }
334                                    };
335                                    if let Some(ref tx) = tx_events_inner {
336                                        let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
337                                            subagent_id,
338                                            agent_name: label_inner.clone(),
339                                            status: detail,
340                                        }));
341                                    }
342                                }
343                                crate::StreamEvent::Llm(LlmEvent::ToolResult { result, .. }) => {
344                                    let preview: String = result.chars().take(300).collect();
345                                    tool_log.push(format!("[tool_result]: {}", preview));
346                                }
347                                crate::StreamEvent::Session(SessionEvent::Usage {
348                                    input_tokens, output_tokens,
349                                    cache_read_input_tokens, cache_creation_input_tokens,
350                                    cache_creation_5m, cache_creation_1h,
351                                    model: _,
352                                }) => {
353                                    total_input_tokens += input_tokens;
354                                    total_output_tokens += output_tokens;
355                                    total_cache_read += cache_read_input_tokens;
356                                    total_cache_creation += cache_creation_input_tokens;
357                                    crate::core::rpc_dispatch::merge_split(&mut total_cache_5m, cache_creation_5m);
358                                    crate::core::rpc_dispatch::merge_split(&mut total_cache_1h, cache_creation_1h);
359                                }
360                                crate::StreamEvent::Session(SessionEvent::Error(e)) => {
361                                    // Typed outcome from the engine — keep the
362                                    // terminal category + correlation ID in the
363                                    // subagent's failure state (spec §5.2).
364                                    return Err(format!("{} [{}]", e.message, e.category_label()));
365                                }
366                                crate::StreamEvent::Session(SessionEvent::Done) => break,
367                                _ => {}
368                            }
369                        }
370                        _ = &mut timeout_fut => {
371                            // Return partial work instead of just an error
372                            let mut partial = format!("[TIMED OUT after {}s — partial results below]\n\n", timeout_secs);
373                            if !tool_log.is_empty() {
374                                partial.push_str(&tool_log.join("\n"));
375                                partial.push('\n');
376                            }
377                            if !final_text.is_empty() {
378                                partial.push_str("\n[partial response]:\n");
379                                partial.push_str(&final_text);
380                            }
381                            return Ok(SubagentResult {
382                                text: partial,
383                                model: model_inner,
384                                input_tokens: total_input_tokens,
385                                output_tokens: total_output_tokens,
386                                cache_read: total_cache_read,
387                                cache_creation: total_cache_creation,
388                                cache_creation_5m: total_cache_5m,
389                                cache_creation_1h: total_cache_1h,
390                                tool_count,
391                                timed_out: true,
392                            });
393                        }
394                    }
395                }
396
397                Ok(SubagentResult {
398                    text: final_text,
399                    model: model_inner,
400                    input_tokens: total_input_tokens,
401                    output_tokens: total_output_tokens,
402                    cache_read: total_cache_read,
403                    cache_creation: total_cache_creation,
404                    cache_creation_5m: total_cache_5m,
405                    cache_creation_1h: total_cache_1h,
406                    tool_count,
407                    timed_out: false,
408                })
409            });
410
411                let _ = result_tx.send(result);
412            }));
413
414            if let Err(panic_info) = result {
415                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
416                    s.to_string()
417                } else if let Some(s) = panic_info.downcast_ref::<String>() {
418                    s.clone()
419                } else {
420                    "unknown panic".to_string()
421                };
422                tracing::error!("Subagent thread panicked: {}", msg);
423                // result_tx is consumed inside the closure, so we can't send here —
424                // the oneshot receiver will see a RecvError, handled below.
425            }
426        }) {
427            Ok(handle) => handle,
428            Err(_) => {
429                orchestration.rollback(&orchestration_id);
430                return Err(RuntimeError::Tool("worker thread initialization failed".into()));
431            }
432        };
433        if let Err(error) = orchestration.mark_running(&orchestration_id) {
434            drop(shutdown_tx);
435            let _ = thread_handle.join();
436            orchestration.rollback(&orchestration_id);
437            return Err(RuntimeError::Tool(error));
438        }
439
440        let result = result_rx.await;
441        let elapsed = start_time.elapsed().as_secs_f64();
442
443        drop(shutdown_tx);
444        let _ = thread_handle.join();
445
446        // One-shot workers have no separate collect call: every outcome must pass
447        // through terminal, collected, and reconciled before this tool exits.
448        if let Some(policy) = &ctx.capabilities.orchestration {
449            let terminal = match &result {
450                Ok(Ok(result)) if result.timed_out => {
451                    agent_core::orchestration::WorkerTerminal::TimedOut
452                }
453                Ok(Ok(_)) => agent_core::orchestration::WorkerTerminal::Completed,
454                Ok(Err(_)) | Err(_) => agent_core::orchestration::WorkerTerminal::Failed,
455            };
456            policy
457                .finish_one_shot(&orchestration_id, terminal)
458                .map_err(RuntimeError::Tool)?;
459        }
460
461        let log_dir = crate::config::base_dir().join("logs").join("subagents");
462        let _ = tokio::fs::create_dir_all(&log_dir).await;
463        let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S").to_string();
464
465        match result {
466            Ok(Ok(sa_result)) => {
467                let preview: String = sa_result.text.chars().take(120).collect();
468
469                if let Some(ref tx) = ctx.channels.tx_events {
470                    let _ = tx.send(crate::StreamEvent::Session(SessionEvent::Usage {
471                        input_tokens: sa_result.input_tokens,
472                        output_tokens: sa_result.output_tokens,
473                        cache_read_input_tokens: sa_result.cache_read,
474                        cache_creation_input_tokens: sa_result.cache_creation,
475                        // TTL split aggregated across the subagent's turns —
476                        // forwarded so the parent bills 1h writes at 2.0x, not 1.25x.
477                        cache_creation_5m: sa_result.cache_creation_5m,
478                        cache_creation_1h: sa_result.cache_creation_1h,
479                        model: Some(sa_result.model),
480                    }));
481                    let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentDone {
482                        subagent_id,
483                        agent_name: label.clone(),
484                        result_preview: preview,
485                        duration_secs: elapsed,
486                    }));
487                }
488
489                let log_content = format!(
490                    "# Subagent: {}\nDate: {}\nModel: {}\nTask: {}\nDuration: {:.1}s\nTokens: {}in/{}out ({}cr/{}cw)\nTools used: {}\n\n## Result\n\n{}\n",
491                    label, timestamp, params["model"].as_str().unwrap_or("sonnet"),
492                    task_preview, elapsed,
493                    sa_result.input_tokens, sa_result.output_tokens,
494                    sa_result.cache_read, sa_result.cache_creation,
495                    sa_result.tool_count, sa_result.text,
496                );
497                let log_path = super::subagent_log_path(&log_dir, &timestamp, &label, false);
498                let _ = tokio::fs::write(&log_path, &log_content).await;
499
500                Ok(format!("[subagent:{}] {}", label, sa_result.text))
501            }
502            Ok(Err(e)) => {
503                if let Some(ref tx) = ctx.channels.tx_events {
504                    let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentDone {
505                        subagent_id,
506                        agent_name: label.clone(),
507                        result_preview: format!("ERROR: {}", e),
508                        duration_secs: elapsed,
509                    }));
510                }
511                let log_path = super::subagent_log_path(&log_dir, &timestamp, &label, true);
512                let _ = tokio::fs::write(
513                    &log_path,
514                    format!(
515                        "# Subagent ERROR: {}\nTask: {}\nError: {}\n",
516                        label, task_preview, e
517                    ),
518                )
519                .await;
520                Ok(format!("[subagent:{} ERROR] {}", label, e))
521            }
522            Err(_) => {
523                if let Some(ref tx) = ctx.channels.tx_events {
524                    let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentDone {
525                        subagent_id,
526                        agent_name: label.clone(),
527                        result_preview: "Task panicked or dropped".to_string(),
528                        duration_secs: elapsed,
529                    }));
530                }
531                Ok(format!(
532                    "[subagent:{} ERROR] Subagent task panicked or was dropped",
533                    label
534                ))
535            }
536        }
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::tools::test_helpers::create_tool_context;
544    use crate::tools::Tool;
545    use serde_json::json;
546
547    #[test]
548    fn test_subagent_tool_schema() {
549        let tool = SubagentTool;
550        assert_eq!(tool.name(), "subagent");
551        assert!(!tool.description().is_empty());
552
553        let params = tool.parameters();
554        assert_eq!(params["type"], "object");
555        assert!(params["properties"].is_object());
556        assert!(params["required"].is_array());
557    }
558
559    // Runs a real worker: pin an empty base dir so it never reads the real
560    // auth.json or writes its error log under the real ~/.synaps-cli, and
561    // clear the environment's provider credentials/endpoints, so it fails
562    // at auth, offline.
563    #[tokio::test]
564    #[serial_test::serial(synaps_base_dir)]
565    async fn test_subagent_blank_agent_uses_system_prompt() {
566        let _base = crate::test_env::BaseDirGuard::new();
567        let _offline = crate::test_env::OfflineProviderEnv::new();
568        let tool = SubagentTool;
569        let ctx = create_tool_context();
570        let params = json!({
571            "agent": "",
572            "system_prompt": "You are a concise test subagent. Reply with only: ok",
573            "task": "Say ok",
574            "model": "anthropic/claude-sonnet-4-6",
575            "timeout": 1
576        });
577
578        let result = tool.execute(params, ctx).await;
579        assert!(
580            result.is_ok(),
581            "blank agent should not be resolved as ~/.synaps-cli/agents/.md: {result:?}"
582        );
583    }
584}