perspt_agent/
agent.rs

1//! Agent Trait and Implementations
2//!
3//! Defines the interface for all agent implementations and provides
4//! LLM-integrated implementations for Architect, Actuator, and Verifier roles.
5
6use crate::types::{AgentContext, AgentMessage, ModelTier, SRBNNode};
7use anyhow::Result;
8use async_trait::async_trait;
9use perspt_core::llm_provider::GenAIProvider;
10use std::sync::Arc;
11
12/// The Agent trait defines the interface for SRBN agents.
13///
14/// Each agent role (Architect, Actuator, Verifier, Speculator) implements
15/// this trait to provide specialized behavior.
16#[async_trait]
17pub trait Agent: Send + Sync {
18    /// Process a task and return a message
19    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage>;
20
21    /// Get the agent's display name
22    fn name(&self) -> &str;
23
24    /// Check if this agent can handle the given node
25    fn can_handle(&self, node: &SRBNNode) -> bool;
26}
27
28/// Architect agent - handles planning and DAG construction
29pub struct ArchitectAgent {
30    model: String,
31    provider: Arc<GenAIProvider>,
32}
33
34impl ArchitectAgent {
35    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
36        Self {
37            model: model.unwrap_or_else(|| ModelTier::Architect.default_model().to_string()),
38            provider,
39        }
40    }
41
42    fn build_planning_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
43        format!(
44            r#"You are an Architect agent in a multi-agent coding system.
45
46## Task
47Goal: {}
48
49## Context
50Working Directory: {:?}
51Context Files: {:?}
52Output Targets: {:?}
53
54## Requirements
551. Break down this task into subtasks if needed
562. Define behavioral contracts for each subtask
573. Identify dependencies between subtasks
584. Specify required interfaces and invariants
59
60## Output Format
61Provide a structured plan with:
62- Subtask list with goals
63- File dependencies
64- Interface signatures
65- Test criteria"#,
66            node.goal, ctx.working_dir, node.context_files, node.output_targets,
67        )
68    }
69}
70
71#[async_trait]
72impl Agent for ArchitectAgent {
73    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
74        log::info!(
75            "[Architect] Processing node: {} with model {}",
76            node.node_id,
77            self.model
78        );
79
80        let prompt = self.build_planning_prompt(node, ctx);
81
82        let response = self
83            .provider
84            .generate_response_simple(&self.model, &prompt)
85            .await?;
86
87        Ok(AgentMessage::new(ModelTier::Architect, response))
88    }
89
90    fn name(&self) -> &str {
91        "Architect"
92    }
93
94    fn can_handle(&self, node: &SRBNNode) -> bool {
95        matches!(node.tier, ModelTier::Architect)
96    }
97}
98
99/// Actuator agent - handles code generation
100pub struct ActuatorAgent {
101    model: String,
102    provider: Arc<GenAIProvider>,
103}
104
105impl ActuatorAgent {
106    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
107        Self {
108            model: model.unwrap_or_else(|| ModelTier::Actuator.default_model().to_string()),
109            provider,
110        }
111    }
112
113    fn build_coding_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
114        let contract = &node.contract;
115
116        format!(
117            r#"You are an Actuator agent responsible for implementing code.
118
119## Task
120Goal: {}
121
122## Behavioral Contract
123Interface Signature: {}
124Invariants: {:?}
125Forbidden Patterns: {:?}
126
127## Context
128Working Directory: {:?}
129Files to Read: {:?}
130Files to Modify: {:?}
131
132## Instructions
1331. Implement the required functionality
1342. Follow the interface signature exactly
1353. Maintain all specified invariants
1364. Avoid all forbidden patterns
1375. Write clean, documented code
138
139## Output Format
140Provide the complete implementation with:
141- File path
142- Code content
143- Brief explanation of changes"#,
144            node.goal,
145            contract.interface_signature,
146            contract.invariants,
147            contract.forbidden_patterns,
148            ctx.working_dir,
149            node.context_files,
150            node.output_targets,
151        )
152    }
153}
154
155#[async_trait]
156impl Agent for ActuatorAgent {
157    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
158        log::info!(
159            "[Actuator] Processing node: {} with model {}",
160            node.node_id,
161            self.model
162        );
163
164        let prompt = self.build_coding_prompt(node, ctx);
165
166        let response = self
167            .provider
168            .generate_response_simple(&self.model, &prompt)
169            .await?;
170
171        Ok(AgentMessage::new(ModelTier::Actuator, response))
172    }
173
174    fn name(&self) -> &str {
175        "Actuator"
176    }
177
178    fn can_handle(&self, node: &SRBNNode) -> bool {
179        matches!(node.tier, ModelTier::Actuator)
180    }
181}
182
183/// Verifier agent - handles stability verification and contract checking
184pub struct VerifierAgent {
185    model: String,
186    provider: Arc<GenAIProvider>,
187}
188
189impl VerifierAgent {
190    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
191        Self {
192            model: model.unwrap_or_else(|| ModelTier::Verifier.default_model().to_string()),
193            provider,
194        }
195    }
196
197    fn build_verification_prompt(&self, node: &SRBNNode, implementation: &str) -> String {
198        let contract = &node.contract;
199
200        format!(
201            r#"You are a Verifier agent responsible for checking code correctness.
202
203## Task
204Verify the implementation satisfies the behavioral contract.
205
206## Behavioral Contract
207Interface Signature: {}
208Invariants: {:?}
209Forbidden Patterns: {:?}
210Weighted Tests: {:?}
211
212## Implementation
213{}
214
215## Verification Criteria
2161. Does the interface match the signature?
2172. Are all invariants satisfied?
2183. Are any forbidden patterns present?
2194. Would the weighted tests pass?
220
221## Output Format
222Provide:
223- PASS or FAIL status
224- Energy score (0.0 = perfect, 1.0 = total failure)
225- List of violations if any
226- Suggested fixes for each violation"#,
227            contract.interface_signature,
228            contract.invariants,
229            contract.forbidden_patterns,
230            contract.weighted_tests,
231            implementation,
232        )
233    }
234}
235
236#[async_trait]
237impl Agent for VerifierAgent {
238    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
239        log::info!(
240            "[Verifier] Processing node: {} with model {}",
241            node.node_id,
242            self.model
243        );
244
245        // In a real implementation, we would get the actual implementation from the context
246        let implementation = ctx
247            .history
248            .last()
249            .map(|m| m.content.as_str())
250            .unwrap_or("No implementation provided");
251
252        let prompt = self.build_verification_prompt(node, implementation);
253
254        let response = self
255            .provider
256            .generate_response_simple(&self.model, &prompt)
257            .await?;
258
259        Ok(AgentMessage::new(ModelTier::Verifier, response))
260    }
261
262    fn name(&self) -> &str {
263        "Verifier"
264    }
265
266    fn can_handle(&self, node: &SRBNNode) -> bool {
267        matches!(node.tier, ModelTier::Verifier)
268    }
269}
270
271/// Speculator agent - handles fast lookahead for exploration
272pub struct SpeculatorAgent {
273    model: String,
274    provider: Arc<GenAIProvider>,
275}
276
277impl SpeculatorAgent {
278    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
279        Self {
280            model: model.unwrap_or_else(|| ModelTier::Speculator.default_model().to_string()),
281            provider,
282        }
283    }
284}
285
286#[async_trait]
287impl Agent for SpeculatorAgent {
288    async fn process(&self, node: &SRBNNode, _ctx: &AgentContext) -> Result<AgentMessage> {
289        log::info!(
290            "[Speculator] Processing node: {} with model {}",
291            node.node_id,
292            self.model
293        );
294
295        let prompt = format!(
296            "Quickly evaluate if this approach is viable: {}\nProvide a brief YES/NO with one sentence justification.",
297            node.goal
298        );
299
300        let response = self
301            .provider
302            .generate_response_simple(&self.model, &prompt)
303            .await?;
304
305        Ok(AgentMessage::new(ModelTier::Speculator, response))
306    }
307
308    fn name(&self) -> &str {
309        "Speculator"
310    }
311
312    fn can_handle(&self, node: &SRBNNode) -> bool {
313        matches!(node.tier, ModelTier::Speculator)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    // Note: Integration tests would require actual API keys
320    // These are unit tests for the prompt building logic
321
322    #[test]
323    fn test_architect_prompt_building() {
324        // Would need provider mock for full test
325    }
326}