Skip to main content

fluers_runtime/
agent.rs

1//! Agent definition — `define_agent` / `AgentProfile`.
2//!
3//! Mirrors Flue's `defineAgent` / `defineAgentProfile` from
4//! `@flue/runtime`. Composes a model with tools, skills, a sandbox, and
5//! instructions into a runnable [`Agent`].
6
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11use fluers_core::{Model, ThinkingLevel, Tool};
12
13use crate::env::Limits;
14use crate::error::{RuntimeError, RuntimeResult};
15use crate::sandbox::Sandbox;
16use crate::skill::Skill;
17
18/// A fully-resolved agent profile.
19///
20/// Built incrementally via [`AgentSpec`] and frozen into an [`Agent`] by
21/// [`define_agent`]. This is the Rust shape of Flue's `AgentProfile`.
22#[derive(Clone)]
23pub struct AgentProfile {
24    /// Which model to use.
25    pub model: Model,
26    /// System instructions.
27    pub instructions: String,
28    /// Tools the agent may call.
29    pub tools: Vec<Arc<dyn Tool>>,
30    /// Skills (SKILL.md) injected into context.
31    pub skills: Vec<Arc<Skill>>,
32    /// The sandbox providing the session environment.
33    pub sandbox: Arc<dyn Sandbox>,
34    /// Reasoning effort.
35    pub thinking: ThinkingLevel,
36    /// Resource limits forwarded to the sandbox.
37    pub limits: Limits,
38}
39
40/// A declarative, serializable agent specification.
41///
42/// Unlike [`AgentProfile`] (which holds trait objects), `AgentSpec` is plain
43/// data: it can be read from a config file and resolved at startup.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AgentSpec {
46    /// Model id, e.g. `anthropic/claude-sonnet-4-6`.
47    pub model: String,
48    /// System instructions.
49    #[serde(default)]
50    pub instructions: String,
51    /// Tool names to enable (resolved against the runtime registry).
52    #[serde(default)]
53    pub tools: Vec<String>,
54    /// Skill directories or packaged-skill ids.
55    #[serde(default)]
56    pub skills: Vec<String>,
57    /// Sandbox flavour: `local` | `virtual` | `container`.
58    #[serde(default = "default_sandbox")]
59    pub sandbox: String,
60    /// Reasoning effort.
61    #[serde(default)]
62    pub thinking: ThinkingLevel,
63}
64
65fn default_sandbox() -> String {
66    "local".to_string()
67}
68
69/// A runnable agent: a profile plus the resolved runtime handle.
70pub struct Agent {
71    /// The frozen profile.
72    pub profile: AgentProfile,
73}
74
75/// Build an [`Agent`] from a closure that configures an [`AgentSpec`]-like
76/// profile, mirroring Flue's `defineAgent(() => ({ ... }))`.
77///
78/// In MVP this resolves the sandbox and wires defaults; tool/skill resolution
79/// from the registry arrives in a later phase.
80pub async fn define_agent<F>(build: F) -> RuntimeResult<Agent>
81where
82    F: FnOnce(&mut AgentBuilder) -> RuntimeResult<()>,
83{
84    let mut b = AgentBuilder::default();
85    build(&mut b)?;
86    let profile = b.finish()?;
87    Ok(Agent { profile })
88}
89
90/// Incremental builder used inside [`define_agent`].
91#[derive(Default)]
92pub struct AgentBuilder {
93    /// Model.
94    pub model: Option<Model>,
95    /// Instructions.
96    pub instructions: Option<String>,
97    /// Tools.
98    pub tools: Vec<Arc<dyn Tool>>,
99    /// Skills.
100    pub skills: Vec<Arc<Skill>>,
101    /// Sandbox.
102    pub sandbox: Option<Arc<dyn Sandbox>>,
103    /// Thinking level.
104    pub thinking: ThinkingLevel,
105}
106
107impl AgentBuilder {
108    /// Set the model.
109    pub fn model(&mut self, model: impl Into<String>) -> &mut Self {
110        self.model = Some(Model::new(model));
111        self
112    }
113
114    /// Set the instructions.
115    pub fn instructions(&mut self, text: impl Into<String>) -> &mut Self {
116        self.instructions = Some(text.into());
117        self
118    }
119
120    /// Add a tool.
121    pub fn tool(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
122        self.tools.push(tool);
123        self
124    }
125
126    /// Add a skill.
127    pub fn skill(&mut self, skill: Arc<Skill>) -> &mut Self {
128        self.skills.push(skill);
129        self
130    }
131
132    /// Set the sandbox.
133    pub fn sandbox(&mut self, sandbox: Arc<dyn Sandbox>) -> &mut Self {
134        self.sandbox = Some(sandbox);
135        self
136    }
137
138    fn finish(self) -> RuntimeResult<AgentProfile> {
139        let model = self
140            .model
141            .ok_or_else(|| RuntimeError::InvalidSkill("agent requires a model".into()))?;
142        let sandbox = self
143            .sandbox
144            .unwrap_or_else(|| Arc::new(crate::sandbox::local()));
145        Ok(AgentProfile {
146            model,
147            instructions: self.instructions.unwrap_or_default(),
148            tools: self.tools,
149            skills: self.skills,
150            sandbox,
151            thinking: self.thinking,
152            limits: Limits::default(),
153        })
154    }
155}