use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
use futures_util::{
StreamExt,
future::{Either, select},
};
use runifold_core::{
Budget, BudgetEvent, BudgetTracker, CapabilitySet, DomainEvent, EffectId, EffectKind,
EffectRequest, EventId, Instant, InvocationId, LifecycleEvent, RetrySafety, RunContext,
RunError, RunErrorKind, RunEventKind, Usage,
};
use runifold_effect::{
EffectExecutionContext, EffectExecutor, EffectExecutorErrorKind, EffectFuture, EffectHandler,
EffectRecoveryPolicy, InMemoryEffectStore,
};
use runifold_model::{
ContentPart, FeaturePolicy, GenerationOptions, Message, Model, ModelCallContext, ModelError,
ModelErrorKind, ModelRef, ModelRequest, ModelResponse, ModelStreamAccumulator, OutputFormat,
ProviderToolSpec, ResponseMode, Role, ToolCall, ToolChoice, ToolResult,
};
use runifold_retrieval::{Document, Retriever};
use runifold_tool::{ToolError, ToolErrorKind, ToolOutput, ToolRegistry};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::checkpoint::CheckpointCursor;
use crate::stream::{AgentObserver, BufferedObserver, NoopObserver, emit_agent_event};
use crate::{
AgentCheckpoint, AgentCheckpointPhase, AgentCheckpointState, DurableConversationCheckpoint,
ResumePolicy,
};
const TOOL_RESULT_EXECUTION_ID_METADATA: &str = "runifold.agent.execution_id";
use crate::{
AgentError, AgentEventStream, AgentGateway, AgentOutcome, AgentStreamEvent, CallableKind,
CompletionRequirement, GatewayError, GatewayErrorKind, StructuredAgent,
};
mod callable;
mod checkpointing;
pub(crate) mod completion;
mod execution;
mod observability;
mod retrieval;
#[cfg(not(target_arch = "wasm32"))]
pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type AgentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[derive(Clone)]
pub(crate) struct DynamicContext {
pub(crate) limit: usize,
pub(crate) retriever: Arc<dyn Retriever>,
}
impl std::fmt::Debug for DynamicContext {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DynamicContext")
.field("limit", &self.limit)
.field("retriever", self.retriever.descriptor())
.finish()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum ToolErrorPolicy {
#[default]
ReturnToModel,
FailFast,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentConfig {
pub max_turns: u32,
pub tool_error_policy: ToolErrorPolicy,
pub feature_policy: FeaturePolicy,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_turns: 16,
tool_error_policy: ToolErrorPolicy::ReturnToModel,
feature_policy: FeaturePolicy::Strict,
}
}
}
#[derive(Clone)]
pub struct Agent {
pub(crate) name: String,
pub(crate) model: Arc<dyn Model>,
pub(crate) model_ref: ModelRef,
pub(crate) instructions: Vec<Message>,
pub(crate) context: Vec<Document>,
pub(crate) dynamic_context: Vec<DynamicContext>,
pub(crate) tools: ToolRegistry,
pub(crate) agents: AgentGateway,
pub(crate) effects: EffectExecutor,
pub(crate) effect_recovery: EffectRecoveryPolicy,
pub(crate) config: AgentConfig,
pub(crate) min_successful_tool_calls: u32,
pub(crate) output_format: OutputFormat,
pub(crate) generation: GenerationOptions,
pub(crate) response_mode: ResponseMode,
pub(crate) provider_tools: Vec<ProviderToolSpec>,
pub(crate) provider_options: BTreeMap<String, serde_json::Value>,
pub(crate) completion_requirement: CompletionRequirement,
pub(crate) completion_validator: completion::CompletionValidator,
}
impl Agent {
pub fn builder(
name: impl Into<String>,
model: Arc<dyn Model>,
model_ref: ModelRef,
) -> crate::AgentBuilder {
crate::AgentBuilder::new(name, model, model_ref)
}
pub fn new(name: impl Into<String>, model: Arc<dyn Model>, model_ref: ModelRef) -> Self {
Self {
name: name.into(),
model,
model_ref,
instructions: Vec::new(),
context: Vec::new(),
dynamic_context: Vec::new(),
tools: ToolRegistry::new(),
agents: AgentGateway::new(),
effects: EffectExecutor::new(Arc::new(InMemoryEffectStore::new())),
effect_recovery: EffectRecoveryPolicy::RejectAmbiguous,
config: AgentConfig::default(),
min_successful_tool_calls: 0,
output_format: OutputFormat::Text,
generation: GenerationOptions::default(),
response_mode: ResponseMode::Streaming,
provider_tools: Vec::new(),
provider_options: BTreeMap::new(),
completion_requirement: CompletionRequirement::default(),
completion_validator: completion::CompletionValidator::content(),
}
}
#[must_use]
pub fn system(mut self, instruction: impl Into<String>) -> Self {
self.instructions.push(Message::system(instruction));
self
}
#[must_use]
pub fn tools(mut self, tools: ToolRegistry) -> Self {
self.tools = tools;
self
}
#[must_use]
pub fn agents(mut self, agents: AgentGateway) -> Self {
self.agents = agents;
self
}
#[must_use]
pub fn effect_executor(mut self, effects: EffectExecutor) -> Self {
self.effects = effects;
self
}
#[must_use]
pub const fn effect_recovery_policy(mut self, policy: EffectRecoveryPolicy) -> Self {
self.effect_recovery = policy;
self
}
#[must_use]
pub const fn with_config(mut self, config: AgentConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub const fn completion_requirement(mut self, requirement: CompletionRequirement) -> Self {
self.completion_requirement = requirement;
self
}
#[must_use]
pub const fn min_successful_tool_calls(mut self, minimum: u32) -> Self {
self.min_successful_tool_calls = minimum;
self
}
#[must_use]
pub fn output_format(mut self, output_format: OutputFormat) -> Self {
self.output_format = output_format;
self
}
#[must_use]
pub fn structured_output<T>(self, name: impl Into<String>) -> Self
where
T: JsonSchema,
{
self.output_format(OutputFormat::typed::<T>(name))
}
pub fn into_structured<T>(mut self, name: impl Into<String>) -> StructuredAgent<T>
where
T: JsonSchema + serde::de::DeserializeOwned + Send + 'static,
{
self = self.structured_output::<T>(name);
self.completion_validator = completion::CompletionValidator::structured::<T>();
StructuredAgent::new(self)
}
pub fn name(&self) -> &str {
&self.name
}
pub const fn model_ref(&self) -> &ModelRef {
&self.model_ref
}
pub fn callable_capabilities(&self) -> CapabilitySet {
let mut capabilities = CapabilitySet::new();
for spec in self.tools.model_specs() {
if let Some(descriptor) = self.tools.descriptor(&spec.name) {
capabilities.grant(descriptor.capability());
}
}
for spec in self.agents.model_specs() {
if let Some(descriptor) = self.agents.descriptor(&spec.name) {
capabilities.grant(descriptor.capability());
}
}
for source in &self.dynamic_context {
capabilities.grant(source.retriever.descriptor().capability());
}
capabilities
}
#[must_use]
pub fn default_run_context(&self) -> RunContext {
RunContext::root(
BudgetTracker::new(Budget::default()),
self.callable_capabilities(),
)
}
}
impl std::fmt::Debug for Agent {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Agent")
.field("name", &self.name)
.field("model_ref", &self.model_ref)
.field("instructions", &self.instructions)
.field("context", &self.context)
.field("dynamic_context", &self.dynamic_context)
.field("tools", &self.tools)
.field("agents", &self.agents)
.field("effects", &self.effects)
.field("effect_recovery", &self.effect_recovery)
.field("config", &self.config)
.field("output_format", &self.output_format)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;