machi_agent/
definition.rs1use machi_tools::CapabilityMode;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(untagged)]
10#[non_exhaustive]
11pub enum Instructions {
12 Static(String),
14}
15
16impl Instructions {
17 #[must_use]
19 pub fn resolve(&self) -> String {
20 match self {
21 Self::Static(s) => s.clone(),
22 }
23 }
24}
25
26impl From<String> for Instructions {
27 fn from(value: String) -> Self {
28 Self::Static(value)
29 }
30}
31
32impl From<&str> for Instructions {
33 fn from(value: &str) -> Self {
34 Self::Static(value.to_owned())
35 }
36}
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum ToolPolicy {
42 #[default]
44 InheritAll,
45 Allowlist(Vec<String>),
47 Denylist(Vec<String>),
49}
50
51impl ToolPolicy {
52 #[must_use]
54 pub fn admits(&self, name: &str) -> bool {
55 match self {
56 Self::InheritAll => true,
57 Self::Allowlist(allow) => allow.iter().any(|n| n == name),
58 Self::Denylist(deny) => !deny.iter().any(|n| n == name),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CompletionRequirement {
70 pub tool: String,
72 pub reminder: String,
74 pub max_retries: u32,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
80#[serde(rename_all = "snake_case")]
81#[non_exhaustive]
82pub enum AgentSource {
83 Builtin,
85 User,
87 #[default]
89 Project,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct AgentDefinition {
95 pub name: String,
97 pub description: String,
99 pub instructions: Instructions,
101 pub model: String,
103 #[serde(default)]
105 pub tools: ToolPolicy,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub output_schema: Option<Value>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub completion: Option<CompletionRequirement>,
112 #[serde(default = "default_max_steps")]
114 pub max_steps: usize,
115 #[serde(default = "default_enabled")]
117 pub enabled: bool,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub capability: Option<CapabilityMode>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub source: Option<AgentSource>,
124}
125
126fn default_max_steps() -> usize {
127 32
128}
129
130const fn default_enabled() -> bool {
131 true
132}
133
134impl AgentDefinition {
135 #[must_use]
137 pub fn new(name: impl Into<String>) -> Self {
138 Self {
139 name: name.into(),
140 description: String::new(),
141 instructions: Instructions::Static(String::new()),
142 model: "default".into(),
143 tools: ToolPolicy::InheritAll,
144 output_schema: None,
145 completion: None,
146 max_steps: default_max_steps(),
147 enabled: true,
148 capability: None,
149 source: None,
150 }
151 }
152
153 pub fn validate(&self) -> Result<(), machi_types::MachiError> {
159 use machi_types::{ErrorCode, MachiError};
160 if self.name.trim().is_empty() {
161 return Err(MachiError::new(
162 ErrorCode::AgentInvalidDefinition,
163 "agent name must be non-empty",
164 ));
165 }
166 if self.model.trim().is_empty() {
167 return Err(MachiError::new(
168 ErrorCode::AgentInvalidDefinition,
169 "agent model must be non-empty",
170 ));
171 }
172 if self.max_steps == 0 {
173 return Err(MachiError::new(
174 ErrorCode::AgentInvalidDefinition,
175 "max_steps must be >= 1",
176 ));
177 }
178 Ok(())
179 }
180}