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