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, Deserialize, Eq, PartialEq, Serialize)]
76#[non_exhaustive]
77pub enum ToolErrorPolicy {
78 #[default]
80 ReturnToModel,
81 FailFast,
83}
84
85#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
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) tool_concurrency: std::num::NonZeroUsize,
121 pub(crate) min_successful_tool_calls: u32,
122 pub(crate) output_format: OutputFormat,
123 pub(crate) generation: GenerationOptions,
124 pub(crate) response_mode: ResponseMode,
125 pub(crate) provider_tools: Vec<ProviderToolSpec>,
126 pub(crate) provider_options: BTreeMap<String, serde_json::Value>,
127 pub(crate) completion_requirement: CompletionRequirement,
128 pub(crate) completion_validator: completion::CompletionValidator,
129 pub(crate) turn_review: Option<TurnReviewConfig>,
130 pub(crate) terminal_review: Option<TerminalReviewConfig>,
131}
132
133impl Agent {
134 pub fn builder(
136 name: impl Into<String>,
137 model: Arc<dyn Model>,
138 model_ref: ModelRef,
139 ) -> crate::AgentBuilder {
140 crate::AgentBuilder::new(name, model, model_ref)
141 }
142
143 pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
150 Self {
151 name: name.into(),
152 model,
153 model_ref,
154 instructions: Vec::new(),
155 context: Vec::new(),
156 dynamic_context: Vec::new(),
157 tools: ToolRegistry::new(),
158 agents: AgentGateway::new(),
159 effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
160 effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
161 config: AgentConfig::default(),
162 tool_concurrency: std::num::NonZeroUsize::MIN,
163 min_successful_tool_calls: 0,
164 output_format: OutputFormat::Text,
165 generation: GenerationOptions::default(),
166 response_mode: ResponseMode::Streaming,
167 provider_tools: Vec::new(),
168 provider_options: BTreeMap::new(),
169 completion_requirement: CompletionRequirement::default(),
170 completion_validator: completion::CompletionValidator::content(),
171 turn_review: None,
172 terminal_review: None,
173 }
174 }
175
176 #[must_use]
180 pub const fn tool_concurrency(mut self, limit: std::num::NonZeroUsize) -> Self {
181 self.tool_concurrency = limit;
182 self
183 }
184
185 #[must_use]
187 pub fn system(mut self, instruction: impl Into<String>) -> Self {
188 self.instructions.push(Message::system(instruction));
189 self
190 }
191
192 #[must_use]
194 pub fn tools(mut self, tools: ToolRegistry) -> Self {
195 self.tools = tools;
196 self
197 }
198
199 #[must_use]
201 pub fn agents(mut self, agents: AgentGateway) -> Self {
202 self.agents = agents;
203 self
204 }
205
206 #[must_use]
208 pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
209 self.effects = effects;
210 self
211 }
212
213 #[must_use]
215 pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
216 self.effect_recovery = policy;
217 self
218 }
219
220 #[must_use]
222 pub const fn with_config(mut self, config: AgentConfig) -> Self {
223 self.config = config;
224 self
225 }
226
227 #[must_use]
229 pub const fn completion_requirement(mut self, requirement: CompletionRequirement) -> Self {
230 self.completion_requirement = requirement;
231 self
232 }
233
234 #[must_use]
237 pub fn turn_reviewer<R>(
238 self,
239 reviewer: R,
240 policy: TurnReviewPolicy,
241 capabilities: CapabilitySet,
242 ) -> Self
243 where
244 R: TurnReviewer + 'static,
245 {
246 self.shared_turn_reviewer(Arc::new(reviewer), policy, capabilities)
247 }
248
249 #[must_use]
251 pub fn shared_turn_reviewer(
252 mut self,
253 reviewer: Arc<dyn TurnReviewer>,
254 policy: TurnReviewPolicy,
255 capabilities: CapabilitySet,
256 ) -> Self {
257 let descriptor = reviewer.turn_descriptor().clone();
258 self.turn_review = Some(TurnReviewConfig {
259 reviewer,
260 descriptor,
261 policy,
262 capabilities,
263 });
264 self
265 }
266
267 #[must_use]
273 pub fn terminal_reviewer<R>(
274 self,
275 reviewer: R,
276 policy: TerminalReviewPolicy,
277 capabilities: CapabilitySet,
278 ) -> Self
279 where
280 R: TerminalReviewer + 'static,
281 {
282 self.shared_terminal_reviewer(Arc::new(reviewer), policy, capabilities)
283 }
284
285 #[must_use]
287 pub fn shared_terminal_reviewer(
288 mut self,
289 reviewer: Arc<dyn TerminalReviewer>,
290 policy: TerminalReviewPolicy,
291 capabilities: CapabilitySet,
292 ) -> Self {
293 let descriptor = reviewer.descriptor().clone();
294 self.terminal_review = Some(TerminalReviewConfig {
295 reviewer,
296 descriptor,
297 policy,
298 capabilities,
299 });
300 self
301 }
302
303 #[must_use]
309 pub const fn min_successful_tool_calls(mut self, minimum: u32) -> Self {
310 self.min_successful_tool_calls = minimum;
311 self
312 }
313
314 #[must_use]
316 pub fn output_format(mut self, output_format: OutputFormat) -> Self {
317 self.output_format = output_format;
318 self
319 }
320
321 #[must_use]
323 pub fn structured_output<T>(self, name: impl Into<String>) -> Self
324 where
325 T: JsonSchema,
326 {
327 self.structured_output_with_strictness::<T>(name, true)
328 }
329
330 #[must_use]
333 pub fn structured_output_with_strictness<T>(self, name: impl Into<String>, strict: bool) -> Self
334 where
335 T: JsonSchema,
336 {
337 self.output_format(OutputFormat::typed_with_strictness::<T>(name, strict))
338 }
339
340 pub fn into_structured<T>(self, name: impl Into<String>) -> StructuredAgent<T>
342 where
343 T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
344 {
345 self.into_structured_with_strictness::<T>(name, true)
346 }
347
348 pub fn into_structured_with_strictness<T>(
351 mut self,
352 name: impl Into<String>,
353 strict: bool,
354 ) -> StructuredAgent<T>
355 where
356 T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
357 {
358 self = self.structured_output_with_strictness::<T>(name, strict);
359 self.completion_validator = completion::CompletionValidator::structured::<T>();
360 StructuredAgent::new(self)
361 }
362
363 pub fn name(&self) -> &str {
365 &self.name
366 }
367
368 pub const fn model_ref(&self) -> &ModelRef {
370 &self.model_ref
371 }
372
373 pub fn callable_capabilities(&self) -> CapabilitySet {
379 let mut capabilities = CapabilitySet::new();
380 for spec in self.tools.model_specs() {
381 if let Some(descriptor) = self.tools.descriptor(&spec.name) {
382 capabilities.grant(descriptor.capability());
383 }
384 }
385 for spec in self.agents.model_specs() {
386 if let Some(descriptor) = self.agents.descriptor(&spec.name) {
387 capabilities.grant(descriptor.capability());
388 }
389 }
390 for source in &self.dynamic_context {
391 capabilities.grant(source.retriever.descriptor().capability());
392 }
393 if let Some(review) = &self.terminal_review {
394 for capability in review.capabilities.iter() {
395 capabilities.grant(capability.clone());
396 }
397 }
398 capabilities
399 }
400
401 #[must_use]
409 pub fn default_run_context(&self) -> RunContext {
410 RunContext::root(
411 BudgetTracker::new(Budget::default()),
412 self.callable_capabilities(),
413 )
414 }
415}
416
417impl std::fmt::Debug for Agent {
418 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 formatter
420 .debug_struct("Agent")
421 .field("name", &self.name)
422 .field("model_ref", &self.model_ref)
423 .field("instructions", &self.instructions)
424 .field("context", &self.context)
425 .field("dynamic_context", &self.dynamic_context)
426 .field("tools", &self.tools)
427 .field("agents", &self.agents)
428 .field("effects", &self.effects)
429 .field("effect_recovery", &self.effect_recovery)
430 .field("config", &self.config)
431 .field("output_format", &self.output_format)
432 .field("terminal_review", &self.terminal_review)
433 .finish_non_exhaustive()
434 }
435}
436
437#[cfg(test)]
438mod tests;