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