Skip to main content

vv_agent/
runner.rs

1mod event_stream;
2mod session_blocking;
3
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use serde_json::{json, Value};
8
9use crate::agent::Agent;
10use crate::config::apply_resolved_model_limits;
11use crate::context::RunContext;
12use crate::events::RunEvent;
13use crate::guardrails::GuardrailOutcome;
14use crate::llm::LlmClient;
15use crate::model::{ModelError, ModelProvider, VvLlmModelProvider};
16use crate::result::{RunResult, RunResumeContext, RunState};
17use crate::run_config::RunConfig;
18use crate::runtime::{
19    AgentRuntime, BeforeToolCallEvent, BeforeToolCallPatch, ExecutionContext, RuntimeHook,
20    RuntimeRunControls,
21};
22use crate::sessions::SessionItem;
23use crate::tools::{ApprovalPolicy, ToolPolicy, ToolRegistry};
24use crate::tracing::Span;
25use crate::types::{
26    AgentResult, AgentTask, NoToolPolicy, ToolDirective, ToolExecutionResult, ToolResultStatus,
27};
28use crate::workspace::LocalWorkspaceBackend;
29
30use event_stream::map_runtime_event;
31pub use event_stream::RunEventStream;
32use session_blocking::block_on_session;
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct NormalizedInput {
36    pub text: String,
37}
38
39impl From<&str> for NormalizedInput {
40    fn from(value: &str) -> Self {
41        Self {
42            text: value.to_string(),
43        }
44    }
45}
46
47impl From<String> for NormalizedInput {
48    fn from(value: String) -> Self {
49        Self { text: value }
50    }
51}
52
53#[derive(Clone)]
54pub struct Runner {
55    model_provider: Arc<dyn ModelProvider>,
56    workspace: PathBuf,
57    tool_registry: ToolRegistry,
58    default_run_config: RunConfig,
59}
60
61impl Runner {
62    pub fn builder() -> RunnerBuilder {
63        RunnerBuilder::default()
64    }
65
66    pub async fn run(
67        &self,
68        agent: &Agent,
69        input: impl Into<NormalizedInput>,
70    ) -> Result<RunResult, String> {
71        self.run_with_config(agent, input, RunConfig::default())
72            .await
73    }
74
75    pub async fn run_with_config(
76        &self,
77        agent: &Agent,
78        input: impl Into<NormalizedInput>,
79        config: RunConfig,
80    ) -> Result<RunResult, String> {
81        self.run_blocking(agent, input.into(), config, None)
82    }
83
84    pub async fn resume(&self, state: RunState) -> Result<RunResult, String> {
85        let (source, approved_ids) = state.into_inner();
86        let Some(resume_context) = source.resume_context().cloned() else {
87            return Err("run state does not include resume context".to_string());
88        };
89        if let Some(result) =
90            self.resume_approved_tool_call(&source, &resume_context, &approved_ids)
91        {
92            return result;
93        }
94        let mut config = resume_context.config;
95        config.metadata.insert(
96            "approved_tool_interruption_ids".to_string(),
97            Value::Array(approved_ids.iter().cloned().map(Value::String).collect()),
98        );
99        let mut result = self
100            .run_with_config(&resume_context.agent, resume_context.input, config)
101            .await
102            .map_err(|error| format!("resume failed: {error}"))?;
103        if result.status() == crate::types::AgentStatus::MaxCycles {
104            result = completed_from_first_tool_result(result);
105        }
106        Ok(result)
107    }
108
109    fn resume_approved_tool_call(
110        &self,
111        source: &RunResult,
112        resume_context: &RunResumeContext,
113        approved_ids: &[String],
114    ) -> Option<Result<RunResult, String>> {
115        let approval = find_approved_tool_call(source.result(), approved_ids)?;
116        let mut registry = self.tool_registry.clone();
117        for handoff in resume_context.agent.handoffs() {
118            if let Err(error) = registry.register(handoff.as_tool_spec(resume_context.agent.name()))
119            {
120                return Some(Err(error));
121            }
122        }
123        for tool in resume_context.agent.tools() {
124            if let Err(error) = registry.register(tool.as_tool_spec()) {
125                return Some(Err(error));
126            }
127        }
128        let workspace = resume_context
129            .config
130            .workspace
131            .clone()
132            .or_else(|| self.default_run_config.workspace.clone())
133            .unwrap_or_else(|| self.workspace.clone());
134        let workspace_backend = resume_context
135            .config
136            .workspace_backend
137            .clone()
138            .or_else(|| self.default_run_config.workspace_backend.clone())
139            .unwrap_or_else(|| Arc::new(LocalWorkspaceBackend::new(workspace.clone())));
140        let mut context = crate::tools::ToolContext {
141            workspace: workspace.clone(),
142            shared_state: source.result().shared_state.clone(),
143            cycle_index: approval.cycle_index,
144            task_id: format!("{}_run", resume_context.agent.name()),
145            metadata: resume_context.agent.metadata().clone(),
146            workspace_backend,
147            model_provider: Some(self.model_provider.clone()),
148            sub_task_runner: None,
149            sub_task_manager: None,
150            execution_backend: None,
151        };
152        context
153            .metadata
154            .extend(resume_context.config.metadata.clone());
155        context
156            .metadata
157            .entry("agent_name".to_string())
158            .or_insert_with(|| Value::String(resume_context.agent.name().to_string()));
159        let tool_result = crate::tools::dispatch_tool_call(&registry, &mut context, &approval.call);
160        if tool_result.status != ToolResultStatus::Success {
161            return Some(Err(tool_result.content));
162        }
163        let mut agent_result = source.result().clone();
164        agent_result.status = crate::types::AgentStatus::Completed;
165        agent_result.final_answer = Some(tool_result.content.clone());
166        if let Some(cycle) = agent_result
167            .cycles
168            .iter_mut()
169            .find(|cycle| cycle.index == approval.cycle_index)
170        {
171            cycle.tool_results.push(tool_result.clone());
172        }
173        agent_result.messages.push(tool_result.to_message());
174        Some(Ok(RunResult::new(
175            resume_context.agent.name().to_string(),
176            agent_result,
177            source.resolved_model().clone(),
178        )
179        .with_resume_context(resume_context.clone())))
180    }
181
182    pub fn run_blocking(
183        &self,
184        agent: &Agent,
185        input: NormalizedInput,
186        config: RunConfig,
187        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
188    ) -> Result<RunResult, String> {
189        self.run_agent_chain(agent, input, config, event_collector)
190    }
191
192    fn run_agent_chain(
193        &self,
194        agent: &Agent,
195        input: NormalizedInput,
196        config: RunConfig,
197        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
198    ) -> Result<RunResult, String> {
199        let mut current_agent = agent.clone();
200        let mut current_input = input;
201        for _ in 0..=max_handoff_depth(&config, &current_agent) {
202            let outcome = self.run_single_agent(
203                &current_agent,
204                current_input.clone(),
205                config.clone(),
206                event_collector.clone(),
207            )?;
208            let Some(handoff) = outcome.handoff else {
209                return Ok(outcome.result);
210            };
211            let target = current_agent
212                .handoffs()
213                .iter()
214                .find(|candidate| candidate.target().name() == handoff.to_agent)
215                .map(|candidate| candidate.target().clone())
216                .ok_or_else(|| {
217                    format!(
218                        "handoff target `{}` is not registered on agent `{}`",
219                        handoff.to_agent,
220                        current_agent.name()
221                    )
222                })?;
223            push_event(
224                event_collector.as_ref(),
225                RunEvent::Handoff {
226                    run_id: format!("{}_run", handoff.from_agent),
227                    from_agent: handoff.from_agent.clone(),
228                    to_agent: handoff.to_agent.clone(),
229                },
230            );
231            current_input = NormalizedInput {
232                text: handoff.input,
233            };
234            current_agent = target;
235        }
236        Err("maximum handoff depth exceeded".to_string())
237    }
238
239    fn run_single_agent(
240        &self,
241        agent: &Agent,
242        input: NormalizedInput,
243        config: RunConfig,
244        event_collector: Option<Arc<std::sync::Mutex<Vec<RunEvent>>>>,
245    ) -> Result<SingleRunOutcome, String> {
246        let provider = config
247            .model_provider
248            .clone()
249            .or_else(|| self.default_run_config.model_provider.clone())
250            .unwrap_or_else(|| self.model_provider.clone());
251        let model_ref = config
252            .model
253            .clone()
254            .or_else(|| self.default_run_config.model.clone())
255            .or_else(|| agent.model().cloned())
256            .ok_or_else(|| "agent model is not configured".to_string())?;
257        let resolved = provider.resolve(&model_ref).map_err(format_model_error)?;
258        let llm = provider.client(&resolved).map_err(format_model_error)?;
259        let provider_settings = provider.default_settings(&resolved);
260        let settings = provider_settings
261            .merge(agent.model_settings())
262            .merge(
263                self.default_run_config
264                    .model_settings
265                    .as_ref()
266                    .unwrap_or(&crate::model_settings::ModelSettings::default()),
267            )
268            .merge(
269                config
270                    .model_settings
271                    .as_ref()
272                    .unwrap_or(&crate::model_settings::ModelSettings::default()),
273            );
274        let workspace = config
275            .workspace
276            .clone()
277            .or_else(|| self.default_run_config.workspace.clone())
278            .unwrap_or_else(|| self.workspace.clone());
279        let session = config
280            .session
281            .clone()
282            .or_else(|| self.default_run_config.session.clone());
283        let session_items = if let Some(session) = session.as_ref() {
284            block_on_session(session.get_items(None))?
285        } else {
286            Vec::new()
287        };
288        let tool_policy = merged_tool_policy(
289            agent.tool_policy(),
290            &self.default_run_config.tool_policy,
291            &config.tool_policy,
292        );
293        let run_context = RunContext {
294            run_id: format!("{}_run", agent.name()),
295            agent_name: agent.name().to_string(),
296            model: Some(model_ref.clone()),
297            workspace: Some(workspace.clone()),
298            metadata: config.metadata.clone(),
299        };
300        let guarded_input = apply_input_guardrails(agent, &run_context, input)?;
301        let input_text = guarded_input.text;
302        let mut task = AgentTask::new(
303            format!("{}_run", agent.name()),
304            resolved.model_id.clone(),
305            agent.instructions().to_string(),
306            input_text.clone(),
307        );
308        task.max_cycles = config
309            .max_cycles
310            .or(self.default_run_config.max_cycles)
311            .or(agent.max_cycles())
312            .unwrap_or(10)
313            .max(1);
314        task.no_tool_policy = NoToolPolicy::Continue;
315        task.metadata = agent.metadata().clone();
316        task.metadata
317            .extend(self.default_run_config.metadata.clone());
318        task.metadata.extend(config.metadata.clone());
319        task.metadata
320            .entry("agent_name".to_string())
321            .or_insert_with(|| Value::String(agent.name().to_string()));
322        task.metadata
323            .insert("model_settings".to_string(), settings.to_value());
324        task.metadata.insert(
325            "runtime_model".to_string(),
326            serde_json::to_value(&resolved).unwrap_or(Value::Null),
327        );
328        apply_resolved_model_limits(&mut task, &resolved);
329        task.initial_messages = session_items
330            .iter()
331            .map(SessionItem::to_message)
332            .collect::<Vec<_>>();
333        let mut registry = self.tool_registry.clone();
334        for handoff in agent.handoffs() {
335            registry.register(handoff.as_tool_spec(agent.name()))?;
336        }
337        for tool in agent.tools() {
338            registry.register(tool.as_tool_spec())?;
339        }
340        let mut runtime = AgentRuntime::new(ArcLlmClient(llm))
341            .with_tool_registry(registry)
342            .with_settings_file("__runner_model_provider__")
343            .with_default_backend(resolved.backend.clone());
344        runtime.default_workspace = Some(workspace.clone());
345        runtime.workspace_backend = config
346            .workspace_backend
347            .clone()
348            .or_else(|| self.default_run_config.workspace_backend.clone())
349            .unwrap_or_else(|| Arc::new(LocalWorkspaceBackend::new(workspace.clone())));
350        if let Some(execution_backend) = config
351            .execution_backend
352            .clone()
353            .or_else(|| self.default_run_config.execution_backend.clone())
354        {
355            runtime.execution_backend = execution_backend;
356        }
357        runtime.hooks.extend(agent.hooks().iter().cloned());
358        runtime
359            .hooks
360            .extend(self.default_run_config.hooks.iter().cloned());
361        runtime.hooks.extend(config.hooks.iter().cloned());
362        if approval_required(&tool_policy) {
363            runtime.hooks.push(Arc::new(ApprovalHook::new(
364                tool_policy.clone(),
365                task.metadata.clone(),
366            )));
367        }
368        let log_handler = event_collector.as_ref().map(|collector| {
369            let collector = collector.clone();
370            Arc::new(
371                move |event: &str, payload: &std::collections::BTreeMap<String, Value>| {
372                    if let Some(mapped) = map_runtime_event(event, payload) {
373                        if let Ok(mut events) = collector.lock() {
374                            events.push(mapped);
375                        }
376                    }
377                },
378            ) as crate::runtime::RuntimeEventHandler
379        });
380        let controls = RuntimeRunControls {
381            log_handler,
382            cancellation_token: config
383                .cancellation_token
384                .clone()
385                .or_else(|| self.default_run_config.cancellation_token.clone()),
386            execution_context: Some(ExecutionContext {
387                metadata: task.metadata.clone(),
388                ..ExecutionContext::default()
389            }),
390            workspace: Some(workspace),
391            workspace_backend: runtime.workspace_backend.clone().into(),
392            model_provider: Some(provider.clone()),
393            ..RuntimeRunControls::default()
394        };
395        let trace_sink = config
396            .trace_sink
397            .clone()
398            .or_else(|| self.default_run_config.trace_sink.clone());
399        let run_span = Span::new(
400            format!("{}_run", agent.name()),
401            "run",
402            Some(agent.name().to_string()),
403        );
404        let agent_span = Span::new(
405            format!("{}_run", agent.name()),
406            "agent",
407            Some(agent.name().to_string()),
408        );
409        if let Some(trace_sink) = trace_sink.as_ref() {
410            trace_sink.on_span_start(&run_span);
411            trace_sink.on_span_start(&agent_span);
412        }
413        let result = runtime
414            .run_with_controls(task, controls)
415            .map_err(|error| error.to_string())?;
416        if let Some(trace_sink) = trace_sink.as_ref() {
417            trace_sink.on_span_end(&agent_span);
418            trace_sink.on_span_end(&run_span);
419            trace_sink.flush()?;
420        }
421        let result = apply_output_guardrails(agent, &run_context, result);
422        let handoff = extract_handoff(&result);
423        if let Some(session) = session.as_ref() {
424            let mut new_items = Vec::new();
425            new_items.push(SessionItem::User {
426                content: input_text.clone(),
427            });
428            if handoff.is_none() {
429                if let Some(answer) = result.final_answer.as_ref() {
430                    new_items.push(SessionItem::Assistant {
431                        content: answer.clone(),
432                    });
433                }
434            } else if let Some(handoff) = handoff.as_ref() {
435                new_items.push(SessionItem::Assistant {
436                    content: format!(
437                        "Handed off from {} to {}.",
438                        handoff.from_agent, handoff.to_agent
439                    ),
440                });
441                if !handoff.input.is_empty() {
442                    new_items.push(SessionItem::User {
443                        content: handoff.input.clone(),
444                    });
445                }
446            }
447            block_on_session(session.add_items(new_items))?;
448        }
449        Ok(SingleRunOutcome {
450            result: RunResult::new(agent.name().to_string(), result, resolved).with_resume_context(
451                RunResumeContext {
452                    agent: agent.clone(),
453                    input: NormalizedInput {
454                        text: input_text.clone(),
455                    },
456                    config,
457                },
458            ),
459            handoff,
460        })
461    }
462
463    pub async fn stream(
464        &self,
465        agent: &Agent,
466        input: impl Into<NormalizedInput>,
467    ) -> Result<RunEventStream, String> {
468        let events = Arc::new(std::sync::Mutex::new(Vec::new()));
469        let result = self.run_blocking(
470            agent,
471            input.into(),
472            RunConfig::default(),
473            Some(events.clone()),
474        )?;
475        let collected = events
476            .lock()
477            .map_err(|_| "stream event lock poisoned".to_string())?
478            .clone();
479        Ok(RunEventStream::from_events(result, collected))
480    }
481}
482
483#[derive(Clone, Debug, PartialEq, Eq)]
484struct HandoffRequest {
485    from_agent: String,
486    to_agent: String,
487    input: String,
488}
489
490struct SingleRunOutcome {
491    result: RunResult,
492    handoff: Option<HandoffRequest>,
493}
494
495struct ApprovedToolCall {
496    call: crate::types::ToolCall,
497    cycle_index: u32,
498}
499
500fn apply_input_guardrails(
501    agent: &Agent,
502    context: &RunContext,
503    input: NormalizedInput,
504) -> Result<NormalizedInput, String> {
505    let mut current = input;
506    for guardrail in agent.input_guardrails() {
507        current = match guardrail.check(context, &current) {
508            GuardrailOutcome::Allow(input) => input,
509            GuardrailOutcome::Block { message } => return Err(message),
510            GuardrailOutcome::RequireApproval { message } => return Err(message),
511        };
512    }
513    Ok(current)
514}
515
516fn apply_output_guardrails(
517    agent: &Agent,
518    context: &RunContext,
519    result: AgentResult,
520) -> AgentResult {
521    let mut current = result;
522    for guardrail in agent.output_guardrails() {
523        current = match guardrail.check(context, &current) {
524            GuardrailOutcome::Allow(output) => output,
525            GuardrailOutcome::Block { message } | GuardrailOutcome::RequireApproval { message } => {
526                let mut failed = current.clone();
527                failed.status = crate::types::AgentStatus::Failed;
528                failed.error = Some(message);
529                failed.final_answer = None;
530                failed
531            }
532        };
533    }
534    current
535}
536
537fn max_handoff_depth(config: &RunConfig, agent: &Agent) -> u32 {
538    config
539        .max_cycles
540        .or(agent.max_cycles())
541        .unwrap_or(10)
542        .max(1)
543}
544
545fn push_event(collector: Option<&Arc<std::sync::Mutex<Vec<RunEvent>>>>, event: RunEvent) {
546    if let Some(collector) = collector {
547        if let Ok(mut events) = collector.lock() {
548            events.push(event);
549        }
550    }
551}
552
553fn find_approved_tool_call(
554    result: &AgentResult,
555    approved_ids: &[String],
556) -> Option<ApprovedToolCall> {
557    for cycle in &result.cycles {
558        for tool_result in &cycle.tool_results {
559            let interruption_id = tool_result
560                .metadata
561                .get("approval_interruption_id")
562                .and_then(Value::as_str)?;
563            if !approved_ids.iter().any(|id| id == interruption_id) {
564                continue;
565            }
566            let tool_name = tool_result
567                .metadata
568                .get("tool_name")
569                .and_then(Value::as_str)?;
570            let call = cycle
571                .tool_calls
572                .iter()
573                .find(|call| call.id == tool_result.tool_call_id && call.name == tool_name)
574                .cloned()
575                .or_else(|| {
576                    cycle
577                        .tool_calls
578                        .iter()
579                        .find(|call| call.name == tool_name)
580                        .cloned()
581                })?;
582            return Some(ApprovedToolCall {
583                call,
584                cycle_index: cycle.index,
585            });
586        }
587    }
588    None
589}
590
591fn extract_handoff(result: &AgentResult) -> Option<HandoffRequest> {
592    result
593        .cycles
594        .iter()
595        .flat_map(|cycle| cycle.tool_results.iter())
596        .find_map(|tool_result| {
597            let is_handoff = tool_result
598                .metadata
599                .get("handoff")
600                .and_then(Value::as_bool)
601                .unwrap_or(false);
602            if !is_handoff {
603                return None;
604            }
605            let from_agent = tool_result
606                .metadata
607                .get("from_agent")
608                .and_then(Value::as_str)?
609                .to_string();
610            let to_agent = tool_result
611                .metadata
612                .get("to_agent")
613                .and_then(Value::as_str)?
614                .to_string();
615            let input = tool_result
616                .metadata
617                .get("handoff_input")
618                .and_then(Value::as_str)
619                .unwrap_or_default()
620                .to_string();
621            Some(HandoffRequest {
622                from_agent,
623                to_agent,
624                input,
625            })
626        })
627}
628
629fn approval_required(policy: &ToolPolicy) -> bool {
630    !matches!(policy.approval, ApprovalPolicy::Never)
631}
632
633fn merged_tool_policy(agent: &ToolPolicy, runner: &ToolPolicy, run: &ToolPolicy) -> ToolPolicy {
634    let mut merged = agent.clone();
635    if runner.allowed_tools.is_some() {
636        merged.allowed_tools = runner.allowed_tools.clone();
637    }
638    if run.allowed_tools.is_some() {
639        merged.allowed_tools = run.allowed_tools.clone();
640    }
641    merged
642        .disallowed_tools
643        .extend(runner.disallowed_tools.clone());
644    merged.disallowed_tools.extend(run.disallowed_tools.clone());
645    merged.approval = match run.approval {
646        ApprovalPolicy::Never if !matches!(runner.approval, ApprovalPolicy::Never) => {
647            runner.approval.clone()
648        }
649        ApprovalPolicy::Never if !matches!(agent.approval, ApprovalPolicy::Never) => {
650            agent.approval.clone()
651        }
652        _ => run.approval.clone(),
653    };
654    if let Some(max_concurrency) = runner.max_concurrency {
655        merged.max_concurrency = Some(max_concurrency);
656    }
657    if let Some(max_concurrency) = run.max_concurrency {
658        merged.max_concurrency = Some(max_concurrency);
659    }
660    merged
661}
662
663struct ApprovalHook {
664    policy: ToolPolicy,
665    approved_ids: Vec<String>,
666}
667
668impl ApprovalHook {
669    fn new(policy: ToolPolicy, metadata: crate::types::Metadata) -> Self {
670        let approved_ids = metadata
671            .get("approved_tool_interruption_ids")
672            .and_then(Value::as_array)
673            .map(|items| {
674                items
675                    .iter()
676                    .filter_map(Value::as_str)
677                    .map(str::to_string)
678                    .collect()
679            })
680            .unwrap_or_default();
681        Self {
682            policy,
683            approved_ids,
684        }
685    }
686}
687
688impl RuntimeHook for ApprovalHook {
689    fn before_tool_call(&self, event: BeforeToolCallEvent<'_>) -> Option<BeforeToolCallPatch> {
690        if let Some(allowed) = self.policy.allowed_tools.as_ref() {
691            if !allowed.iter().any(|tool| tool == &event.call.name) {
692                return Some(BeforeToolCallPatch::result(approval_error(
693                    &event.call.id,
694                    &event.call.name,
695                    "tool_not_allowed",
696                    "Tool is not in the allowed tool list.",
697                )));
698            }
699        }
700        if self
701            .policy
702            .disallowed_tools
703            .iter()
704            .any(|tool| tool == &event.call.name)
705        {
706            return Some(BeforeToolCallPatch::result(approval_error(
707                &event.call.id,
708                &event.call.name,
709                "tool_disallowed",
710                "Tool is disallowed by policy.",
711            )));
712        }
713        if !matches!(self.policy.approval, ApprovalPolicy::Always) {
714            return None;
715        }
716        let interruption_id = approval_interruption_id(event.task.task_id.as_str(), event.call);
717        if self
718            .approved_ids
719            .iter()
720            .any(|approved| approved == &interruption_id)
721        {
722            return None;
723        }
724        Some(BeforeToolCallPatch::result(approval_required_result(
725            &event.call.id,
726            &event.call.name,
727            &interruption_id,
728        )))
729    }
730}
731
732fn approval_interruption_id(task_id: &str, call: &crate::types::ToolCall) -> String {
733    format!("approval:{task_id}:{}:{}", call.name, call.id)
734}
735
736fn approval_required_result(
737    tool_call_id: &str,
738    tool_name: &str,
739    interruption_id: &str,
740) -> ToolExecutionResult {
741    let mut metadata = crate::types::Metadata::new();
742    metadata.insert("approval_required".to_string(), Value::Bool(true));
743    metadata.insert(
744        "approval_interruption_id".to_string(),
745        Value::String(interruption_id.to_string()),
746    );
747    metadata.insert(
748        "tool_name".to_string(),
749        Value::String(tool_name.to_string()),
750    );
751    ToolExecutionResult {
752        tool_call_id: tool_call_id.to_string(),
753        content: json!({
754            "ok": false,
755            "approval_required": true,
756            "interruption_id": interruption_id,
757            "tool_name": tool_name,
758        })
759        .to_string(),
760        status: ToolResultStatus::WaitResponse,
761        directive: ToolDirective::WaitUser,
762        error_code: Some("tool_approval_required".to_string()),
763        metadata,
764        image_url: None,
765        image_path: None,
766    }
767}
768
769fn approval_error(
770    tool_call_id: &str,
771    tool_name: &str,
772    error_code: &str,
773    message: &str,
774) -> ToolExecutionResult {
775    ToolExecutionResult {
776        tool_call_id: tool_call_id.to_string(),
777        content: json!({
778            "ok": false,
779            "error": message,
780            "error_code": error_code,
781            "tool_name": tool_name,
782        })
783        .to_string(),
784        status: ToolResultStatus::Error,
785        directive: ToolDirective::Continue,
786        error_code: Some(error_code.to_string()),
787        metadata: crate::types::Metadata::new(),
788        image_url: None,
789        image_path: None,
790    }
791}
792
793fn completed_from_first_tool_result(result: RunResult) -> RunResult {
794    if result.status() != crate::types::AgentStatus::MaxCycles {
795        return result;
796    }
797    let Some(tool_result) = result
798        .result()
799        .cycles
800        .iter()
801        .flat_map(|cycle| cycle.tool_results.iter())
802        .find(|tool_result| tool_result.status == ToolResultStatus::Success)
803        .cloned()
804    else {
805        return result;
806    };
807    let final_answer = tool_result.content.clone();
808    let agent_name = result.agent_name().to_string();
809    let resolved = result.resolved_model().clone();
810    let mut agent_result = result.result().clone();
811    agent_result.status = crate::types::AgentStatus::Completed;
812    agent_result.final_answer = Some(final_answer);
813    RunResult::new(agent_name, agent_result, resolved)
814}
815
816#[derive(Default)]
817pub struct RunnerBuilder {
818    model_provider: Option<Arc<dyn ModelProvider>>,
819    settings_file: Option<PathBuf>,
820    default_backend: Option<String>,
821    workspace: Option<PathBuf>,
822    tool_registry: Option<ToolRegistry>,
823    default_run_config: RunConfig,
824}
825
826impl RunnerBuilder {
827    pub fn model_provider(mut self, provider: impl ModelProvider + 'static) -> Self {
828        self.model_provider = Some(Arc::new(provider));
829        self
830    }
831
832    pub fn model_provider_arc(mut self, provider: Arc<dyn ModelProvider>) -> Self {
833        self.model_provider = Some(provider);
834        self
835    }
836
837    pub fn settings_file(mut self, settings_file: impl Into<PathBuf>) -> Self {
838        self.settings_file = Some(settings_file.into());
839        self
840    }
841
842    pub fn default_backend(mut self, default_backend: impl Into<String>) -> Self {
843        self.default_backend = Some(default_backend.into());
844        self
845    }
846
847    pub fn workspace(mut self, workspace: impl Into<PathBuf>) -> Self {
848        self.workspace = Some(workspace.into());
849        self
850    }
851
852    pub fn tool_registry(mut self, registry: ToolRegistry) -> Self {
853        self.tool_registry = Some(registry);
854        self
855    }
856
857    pub fn default_run_config(mut self, config: RunConfig) -> Self {
858        self.default_run_config = config;
859        self
860    }
861
862    pub fn build(self) -> Result<Runner, String> {
863        let model_provider = if let Some(provider) = self.model_provider {
864            provider
865        } else {
866            let settings_file = self
867                .settings_file
868                .unwrap_or_else(|| PathBuf::from("local_settings.json"));
869            let mut provider = VvLlmModelProvider::from_settings_file(settings_file);
870            if let Some(default_backend) = self.default_backend {
871                provider = provider.with_default_backend(default_backend);
872            }
873            Arc::new(provider)
874        };
875        Ok(Runner {
876            model_provider,
877            workspace: self
878                .workspace
879                .unwrap_or_else(|| PathBuf::from("./workspace")),
880            tool_registry: self
881                .tool_registry
882                .unwrap_or_else(crate::tools::build_default_registry),
883            default_run_config: self.default_run_config,
884        })
885    }
886}
887
888#[derive(Clone)]
889struct ArcLlmClient(Arc<dyn LlmClient>);
890
891impl LlmClient for ArcLlmClient {
892    fn complete(
893        &self,
894        request: crate::llm::LlmRequest,
895    ) -> Result<crate::types::LLMResponse, crate::llm::LlmError> {
896        self.0.complete(request)
897    }
898
899    fn complete_with_stream(
900        &self,
901        request: crate::llm::LlmRequest,
902        stream_callback: Option<crate::llm::LlmStreamCallback>,
903    ) -> Result<crate::types::LLMResponse, crate::llm::LlmError> {
904        self.0.complete_with_stream(request, stream_callback)
905    }
906}
907
908fn format_model_error(error: ModelError) -> String {
909    error.to_string()
910}