1use std::sync::Arc;
6
7use futures_util::future::join_all;
8use serde_json::Value;
9use tokio_util::sync::CancellationToken;
10use yapi_ai::stream::{EventStream, Request, StreamEvent, StreamOptions};
11use yapi_ai::transcript::{current_tools, tool_state_changes};
12use yapi_ai::validation::validate_tool_arguments;
13use yapi_types::event::{AgentEvent, ToolResult};
14use yapi_types::message::{
15 AssistantMessage, Content, ContentBlock, Message, StopReason, SystemMessage, ThinkingLevel,
16 ToolCall, ToolDeclaration, ToolResultMessage,
17};
18use yapi_types::model::Model;
19
20use crate::hooks::{AfterToolCall, AgentHooks, BeforeToolCall};
21use crate::tool::{ExecutionMode, Tool, UpdateSink};
22
23pub type StreamFn = Arc<dyn Fn(Request) -> Result<EventStream, String> + Send + Sync>;
27
28#[derive(Clone, Default)]
30pub struct AgentContext {
31 pub messages: Vec<Message>,
33 pub tools: Vec<Arc<dyn Tool>>,
35}
36
37#[derive(Clone)]
39pub struct LoopConfig {
40 pub model: Model,
42 pub thinking_level: ThinkingLevel,
44 pub stream: StreamFn,
46 pub options: StreamOptions,
48 pub tool_execution: ExecutionMode,
50}
51
52pub async fn run(
58 prompts: Vec<Message>,
59 context: &mut AgentContext,
60 config: LoopConfig,
61 hooks: &dyn AgentHooks,
62) -> Vec<Message> {
63 let initial = declare_tool_changes(&context.messages, &context.tools, prompts);
64 let mut new_messages = Vec::new();
65
66 hooks.on_event(&AgentEvent::AgentStart).await;
67 hooks.on_event(&AgentEvent::TurnStart).await;
68 for message in initial {
69 let message = emit_message(hooks, message).await;
70 context.messages.push(message.clone());
71 new_messages.push(message);
72 }
73 run_loop(context, &mut new_messages, config, hooks).await;
74 new_messages
75}
76
77pub async fn run_continue(
80 context: &mut AgentContext,
81 config: LoopConfig,
82 hooks: &dyn AgentHooks,
83) -> Result<Vec<Message>, String> {
84 match context.messages.last() {
85 None => return Err("Cannot continue: no messages in context".into()),
86 Some(Message::Assistant(_)) => {
87 return Err("Cannot continue from message role: assistant".into());
88 }
89 Some(_) => {}
90 }
91 let mut new_messages = Vec::new();
92 hooks.on_event(&AgentEvent::AgentStart).await;
93 hooks.on_event(&AgentEvent::TurnStart).await;
94 run_loop(context, &mut new_messages, config, hooks).await;
95 Ok(new_messages)
96}
97
98async fn emit_message(hooks: &dyn AgentHooks, message: Message) -> Message {
101 hooks
102 .on_event(&AgentEvent::MessageStart {
103 message: message.clone(),
104 })
105 .await;
106 end_message(hooks, message).await
107}
108
109async fn end_message(hooks: &dyn AgentHooks, message: Message) -> Message {
111 let message = hooks.finish_message(message).await;
112 hooks
113 .on_event(&AgentEvent::MessageEnd {
114 message: message.clone(),
115 })
116 .await;
117 message
118}
119
120async fn run_loop(
121 context: &mut AgentContext,
122 new_messages: &mut Vec<Message>,
123 config: LoopConfig,
124 hooks: &dyn AgentHooks,
125) {
126 let mut first_turn = true;
127 let mut pending = hooks.steering_messages().await;
128 let mut explicit_continuation = false;
131
132 loop {
133 let mut more_tool_calls = true;
134 while more_tool_calls || !pending.is_empty() {
135 if !first_turn {
136 match hooks.next_turn_messages().await {
137 Ok(Some(messages)) => context.messages = messages,
138 Ok(None) => {}
139 Err(error) => return fail_run(context, &config, hooks, error).await,
140 }
141 if pending.is_empty() {
142 pending = hooks.steering_messages().await;
143 }
144 hooks.on_event(&AgentEvent::TurnStart).await;
145 }
146 first_turn = false;
147
148 if let Some(tools) = hooks.current_tools().await {
149 context.tools = tools;
150 }
151 for message in declare_tool_changes(
152 &context.messages,
153 &context.tools,
154 std::mem::take(&mut pending),
155 ) {
156 let message = emit_message(hooks, message).await;
157 context.messages.push(message.clone());
158 new_messages.push(message);
159 }
160
161 let message = match stream_response(context, &config, hooks).await {
162 Ok(message) => message,
163 Err(error) => return fail_run(context, &config, hooks, error).await,
164 };
165 new_messages.push(Message::Assistant(Box::new(message.clone())));
166
167 if matches!(message.stop_reason, StopReason::Error | StopReason::Aborted) {
168 hooks.finish_turn(&message, &[]).await;
169 hooks
170 .on_event(&AgentEvent::TurnEnd {
171 message: Message::Assistant(Box::new(message)),
172 tool_results: Vec::new(),
173 })
174 .await;
175 end(hooks, new_messages).await;
176 return;
177 }
178
179 let calls: Vec<ToolCall> = message
180 .content
181 .iter()
182 .filter_map(|block| match block {
183 ContentBlock::ToolCall(call) => Some(call.clone()),
184 _ => None,
185 })
186 .collect();
187 let mut tool_results = Vec::new();
188 more_tool_calls = false;
189 if !calls.is_empty() {
190 let (results, terminate) = if message.stop_reason == StopReason::Length {
191 (fail_truncated_calls(&calls, hooks).await, false)
192 } else {
193 execute_tool_calls(context, &message, &calls, &config, hooks).await
194 };
195 more_tool_calls = !terminate;
196 for result in &results {
197 context.messages.push(Message::ToolResult(result.clone()));
198 new_messages.push(Message::ToolResult(result.clone()));
199 }
200 tool_results = results;
201 }
202
203 let decision = hooks.finish_turn(&message, &tool_results).await;
204 hooks
205 .on_event(&AgentEvent::TurnEnd {
206 message: Message::Assistant(Box::new(message)),
207 tool_results,
208 })
209 .await;
210 pending = hooks.steering_messages().await;
211 explicit_continuation = decision.continue_run && !more_tool_calls && pending.is_empty();
212 }
213
214 let follow_up = hooks.follow_up_messages().await;
215 if !follow_up.is_empty() {
216 explicit_continuation = false;
217 pending = follow_up;
218 continue;
219 }
220 if std::mem::take(&mut explicit_continuation) {
221 continue;
222 }
223 break;
224 }
225 end(hooks, new_messages).await;
226}
227
228async fn fail_run(
231 context: &mut AgentContext,
232 config: &LoopConfig,
233 hooks: &dyn AgentHooks,
234 error: String,
235) {
236 let mut failure = yapi_ai::stream::new_output(&config.model, yapi_ai::stream::now_ms());
237 failure.content = vec![ContentBlock::text("")];
238 failure.stop_reason = if config.options.cancel.is_cancelled() {
239 StopReason::Aborted
240 } else {
241 StopReason::Error
242 };
243 failure.error_message = Some(error);
244 let message = emit_message(hooks, Message::Assistant(Box::new(failure))).await;
245 context.messages.push(message.clone());
246 hooks
247 .on_event(&AgentEvent::TurnEnd {
248 message: message.clone(),
249 tool_results: Vec::new(),
250 })
251 .await;
252 end(hooks, &[message]).await;
253}
254
255async fn end(hooks: &dyn AgentHooks, new_messages: &[Message]) {
256 hooks
257 .on_event(&AgentEvent::AgentEnd {
258 messages: new_messages.to_vec(),
259 will_retry: false,
260 })
261 .await;
262}
263
264fn declare_tool_changes(
268 messages: &[Message],
269 tools: &[Arc<dyn Tool>],
270 pending: Vec<Message>,
271) -> Vec<Message> {
272 let system_index = pending
273 .iter()
274 .rposition(|message| matches!(message, Message::System(_)));
275 let mut baseline = pending.clone();
276 let pending_system = system_index.and_then(|index| match &pending[index] {
277 Message::System(system) => Some(system.clone()),
278 _ => None,
279 });
280 if let (Some(index), Some(system)) = (system_index, &pending_system) {
281 baseline[index] = Message::System(with_tool_changes(system, Vec::new(), Vec::new()));
282 }
283 let declared: Vec<Message> = messages.iter().chain(&baseline).cloned().collect();
284 let current: Vec<ToolDeclaration> = tools
285 .iter()
286 .map(|tool| tool.declaration().clone())
287 .collect();
288 let (added, removed) = tool_state_changes(¤t_tools(&declared), ¤t);
289 let unchanged = added.is_empty() && removed.is_empty();
290
291 if let (Some(index), Some(system)) = (system_index, pending_system) {
292 let had_changes = system.tools_added.as_ref().is_some_and(|t| !t.is_empty())
293 || system.tools_removed.as_ref().is_some_and(|t| !t.is_empty());
294 if unchanged && !had_changes {
295 return pending;
296 }
297 baseline[index] = Message::System(with_tool_changes(&system, added, removed));
298 return baseline;
299 }
300 if unchanged {
301 return pending;
302 }
303 let update = SystemMessage {
304 content: Content::Text(String::new()),
305 sections: None,
306 timestamp: yapi_ai::stream::now_ms(),
307 tools_added: None,
308 tools_removed: None,
309 };
310 let update = Message::System(with_tool_changes(&update, added, removed));
311 let index = pending
312 .iter()
313 .position(|message| !matches!(message, Message::System(_)))
314 .unwrap_or(pending.len());
315 let mut result = pending;
316 result.insert(index, update);
317 result
318}
319
320fn with_tool_changes(
321 system: &SystemMessage,
322 added: Vec<ToolDeclaration>,
323 removed: Vec<yapi_types::message::ToolReference>,
324) -> SystemMessage {
325 SystemMessage {
326 tools_added: (!added.is_empty()).then_some(added),
327 tools_removed: (!removed.is_empty()).then_some(removed),
328 ..system.clone()
329 }
330}
331
332async fn stream_response(
335 context: &mut AgentContext,
336 config: &LoopConfig,
337 hooks: &dyn AgentHooks,
338) -> Result<AssistantMessage, String> {
339 let messages = hooks.transform_context(context.messages.clone()).await;
340 let messages = hooks.convert_to_llm(messages);
341 let auth = hooks.auth(&config.model).await;
342 let reasoning = (config.thinking_level != ThinkingLevel::Off).then_some(config.thinking_level);
343 let mut request = Request {
344 model: config.model.clone(),
345 messages,
346 options: StreamOptions {
347 reasoning,
348 ..config.options.clone()
349 },
350 };
351 let mut stream = match auth.apply(&mut request) {
352 Ok(()) => {
353 yapi_ai::stream::RequestHooks::prepare(&mut request).await;
354 (config.stream)(request)?
355 }
356 Err(message) => {
357 yapi_ai::api::failed_stream(&request.model, &request.options.cancel, message)
358 }
359 };
360 let mut started = false;
361 let finish = |mut message: AssistantMessage| {
362 message.thinking_level = Some(config.thinking_level);
363 message
364 };
365 while let Some(event) = stream.next().await {
366 match event {
367 StreamEvent::Start(partial) => {
368 started = true;
369 hooks
370 .on_event(&AgentEvent::MessageStart {
371 message: Message::Assistant(Box::new(partial)),
372 })
373 .await;
374 }
375 StreamEvent::Update { event, usage } => {
376 if started {
377 hooks
378 .on_event(&AgentEvent::MessageUpdate {
379 usage,
380 assistant_message_event: event,
381 })
382 .await;
383 }
384 }
385 StreamEvent::Done(message) | StreamEvent::Error(message) => {
386 let message = finish(message);
387 return Ok(complete(context, hooks, message, started).await);
388 }
389 }
390 }
391 let mut message = yapi_ai::stream::new_output(&config.model, yapi_ai::stream::now_ms());
393 message.stop_reason = StopReason::Error;
394 message.error_message = Some("Provider stream ended without a result".into());
395 Ok(complete(context, hooks, finish(message), started).await)
396}
397
398async fn complete(
399 context: &mut AgentContext,
400 hooks: &dyn AgentHooks,
401 message: AssistantMessage,
402 started: bool,
403) -> AssistantMessage {
404 let wrapped = Message::Assistant(Box::new(message.clone()));
405 if !started {
406 hooks
407 .on_event(&AgentEvent::MessageStart {
408 message: wrapped.clone(),
409 })
410 .await;
411 }
412 let finished = end_message(hooks, wrapped).await;
413 context.messages.push(finished.clone());
414 match finished {
415 Message::Assistant(finished) => *finished,
416 _ => message,
417 }
418}
419
420fn error_result(message: &str) -> ToolResult {
421 ToolResult {
422 content: vec![ContentBlock::text(message)],
423 details: Some(Value::Object(Default::default())),
424 ..ToolResult::default()
425 }
426}
427
428#[derive(Clone, Debug)]
430pub struct ToolCallOutcome {
431 pub call: ToolCall,
433 pub result: ToolResult,
435 pub is_error: bool,
437}
438
439impl ToolCallOutcome {
440 fn error(call: ToolCall, message: &str) -> ToolCallOutcome {
442 ToolCallOutcome {
443 call,
444 result: error_result(message),
445 is_error: true,
446 }
447 }
448}
449
450pub struct ToolCallScope<'a> {
453 pub tools: &'a [Arc<dyn Tool>],
455 pub assistant: &'a AssistantMessage,
457 pub messages: &'a [Message],
459 pub parent: Option<&'a str>,
461 pub cancel: &'a CancellationToken,
463}
464
465async fn fail_truncated_calls(
466 calls: &[ToolCall],
467 hooks: &dyn AgentHooks,
468) -> Vec<ToolResultMessage> {
469 let mut messages = Vec::new();
470 for call in calls {
471 emit_start(hooks, call).await;
472 let outcome = ToolCallOutcome::error(
473 call.clone(),
474 &format!(
475 "Tool call \"{}\" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.",
476 call.name
477 ),
478 );
479 emit_end(hooks, &outcome).await;
480 messages.push(emit_result(&outcome, hooks).await);
481 }
482 messages
483}
484
485async fn emit_start(hooks: &dyn AgentHooks, call: &ToolCall) {
486 hooks
487 .on_event(&AgentEvent::ToolExecutionStart {
488 tool_call_id: call.id.clone(),
489 tool_name: call.name.clone(),
490 args: Value::Object(call.arguments.clone()),
491 parent_tool_call_id: None,
492 })
493 .await;
494}
495
496async fn emit_end(hooks: &dyn AgentHooks, outcome: &ToolCallOutcome) {
497 hooks
498 .on_event(&AgentEvent::ToolExecutionEnd {
499 tool_call_id: outcome.call.id.clone(),
500 tool_name: outcome.call.name.clone(),
501 result: outcome.result.clone(),
502 is_error: outcome.is_error,
503 parent_tool_call_id: None,
504 })
505 .await;
506}
507
508async fn emit_result(outcome: &ToolCallOutcome, hooks: &dyn AgentHooks) -> ToolResultMessage {
510 let mut message = ToolResultMessage {
511 tool_call_id: outcome.call.id.clone(),
512 tool_name: outcome.call.name.clone(),
513 content: outcome.result.content.clone(),
514 details: outcome.result.details.clone(),
515 usage: outcome.result.usage.clone(),
516 is_error: outcome.is_error,
517 timestamp: yapi_ai::stream::now_ms(),
518 nested_calls: None,
519 };
520 hooks.complete_tool_result(&mut message);
521 match emit_message(hooks, Message::ToolResult(message.clone())).await {
522 Message::ToolResult(finished) => finished,
523 _ => message,
524 }
525}
526
527struct Prepared {
529 call: ToolCall,
530 tool: Arc<dyn Tool>,
531 args: Value,
532}
533
534type Preparation = Result<Prepared, Box<ToolCallOutcome>>;
536
537async fn prepare(
538 scope: &ToolCallScope<'_>,
539 call: &ToolCall,
540 hooks: &dyn AgentHooks,
541) -> Preparation {
542 let immediate = |message: &str, terminate: bool| {
543 let mut outcome = ToolCallOutcome::error(call.clone(), message);
544 if terminate {
545 outcome.result.terminate = Some(true);
546 }
547 Err(Box::new(outcome))
548 };
549 let Some(tool) = scope
550 .tools
551 .iter()
552 .find(|tool| tool.declaration().name == call.name)
553 else {
554 return immediate(&format!("Tool {} not found", call.name), false);
555 };
556 let prepared_call = ToolCall {
557 arguments: tool.prepare_arguments(call.arguments.clone()),
558 ..call.clone()
559 };
560 let mut args = match validate_tool_arguments(tool.declaration(), &prepared_call) {
561 Ok(args) => args,
562 Err(message) => return immediate(&message, false),
563 };
564 let block = hooks
565 .before_tool_call(BeforeToolCall {
566 assistant_message: scope.assistant,
567 tool_call: call,
568 args: &mut args,
569 messages: scope.messages,
570 parent_tool_call_id: scope.parent,
571 })
572 .await;
573 if scope.cancel.is_cancelled() {
574 return immediate("Operation aborted", false);
575 }
576 if let Some(block) = block {
577 return immediate(
578 block
579 .reason
580 .as_deref()
581 .unwrap_or("Tool execution was blocked"),
582 block.terminate,
583 );
584 }
585 Ok(Prepared {
586 call: call.clone(),
587 tool: tool.clone(),
588 args,
589 })
590}
591
592async fn execute_prepared(
593 scope: &ToolCallScope<'_>,
594 prepared: Prepared,
595 hooks: &dyn AgentHooks,
596 updates: UpdateSink,
597) -> ToolCallOutcome {
598 let Prepared { call, tool, args } = prepared;
599 let executed = tool
600 .execute(
601 call.id.clone(),
602 args.clone(),
603 scope.cancel.child_token(),
604 updates,
605 )
606 .await;
607 let (mut result, mut is_error) = match executed {
608 Ok(result) => {
609 let is_error = result.is_error == Some(true);
610 (result, is_error)
611 }
612 Err(message) => (error_result(&message), true),
613 };
614 if let Some(patch) = hooks
615 .after_tool_call(AfterToolCall {
616 assistant_message: scope.assistant,
617 tool_call: &call,
618 args: &args,
619 result: &result,
620 is_error,
621 parent_tool_call_id: scope.parent,
622 })
623 .await
624 {
625 if let Some(content) = patch.content {
626 result.content = content;
627 result.structured_content = None;
628 }
629 if let Some(details) = patch.details {
630 result.details = Some(details);
631 }
632 if let Some(structured) = patch.structured_content {
633 result.structured_content = Some(structured);
634 }
635 if let Some(usage) = patch.usage {
636 result.usage = Some(usage);
637 }
638 if let Some(terminate) = patch.terminate {
639 result.terminate = Some(terminate);
640 }
641 if let Some(flag) = patch.is_error {
642 is_error = flag;
643 }
644 }
645 ToolCallOutcome {
646 call,
647 result,
648 is_error,
649 }
650}
651
652async fn run_with_updates(
655 scope: &ToolCallScope<'_>,
656 prepared: Prepared,
657 hooks: &dyn AgentHooks,
658) -> ToolCallOutcome {
659 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
660 let (id, name) = (prepared.call.id.clone(), prepared.call.name.clone());
661 let event_args = Value::Object(prepared.call.arguments.clone());
662 let sink: UpdateSink = Arc::new(move |partial: ToolResult| {
663 let _ = sender.send(AgentEvent::ToolExecutionUpdate {
664 tool_call_id: id.clone(),
665 tool_name: name.clone(),
666 args: event_args.clone(),
667 partial_result: partial,
668 parent_tool_call_id: None,
669 });
670 });
671 let execution = execute_prepared(scope, prepared, hooks, sink);
672 tokio::pin!(execution);
673 loop {
674 tokio::select! {
675 biased;
676 Some(event) = receiver.recv() => hooks.on_event(&event).await,
677 outcome = &mut execution => {
678 while let Ok(event) = receiver.try_recv() {
679 hooks.on_event(&event).await;
680 }
681 return outcome;
682 }
683 }
684 }
685}
686
687async fn execute_tool_calls(
688 context: &AgentContext,
689 assistant: &AssistantMessage,
690 calls: &[ToolCall],
691 config: &LoopConfig,
692 hooks: &dyn AgentHooks,
693) -> (Vec<ToolResultMessage>, bool) {
694 let sequential = config.tool_execution == ExecutionMode::Sequential
695 || calls.iter().any(|call| {
696 context.tools.iter().any(|tool| {
697 tool.declaration().name == call.name
698 && tool.execution_mode() == ExecutionMode::Sequential
699 })
700 });
701 let cancel = &config.options.cancel;
702 let scope = ToolCallScope {
703 tools: &context.tools,
704 assistant,
705 messages: &context.messages,
706 parent: None,
707 cancel,
708 };
709 let scope = &scope;
710 let mut outcomes: Vec<ToolCallOutcome> = Vec::new();
711 let mut messages = Vec::new();
712
713 if sequential {
714 for call in calls {
715 emit_start(hooks, call).await;
716 let outcome = match prepare(scope, call, hooks).await {
717 Err(outcome) => *outcome,
718 Ok(prepared) => run_with_updates(scope, prepared, hooks).await,
719 };
720 emit_end(hooks, &outcome).await;
721 messages.push(emit_result(&outcome, hooks).await);
722 outcomes.push(outcome);
723 if cancel.is_cancelled() {
724 break;
725 }
726 }
727 } else {
728 let mut entries = Vec::new();
729 for call in calls {
730 emit_start(hooks, call).await;
731 let preparation = prepare(scope, call, hooks).await;
732 if let Err(outcome) = &preparation {
733 emit_end(hooks, outcome).await;
734 }
735 entries.push(preparation);
736 if cancel.is_cancelled() {
737 break;
738 }
739 }
740 let runs = entries.into_iter().map(|entry| async move {
741 match entry {
742 Err(outcome) => *outcome,
743 Ok(prepared) => {
744 if cancel.is_cancelled() {
745 let outcome = ToolCallOutcome::error(prepared.call, "Operation aborted");
746 emit_end(hooks, &outcome).await;
747 return outcome;
748 }
749 let outcome = run_with_updates(scope, prepared, hooks).await;
750 emit_end(hooks, &outcome).await;
751 outcome
752 }
753 }
754 });
755 outcomes = join_all(runs).await;
756 for outcome in &outcomes {
757 messages.push(emit_result(outcome, hooks).await);
758 }
759 }
760 let terminate = !outcomes.is_empty()
761 && outcomes
762 .iter()
763 .all(|outcome| outcome.result.terminate == Some(true));
764 (messages, terminate)
765}
766
767pub async fn run_tool_call(
773 scope: &ToolCallScope<'_>,
774 call: ToolCall,
775 hooks: &dyn AgentHooks,
776 updates: UpdateSink,
777) -> ToolCallOutcome {
778 match prepare(scope, &call, hooks).await {
779 Err(outcome) => *outcome,
780 Ok(prepared) => execute_prepared(scope, prepared, hooks, updates).await,
781 }
782}