Skip to main content

runifold_agent/
agent.rs

1use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc, time::Instant};
2
3use futures_util::{
4    StreamExt,
5    future::{Either, select},
6};
7use runifold_core::{
8    BudgetEvent, CapabilitySet, DomainEvent, EffectId, EffectKind, EffectRequest, EventId,
9    InvocationId, LifecycleEvent, RetrySafety, RunContext, RunError, RunErrorKind, RunEventKind,
10    Usage,
11};
12use runifold_effect::{
13    EffectExecutionContext, EffectExecutor, EffectExecutorErrorKind, EffectFuture, EffectHandler,
14    EffectRecoveryPolicy, InMemoryEffectStore,
15};
16use runifold_model::{
17    ContentPart, FeaturePolicy, Message, Model, ModelCallContext, ModelError, ModelErrorKind,
18    ModelRef, ModelRequest, ModelResponse, ModelStreamAccumulator, OutputFormat, Role, ToolCall,
19    ToolResult,
20};
21use runifold_tool::{ToolError, ToolErrorKind, ToolOutput, ToolRegistry};
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24
25use crate::checkpoint::CheckpointCursor;
26use crate::stream::{AgentObserver, BufferedObserver, NoopObserver, emit_agent_event};
27use crate::{AgentCheckpoint, AgentCheckpointPhase, AgentCheckpointState, ResumePolicy};
28use crate::{
29    AgentError, AgentEventStream, AgentGateway, AgentOutcome, AgentStreamEvent, CallableKind,
30    GatewayError, GatewayErrorKind, StructuredAgent,
31};
32
33mod callable;
34mod checkpointing;
35mod execution;
36mod observability;
37
38/// A boxed, sendable future returned by an agent.
39pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
40
41/// How the agent handles tool failures that are safe for model recovery.
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
43#[non_exhaustive]
44pub enum ToolErrorPolicy {
45    /// Return safe execution failures to the model as failed tool results.
46    #[default]
47    ReturnToModel,
48    /// Stop the agent immediately on every tool error.
49    FailFast,
50}
51
52/// Local bounds and recovery behavior for an agent.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct AgentConfig {
55    /// Local turn bound in addition to the shared run budget.
56    pub max_turns: u32,
57    /// Tool failure behavior.
58    pub tool_error_policy: ToolErrorPolicy,
59    /// Model capability-degradation policy.
60    pub feature_policy: FeaturePolicy,
61}
62
63impl Default for AgentConfig {
64    fn default() -> Self {
65        Self {
66            max_turns: 16,
67            tool_error_policy: ToolErrorPolicy::ReturnToModel,
68            feature_policy: FeaturePolicy::Strict,
69        }
70    }
71}
72
73/// One configured model-tool agent.
74#[derive(Clone)]
75pub struct Agent {
76    pub(crate) name: String,
77    pub(crate) model: Arc<dyn Model>,
78    pub(crate) model_ref: ModelRef,
79    pub(crate) instructions: Vec<Message>,
80    pub(crate) tools: ToolRegistry,
81    pub(crate) agents: AgentGateway,
82    pub(crate) effects: EffectExecutor,
83    pub(crate) effect_recovery: EffectRecoveryPolicy,
84    pub(crate) config: AgentConfig,
85    pub(crate) output_format: OutputFormat,
86}
87
88impl Agent {
89    /// Starts a fluent builder for an Agent.
90    pub fn builder(
91        name: impl Into<String>,
92        model: Arc<dyn Model>,
93        model_ref: ModelRef,
94    ) -> crate::AgentBuilder {
95        crate::AgentBuilder::new(name, model, model_ref)
96    }
97
98    /// Creates an agent without instructions or tools.
99    pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
100        Self {
101            name: name.into(),
102            model,
103            model_ref,
104            instructions: Vec::new(),
105            tools: ToolRegistry::new(),
106            agents: AgentGateway::new(),
107            effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
108            effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
109            config: AgentConfig::default(),
110            output_format: OutputFormat::Text,
111        }
112    }
113
114    /// Appends a system instruction.
115    #[must_use]
116    pub fn system(mut self, instruction: impl Into<String>) -> Self {
117        self.instructions.push(Message::system(instruction));
118        self
119    }
120
121    /// Installs the registry whose tools are exposed and executable.
122    #[must_use]
123    pub fn tools(mut self, tools: ToolRegistry) -> Self {
124        self.tools = tools;
125        self
126    }
127
128    /// Installs the gateway whose child agents are exposed and callable.
129    #[must_use]
130    pub fn agents(mut self, agents: AgentGateway) -> Self {
131        self.agents = agents;
132        self
133    }
134
135    /// Replaces the write-ahead effect coordinator shared by callables.
136    #[must_use]
137    pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
138        self.effects = effects;
139        self
140    }
141
142    /// Sets recovery behavior for ambiguous callable effects.
143    #[must_use]
144    pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
145        self.effect_recovery = policy;
146        self
147    }
148
149    /// Replaces local execution configuration.
150    #[must_use]
151    pub const fn with_config(mut self, config: AgentConfig) -> Self {
152        self.config = config;
153        self
154    }
155
156    /// Sets the desired format for the terminal model response.
157    #[must_use]
158    pub fn output_format(mut self, output_format: OutputFormat) -> Self {
159        self.output_format = output_format;
160        self
161    }
162
163    /// Requests strict structured output described by the Rust type `T`.
164    #[must_use]
165    pub fn structured_output<T>(self, name: impl Into<String>) -> Self
166    where
167        T: JsonSchema,
168    {
169        self.output_format(OutputFormat::typed::<T>(name))
170    }
171
172    /// Binds provider schema generation and local decoding to the same type.
173    pub fn into_structured<T>(self, name: impl Into<String>) -> StructuredAgent<T>
174    where
175        T: JsonSchema,
176    {
177        StructuredAgent::new(self.structured_output::<T>(name))
178    }
179
180    /// Returns the stable local agent name.
181    pub fn name(&self) -> &str {
182        &self.name
183    }
184
185    /// Returns the provider and model identity used by this Agent.
186    pub const fn model_ref(&self) -> &ModelRef {
187        &self.model_ref
188    }
189
190    /// Returns capabilities for every Tool and child Agent exposed by this
191    /// Agent.
192    ///
193    /// The returned set is not granted automatically. Applications explicitly
194    /// decide whether to install it on a root or delegated Run.
195    pub fn callable_capabilities(&self) -> CapabilitySet {
196        let mut capabilities = CapabilitySet::new();
197        for spec in self.tools.model_specs() {
198            if let Some(descriptor) = self.tools.descriptor(&spec.name) {
199                capabilities.grant(descriptor.capability());
200            }
201        }
202        for spec in self.agents.model_specs() {
203            if let Some(descriptor) = self.agents.descriptor(&spec.name) {
204                capabilities.grant(descriptor.capability());
205            }
206        }
207        capabilities
208    }
209}
210
211impl std::fmt::Debug for Agent {
212    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        formatter
214            .debug_struct("Agent")
215            .field("name", &self.name)
216            .field("model_ref", &self.model_ref)
217            .field("instructions", &self.instructions)
218            .field("tools", &self.tools)
219            .field("agents", &self.agents)
220            .field("effects", &self.effects)
221            .field("effect_recovery", &self.effect_recovery)
222            .field("config", &self.config)
223            .field("output_format", &self.output_format)
224            .finish_non_exhaustive()
225    }
226}
227
228#[cfg(test)]
229mod tests;