Skip to main content

vv_agent/
runner.rs

1mod event_stream;
2mod session_blocking;
3mod support;
4
5use std::path::PathBuf;
6use std::sync::{Arc, Mutex};
7
8use serde_json::Value;
9use tokio::sync::broadcast;
10
11use crate::agent::Agent;
12use crate::approval::ApprovalBroker;
13use crate::config::apply_resolved_model_limits;
14use crate::context::RunContext;
15use crate::context_providers::{
16    assemble_context_fragments, collect_context_fragments, ContextBundle, ContextRequest,
17};
18use crate::events::{RunEvent, RunEventPayload};
19use crate::llm::LlmClient;
20use crate::model::{ModelError, ModelProvider, VvLlmModelProvider};
21use crate::result::{RunResult, RunResumeContext, RunState};
22use crate::run_config::RunConfig;
23use crate::run_handle::{RunEventSenderSlot, RunHandle, RunHandleState, SharedRunResult};
24use crate::runtime::{AgentRuntime, ExecutionContext, RuntimeRunControls};
25use crate::sessions::SessionItem;
26use crate::tools::ToolRegistry;
27use crate::tracing::Span;
28use crate::types::{AgentTask, NoToolPolicy, ToolResultStatus};
29use crate::workspace::LocalWorkspaceBackend;
30
31use event_stream::map_runtime_event;
32pub use event_stream::RunEventStream;
33use session_blocking::block_on_session;
34use support::{
35    apply_input_guardrails, apply_output_guardrails, approval_required, capture_event,
36    completed_from_first_tool_result, effective_event_store, extract_handoff,
37    find_approved_tool_call, insert_context_metadata, max_handoff_depth, merged_tool_policy,
38    ApprovalHook, SingleRunOutcome,
39};
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct NormalizedInput {
43    pub text: String,
44}
45
46impl From<&str> for NormalizedInput {
47    fn from(value: &str) -> Self {
48        Self {
49            text: value.to_string(),
50        }
51    }
52}
53
54impl From<String> for NormalizedInput {
55    fn from(value: String) -> Self {
56        Self { text: value }
57    }
58}
59
60#[derive(Clone)]
61pub struct Runner {
62    model_provider: Arc<dyn ModelProvider>,
63    workspace: PathBuf,
64    tool_registry: ToolRegistry,
65    default_run_config: RunConfig,
66}
67
68impl Runner {
69    pub fn builder() -> RunnerBuilder {
70        RunnerBuilder::default()
71    }
72
73    pub async fn run(
74        &self,
75        agent: &Agent,
76        input: impl Into<NormalizedInput>,
77    ) -> Result<RunResult, String> {
78        self.run_with_config(agent, input, RunConfig::default())
79            .await
80    }
81
82    pub async fn run_with_config(
83        &self,
84        agent: &Agent,
85        input: impl Into<NormalizedInput>,
86        config: RunConfig,
87    ) -> Result<RunResult, String> {
88        self.run_blocking(agent, input.into(), config, None)
89    }
90
91    pub async fn resume(&self, state: RunState) -> Result<RunResult, String> {
92        let (source, approved_ids) = state.into_inner();
93        let Some(resume_context) = source.resume_context().cloned() else {
94            return Err("run state does not include resume context".to_string());
95        };
96        if let Some(result) =
97            self.resume_approved_tool_call(&source, &resume_context, &approved_ids)
98        {
99            return result;
100        }
101        let mut config = resume_context.config;
102        config.metadata.insert(
103            "approved_tool_interruption_ids".to_string(),
104            Value::Array(approved_ids.iter().cloned().map(Value::String).collect()),
105        );
106        let mut result = self
107            .run_with_config(&resume_context.agent, resume_context.input, config)
108            .await
109            .map_err(|error| format!("resume failed: {error}"))?;
110        if result.status() == crate::types::AgentStatus::MaxCycles {
111            result = completed_from_first_tool_result(result);
112        }
113        Ok(result)
114    }
115
116    fn resume_approved_tool_call(
117        &self,
118        source: &RunResult,
119        resume_context: &RunResumeContext,
120        approved_ids: &[String],
121    ) -> Option<Result<RunResult, String>> {
122        let approval = find_approved_tool_call(source.result(), approved_ids)?;
123        let mut registry = self.tool_registry.clone();
124        for handoff in resume_context.agent.handoffs() {
125            if let Err(error) = registry.register(handoff.as_tool_spec(resume_context.agent.name()))
126            {
127                return Some(Err(error));
128            }
129        }
130        for tool in resume_context.agent.tools() {
131            if let Err(error) = registry.register(tool.as_tool_spec()) {
132                return Some(Err(error));
133            }
134        }
135        let workspace = resume_context
136            .config
137            .workspace
138            .clone()
139            .or_else(|| self.default_run_config.workspace.clone())
140            .unwrap_or_else(|| self.workspace.clone());
141        let workspace_backend = resume_context
142            .config
143            .workspace_backend
144            .clone()
145            .or_else(|| self.default_run_config.workspace_backend.clone())
146            .unwrap_or_else(|| Arc::new(LocalWorkspaceBackend::new(workspace.clone())));
147        let mut context = crate::tools::ToolContext {
148            workspace: workspace.clone(),
149            shared_state: source.result().shared_state.clone(),
150            cycle_index: approval.cycle_index,
151            task_id: format!("{}_run", resume_context.agent.name()),
152            metadata: resume_context.agent.metadata().clone(),
153            workspace_backend,
154            model_provider: Some(self.model_provider.clone()),
155            sub_task_runner: None,
156            sub_task_manager: None,
157            execution_backend: None,
158        };
159        context
160            .metadata
161            .extend(resume_context.config.metadata.clone());
162        context
163            .metadata
164            .entry("agent_name".to_string())
165            .or_insert_with(|| Value::String(resume_context.agent.name().to_string()));
166        let tool_result = crate::tools::dispatch_tool_call(&registry, &mut context, &approval.call);
167        if tool_result.status != ToolResultStatus::Success {
168            return Some(Err(tool_result.content));
169        }
170        let mut agent_result = source.result().clone();
171        agent_result.status = crate::types::AgentStatus::Completed;
172        agent_result.final_answer = Some(tool_result.content.clone());
173        if let Some(cycle) = agent_result
174            .cycles
175            .iter_mut()
176            .find(|cycle| cycle.index == approval.cycle_index)
177        {
178            cycle.tool_results.push(tool_result.clone());
179        }
180        agent_result.messages.push(tool_result.to_message());
181        Some(Ok(RunResult::new(
182            resume_context.agent.name().to_string(),
183            agent_result,
184            source.resolved_model().clone(),
185        )
186        .with_resume_context(resume_context.clone())))
187    }
188
189    pub fn run_blocking(
190        &self,
191        agent: &Agent,
192        input: NormalizedInput,
193        config: RunConfig,
194        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
195    ) -> Result<RunResult, String> {
196        self.run_blocking_with_event_sender(agent, input, config, event_collector, None)
197    }
198
199    fn run_blocking_with_event_sender(
200        &self,
201        agent: &Agent,
202        input: NormalizedInput,
203        config: RunConfig,
204        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
205        event_sender: Option<broadcast::Sender<RunEvent>>,
206    ) -> Result<RunResult, String> {
207        self.run_agent_chain(agent, input, config, event_collector, event_sender)
208    }
209
210    fn run_agent_chain(
211        &self,
212        agent: &Agent,
213        input: NormalizedInput,
214        config: RunConfig,
215        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
216        event_sender: Option<broadcast::Sender<RunEvent>>,
217    ) -> Result<RunResult, String> {
218        let (event_store, event_store_fail_closed) =
219            effective_event_store(&self.default_run_config, &config);
220        let mut current_agent = agent.clone();
221        let mut current_input = input;
222        for _ in 0..=max_handoff_depth(&config, &current_agent) {
223            let outcome = self.run_single_agent(
224                &current_agent,
225                current_input.clone(),
226                config.clone(),
227                event_collector.clone(),
228                event_sender.clone(),
229            )?;
230            let Some(handoff) = outcome.handoff else {
231                return Ok(outcome.result);
232            };
233            let target = current_agent
234                .handoffs()
235                .iter()
236                .find(|candidate| candidate.target().name() == handoff.to_agent)
237                .map(|candidate| candidate.target().clone())
238                .ok_or_else(|| {
239                    format!(
240                        "handoff target `{}` is not registered on agent `{}`",
241                        handoff.to_agent,
242                        current_agent.name()
243                    )
244                })?;
245            capture_event(
246                event_collector.as_ref(),
247                event_sender.as_ref(),
248                event_store.as_ref(),
249                event_store_fail_closed,
250                RunEvent::new(
251                    format!("{}_run", handoff.from_agent),
252                    format!("{}_run", handoff.from_agent),
253                    handoff.from_agent.clone(),
254                    None,
255                    RunEventPayload::HandoffStarted {
256                        source_agent: handoff.from_agent.clone(),
257                        target_agent: handoff.to_agent.clone(),
258                        tool_call_id: handoff.tool_call_id.clone(),
259                    },
260                ),
261            );
262            capture_event(
263                event_collector.as_ref(),
264                event_sender.as_ref(),
265                event_store.as_ref(),
266                event_store_fail_closed,
267                RunEvent::handoff_completed(
268                    format!("{}_run", handoff.from_agent),
269                    format!("{}_run", handoff.from_agent),
270                    handoff.from_agent.clone(),
271                    handoff.to_agent.clone(),
272                    handoff.tool_call_id.clone(),
273                ),
274            );
275            current_input = NormalizedInput {
276                text: handoff.input,
277            };
278            current_agent = target;
279        }
280        Err("maximum handoff depth exceeded".to_string())
281    }
282
283    fn run_single_agent(
284        &self,
285        agent: &Agent,
286        input: NormalizedInput,
287        config: RunConfig,
288        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
289        event_sender: Option<broadcast::Sender<RunEvent>>,
290    ) -> Result<SingleRunOutcome, String> {
291        let provider = config
292            .model_provider
293            .clone()
294            .or_else(|| self.default_run_config.model_provider.clone())
295            .unwrap_or_else(|| self.model_provider.clone());
296        let model_ref = config
297            .model
298            .clone()
299            .or_else(|| self.default_run_config.model.clone())
300            .or_else(|| agent.model().cloned())
301            .ok_or_else(|| "agent model is not configured".to_string())?;
302        let resolved = provider.resolve(&model_ref).map_err(format_model_error)?;
303        let llm = provider.client(&resolved).map_err(format_model_error)?;
304        let (event_store, event_store_fail_closed) =
305            effective_event_store(&self.default_run_config, &config);
306        let provider_settings = provider.default_settings(&resolved);
307        let settings = provider_settings
308            .merge(agent.model_settings())
309            .merge(
310                self.default_run_config
311                    .model_settings
312                    .as_ref()
313                    .unwrap_or(&crate::model_settings::ModelSettings::default()),
314            )
315            .merge(
316                config
317                    .model_settings
318                    .as_ref()
319                    .unwrap_or(&crate::model_settings::ModelSettings::default()),
320            );
321        let workspace = config
322            .workspace
323            .clone()
324            .or_else(|| self.default_run_config.workspace.clone())
325            .unwrap_or_else(|| self.workspace.clone());
326        let session = config
327            .session
328            .clone()
329            .or_else(|| self.default_run_config.session.clone());
330        let session_items = if let Some(session) = session.as_ref() {
331            block_on_session(session.get_items(None))?
332        } else {
333            Vec::new()
334        };
335        let tool_policy = merged_tool_policy(
336            agent.tool_policy(),
337            &self.default_run_config.tool_policy,
338            &config.tool_policy,
339        );
340        let approval_provider = config
341            .approval_provider
342            .clone()
343            .or_else(|| self.default_run_config.approval_provider.clone());
344        let approval_broker = config
345            .approval_broker
346            .clone()
347            .or_else(|| self.default_run_config.approval_broker.clone())
348            .or_else(|| {
349                approval_provider
350                    .as_ref()
351                    .map(|_| ApprovalBroker::default())
352            });
353        let approval_timeout = config
354            .approval_timeout
355            .or(self.default_run_config.approval_timeout);
356        let memory_providers = self
357            .default_run_config
358            .memory_providers
359            .iter()
360            .chain(config.memory_providers.iter())
361            .cloned()
362            .collect::<Vec<_>>();
363        let run_context = RunContext {
364            run_id: format!("{}_run", agent.name()),
365            agent_name: agent.name().to_string(),
366            model: Some(model_ref.clone()),
367            workspace: Some(workspace.clone()),
368            metadata: config.metadata.clone(),
369        };
370        let guarded_input = apply_input_guardrails(agent, &run_context, input)?;
371        let input_text = guarded_input.text;
372        let (instructions, context_bundle) =
373            self.build_instructions_with_context(agent, &input_text, &config)?;
374        let mut task = AgentTask::new(
375            format!("{}_run", agent.name()),
376            resolved.model_id.clone(),
377            instructions,
378            input_text.clone(),
379        );
380        task.max_cycles = config
381            .max_cycles
382            .or(self.default_run_config.max_cycles)
383            .or(agent.max_cycles())
384            .unwrap_or(10)
385            .max(1);
386        task.no_tool_policy = NoToolPolicy::Continue;
387        task.metadata = agent.metadata().clone();
388        task.metadata
389            .extend(self.default_run_config.metadata.clone());
390        task.metadata.extend(config.metadata.clone());
391        task.metadata
392            .entry("agent_name".to_string())
393            .or_insert_with(|| Value::String(agent.name().to_string()));
394        if let Some(context_bundle) = context_bundle {
395            insert_context_metadata(&mut task.metadata, &context_bundle);
396        }
397        task.metadata
398            .insert("model_settings".to_string(), settings.to_value());
399        task.metadata.insert(
400            "runtime_model".to_string(),
401            serde_json::to_value(&resolved).unwrap_or(Value::Null),
402        );
403        apply_resolved_model_limits(&mut task, &resolved);
404        task.initial_messages = session_items
405            .iter()
406            .map(SessionItem::to_message)
407            .collect::<Vec<_>>();
408        let mut registry = self.tool_registry.clone();
409        for handoff in agent.handoffs() {
410            registry.register(handoff.as_tool_spec(agent.name()))?;
411        }
412        for tool in agent.tools() {
413            registry.register(tool.as_tool_spec())?;
414        }
415        let mut runtime = AgentRuntime::new(ArcLlmClient(llm))
416            .with_tool_registry(registry)
417            .with_settings_file("__runner_model_provider__")
418            .with_default_backend(resolved.backend.clone());
419        runtime.default_workspace = Some(workspace.clone());
420        runtime.workspace_backend = config
421            .workspace_backend
422            .clone()
423            .or_else(|| self.default_run_config.workspace_backend.clone())
424            .unwrap_or_else(|| Arc::new(LocalWorkspaceBackend::new(workspace.clone())));
425        if let Some(execution_backend) = config
426            .execution_backend
427            .clone()
428            .or_else(|| self.default_run_config.execution_backend.clone())
429        {
430            runtime.execution_backend = execution_backend;
431        }
432        runtime.hooks.extend(agent.hooks().iter().cloned());
433        runtime
434            .hooks
435            .extend(self.default_run_config.hooks.iter().cloned());
436        runtime.hooks.extend(config.hooks.iter().cloned());
437        if approval_required(&tool_policy) {
438            runtime.hooks.push(Arc::new(ApprovalHook::new(
439                tool_policy.clone(),
440                task.metadata.clone(),
441            )));
442        }
443        let log_handler =
444            if event_collector.is_some() || event_sender.is_some() || event_store.is_some() {
445                let collector = event_collector.clone();
446                let event_sender = event_sender.clone();
447                let event_store = event_store.clone();
448                Some(Arc::new(
449                    move |event: &str, payload: &std::collections::BTreeMap<String, Value>| {
450                        if let Some(mapped) = map_runtime_event(event, payload) {
451                            capture_event(
452                                collector.as_ref(),
453                                event_sender.as_ref(),
454                                event_store.as_ref(),
455                                event_store_fail_closed,
456                                mapped,
457                            );
458                        }
459                    },
460                ) as crate::runtime::RuntimeEventHandler)
461            } else {
462                None
463            };
464        let controls = RuntimeRunControls {
465            log_handler,
466            cancellation_token: config
467                .cancellation_token
468                .clone()
469                .or_else(|| self.default_run_config.cancellation_token.clone()),
470            execution_context: Some(ExecutionContext {
471                metadata: task.metadata.clone(),
472                approval_provider,
473                approval_broker,
474                approval_timeout,
475                memory_providers,
476                ..ExecutionContext::default()
477            }),
478            workspace: Some(workspace),
479            workspace_backend: runtime.workspace_backend.clone().into(),
480            model_provider: Some(provider.clone()),
481            ..RuntimeRunControls::default()
482        };
483        let trace_sink = config
484            .trace_sink
485            .clone()
486            .or_else(|| self.default_run_config.trace_sink.clone());
487        let run_span = Span::new(
488            format!("{}_run", agent.name()),
489            "run",
490            Some(agent.name().to_string()),
491        );
492        let agent_span = Span::new(
493            format!("{}_run", agent.name()),
494            "agent",
495            Some(agent.name().to_string()),
496        );
497        if let Some(trace_sink) = trace_sink.as_ref() {
498            trace_sink.on_span_start(&run_span);
499            trace_sink.on_span_start(&agent_span);
500        }
501        let result = runtime
502            .run_with_controls(task, controls)
503            .map_err(|error| error.to_string())?;
504        if let Some(trace_sink) = trace_sink.as_ref() {
505            trace_sink.on_span_end(&agent_span);
506            trace_sink.on_span_end(&run_span);
507            trace_sink.flush()?;
508        }
509        let result = apply_output_guardrails(agent, &run_context, result);
510        let handoff = extract_handoff(&result);
511        if let Some(session) = session.as_ref() {
512            let mut new_items = Vec::new();
513            new_items.push(SessionItem::User {
514                content: input_text.clone(),
515            });
516            if handoff.is_none() {
517                if let Some(answer) = result.final_answer.as_ref() {
518                    new_items.push(SessionItem::Assistant {
519                        content: answer.clone(),
520                    });
521                }
522            } else if let Some(handoff) = handoff.as_ref() {
523                new_items.push(SessionItem::Assistant {
524                    content: format!(
525                        "Handed off from {} to {}.",
526                        handoff.from_agent, handoff.to_agent
527                    ),
528                });
529                if !handoff.input.is_empty() {
530                    new_items.push(SessionItem::User {
531                        content: handoff.input.clone(),
532                    });
533                }
534            }
535            block_on_session(session.add_items(new_items))?;
536        }
537        Ok(SingleRunOutcome {
538            result: RunResult::new(agent.name().to_string(), result, resolved).with_resume_context(
539                RunResumeContext {
540                    agent: agent.clone(),
541                    input: NormalizedInput {
542                        text: input_text.clone(),
543                    },
544                    config,
545                },
546            ),
547            handoff,
548        })
549    }
550
551    pub async fn stream(
552        &self,
553        agent: &Agent,
554        input: impl Into<NormalizedInput>,
555    ) -> Result<RunEventStream, String> {
556        let handle = self.start(agent, input, RunConfig::default()).await?;
557        Ok(handle.into_event_stream())
558    }
559
560    pub async fn start(
561        &self,
562        agent: &Agent,
563        input: impl Into<NormalizedInput>,
564        mut config: RunConfig,
565    ) -> Result<RunHandle, String> {
566        let cancellation_token = config
567            .cancellation_token
568            .clone()
569            .or_else(|| self.default_run_config.cancellation_token.clone())
570            .unwrap_or_default();
571        config.cancellation_token = Some(cancellation_token.clone());
572        let approval_broker = config
573            .approval_broker
574            .clone()
575            .or_else(|| self.default_run_config.approval_broker.clone())
576            .unwrap_or_default();
577        config.approval_broker = Some(approval_broker.clone());
578
579        let (event_sender, _) = broadcast::channel(1024);
580        let event_collector = Arc::new(Mutex::new(Vec::new()));
581        let event_sender_slot: RunEventSenderSlot =
582            Arc::new(Mutex::new(Some(event_sender.clone())));
583        let state = Arc::new(Mutex::new(RunHandleState::running()));
584        let runner = self.clone();
585        let agent = agent.clone();
586        let input = input.into();
587        let state_for_task = state.clone();
588        let sender_slot_for_task = event_sender_slot.clone();
589        let event_collector_for_task = event_collector.clone();
590        let cancellation_token_for_task = cancellation_token.clone();
591        let join = tokio::task::spawn_blocking(move || {
592            let result = runner.run_blocking_with_event_sender(
593                &agent,
594                input,
595                config,
596                Some(event_collector_for_task),
597                Some(event_sender),
598            );
599            if let Ok(mut state) = state_for_task.lock() {
600                *state = match &result {
601                    Ok(_) if cancellation_token_for_task.is_cancelled() => {
602                        RunHandleState::cancelled()
603                    }
604                    Ok(_) => RunHandleState::completed(),
605                    Err(error) if cancellation_token_for_task.is_cancelled() => {
606                        let mut state = RunHandleState::cancelled();
607                        state.error = Some(error.clone());
608                        state
609                    }
610                    Err(error) => RunHandleState::failed(error.clone()),
611                };
612            }
613            if let Ok(mut sender) = sender_slot_for_task.lock() {
614                sender.take();
615            }
616            result
617        });
618        let result = SharedRunResult::new(join);
619        Ok(RunHandle::new(
620            event_sender_slot,
621            event_collector,
622            result,
623            state,
624            cancellation_token,
625            approval_broker,
626        ))
627    }
628
629    fn build_instructions_with_context(
630        &self,
631        agent: &Agent,
632        input_text: &str,
633        config: &RunConfig,
634    ) -> Result<(String, Option<ContextBundle>), String> {
635        let providers = self
636            .default_run_config
637            .context_providers
638            .iter()
639            .chain(config.context_providers.iter())
640            .cloned()
641            .collect::<Vec<_>>();
642        if providers.is_empty() {
643            return Ok((agent.instructions().to_string(), None));
644        }
645        let mut request = ContextRequest::new(agent.name(), input_text);
646        if let Some(max_chars) = config
647            .max_context_chars
648            .or(self.default_run_config.max_context_chars)
649        {
650            request = request.max_prompt_chars(max_chars);
651        }
652        let fragments = collect_context_fragments(&request, &providers)
653            .map_err(|error| format!("context provider failed: {error}"))?;
654        let bundle = assemble_context_fragments(&request, fragments)
655            .map_err(|error| format!("context assembly failed: {error}"))?;
656        let mut instructions = agent.instructions().to_string();
657        if !bundle.prompt.is_empty() {
658            if !instructions.trim().is_empty() {
659                instructions.push_str("\n\n");
660            }
661            instructions.push_str(&bundle.prompt);
662        }
663        Ok((instructions, Some(bundle)))
664    }
665}
666
667#[derive(Default)]
668pub struct RunnerBuilder {
669    model_provider: Option<Arc<dyn ModelProvider>>,
670    settings_file: Option<PathBuf>,
671    default_backend: Option<String>,
672    workspace: Option<PathBuf>,
673    tool_registry: Option<ToolRegistry>,
674    default_run_config: RunConfig,
675}
676
677impl RunnerBuilder {
678    pub fn model_provider(mut self, provider: impl ModelProvider + 'static) -> Self {
679        self.model_provider = Some(Arc::new(provider));
680        self
681    }
682
683    pub fn model_provider_arc(mut self, provider: Arc<dyn ModelProvider>) -> Self {
684        self.model_provider = Some(provider);
685        self
686    }
687
688    pub fn settings_file(mut self, settings_file: impl Into<PathBuf>) -> Self {
689        self.settings_file = Some(settings_file.into());
690        self
691    }
692
693    pub fn default_backend(mut self, default_backend: impl Into<String>) -> Self {
694        self.default_backend = Some(default_backend.into());
695        self
696    }
697
698    pub fn workspace(mut self, workspace: impl Into<PathBuf>) -> Self {
699        self.workspace = Some(workspace.into());
700        self
701    }
702
703    pub fn tool_registry(mut self, registry: ToolRegistry) -> Self {
704        self.tool_registry = Some(registry);
705        self
706    }
707
708    pub fn default_run_config(mut self, config: RunConfig) -> Self {
709        self.default_run_config = config;
710        self
711    }
712
713    pub fn build(self) -> Result<Runner, String> {
714        let model_provider = if let Some(provider) = self.model_provider {
715            provider
716        } else {
717            let settings_file = self
718                .settings_file
719                .unwrap_or_else(|| PathBuf::from("local_settings.json"));
720            let mut provider = VvLlmModelProvider::from_settings_file(settings_file);
721            if let Some(default_backend) = self.default_backend {
722                provider = provider.with_default_backend(default_backend);
723            }
724            Arc::new(provider)
725        };
726        Ok(Runner {
727            model_provider,
728            workspace: self
729                .workspace
730                .unwrap_or_else(|| PathBuf::from("./workspace")),
731            tool_registry: self
732                .tool_registry
733                .unwrap_or_else(crate::tools::build_default_registry),
734            default_run_config: self.default_run_config,
735        })
736    }
737}
738
739#[derive(Clone)]
740struct ArcLlmClient(Arc<dyn LlmClient>);
741
742impl LlmClient for ArcLlmClient {
743    fn complete(
744        &self,
745        request: crate::llm::LlmRequest,
746    ) -> Result<crate::types::LLMResponse, crate::llm::LlmError> {
747        self.0.complete(request)
748    }
749
750    fn complete_with_stream(
751        &self,
752        request: crate::llm::LlmRequest,
753        stream_callback: Option<crate::llm::LlmStreamCallback>,
754    ) -> Result<crate::types::LLMResponse, crate::llm::LlmError> {
755        self.0.complete_with_stream(request, stream_callback)
756    }
757}
758
759fn format_model_error(error: ModelError) -> String {
760    error.to_string()
761}