1#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6
7mod builder;
8mod compactor;
9mod error;
10mod extension;
11mod history;
12mod hooks;
13pub use history::{RunHistory, RunHistoryReplay};
14mod manual_compaction;
15mod model;
16mod output;
17mod recovery;
18mod replay;
19mod tools;
20mod types;
21
22pub use af_agent::CancellationToken;
23pub use af_agent_session::{cancel_events, failure_events, recovery_events};
24use af_context::{InputId, InteractionId, RunId, ToolCallId};
25pub use compactor::ModelCompactor;
26pub use error::RuntimeError;
27use model::ModelTurn;
28use output::assistant_content;
29use replay::{content_text, tool_message, user_message};
30use tools::PlannedCall;
31pub use types::{
32 ApproximateTokenMeter, CompactionResult, Compactor, EventWriter, RuntimeLimits, RuntimeOutcome,
33 TokenMeter, TurnRequest,
34};
35
36use std::{collections::HashSet, sync::Arc};
37
38use af_agent::{
39 ChatModel, ContextContributor, Hook, HookDecision, MountedPlugins, PreStepDecision,
40 PromptRegistry, ToolRegistry, TurnStopDecision,
41};
42use af_agent_session::{
43 DeliveryMode, Event, RecordedToolCall, RunStatus, SessionEvent, ToolAuthorizationStatus,
44};
45use af_llm::{ChatMessage, FinishReason, Role};
46use serde_json::{json, Value};
47
48pub struct AgentRuntime {
50 model: Arc<dyn ChatModel>,
51 model_name: String,
52 prompts: PromptRegistry,
53 contexts: Vec<Arc<dyn ContextContributor>>,
54 tools: ToolRegistry,
55 hooks: Vec<Arc<dyn Hook>>,
56 limits: RuntimeLimits,
57 meter: Arc<dyn TokenMeter>,
58 compactor: Arc<dyn Compactor>,
59 plugins: Option<MountedPlugins>,
60 reasoning_effort: Option<af_llm::ReasoningEffort>,
61 output_policy: Option<Value>,
62}
63
64struct InputDrain<'a> {
65 claimed: &'a mut HashSet<InputId>,
66 transcript: &'a mut Vec<ChatMessage>,
67 context_query: &'a mut String,
68 seen_seq: &'a mut u64,
69}
70
71impl AgentRuntime {
72 pub async fn run(
75 &self,
76 request: TurnRequest,
77 writer: &dyn EventWriter,
78 cancellation: CancellationToken,
79 ) -> Result<RuntimeOutcome, RuntimeError> {
80 if request.history.run_id != request.run_id {
81 return Err(RuntimeError::Invariant(
82 "history belongs to another Run".into(),
83 ));
84 }
85 let projection = &request.history.projection;
86 if projection
87 .active_run_id
88 .as_deref()
89 .is_some_and(|active| active != request.run_id)
90 {
91 return Err(RuntimeError::SessionBusy);
92 }
93 if request.compact {
94 return self.run_compaction(&request, writer, cancellation).await;
95 }
96 let input_message = user_message(&request.content)?;
97 let mut transcript = request.history.transcript.clone();
98 let user_text = content_text(&request.content);
99 if user_text.trim().is_empty() {
100 return Err(RuntimeError::InvalidInput(
101 "text content is required".into(),
102 ));
103 }
104 if projection.active_run_id.is_none() {
105 writer
106 .append(vec![
107 Event::InputClaimed {
108 input_id: request.input_id.clone(),
109 run_id: request.run_id.clone(),
110 },
111 Event::RunStarted {
112 run_id: request.run_id.clone(),
113 input_id: request.input_id.clone(),
114 },
115 ])
116 .await?;
117 }
118 let resuming = projection.open_turn.is_some();
119 if !resuming {
120 writer
121 .append(vec![
122 Event::TurnStarted {
123 run_id: request.run_id.clone(),
124 turn: 1,
125 },
126 Event::UserMessage {
127 run_id: request.run_id.clone(),
128 content: request.content.clone(),
129 },
130 ])
131 .await?;
132 }
133
134 let mut context_query = user_text.clone();
135 if !resuming {
136 transcript.push(input_message);
137 }
138 let (mut prompt_tokens, mut completion_tokens) = projection.usage_for(&request.run_id);
139 let mut tool_calls = request
140 .history.events
141 .iter()
142 .filter(|event| {
143 matches!(&event.event, Event::ToolCall { run_id, .. } if run_id == &request.run_id)
144 })
145 .count() as u32;
146 let mut seen_seq = projection.last_seq;
147 let mut claimed_inputs = projection
148 .claimed_inputs
149 .keys()
150 .cloned()
151 .collect::<HashSet<_>>();
152
153 let recovery = self
154 .recover_open_surface(
155 &request,
156 projection,
157 writer,
158 cancellation.clone(),
159 &mut transcript,
160 (prompt_tokens, completion_tokens),
161 )
162 .await?;
163 if let Some(outcome) = recovery.terminal {
164 return Ok(outcome);
165 }
166 let first_step = recovery.first_step;
167
168 Self::drain_inputs(
169 &request,
170 &request.history.events,
171 writer,
172 InputDrain {
173 claimed: &mut claimed_inputs,
174 transcript: &mut transcript,
175 context_query: &mut context_query,
176 seen_seq: &mut seen_seq,
177 },
178 )
179 .await?;
180
181 for step in first_step..=self.limits.max_steps {
182 if cancellation.is_cancelled() {
183 return self
184 .finish_open(
185 writer,
186 &request.run_id,
187 None,
188 RunStatus::Cancelled,
189 None,
190 (prompt_tokens, completion_tokens),
191 )
192 .await;
193 }
194 let incoming = writer.load_after(seen_seq).await?;
195 if let Some(last) = incoming.last() {
196 seen_seq = last.seq;
197 }
198 Self::drain_inputs(
199 &request,
200 &incoming,
201 writer,
202 InputDrain {
203 claimed: &mut claimed_inputs,
204 transcript: &mut transcript,
205 context_query: &mut context_query,
206 seen_seq: &mut seen_seq,
207 },
208 )
209 .await?;
210 let step_context = self.step_context(&request, step);
211 if let PreStepDecision::Reject { reason } = self.pre_step(&step_context).await {
212 writer
213 .append(vec![Event::Extension {
214 run_id: request.run_id.clone(),
215 plugin_id: "af-agent-runtime".into(),
216 event_type: "step_rejected".into(),
217 payload: json!({ "step": step, "reason": reason }),
218 }])
219 .await?;
220 return self
221 .finish_open(
222 writer,
223 &request.run_id,
224 None,
225 RunStatus::Cancelled,
226 None,
227 (prompt_tokens, completion_tokens),
228 )
229 .await;
230 }
231 writer
232 .append(vec![Event::StepStarted {
233 run_id: request.run_id.clone(),
234 step,
235 }])
236 .await?;
237 let context_messages = match self
238 .context_messages(&request, step, &context_query, writer, cancellation.clone())
239 .await
240 {
241 Ok(messages) => messages,
242 Err(RuntimeError::Cancelled) => {
243 return self
244 .finish_open(
245 writer,
246 &request.run_id,
247 Some(step),
248 RunStatus::Cancelled,
249 None,
250 (prompt_tokens, completion_tokens),
251 )
252 .await
253 }
254 Err(error) => return Err(error),
255 };
256 let compacted = match self
257 .compact_if_needed(
258 writer,
259 &request,
260 step,
261 &mut transcript,
262 &context_messages,
263 cancellation.clone(),
264 )
265 .await
266 {
267 Ok(compacted) => compacted,
268 Err(RuntimeError::Cancelled) => {
269 return self
270 .finish_open(
271 writer,
272 &request.run_id,
273 Some(step),
274 RunStatus::Cancelled,
275 None,
276 (prompt_tokens, completion_tokens),
277 )
278 .await
279 }
280 Err(RuntimeError::CompactionConflict) => {
281 writer
282 .append(vec![Event::StepFinished {
283 run_id: request.run_id.clone(),
284 step,
285 }])
286 .await?;
287 continue;
288 }
289 Err(error) => return Err(error),
290 };
291 prompt_tokens += compacted.0;
292 completion_tokens += compacted.1;
293 let completion = match self
294 .complete_with_retry(
295 writer,
296 ModelTurn {
297 run_id: &request.run_id,
298 step,
299 step_context: &step_context,
300 transcript: &transcript,
301 context: &context_messages,
302 after_seq: seen_seq,
303 },
304 cancellation.clone(),
305 )
306 .await
307 {
308 Ok(completion) => completion,
309 Err(RuntimeError::Cancelled) => {
310 return self
311 .finish_open(
312 writer,
313 &request.run_id,
314 Some(step),
315 RunStatus::Cancelled,
316 None,
317 (prompt_tokens, completion_tokens),
318 )
319 .await
320 }
321 Err(RuntimeError::Steered) => {
322 writer
323 .append(vec![Event::StepFinished {
324 run_id: request.run_id.clone(),
325 step,
326 }])
327 .await?;
328 continue;
329 }
330 Err(error) => return Err(error),
331 };
332 prompt_tokens += completion.prompt_tokens;
333 completion_tokens += completion.completion_tokens;
334 let response = completion.response;
335 if prompt_tokens + completion_tokens > self.limits.max_tokens {
336 return self
337 .finish_open(
338 writer,
339 &request.run_id,
340 Some(step),
341 RunStatus::MaxStepsReached,
342 None,
343 (prompt_tokens, completion_tokens),
344 )
345 .await;
346 }
347 let choice = response
348 .choices
349 .into_iter()
350 .next()
351 .ok_or(RuntimeError::EmptyModelResponse)?;
352 let finish_reason = choice
353 .finish_reason
354 .ok_or_else(|| RuntimeError::Model("missing finish_reason".into()))?;
355 let message = choice.message;
356 let output_blocks = choice.output_blocks;
357 match finish_reason {
358 FinishReason::Length => {
359 return self
360 .finish_open(
361 writer,
362 &request.run_id,
363 Some(step),
364 RunStatus::MaxStepsReached,
365 None,
366 (prompt_tokens, completion_tokens),
367 )
368 .await
369 }
370 FinishReason::ContentFilter | FinishReason::Unknown(_) => {
371 return Err(RuntimeError::FinishReason(finish_reason))
372 }
373 FinishReason::Stop => {
374 let (content, answer) =
375 assistant_content(message.content.as_deref(), output_blocks)?;
376 if let Some(policy) = &self.output_policy {
377 af_agent::validate_json_schema_value(
378 policy,
379 &serde_json::to_value(&content)
380 .map_err(|error| RuntimeError::Invariant(error.to_string()))?,
381 "assistant output",
382 )
383 .map_err(RuntimeError::Model)?;
384 }
385 if let TurnStopDecision::Steer { content: steer } =
386 self.turn_stopping(&step_context).await
387 {
388 let steer_message = user_message(&steer)?;
389 writer
390 .append(vec![
391 Event::AssistantMessage {
392 run_id: request.run_id.clone(),
393 step,
394 attempt: completion.attempt,
395 content,
396 },
397 Event::StepFinished {
398 run_id: request.run_id.clone(),
399 step,
400 },
401 Event::UserMessage {
402 run_id: request.run_id.clone(),
403 content: steer,
404 },
405 ])
406 .await?;
407 transcript.push(ChatMessage::assistant(answer.clone().unwrap_or_default()));
408 transcript.push(steer_message);
409 continue;
410 }
411 let terminal = writer
412 .append(vec![
413 Event::AssistantMessage {
414 run_id: request.run_id.clone(),
415 step,
416 attempt: completion.attempt,
417 content,
418 },
419 Event::StepFinished {
420 run_id: request.run_id.clone(),
421 step,
422 },
423 Event::TurnFinished {
424 run_id: request.run_id.clone(),
425 turn: 1,
426 },
427 Event::RunFinished {
428 run_id: request.run_id.clone(),
429 status: RunStatus::Completed,
430 error_code: None,
431 },
432 ])
433 .await;
434 if let Err(error) = terminal {
435 let pending = writer.load_after(seen_seq).await?;
436 if pending.iter().any(|event| matches!(
437 &event.event,
438 Event::InputQueued { run_id, mode: af_agent_session::DeliveryMode::Steer | af_agent_session::DeliveryMode::Inject, .. }
439 if run_id == &request.run_id
440 )) {
441 writer
442 .append(vec![Event::StepFinished {
443 run_id: request.run_id.clone(),
444 step,
445 }])
446 .await?;
447 continue;
448 }
449 return Err(error);
450 }
451 return Ok(RuntimeOutcome {
452 status: RunStatus::Completed.as_str().into(),
453 final_text: answer,
454 prompt_tokens,
455 completion_tokens,
456 waiting_interaction_id: None,
457 });
458 }
459 FinishReason::ToolCalls => {}
460 }
461 if !output_blocks.is_empty() {
462 return Err(RuntimeError::Model(
463 "structured output cannot accompany tool calls".into(),
464 ));
465 }
466 let calls = message.tool_calls.clone().unwrap_or_default();
467 if calls.is_empty() {
468 return Err(RuntimeError::Model(
469 "finish_reason tool_calls without tool_calls".into(),
470 ));
471 }
472 if tool_calls + calls.len() as u32 > self.limits.max_tool_calls {
473 return self
474 .finish_open(
475 writer,
476 &request.run_id,
477 Some(step),
478 RunStatus::MaxStepsReached,
479 None,
480 (prompt_tokens, completion_tokens),
481 )
482 .await;
483 }
484 let planned = calls
485 .into_iter()
486 .map(|call| {
487 let id: ToolCallId = call.id.clone();
488 let canonical_name = self
489 .tools
490 .canonical_name(&call.function.name)
491 .map(str::to_string);
492 let (arguments, preflight_error) =
493 match serde_json::from_str(&call.function.arguments) {
494 Ok(arguments) => {
495 let error = self
496 .tools
497 .validate_arguments(&call.function.name, &arguments)
498 .err();
499 (arguments, error)
500 }
501 Err(error) => (
502 json!({"_raw":call.function.arguments}),
503 Some(format!("invalid JSON arguments: {error}")),
504 ),
505 };
506 PlannedCall {
507 transcript_id: id.clone(),
508 id,
509 name: canonical_name.unwrap_or(call.function.name),
510 arguments,
511 step,
512 source_event_seq: 0,
513 preflight_error,
514 }
515 })
516 .collect::<Vec<_>>();
517 let mut durable_calls = vec![Event::AssistantToolCalls {
518 run_id: request.run_id.clone(),
519 step,
520 content: message.content.clone(),
521 calls: planned
522 .iter()
523 .map(|call| RecordedToolCall {
524 call_id: call.id.clone(),
525 tool: call.name.clone(),
526 arguments: call.arguments.clone(),
527 })
528 .collect(),
529 }];
530 durable_calls.extend(planned.iter().map(|call| Event::ToolCall {
531 run_id: request.run_id.clone(),
532 step,
533 call_id: call.id.clone(),
534 tool: call.name.clone(),
535 arguments: call.arguments.clone(),
536 }));
537 let appended_calls = writer.append(durable_calls).await?;
538 transcript.push(ChatMessage {
539 images: Vec::new(),
540 role: Role::Assistant,
541 content: message.content,
542 tool_calls: Some(
543 planned
544 .iter()
545 .map(|call| af_llm::ToolCall {
546 id: call.id.clone(),
547 kind: "function".into(),
548 function: af_llm::FunctionCall {
549 name: af_agent::model_tool_name(&call.name),
550 arguments: call.arguments.to_string(),
551 },
552 })
553 .collect(),
554 ),
555 tool_call_id: None,
556 name: None,
557 });
558
559 let mut planned = planned;
560 let mut decisions = Vec::with_capacity(planned.len());
561 for (call, envelope) in planned.iter_mut().zip(appended_calls.iter().skip(1)) {
562 call.source_event_seq = envelope.seq;
563 let decision = if let Some(reason) = &call.preflight_error {
564 HookDecision::Deny {
565 reason: reason.clone(),
566 }
567 } else {
568 self.authorize_call(&request, call, None, cancellation.clone())
569 .await
570 };
571 decisions.push(decision);
572 }
573 tool_calls += planned.len() as u32;
574 if let Some(waiting_index) = decisions
575 .iter()
576 .position(|decision| matches!(decision, HookDecision::WaitForInput { .. }))
577 {
578 let mut events = Vec::with_capacity(planned.len() * 2 + 2);
579 for (index, call) in planned.iter().enumerate() {
580 if index == waiting_index {
581 events.push(Event::ToolAuthorization {
582 run_id: request.run_id.clone(),
583 step,
584 call_id: call.id.clone(),
585 status: ToolAuthorizationStatus::Waiting,
586 reason: None,
587 });
588 } else {
589 let reason = match &decisions[index] {
590 HookDecision::Deny { reason } => reason.clone(),
591 _ => "blocked_by_pending_interaction".into(),
592 };
593 events.extend([
594 Event::ToolAuthorization {
595 run_id: request.run_id.clone(),
596 step,
597 call_id: call.id.clone(),
598 status: ToolAuthorizationStatus::Denied,
599 reason: Some(reason.clone()),
600 },
601 Event::ToolResult {
602 run_id: request.run_id.clone(),
603 step,
604 call_id: call.id.clone(),
605 result: json!({"error":reason}),
606 is_error: true,
607 },
608 ]);
609 }
610 }
611 let call = &planned[waiting_index];
612 let HookDecision::WaitForInput { kind, mut payload } =
613 decisions.swap_remove(waiting_index)
614 else {
615 unreachable!("waiting decision selected above")
616 };
617 if let Value::Object(object) = &mut payload {
618 object.insert("call_id".into(), Value::String(call.id.to_string()));
619 object.insert(
620 "source_event_seq".into(),
621 Value::from(call.source_event_seq),
622 );
623 }
624 let interaction_id = InteractionId::parse(uuid::Uuid::new_v4().to_string())
625 .expect("uuid interaction ids are never blank");
626 events.extend([
627 Event::InteractionRequested {
628 run_id: request.run_id.clone(),
629 interaction_id: interaction_id.clone(),
630 kind: if kind == "question" {
631 af_agent_session::InteractionKind::UserQuestion
632 } else {
633 af_agent_session::InteractionKind::Action
634 },
635 payload,
636 },
637 Event::RunWaiting {
638 run_id: request.run_id.clone(),
639 interaction_id: interaction_id.clone(),
640 },
641 ]);
642 writer.append(events).await?;
643 return Ok(RuntimeOutcome {
644 status: "waiting_for_input".into(),
645 final_text: None,
646 prompt_tokens,
647 completion_tokens,
648 waiting_interaction_id: Some(interaction_id),
649 });
650 }
651
652 let mut executable = Vec::new();
653 for (call, decision) in planned.iter().zip(decisions) {
654 match decision {
655 HookDecision::Continue => {
656 writer
657 .append(vec![Event::ToolAuthorization {
658 run_id: request.run_id.clone(),
659 step,
660 call_id: call.id.clone(),
661 status: ToolAuthorizationStatus::Allowed,
662 reason: None,
663 }])
664 .await?;
665 executable.push(call.clone());
666 }
667 HookDecision::Deny { reason } => {
668 let value = json!({"error":reason});
669 writer
670 .append(vec![
671 Event::ToolAuthorization {
672 run_id: request.run_id.clone(),
673 step,
674 call_id: call.id.clone(),
675 status: ToolAuthorizationStatus::Denied,
676 reason: Some(reason),
677 },
678 Event::ToolResult {
679 run_id: request.run_id.clone(),
680 step,
681 call_id: call.id.clone(),
682 result: value.clone(),
683 is_error: true,
684 },
685 ])
686 .await?;
687 transcript.push(tool_message(call, value));
688 }
689 HookDecision::WaitForInput { .. } => {
690 writer
691 .append(vec![Event::ToolAuthorization {
692 run_id: request.run_id.clone(),
693 step,
694 call_id: call.id.clone(),
695 status: ToolAuthorizationStatus::Waiting,
696 reason: None,
697 }])
698 .await?;
699 unreachable!("waiting decisions are handled as one atomic batch");
700 }
701 }
702 }
703 let executed = self
704 .execute_tools(&request, &executable, writer, cancellation.clone(), None)
705 .await?;
706 let mut tool_outcome_unknown = false;
707 for (call, result) in executable.into_iter().zip(executed) {
708 let outcome_unknown = matches!(
709 &result,
710 Err(error) if error.contains("tool_outcome_unknown")
711 );
712 tool_outcome_unknown |= outcome_unknown;
713 let value = match result {
714 Ok(value) => value,
715 Err(error) => json!({"error": error}),
716 };
717 if !outcome_unknown {
718 if let Err(error) = self
719 .run_after_hooks(&request, &call, &value, None, cancellation.clone())
720 .await
721 {
722 writer
723 .append(vec![Event::Extension {
724 run_id: request.run_id.clone(),
725 plugin_id: "agentfactory.runtime".into(),
726 event_type: "after_tool_failed".into(),
727 payload: json!({"call_id":call.id,"error":error}),
728 }])
729 .await?;
730 }
731 }
732 transcript.push(tool_message(&call, value));
733 }
734 writer
735 .append(vec![Event::StepFinished {
736 run_id: request.run_id.clone(),
737 step,
738 }])
739 .await?;
740 if tool_outcome_unknown {
741 return self
742 .finish_open(
743 writer,
744 &request.run_id,
745 None,
746 RunStatus::Failed,
747 None,
748 (prompt_tokens, completion_tokens),
749 )
750 .await;
751 }
752 }
753 self.finish_open(
754 writer,
755 &request.run_id,
756 None,
757 RunStatus::MaxStepsReached,
758 None,
759 (prompt_tokens, completion_tokens),
760 )
761 .await
762 }
763
764 async fn drain_inputs(
765 request: &TurnRequest,
766 events: &[SessionEvent],
767 writer: &dyn EventWriter,
768 state: InputDrain<'_>,
769 ) -> Result<(), RuntimeError> {
770 for envelope in events {
771 let Event::InputQueued {
772 input_id,
773 run_id,
774 mode: DeliveryMode::Steer | DeliveryMode::Inject,
775 content,
776 ..
777 } = &envelope.event
778 else {
779 continue;
780 };
781 if run_id != &request.run_id || !state.claimed.insert(input_id.clone()) {
782 continue;
783 }
784 let input_message = user_message(content)?;
785 let appended = writer
786 .append(vec![
787 Event::InputClaimed {
788 input_id: input_id.clone(),
789 run_id: request.run_id.clone(),
790 },
791 Event::UserMessage {
792 run_id: request.run_id.clone(),
793 content: content.clone(),
794 },
795 ])
796 .await?;
797 *state.seen_seq = appended.last().map_or(*state.seen_seq, |event| event.seq);
798 *state.context_query = content_text(content);
799 state.transcript.push(input_message);
800 }
801 Ok(())
802 }
803
804 async fn finish_open(
805 &self,
806 writer: &dyn EventWriter,
807 run_id: &RunId,
808 step: Option<u32>,
809 status: RunStatus,
810 final_text: Option<String>,
811 usage: (u64, u64),
812 ) -> Result<RuntimeOutcome, RuntimeError> {
813 let mut events = Vec::with_capacity(3);
814 if let Some(step) = step {
815 events.push(Event::StepFinished {
816 run_id: run_id.clone(),
817 step,
818 });
819 }
820 events.push(Event::TurnFinished {
821 run_id: run_id.clone(),
822 turn: 1,
823 });
824 events.push(Event::RunFinished {
825 run_id: run_id.clone(),
826 status,
827 error_code: None,
828 });
829 writer.append(events).await?;
830 Ok(RuntimeOutcome {
831 status: status.as_str().into(),
832 final_text,
833 prompt_tokens: usage.0,
834 completion_tokens: usage.1,
835 waiting_interaction_id: None,
836 })
837 }
838}