1use std::fmt;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use uuid::Uuid;
13
14use crate::context::{InvestigationContext, NextAction};
15use crate::registry::{KernelError, SkillRegistry, ToolRegistry};
16use crate::skill::{Skill, SkillOutcome};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct AgentId(pub Uuid);
21
22impl AgentId {
23 pub fn new() -> Self {
24 Self(Uuid::new_v4())
25 }
26}
27
28impl Default for AgentId {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl fmt::Display for AgentId {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "{}", self.0)
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct AgentStepResult {
43 pub skills_run: Vec<String>,
45 pub skills_skipped: Vec<String>,
48 pub confidence: f32,
50 pub concluded: bool,
52}
53
54#[async_trait]
61pub trait AgentLifecycleHook: Send + Sync {
62 async fn before_step(
64 &self,
65 _agent: &GenericAgent,
66 _ctx: &InvestigationContext,
67 ) -> Result<(), KernelError> {
68 Ok(())
69 }
70
71 async fn before_skill(
73 &self,
74 _agent: &GenericAgent,
75 _skill_id: &str,
76 _applies: bool,
77 _confidence: f32,
78 ) -> Result<(), KernelError> {
79 Ok(())
80 }
81
82 async fn after_skill(
84 &self,
85 _agent: &GenericAgent,
86 _skill_id: &str,
87 _outcome: &SkillOutcome,
88 _confidence: f32,
89 ) -> Result<(), KernelError> {
90 Ok(())
91 }
92
93 async fn after_step(
95 &self,
96 _agent: &GenericAgent,
97 _result: &AgentStepResult,
98 ) -> Result<(), KernelError> {
99 Ok(())
100 }
101
102 async fn on_step_error(
104 &self,
105 _agent: &GenericAgent,
106 _error: &KernelError,
107 ) -> Result<(), KernelError> {
108 Ok(())
109 }
110}
111
112#[async_trait]
114pub trait Agent: Send + Sync {
115 fn id(&self) -> AgentId;
116 fn name(&self) -> &str;
117
118 async fn step(&self, ctx: &mut InvestigationContext) -> Result<AgentStepResult, KernelError>;
121}
122
123pub struct GenericAgent {
128 id: AgentId,
129 name: String,
130 skills: Vec<Arc<dyn Skill>>,
131 tools: ToolRegistry,
132 lifecycle_hooks: Vec<Arc<dyn AgentLifecycleHook>>,
133 pub short_circuit_on_conclude: bool,
136}
137
138impl GenericAgent {
139 pub fn builder(name: impl Into<String>) -> GenericAgentBuilder {
140 GenericAgentBuilder {
141 name: name.into(),
142 skill_ids: Vec::new(),
143 allowed_tools: None,
144 lifecycle_hooks: Vec::new(),
145 short_circuit_on_conclude: true,
146 }
147 }
148
149 pub fn skills(&self) -> &[Arc<dyn Skill>] {
150 &self.skills
151 }
152
153 pub fn tools(&self) -> &ToolRegistry {
154 &self.tools
155 }
156}
157
158#[async_trait]
159impl Agent for GenericAgent {
160 fn id(&self) -> AgentId {
161 self.id
162 }
163
164 fn name(&self) -> &str {
165 &self.name
166 }
167
168 async fn step(&self, ctx: &mut InvestigationContext) -> Result<AgentStepResult, KernelError> {
169 let mut skills_run = Vec::new();
170 let mut skills_skipped = Vec::new();
171 let mut concluded = false;
172
173 self.notify_before_step(ctx).await;
177
178 for skill in &self.skills {
179 if concluded && self.short_circuit_on_conclude {
180 let skill_id = skill.id();
181 self.notify_before_skill(skill_id, false, ctx.confidence)
182 .await;
183 skills_skipped.push(skill.id().to_string());
184 continue;
185 }
186 if !skill.applies(ctx) {
187 let skill_id = skill.id();
188 self.notify_before_skill(skill_id, false, ctx.confidence)
189 .await;
190 skills_skipped.push(skill.id().to_string());
191 continue;
192 }
193
194 let skill_id = skill.id();
195 self.notify_before_skill(skill_id, true, ctx.confidence)
196 .await;
197
198 let outcome = match skill.execute(ctx, &self.tools).await {
199 Ok(outcome) => outcome,
200 Err(error) => {
201 self.notify_step_error(&error).await;
202 return Err(error);
203 }
204 };
205 ctx.confidence = (ctx.confidence + outcome.confidence_delta).clamp(0.0, 1.0);
206 ctx.pending_actions = outcome.next_actions.clone();
207 self.notify_after_skill(skill_id, &outcome, ctx.confidence)
208 .await;
209 if outcome
210 .next_actions
211 .iter()
212 .any(|a| matches!(a, NextAction::Conclude | NextAction::Discard))
213 {
214 concluded = true;
215 }
216 skills_run.push(skill_id.to_string());
217 }
218
219 let result = AgentStepResult {
220 skills_run,
221 skills_skipped,
222 confidence: ctx.confidence,
223 concluded,
224 };
225 self.notify_after_step(&result).await;
226 Ok(result)
227 }
228}
229
230impl GenericAgent {
231 async fn notify_before_step(&self, ctx: &InvestigationContext) {
232 for hook in &self.lifecycle_hooks {
233 if let Err(err) = hook.before_step(self, ctx).await {
234 tracing::warn!(error = %err, "lifecycle hook before_step failed; continuing");
235 }
236 }
237 }
238
239 async fn notify_before_skill(&self, skill_id: &str, applies: bool, confidence: f32) {
240 for hook in &self.lifecycle_hooks {
241 if let Err(err) = hook.before_skill(self, skill_id, applies, confidence).await {
242 tracing::warn!(
243 error = %err,
244 skill_id,
245 "lifecycle hook before_skill failed; continuing"
246 );
247 }
248 }
249 }
250
251 async fn notify_after_skill(&self, skill_id: &str, outcome: &SkillOutcome, confidence: f32) {
252 for hook in &self.lifecycle_hooks {
253 if let Err(err) = hook.after_skill(self, skill_id, outcome, confidence).await {
254 tracing::warn!(
255 error = %err,
256 skill_id,
257 "lifecycle hook after_skill failed; continuing"
258 );
259 }
260 }
261 }
262
263 async fn notify_after_step(&self, result: &AgentStepResult) {
264 for hook in &self.lifecycle_hooks {
265 if let Err(err) = hook.after_step(self, result).await {
266 tracing::warn!(error = %err, "lifecycle hook after_step failed; continuing");
267 }
268 }
269 }
270
271 async fn notify_step_error(&self, error: &KernelError) {
272 for hook in &self.lifecycle_hooks {
273 if let Err(err) = hook.on_step_error(self, error).await {
274 tracing::warn!(error = %err, "lifecycle hook on_step_error failed; continuing");
275 }
276 }
277 }
278}
279
280pub struct GenericAgentBuilder {
283 name: String,
284 skill_ids: Vec<String>,
285 allowed_tools: Option<Vec<String>>,
286 lifecycle_hooks: Vec<Arc<dyn AgentLifecycleHook>>,
287 short_circuit_on_conclude: bool,
288}
289
290impl GenericAgentBuilder {
291 pub fn with_skills<I, S>(mut self, ids: I) -> Self
292 where
293 I: IntoIterator<Item = S>,
294 S: Into<String>,
295 {
296 self.skill_ids.extend(ids.into_iter().map(Into::into));
297 self
298 }
299
300 pub fn with_tools<I, S>(mut self, names: I) -> Self
301 where
302 I: IntoIterator<Item = S>,
303 S: Into<String>,
304 {
305 self.allowed_tools = Some(names.into_iter().map(Into::into).collect());
306 self
307 }
308
309 pub fn short_circuit_on_conclude(mut self, v: bool) -> Self {
310 self.short_circuit_on_conclude = v;
311 self
312 }
313
314 pub fn with_lifecycle_hook(mut self, hook: Arc<dyn AgentLifecycleHook>) -> Self {
316 self.lifecycle_hooks.push(hook);
317 self
318 }
319
320 pub fn build(
321 self,
322 skills: &SkillRegistry,
323 tools: &ToolRegistry,
324 ) -> Result<GenericAgent, KernelError> {
325 let resolved = skills.resolve_chain(self.skill_ids.iter())?;
326 let scoped_tools = match self.allowed_tools {
327 Some(list) => tools.scoped(list),
328 None => tools.clone(),
329 };
330 Ok(GenericAgent {
331 id: AgentId::new(),
332 name: self.name,
333 skills: resolved,
334 tools: scoped_tools,
335 lifecycle_hooks: self.lifecycle_hooks,
336 short_circuit_on_conclude: self.short_circuit_on_conclude,
337 })
338 }
339}