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(®istry, &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, ¤t_agent) {
202 let outcome = self.run_single_agent(
203 ¤t_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, ¤t) {
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, ¤t) {
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}