1use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
2
3use futures_util::{
4 StreamExt,
5 future::{Either, select},
6};
7use runifold_core::{
8 Budget, BudgetEvent, BudgetTracker, CapabilitySet, DomainEvent, EffectId, EffectKind,
9 EffectRequest, EventId, Instant, InvocationId, LifecycleEvent, RetrySafety, RunContext,
10 RunError, RunErrorKind, RunEventKind, Usage,
11};
12use runifold_effect::{
13 EffectExecutionContext, EffectExecutor, EffectFuture, EffectHandler, EffectRecoveryPolicy,
14 InMemoryEffectStore,
15};
16use runifold_model::{
17 ContentPart, FeaturePolicy, GenerationOptions, Message, Model, ModelCallContext, ModelError,
18 ModelErrorKind, ModelRef, ModelRequest, ModelResponse, ModelStreamAccumulator, OutputFormat,
19 ProviderToolSpec, ResponseMode, Role, ToolCall, ToolChoice, ToolResult,
20};
21use runifold_retrieval::{Document, Retriever};
22use runifold_tool::{ToolError, ToolErrorKind, ToolOutput, ToolRegistry};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25
26use crate::checkpoint::CheckpointCursor;
27use crate::stream::{AgentObserver, BufferedObserver, NoopObserver, emit_agent_event};
28use crate::{
29 AgentCheckpoint, AgentCheckpointPhase, AgentCheckpointState, DurableConversationCheckpoint,
30 ResumePolicy,
31};
32
33const TOOL_RESULT_EXECUTION_ID_METADATA: &str = "runifold.agent.execution_id";
34use crate::terminal_review::{TerminalReviewConfig, TurnReviewConfig};
35use crate::{
36 AgentError, AgentEventStream, AgentGateway, AgentOutcome, AgentStreamEvent, CallableKind,
37 CompletionRequirement, GatewayError, GatewayErrorKind, StructuredAgent,
38};
39use crate::{TerminalReviewPolicy, TerminalReviewer, TurnReviewPolicy, TurnReviewer};
40
41mod callable;
42mod checkpointing;
43pub(crate) mod completion;
44mod execution;
45mod observability;
46mod retrieval;
47mod review;
48
49#[cfg(not(target_arch = "wasm32"))]
51pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
52
53#[cfg(target_arch = "wasm32")]
55pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
56
57#[derive(Clone)]
59pub(crate) struct DynamicContext {
60 pub(crate) limit: usize,
61 pub(crate) retriever: Arc<dyn Retriever>,
62}
63
64impl std::fmt::Debug for DynamicContext {
65 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 formatter
67 .debug_struct("DynamicContext")
68 .field("limit", &self.limit)
69 .field("retriever", self.retriever.descriptor())
70 .finish()
71 }
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum ToolErrorPolicy {
78 #[default]
80 ReturnToModel,
81 FailFast,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct AgentConfig {
88 pub max_turns: u32,
90 pub tool_error_policy: ToolErrorPolicy,
92 pub feature_policy: FeaturePolicy,
94}
95
96impl Default for AgentConfig {
97 fn default() -> Self {
98 Self {
99 max_turns: 16,
100 tool_error_policy: ToolErrorPolicy::ReturnToModel,
101 feature_policy: FeaturePolicy::Strict,
102 }
103 }
104}
105
106#[derive(Clone)]
108pub struct Agent {
109 pub(crate) name: String,
110 pub(crate) model: Arc<dyn Model>,
111 pub(crate) model_ref: ModelRef,
112 pub(crate) instructions: Vec<Message>,
113 pub(crate) context: Vec<Document>,
114 pub(crate) dynamic_context: Vec<DynamicContext>,
115 pub(crate) tools: ToolRegistry,
116 pub(crate) agents: AgentGateway,
117 pub(crate) effects: EffectExecutor,
118 pub(crate) effect_recovery: EffectRecoveryPolicy,
119 pub(crate) config: AgentConfig,
120 pub(crate) min_successful_tool_calls: u32,
121 pub(crate) output_format: OutputFormat,
122 pub(crate) generation: GenerationOptions,
123 pub(crate) response_mode: ResponseMode,
124 pub(crate) provider_tools: Vec<ProviderToolSpec>,
125 pub(crate) provider_options: BTreeMap<String, serde_json::Value>,
126 pub(crate) completion_requirement: CompletionRequirement,
127 pub(crate) completion_validator: completion::CompletionValidator,
128 pub(crate) turn_review: Option<TurnReviewConfig>,
129 pub(crate) terminal_review: Option<TerminalReviewConfig>,
130}
131
132impl Agent {
133 pub fn builder(
135 name: impl Into<String>,
136 model: Arc<dyn Model>,
137 model_ref: ModelRef,
138 ) -> crate::AgentBuilder {
139 crate::AgentBuilder::new(name, model, model_ref)
140 }
141
142 pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
144 Self {
145 name: name.into(),
146 model,
147 model_ref,
148 instructions: Vec::new(),
149 context: Vec::new(),
150 dynamic_context: Vec::new(),
151 tools: ToolRegistry::new(),
152 agents: AgentGateway::new(),
153 effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
154 effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
155 config: AgentConfig::default(),
156 min_successful_tool_calls: 0,
157 output_format: OutputFormat::Text,
158 generation: GenerationOptions::default(),
159 response_mode: ResponseMode::Streaming,
160 provider_tools: Vec::new(),
161 provider_options: BTreeMap::new(),
162 completion_requirement: CompletionRequirement::default(),
163 completion_validator: completion::CompletionValidator::content(),
164 turn_review: None,
165 terminal_review: None,
166 }
167 }
168
169 #[must_use]
171 pub fn system(mut self, instruction: impl Into<String>) -> Self {
172 self.instructions.push(Message::system(instruction));
173 self
174 }
175
176 #[must_use]
178 pub fn tools(mut self, tools: ToolRegistry) -> Self {
179 self.tools = tools;
180 self
181 }
182
183 #[must_use]
185 pub fn agents(mut self, agents: AgentGateway) -> Self {
186 self.agents = agents;
187 self
188 }
189
190 #[must_use]
192 pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
193 self.effects = effects;
194 self
195 }
196
197 #[must_use]
199 pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
200 self.effect_recovery = policy;
201 self
202 }
203
204 #[must_use]
206 pub const fn with_config(mut self, config: AgentConfig) -> Self {
207 self.config = config;
208 self
209 }
210
211 #[must_use]
213 pub const fn completion_requirement(mut self, requirement: CompletionRequirement) -> Self {
214 self.completion_requirement = requirement;
215 self
216 }
217
218 #[must_use]
221 pub fn turn_reviewer<R>(
222 self,
223 reviewer: R,
224 policy: TurnReviewPolicy,
225 capabilities: CapabilitySet,
226 ) -> Self
227 where
228 R: TurnReviewer + 'static,
229 {
230 self.shared_turn_reviewer(Arc::new(reviewer), policy, capabilities)
231 }
232
233 #[must_use]
235 pub fn shared_turn_reviewer(
236 mut self,
237 reviewer: Arc<dyn TurnReviewer>,
238 policy: TurnReviewPolicy,
239 capabilities: CapabilitySet,
240 ) -> Self {
241 let descriptor = reviewer.turn_descriptor().clone();
242 self.turn_review = Some(TurnReviewConfig {
243 reviewer,
244 descriptor,
245 policy,
246 capabilities,
247 });
248 self
249 }
250
251 #[must_use]
257 pub fn terminal_reviewer<R>(
258 self,
259 reviewer: R,
260 policy: TerminalReviewPolicy,
261 capabilities: CapabilitySet,
262 ) -> Self
263 where
264 R: TerminalReviewer + 'static,
265 {
266 self.shared_terminal_reviewer(Arc::new(reviewer), policy, capabilities)
267 }
268
269 #[must_use]
271 pub fn shared_terminal_reviewer(
272 mut self,
273 reviewer: Arc<dyn TerminalReviewer>,
274 policy: TerminalReviewPolicy,
275 capabilities: CapabilitySet,
276 ) -> Self {
277 let descriptor = reviewer.descriptor().clone();
278 self.terminal_review = Some(TerminalReviewConfig {
279 reviewer,
280 descriptor,
281 policy,
282 capabilities,
283 });
284 self
285 }
286
287 #[must_use]
293 pub const fn min_successful_tool_calls(mut self, minimum: u32) -> Self {
294 self.min_successful_tool_calls = minimum;
295 self
296 }
297
298 #[must_use]
300 pub fn output_format(mut self, output_format: OutputFormat) -> Self {
301 self.output_format = output_format;
302 self
303 }
304
305 #[must_use]
307 pub fn structured_output<T>(self, name: impl Into<String>) -> Self
308 where
309 T: JsonSchema,
310 {
311 self.structured_output_with_strictness::<T>(name, true)
312 }
313
314 #[must_use]
317 pub fn structured_output_with_strictness<T>(self, name: impl Into<String>, strict: bool) -> Self
318 where
319 T: JsonSchema,
320 {
321 self.output_format(OutputFormat::typed_with_strictness::<T>(name, strict))
322 }
323
324 pub fn into_structured<T>(self, name: impl Into<String>) -> StructuredAgent<T>
326 where
327 T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
328 {
329 self.into_structured_with_strictness::<T>(name, true)
330 }
331
332 pub fn into_structured_with_strictness<T>(
335 mut self,
336 name: impl Into<String>,
337 strict: bool,
338 ) -> StructuredAgent<T>
339 where
340 T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
341 {
342 self = self.structured_output_with_strictness::<T>(name, strict);
343 self.completion_validator = completion::CompletionValidator::structured::<T>();
344 StructuredAgent::new(self)
345 }
346
347 pub fn name(&self) -> &str {
349 &self.name
350 }
351
352 pub const fn model_ref(&self) -> &ModelRef {
354 &self.model_ref
355 }
356
357 pub fn callable_capabilities(&self) -> CapabilitySet {
363 let mut capabilities = CapabilitySet::new();
364 for spec in self.tools.model_specs() {
365 if let Some(descriptor) = self.tools.descriptor(&spec.name) {
366 capabilities.grant(descriptor.capability());
367 }
368 }
369 for spec in self.agents.model_specs() {
370 if let Some(descriptor) = self.agents.descriptor(&spec.name) {
371 capabilities.grant(descriptor.capability());
372 }
373 }
374 for source in &self.dynamic_context {
375 capabilities.grant(source.retriever.descriptor().capability());
376 }
377 if let Some(review) = &self.terminal_review {
378 for capability in review.capabilities.iter() {
379 capabilities.grant(capability.clone());
380 }
381 }
382 capabilities
383 }
384
385 #[must_use]
393 pub fn default_run_context(&self) -> RunContext {
394 RunContext::root(
395 BudgetTracker::new(Budget::default()),
396 self.callable_capabilities(),
397 )
398 }
399}
400
401impl std::fmt::Debug for Agent {
402 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 formatter
404 .debug_struct("Agent")
405 .field("name", &self.name)
406 .field("model_ref", &self.model_ref)
407 .field("instructions", &self.instructions)
408 .field("context", &self.context)
409 .field("dynamic_context", &self.dynamic_context)
410 .field("tools", &self.tools)
411 .field("agents", &self.agents)
412 .field("effects", &self.effects)
413 .field("effect_recovery", &self.effect_recovery)
414 .field("config", &self.config)
415 .field("output_format", &self.output_format)
416 .field("terminal_review", &self.terminal_review)
417 .finish_non_exhaustive()
418 }
419}
420
421#[cfg(test)]
422mod tests;