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