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, EffectFuture, EffectHandler, EffectRecoveryPolicy,
14    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::terminal_review::{TerminalReviewConfig, TurnReviewConfig};
35use crate::{
36    AgentError, AgentEventStream, AgentGateway, AgentOutcome, AgentStreamEvent, CallableKind,
37    CompletionRequirement, GatewayError, GatewayErrorKind, StructuredAgent,
38};
39use crate::{TerminalReviewPolicy, TerminalReviewer, TurnReviewPolicy, TurnReviewer};
40
41mod callable;
42mod checkpointing;
43pub(crate) mod completion;
44mod execution;
45mod observability;
46mod retrieval;
47mod review;
48
49/// A boxed, sendable future returned by an agent.
50#[cfg(not(target_arch = "wasm32"))]
51pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
52
53/// A boxed future returned by an agent on single-threaded WASM.
54#[cfg(target_arch = "wasm32")]
55pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
56
57/// One dynamic, capability-gated context source.
58#[derive(Clone)]
59pub(crate) struct DynamicContext {
60    pub(crate) limit: usize,
61    pub(crate) retriever: Arc<dyn Retriever>,
62}
63
64impl std::fmt::Debug for DynamicContext {
65    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        formatter
67            .debug_struct("DynamicContext")
68            .field("limit", &self.limit)
69            .field("retriever", self.retriever.descriptor())
70            .finish()
71    }
72}
73
74/// How the agent handles tool failures that are safe for model recovery.
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum ToolErrorPolicy {
78    /// Return safe execution failures to the model as failed tool results.
79    #[default]
80    ReturnToModel,
81    /// Stop the agent immediately on every tool error.
82    FailFast,
83}
84
85/// Local bounds and recovery behavior for an agent.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct AgentConfig {
88    /// Local turn bound in addition to the shared run budget.
89    pub max_turns: u32,
90    /// Tool failure behavior.
91    pub tool_error_policy: ToolErrorPolicy,
92    /// Model capability-degradation policy.
93    pub feature_policy: FeaturePolicy,
94}
95
96impl Default for AgentConfig {
97    fn default() -> Self {
98        Self {
99            max_turns: 16,
100            tool_error_policy: ToolErrorPolicy::ReturnToModel,
101            feature_policy: FeaturePolicy::Strict,
102        }
103    }
104}
105
106/// One configured model-tool agent.
107#[derive(Clone)]
108pub struct Agent {
109    pub(crate) name: String,
110    pub(crate) model: Arc<dyn Model>,
111    pub(crate) model_ref: ModelRef,
112    pub(crate) instructions: Vec<Message>,
113    pub(crate) context: Vec<Document>,
114    pub(crate) dynamic_context: Vec<DynamicContext>,
115    pub(crate) tools: ToolRegistry,
116    pub(crate) agents: AgentGateway,
117    pub(crate) effects: EffectExecutor,
118    pub(crate) effect_recovery: EffectRecoveryPolicy,
119    pub(crate) config: AgentConfig,
120    pub(crate) min_successful_tool_calls: u32,
121    pub(crate) output_format: OutputFormat,
122    pub(crate) generation: GenerationOptions,
123    pub(crate) response_mode: ResponseMode,
124    pub(crate) provider_tools: Vec<ProviderToolSpec>,
125    pub(crate) provider_options: BTreeMap<String, serde_json::Value>,
126    pub(crate) completion_requirement: CompletionRequirement,
127    pub(crate) completion_validator: completion::CompletionValidator,
128    pub(crate) turn_review: Option<TurnReviewConfig>,
129    pub(crate) terminal_review: Option<TerminalReviewConfig>,
130}
131
132impl Agent {
133    /// Starts a fluent builder for an Agent.
134    pub fn builder(
135        name: impl Into<String>,
136        model: Arc<dyn Model>,
137        model_ref: ModelRef,
138    ) -> crate::AgentBuilder {
139        crate::AgentBuilder::new(name, model, model_ref)
140    }
141
142    /// Creates an agent without instructions or tools.
143    pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
144        Self {
145            name: name.into(),
146            model,
147            model_ref,
148            instructions: Vec::new(),
149            context: Vec::new(),
150            dynamic_context: Vec::new(),
151            tools: ToolRegistry::new(),
152            agents: AgentGateway::new(),
153            effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
154            effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
155            config: AgentConfig::default(),
156            min_successful_tool_calls: 0,
157            output_format: OutputFormat::Text,
158            generation: GenerationOptions::default(),
159            response_mode: ResponseMode::Streaming,
160            provider_tools: Vec::new(),
161            provider_options: BTreeMap::new(),
162            completion_requirement: CompletionRequirement::default(),
163            completion_validator: completion::CompletionValidator::content(),
164            turn_review: None,
165            terminal_review: None,
166        }
167    }
168
169    /// Appends a system instruction.
170    #[must_use]
171    pub fn system(mut self, instruction: impl Into<String>) -> Self {
172        self.instructions.push(Message::system(instruction));
173        self
174    }
175
176    /// Installs the registry whose tools are exposed and executable.
177    #[must_use]
178    pub fn tools(mut self, tools: ToolRegistry) -> Self {
179        self.tools = tools;
180        self
181    }
182
183    /// Installs the gateway whose child agents are exposed and callable.
184    #[must_use]
185    pub fn agents(mut self, agents: AgentGateway) -> Self {
186        self.agents = agents;
187        self
188    }
189
190    /// Replaces the write-ahead effect coordinator shared by callables.
191    #[must_use]
192    pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
193        self.effects = effects;
194        self
195    }
196
197    /// Sets recovery behavior for ambiguous callable effects.
198    #[must_use]
199    pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
200        self.effect_recovery = policy;
201        self
202    }
203
204    /// Replaces local execution configuration.
205    #[must_use]
206    pub const fn with_config(mut self, config: AgentConfig) -> Self {
207        self.config = config;
208        self
209    }
210
211    /// Sets terminal validation and bounded repair behavior.
212    #[must_use]
213    pub const fn completion_requirement(mut self, requirement: CompletionRequirement) -> Self {
214        self.completion_requirement = requirement;
215        self
216    }
217
218    /// Installs semantic review after selected model responses and before any
219    /// tool call from those responses can execute.
220    #[must_use]
221    pub fn turn_reviewer<R>(
222        self,
223        reviewer: R,
224        policy: TurnReviewPolicy,
225        capabilities: CapabilitySet,
226    ) -> Self
227    where
228        R: TurnReviewer + 'static,
229    {
230        self.shared_turn_reviewer(Arc::new(reviewer), policy, capabilities)
231    }
232
233    /// Installs a shared, type-erased internal-turn reviewer.
234    #[must_use]
235    pub fn shared_turn_reviewer(
236        mut self,
237        reviewer: Arc<dyn TurnReviewer>,
238        policy: TurnReviewPolicy,
239        capabilities: CapabilitySet,
240    ) -> Self {
241        let descriptor = reviewer.turn_descriptor().clone();
242        self.turn_review = Some(TurnReviewConfig {
243            reviewer,
244            descriptor,
245            policy,
246            capabilities,
247        });
248        self
249    }
250
251    /// Installs semantic review before a locally valid terminal candidate is
252    /// committed as the Agent outcome.
253    ///
254    /// Reviewer capabilities are attenuated against the parent Run and review
255    /// repairs consume ordinary shared budgets.
256    #[must_use]
257    pub fn terminal_reviewer<R>(
258        self,
259        reviewer: R,
260        policy: TerminalReviewPolicy,
261        capabilities: CapabilitySet,
262    ) -> Self
263    where
264        R: TerminalReviewer + 'static,
265    {
266        self.shared_terminal_reviewer(Arc::new(reviewer), policy, capabilities)
267    }
268
269    /// Installs a shared, type-erased terminal reviewer.
270    #[must_use]
271    pub fn shared_terminal_reviewer(
272        mut self,
273        reviewer: Arc<dyn TerminalReviewer>,
274        policy: TerminalReviewPolicy,
275        capabilities: CapabilitySet,
276    ) -> Self {
277        let descriptor = reviewer.descriptor().clone();
278        self.terminal_review = Some(TerminalReviewConfig {
279            reviewer,
280            descriptor,
281            policy,
282            capabilities,
283        });
284        self
285    }
286
287    /// Requires this many successful local Tool calls before terminal output.
288    ///
289    /// Failed Tool results, child-Agent delegations, provider-hosted Tools,
290    /// and Tool results from earlier conversation turns do not satisfy this
291    /// execution-local completion contract. A value of zero disables it.
292    #[must_use]
293    pub const fn min_successful_tool_calls(mut self, minimum: u32) -> Self {
294        self.min_successful_tool_calls = minimum;
295        self
296    }
297
298    /// Sets the desired format for the terminal model response.
299    #[must_use]
300    pub fn output_format(mut self, output_format: OutputFormat) -> Self {
301        self.output_format = output_format;
302        self
303    }
304
305    /// Requests strict structured output described by the Rust type `T`.
306    #[must_use]
307    pub fn structured_output<T>(self, name: impl Into<String>) -> Self
308    where
309        T: JsonSchema,
310    {
311        self.structured_output_with_strictness::<T>(name, true)
312    }
313
314    /// Requests structured output described by `T` with explicit provider
315    /// strictness.
316    #[must_use]
317    pub fn structured_output_with_strictness<T>(self, name: impl Into<String>, strict: bool) -> Self
318    where
319        T: JsonSchema,
320    {
321        self.output_format(OutputFormat::typed_with_strictness::<T>(name, strict))
322    }
323
324    /// Binds provider schema generation and local decoding to the same type.
325    pub fn into_structured<T>(self, name: impl Into<String>) -> StructuredAgent<T>
326    where
327        T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
328    {
329        self.into_structured_with_strictness::<T>(name, true)
330    }
331
332    /// Binds provider schema generation and local decoding to `T` with
333    /// explicit provider strictness.
334    pub fn into_structured_with_strictness<T>(
335        mut self,
336        name: impl Into<String>,
337        strict: bool,
338    ) -> StructuredAgent<T>
339    where
340        T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
341    {
342        self = self.structured_output_with_strictness::<T>(name, strict);
343        self.completion_validator = completion::CompletionValidator::structured::<T>();
344        StructuredAgent::new(self)
345    }
346
347    /// Returns the stable local agent name.
348    pub fn name(&self) -> &str {
349        &self.name
350    }
351
352    /// Returns the provider and model identity used by this Agent.
353    pub const fn model_ref(&self) -> &ModelRef {
354        &self.model_ref
355    }
356
357    /// Returns capabilities for every Tool, child Agent, context source, and
358    /// terminal reviewer exposed by this Agent.
359    ///
360    /// The returned set is not granted automatically. Applications explicitly
361    /// decide whether to install it on a root or delegated Run.
362    pub fn callable_capabilities(&self) -> CapabilitySet {
363        let mut capabilities = CapabilitySet::new();
364        for spec in self.tools.model_specs() {
365            if let Some(descriptor) = self.tools.descriptor(&spec.name) {
366                capabilities.grant(descriptor.capability());
367            }
368        }
369        for spec in self.agents.model_specs() {
370            if let Some(descriptor) = self.agents.descriptor(&spec.name) {
371                capabilities.grant(descriptor.capability());
372            }
373        }
374        for source in &self.dynamic_context {
375            capabilities.grant(source.retriever.descriptor().capability());
376        }
377        if let Some(review) = &self.terminal_review {
378            for capability in review.capabilities.iter() {
379                capabilities.grant(capability.clone());
380            }
381        }
382        capabilities
383    }
384
385    /// Creates a root context for the ergonomic prompt surface.
386    ///
387    /// The context has no hard budget limits and grants only the Tool and child
388    /// Agent capabilities explicitly registered on this Agent. Applications
389    /// that need deadlines, tighter budgets, narrower authority, durable
390    /// journals, or shared run trees should construct a [`RunContext`] and use
391    /// [`Self::run`] instead.
392    #[must_use]
393    pub fn default_run_context(&self) -> RunContext {
394        RunContext::root(
395            BudgetTracker::new(Budget::default()),
396            self.callable_capabilities(),
397        )
398    }
399}
400
401impl std::fmt::Debug for Agent {
402    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403        formatter
404            .debug_struct("Agent")
405            .field("name", &self.name)
406            .field("model_ref", &self.model_ref)
407            .field("instructions", &self.instructions)
408            .field("context", &self.context)
409            .field("dynamic_context", &self.dynamic_context)
410            .field("tools", &self.tools)
411            .field("agents", &self.agents)
412            .field("effects", &self.effects)
413            .field("effect_recovery", &self.effect_recovery)
414            .field("config", &self.config)
415            .field("output_format", &self.output_format)
416            .field("terminal_review", &self.terminal_review)
417            .finish_non_exhaustive()
418    }
419}
420
421#[cfg(test)]
422mod tests;