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