Skip to main content

runifold_agent/
agent.rs

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, 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_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::{AgentCheckpoint, AgentCheckpointPhase, AgentCheckpointState, ResumePolicy};
29use crate::{
30    AgentError, AgentEventStream, AgentGateway, AgentOutcome, AgentStreamEvent, CallableKind,
31    GatewayError, GatewayErrorKind, StructuredAgent,
32};
33
34mod callable;
35mod checkpointing;
36mod execution;
37mod observability;
38mod retrieval;
39
40/// A boxed, sendable future returned by an agent.
41#[cfg(not(target_arch = "wasm32"))]
42pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
43
44/// A boxed future returned by an agent on single-threaded WASM.
45#[cfg(target_arch = "wasm32")]
46pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
47
48/// One dynamic, capability-gated context source.
49#[derive(Clone)]
50pub(crate) struct DynamicContext {
51    pub(crate) limit: usize,
52    pub(crate) retriever: Arc<dyn Retriever>,
53}
54
55impl std::fmt::Debug for DynamicContext {
56    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        formatter
58            .debug_struct("DynamicContext")
59            .field("limit", &self.limit)
60            .field("retriever", self.retriever.descriptor())
61            .finish()
62    }
63}
64
65/// How the agent handles tool failures that are safe for model recovery.
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
67#[non_exhaustive]
68pub enum ToolErrorPolicy {
69    /// Return safe execution failures to the model as failed tool results.
70    #[default]
71    ReturnToModel,
72    /// Stop the agent immediately on every tool error.
73    FailFast,
74}
75
76/// Local bounds and recovery behavior for an agent.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct AgentConfig {
79    /// Local turn bound in addition to the shared run budget.
80    pub max_turns: u32,
81    /// Tool failure behavior.
82    pub tool_error_policy: ToolErrorPolicy,
83    /// Model capability-degradation policy.
84    pub feature_policy: FeaturePolicy,
85}
86
87impl Default for AgentConfig {
88    fn default() -> Self {
89        Self {
90            max_turns: 16,
91            tool_error_policy: ToolErrorPolicy::ReturnToModel,
92            feature_policy: FeaturePolicy::Strict,
93        }
94    }
95}
96
97/// One configured model-tool agent.
98#[derive(Clone)]
99pub struct Agent {
100    pub(crate) name: String,
101    pub(crate) model: Arc<dyn Model>,
102    pub(crate) model_ref: ModelRef,
103    pub(crate) instructions: Vec<Message>,
104    pub(crate) context: Vec<Document>,
105    pub(crate) dynamic_context: Vec<DynamicContext>,
106    pub(crate) tools: ToolRegistry,
107    pub(crate) agents: AgentGateway,
108    pub(crate) effects: EffectExecutor,
109    pub(crate) effect_recovery: EffectRecoveryPolicy,
110    pub(crate) config: AgentConfig,
111    pub(crate) output_format: OutputFormat,
112}
113
114impl Agent {
115    /// Starts a fluent builder for an Agent.
116    pub fn builder(
117        name: impl Into<String>,
118        model: Arc<dyn Model>,
119        model_ref: ModelRef,
120    ) -> crate::AgentBuilder {
121        crate::AgentBuilder::new(name, model, model_ref)
122    }
123
124    /// Creates an agent without instructions or tools.
125    pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
126        Self {
127            name: name.into(),
128            model,
129            model_ref,
130            instructions: Vec::new(),
131            context: Vec::new(),
132            dynamic_context: Vec::new(),
133            tools: ToolRegistry::new(),
134            agents: AgentGateway::new(),
135            effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
136            effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
137            config: AgentConfig::default(),
138            output_format: OutputFormat::Text,
139        }
140    }
141
142    /// Appends a system instruction.
143    #[must_use]
144    pub fn system(mut self, instruction: impl Into<String>) -> Self {
145        self.instructions.push(Message::system(instruction));
146        self
147    }
148
149    /// Installs the registry whose tools are exposed and executable.
150    #[must_use]
151    pub fn tools(mut self, tools: ToolRegistry) -> Self {
152        self.tools = tools;
153        self
154    }
155
156    /// Installs the gateway whose child agents are exposed and callable.
157    #[must_use]
158    pub fn agents(mut self, agents: AgentGateway) -> Self {
159        self.agents = agents;
160        self
161    }
162
163    /// Replaces the write-ahead effect coordinator shared by callables.
164    #[must_use]
165    pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
166        self.effects = effects;
167        self
168    }
169
170    /// Sets recovery behavior for ambiguous callable effects.
171    #[must_use]
172    pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
173        self.effect_recovery = policy;
174        self
175    }
176
177    /// Replaces local execution configuration.
178    #[must_use]
179    pub const fn with_config(mut self, config: AgentConfig) -> Self {
180        self.config = config;
181        self
182    }
183
184    /// Sets the desired format for the terminal model response.
185    #[must_use]
186    pub fn output_format(mut self, output_format: OutputFormat) -> Self {
187        self.output_format = output_format;
188        self
189    }
190
191    /// Requests strict structured output described by the Rust type `T`.
192    #[must_use]
193    pub fn structured_output<T>(self, name: impl Into<String>) -> Self
194    where
195        T: JsonSchema,
196    {
197        self.output_format(OutputFormat::typed::<T>(name))
198    }
199
200    /// Binds provider schema generation and local decoding to the same type.
201    pub fn into_structured<T>(self, name: impl Into<String>) -> StructuredAgent<T>
202    where
203        T: JsonSchema,
204    {
205        StructuredAgent::new(self.structured_output::<T>(name))
206    }
207
208    /// Returns the stable local agent name.
209    pub fn name(&self) -> &str {
210        &self.name
211    }
212
213    /// Returns the provider and model identity used by this Agent.
214    pub const fn model_ref(&self) -> &ModelRef {
215        &self.model_ref
216    }
217
218    /// Returns capabilities for every Tool and child Agent exposed by this
219    /// Agent.
220    ///
221    /// The returned set is not granted automatically. Applications explicitly
222    /// decide whether to install it on a root or delegated Run.
223    pub fn callable_capabilities(&self) -> CapabilitySet {
224        let mut capabilities = CapabilitySet::new();
225        for spec in self.tools.model_specs() {
226            if let Some(descriptor) = self.tools.descriptor(&spec.name) {
227                capabilities.grant(descriptor.capability());
228            }
229        }
230        for spec in self.agents.model_specs() {
231            if let Some(descriptor) = self.agents.descriptor(&spec.name) {
232                capabilities.grant(descriptor.capability());
233            }
234        }
235        for source in &self.dynamic_context {
236            capabilities.grant(source.retriever.descriptor().capability());
237        }
238        capabilities
239    }
240
241    /// Creates a root context for the ergonomic prompt surface.
242    ///
243    /// The context has no hard budget limits and grants only the Tool and child
244    /// Agent capabilities explicitly registered on this Agent. Applications
245    /// that need deadlines, tighter budgets, narrower authority, durable
246    /// journals, or shared run trees should construct a [`RunContext`] and use
247    /// [`Self::run`] instead.
248    #[must_use]
249    pub fn default_run_context(&self) -> RunContext {
250        RunContext::root(
251            BudgetTracker::new(Budget::default()),
252            self.callable_capabilities(),
253        )
254    }
255}
256
257impl std::fmt::Debug for Agent {
258    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        formatter
260            .debug_struct("Agent")
261            .field("name", &self.name)
262            .field("model_ref", &self.model_ref)
263            .field("instructions", &self.instructions)
264            .field("context", &self.context)
265            .field("dynamic_context", &self.dynamic_context)
266            .field("tools", &self.tools)
267            .field("agents", &self.agents)
268            .field("effects", &self.effects)
269            .field("effect_recovery", &self.effect_recovery)
270            .field("config", &self.config)
271            .field("output_format", &self.output_format)
272            .finish_non_exhaustive()
273    }
274}
275
276#[cfg(test)]
277mod tests;