1use super::checkpointing::{
4 AgentProgress, save_checkpoint, validate_exact_usage, validate_usage_floor,
5};
6use super::observability::{consume_budget, emit_usage, record_domain, terminal_event};
7use super::{
8 Agent, AgentCheckpoint, AgentCheckpointPhase, AgentCheckpointState, AgentError,
9 AgentEventStream, AgentFuture, AgentObserver, AgentOutcome, AgentStreamEvent, Arc,
10 BufferedObserver, CheckpointCursor, ContentPart, Either, EventId, Instant, LifecycleEvent,
11 Message, ModelCallContext, ModelError, ModelErrorKind, ModelRequest, ModelResponse,
12 ModelStreamAccumulator, NoopObserver, ResumePolicy, Role, RunContext, RunEventKind, StreamExt,
13 ToolCall, Usage, emit_agent_event, select,
14};
15use crate::conversation::{
16 AgentConversationError, AgentConversationOutcome, AutomaticConversationSummary,
17 ConversationAppend, ConversationContextPolicy, ConversationId, ConversationStore,
18 ConversationSummaryCommit, ConversationSummaryRequest, MemoryNamespace, SemanticMemoryQuery,
19 is_transient_context, semantic_memory_message, summary_message,
20};
21use runifold_retrieval::RetrievalContext;
22
23impl Agent {
24 pub fn prompt<'a>(
31 &'a self,
32 input: impl Into<String> + Send + 'a,
33 ) -> AgentFuture<'a, Result<AgentOutcome, AgentError>> {
34 let input = input.into();
35 Box::pin(async move {
36 let run = self.default_run_context();
37 self.run(input, &run).await
38 })
39 }
40
41 pub fn prompt_text<'a>(
47 &'a self,
48 input: impl Into<String> + Send + 'a,
49 ) -> AgentFuture<'a, Result<String, AgentError>> {
50 let input = input.into();
51 Box::pin(async move { self.prompt(input).await.map(AgentOutcome::into_text) })
52 }
53
54 pub fn run<'a>(
56 &'a self,
57 input: impl Into<String> + Send + 'a,
58 run: &'a RunContext,
59 ) -> AgentFuture<'a, Result<AgentOutcome, AgentError>> {
60 let input = input.into();
61 let state = self.initial_state(input, run.root_run_id().to_string());
62 Box::pin(async move {
63 self.execute_state(state, run, None, Arc::new(NoopObserver), true)
64 .await
65 })
66 }
67
68 pub fn run_conversation<'a>(
74 &'a self,
75 input: impl Into<String> + Send + 'a,
76 run: &'a RunContext,
77 store: &'a dyn ConversationStore,
78 conversation_id: ConversationId,
79 namespace: MemoryNamespace,
80 policy: ConversationContextPolicy,
81 ) -> AgentFuture<'a, Result<AgentConversationOutcome, AgentConversationError>> {
82 let input = input.into();
83 Box::pin(async move {
84 store.create(conversation_id, namespace.clone()).await?;
85 let view = store
86 .load_view(
87 conversation_id,
88 namespace.clone(),
89 policy.window,
90 policy.summary_batch,
91 )
92 .await?;
93 if view.requires_summary() {
94 return Err(AgentConversationError::SummaryRequired {
95 conversation_id,
96 buffered_entries: u64::try_from(view.summary_buffer.len())
97 .unwrap_or(u64::MAX)
98 .saturating_add(view.summary_backlog),
99 });
100 }
101 let mut transcript = self.instructions.clone();
102 if let Some(summary) = &view.summary {
103 transcript.push(summary_message(summary));
104 }
105 if let Some(limit) = policy.semantic_memory_limit {
106 let query =
107 SemanticMemoryQuery::new(namespace.clone(), input.clone(), limit.get())?;
108 let search = store
109 .search_memory_scoped(query, RetrievalContext::for_run(run))
110 .await?;
111 if search.usage != Usage::default() {
112 consume_budget(run, search.usage, None).map_err(AgentConversationError::Run)?;
113 }
114 if let Some(message) = semantic_memory_message(&search.memories) {
115 transcript.push(message);
116 }
117 }
118 transcript.extend(view.window.iter().map(|entry| entry.message.clone()));
119 let persisted_prefix_len = transcript.len();
120 transcript.push(Message::user(input));
121 let state =
122 self.initial_state_from_transcript(transcript, run.root_run_id().to_string());
123 let outcome = self
124 .execute_state(state, run, None, Arc::new(NoopObserver), true)
125 .await
126 .map_err(AgentConversationError::Run)?;
127 let messages = outcome
128 .transcript
129 .iter()
130 .skip(persisted_prefix_len)
131 .filter(|message| !is_transient_context(message))
132 .cloned()
133 .collect();
134 let append = ConversationAppend {
135 conversation_id,
136 expected_version: view.version,
137 messages,
138 };
139 match store.append(namespace, append).await {
140 Ok(conversation_version) => Ok(AgentConversationOutcome {
141 outcome,
142 conversation_version,
143 }),
144 Err(source) => Err(AgentConversationError::Commit {
145 source,
146 outcome: Box::new(outcome),
147 }),
148 }
149 })
150 }
151
152 pub fn run_conversation_with_summary<'a>(
158 &'a self,
159 input: impl Into<String> + Send + 'a,
160 run: &'a RunContext,
161 store: &'a dyn ConversationStore,
162 conversation_id: ConversationId,
163 namespace: MemoryNamespace,
164 automatic_summary: AutomaticConversationSummary<'a>,
165 ) -> AgentFuture<'a, Result<AgentConversationOutcome, AgentConversationError>> {
166 let input = input.into();
167 Box::pin(async move {
168 let policy = automatic_summary.context;
169 store.create(conversation_id, namespace.clone()).await?;
170 for pass in 0..automatic_summary.max_passes.get() {
171 let view = store
172 .load_view(
173 conversation_id,
174 namespace.clone(),
175 policy.window,
176 policy.summary_batch,
177 )
178 .await?;
179 let Some(through_sequence) = view.summary_buffer.last().map(|entry| entry.sequence)
180 else {
181 break;
182 };
183 let summary_backlog = view.summary_backlog;
184 let summary = automatic_summary
185 .summarizer
186 .summarize(
187 ConversationSummaryRequest {
188 transcript_version: view.version,
189 previous_summary: view.summary,
190 entries: view.summary_buffer,
191 },
192 run,
193 )
194 .await?;
195 store
196 .commit_summary(
197 namespace.clone(),
198 ConversationSummaryCommit {
199 conversation_id,
200 expected_version: view.version,
201 through_sequence,
202 content: summary,
203 },
204 )
205 .await?;
206 if summary_backlog == 0 {
207 break;
208 }
209 if pass + 1 == automatic_summary.max_passes.get() {
210 return Err(AgentConversationError::SummaryPassLimitExceeded {
211 conversation_id,
212 remaining_entries: summary_backlog,
213 });
214 }
215 }
216 self.run_conversation(input, run, store, conversation_id, namespace, policy)
217 .await
218 })
219 }
220
221 pub fn stream<'a>(
223 &'a self,
224 input: impl Into<String> + Send + 'a,
225 run: &'a RunContext,
226 ) -> AgentEventStream<'a> {
227 let state = self.initial_state(input.into(), run.root_run_id().to_string());
228 let observer = BufferedObserver::default();
229 let events = observer.events();
230 let execution = Box::pin(self.execute_state(state, run, None, Arc::new(observer), true));
231 AgentEventStream::new(execution, events)
232 }
233
234 pub fn run_checkpointed<'a>(
236 &'a self,
237 input: impl Into<String> + Send + 'a,
238 run: &'a RunContext,
239 checkpoint: &'a AgentCheckpoint,
240 ) -> AgentFuture<'a, Result<AgentOutcome, AgentError>> {
241 let input = input.into();
242 Box::pin(async move {
243 let mut state = self.initial_state(input, checkpoint.id().to_string());
244 state.usage = run.budget().usage();
245 let mut cursor = CheckpointCursor::create(checkpoint, run, &state)?;
246 self.execute_state(state, run, Some(&mut cursor), Arc::new(NoopObserver), true)
247 .await
248 })
249 }
250
251 pub fn resume<'a>(
253 &'a self,
254 checkpoint: &'a AgentCheckpoint,
255 run: &'a RunContext,
256 policy: ResumePolicy,
257 ) -> AgentFuture<'a, Result<AgentOutcome, AgentError>> {
258 Box::pin(async move {
259 let (envelope, mut state) = checkpoint.load()?;
260 self.validate_checkpoint_identity(&state)?;
261 if let Some(outcome) = state.outcome() {
262 validate_exact_usage(state.usage, run.budget().usage())?;
263 return Ok(outcome);
264 }
265 if let AgentCheckpointPhase::TurnInFlight { turn } = state.phase {
266 if policy == ResumePolicy::RejectAmbiguous {
267 return Err(AgentError::AmbiguousCheckpoint { turn });
268 }
269 validate_usage_floor(state.usage, run.budget().usage())?;
270 state.usage = run.budget().usage();
271 state.phase = AgentCheckpointPhase::ReadyForTurn;
272 } else {
273 validate_exact_usage(state.usage, run.budget().usage())?;
274 }
275 let mut cursor = CheckpointCursor::loaded(checkpoint, envelope);
276 self.execute_state(state, run, Some(&mut cursor), Arc::new(NoopObserver), false)
277 .await
278 })
279 }
280
281 fn initial_state(&self, input: String, execution_id: String) -> AgentCheckpointState {
282 let mut transcript = self.instructions.clone();
283 transcript.push(Message::user(input));
284 self.initial_state_from_transcript(transcript, execution_id)
285 }
286
287 fn initial_state_from_transcript(
288 &self,
289 transcript: Vec<Message>,
290 execution_id: String,
291 ) -> AgentCheckpointState {
292 AgentCheckpointState {
293 execution_id,
294 agent: self.name.clone(),
295 model: self.model_ref.clone(),
296 transcript,
297 turns: 0,
298 tool_calls: 0,
299 delegations: 0,
300 usage: Usage::default(),
301 phase: AgentCheckpointPhase::ReadyForTurn,
302 }
303 }
304
305 async fn execute_state(
306 &self,
307 state: AgentCheckpointState,
308 run: &RunContext,
309 mut checkpoint: Option<&mut CheckpointCursor>,
310 observer: Arc<dyn AgentObserver>,
311 retrieve_context: bool,
312 ) -> Result<AgentOutcome, AgentError> {
313 let started = run
314 .record(
315 RunEventKind::Lifecycle(LifecycleEvent::Started),
316 run.caused_by(),
317 )?
318 .map(|event| event.meta.event_id);
319 emit_agent_event(
320 observer.as_ref(),
321 AgentStreamEvent::Started {
322 agent: self.name.clone(),
323 },
324 )
325 .await;
326 let result = async {
327 let has_context = !self.context.is_empty() || !self.dynamic_context.is_empty();
328 let state = if retrieve_context && has_context {
329 let mut prepared = self
330 .prepare_context(state, run, started, observer.as_ref())
331 .await?;
332 prepared.usage = run.budget().usage();
333 save_checkpoint(&mut checkpoint, &prepared)?;
334 prepared
335 } else {
336 state
337 };
338 self.run_loop(state, run, started, checkpoint, observer.as_ref())
339 .await
340 }
341 .await;
342 let terminal = terminal_event(&self.name, &result);
343 run.record(terminal, started)?;
344 if let Ok(outcome) = &result {
345 emit_agent_event(
346 observer.as_ref(),
347 AgentStreamEvent::Completed {
348 outcome: outcome.clone(),
349 },
350 )
351 .await;
352 }
353 result
354 }
355
356 async fn run_loop(
357 &self,
358 state: AgentCheckpointState,
359 run: &RunContext,
360 caused_by: Option<EventId>,
361 mut checkpoint: Option<&mut CheckpointCursor>,
362 observer: &dyn AgentObserver,
363 ) -> Result<AgentOutcome, AgentError> {
364 self.validate_config()?;
365 let mut progress = AgentProgress::from(state);
366
367 loop {
368 Self::check_lifecycle(run)?;
369 if progress.turns >= self.config.max_turns {
370 return Err(AgentError::MaxTurns {
371 max_turns: self.config.max_turns,
372 });
373 }
374 save_checkpoint(
375 &mut checkpoint,
376 &self.checkpoint_state(
377 &progress,
378 run,
379 AgentCheckpointPhase::TurnInFlight {
380 turn: progress.turns + 1,
381 },
382 ),
383 )?;
384 consume_budget(
385 run,
386 Usage {
387 turns: 1,
388 ..Usage::default()
389 },
390 caused_by,
391 )?;
392 progress.turns += 1;
393 emit_agent_event(
394 observer,
395 AgentStreamEvent::TurnStarted {
396 turn: progress.turns,
397 },
398 )
399 .await;
400 emit_usage(observer, run).await;
401 record_domain(
402 run,
403 "turn.started",
404 serde_json::json!({"agent": self.name, "turn": progress.turns}),
405 caused_by,
406 )?;
407
408 let response = self
409 .invoke_model(
410 &progress.transcript,
411 run,
412 progress.turns,
413 caused_by,
414 observer,
415 )
416 .await?;
417
418 let calls = tool_calls_from(&response.content);
419 let assistant = Message::new(Role::Assistant, response.content.clone())
420 .map_err(|error| AgentError::Protocol(error.to_string()))?;
421 progress.transcript.push(assistant);
422
423 if calls.is_empty() {
424 if matches!(
425 response.finish_reason,
426 runifold_model::FinishReason::ToolCalls
427 ) {
428 return Err(AgentError::Protocol(
429 "model stopped for tool calls without emitting a tool call".into(),
430 ));
431 }
432 save_checkpoint(
433 &mut checkpoint,
434 &self.checkpoint_state(
435 &progress,
436 run,
437 AgentCheckpointPhase::Completed {
438 response: Box::new(response.clone()),
439 },
440 ),
441 )?;
442 return Ok(progress.outcome(response, run.budget().usage()));
443 }
444
445 self.execute_calls(calls, run, caused_by, &mut progress, observer)
446 .await?;
447 save_checkpoint(
448 &mut checkpoint,
449 &self.checkpoint_state(&progress, run, AgentCheckpointPhase::ReadyForTurn),
450 )?;
451 }
452 }
453
454 async fn invoke_model(
455 &self,
456 transcript: &[Message],
457 run: &RunContext,
458 turn: u32,
459 caused_by: Option<EventId>,
460 observer: &dyn AgentObserver,
461 ) -> Result<ModelResponse, AgentError> {
462 record_domain(
463 run,
464 "model.started",
465 serde_json::json!({
466 "agent": self.name,
467 "turn": turn,
468 "provider": self.model_ref.provider,
469 "model": self.model_ref.name,
470 }),
471 caused_by,
472 )?;
473 let response = match self
474 .stream_model_response(self.request(transcript)?, run, turn, observer)
475 .await
476 {
477 Ok(response) => response,
478 Err(error) => {
479 record_domain(
480 run,
481 "model.failed",
482 serde_json::json!({
483 "agent": self.name,
484 "turn": turn,
485 "kind": format!("{:?}", error.kind),
486 }),
487 caused_by,
488 )?;
489 return Err(error.into());
490 }
491 };
492 record_domain(
493 run,
494 "model.completed",
495 serde_json::json!({
496 "agent": self.name,
497 "turn": turn,
498 "finish_reason": response.finish_reason,
499 "usage": response.usage,
500 }),
501 caused_by,
502 )?;
503 consume_budget(run, response.usage.into(), caused_by)?;
504 emit_usage(observer, run).await;
505 Ok(response)
506 }
507
508 async fn stream_model_response(
509 &self,
510 request: ModelRequest,
511 run: &RunContext,
512 turn: u32,
513 observer: &dyn AgentObserver,
514 ) -> Result<ModelResponse, ModelError> {
515 let context = ModelCallContext::for_run(run);
516 let cancellation = context.cancellation().clone();
517 let opening = self.model.stream(request, context);
518 let mut stream = match select(Box::pin(cancellation.cancelled()), Box::pin(opening)).await {
519 Either::Left(_) => return Err(cancelled_model_error()),
520 Either::Right((result, _)) => result?,
521 };
522 let mut accumulator = ModelStreamAccumulator::new();
523 loop {
524 let next = stream.next();
525 let event = match select(Box::pin(cancellation.cancelled()), Box::pin(next)).await {
526 Either::Left(_) => return Err(cancelled_model_error()),
527 Either::Right((Some(event), _)) => event?,
528 Either::Right((None, _)) => {
529 return Err(ModelError::local(
530 ModelErrorKind::Protocol,
531 "model stream ended before a terminal response event",
532 ));
533 }
534 };
535 let response = accumulator.push(event.clone())?;
536 emit_agent_event(observer, AgentStreamEvent::Model { turn, event }).await;
537 if let Some(response) = response {
538 return Ok(response);
539 }
540 }
541 }
542
543 fn validate_config(&self) -> Result<(), AgentError> {
544 if self.name.trim().is_empty() {
545 return Err(AgentError::InvalidConfig(
546 "agent name cannot be empty".into(),
547 ));
548 }
549 if self.config.max_turns == 0 {
550 return Err(AgentError::InvalidConfig(
551 "max_turns must be greater than zero".into(),
552 ));
553 }
554 if let Some(collision) = self
555 .agents
556 .model_specs()
557 .into_iter()
558 .find(|spec| self.tools.contains(&spec.name))
559 {
560 return Err(AgentError::InvalidConfig(format!(
561 "callable name `{}` is registered as both a tool and an agent",
562 collision.name
563 )));
564 }
565 Ok(())
566 }
567
568 fn validate_checkpoint_identity(&self, state: &AgentCheckpointState) -> Result<(), AgentError> {
569 if state.agent != self.name || state.model != self.model_ref {
570 return Err(runifold_core::CheckpointError::new(
571 runifold_core::CheckpointErrorKind::InvalidPayload,
572 "checkpoint Agent or model identity does not match",
573 )
574 .into());
575 }
576 Ok(())
577 }
578
579 fn checkpoint_state(
580 &self,
581 progress: &AgentProgress,
582 run: &RunContext,
583 phase: AgentCheckpointPhase,
584 ) -> AgentCheckpointState {
585 AgentCheckpointState {
586 execution_id: progress.execution_id.clone(),
587 agent: self.name.clone(),
588 model: self.model_ref.clone(),
589 transcript: progress.transcript.clone(),
590 turns: progress.turns,
591 tool_calls: progress.tool_calls,
592 delegations: progress.delegations,
593 usage: run.budget().usage(),
594 phase,
595 }
596 }
597
598 pub(super) fn check_lifecycle(run: &RunContext) -> Result<(), AgentError> {
599 let error = if run.cancellation().is_cancelled() {
600 Some((
601 runifold_model::ModelErrorKind::Cancelled,
602 "agent run was cancelled",
603 ))
604 } else if run
605 .deadline()
606 .is_some_and(|deadline| deadline <= Instant::now())
607 {
608 Some((
609 runifold_model::ModelErrorKind::DeadlineExceeded,
610 "agent run deadline elapsed",
611 ))
612 } else {
613 None
614 };
615 if let Some((kind, message)) = error {
616 return Err(runifold_model::ModelError::local(kind, message).into());
617 }
618 Ok(())
619 }
620
621 fn request(&self, transcript: &[Message]) -> Result<ModelRequest, AgentError> {
622 let (first, rest) = transcript
623 .split_first()
624 .ok_or_else(|| AgentError::Protocol("agent transcript is empty".into()))?;
625 let mut request = ModelRequest::new(self.model_ref.clone(), first.clone());
626 request.messages.extend_from_slice(rest);
627 request.tools = self.tools.model_specs();
628 request.tools.extend(self.agents.model_specs());
629 request.feature_policy = self.config.feature_policy;
630 request.output_format.clone_from(&self.output_format);
631 Ok(request)
632 }
633}
634
635fn cancelled_model_error() -> ModelError {
636 ModelError::local(ModelErrorKind::Cancelled, "model invocation was cancelled")
637}
638
639fn tool_calls_from(content: &[ContentPart]) -> Vec<ToolCall> {
640 content
641 .iter()
642 .filter_map(|part| match part {
643 ContentPart::ToolCall(call) => Some(call.clone()),
644 _ => None,
645 })
646 .collect()
647}