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    /// Get the model name used by this agent (for logging)
28    fn model(&self) -> &str;
29
30    /// Build the prompt for this agent (for logging)
31    fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String;
32}
33
34/// Architect agent - handles planning and DAG construction
35pub struct ArchitectAgent {
36    model: String,
37    provider: Arc<GenAIProvider>,
38}
39
40impl ArchitectAgent {
41    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
42        Self {
43            model: model.unwrap_or_else(|| ModelTier::Architect.default_model().to_string()),
44            provider,
45        }
46    }
47
48    pub fn build_planning_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
49        format!(
50            r#"You are an Architect agent in a multi-agent coding system.
51
52## Task
53Goal: {}
54
55## Context
56Working Directory: {:?}
57Context Files: {:?}
58Output Targets: {:?}
59
60## Requirements
611. Break down this task into subtasks if needed
622. Define behavioral contracts for each subtask
633. Identify dependencies between subtasks
644. Specify required interfaces and invariants
65
66## Output Format
67Provide a structured plan with:
68- Subtask list with goals
69- File dependencies
70- Interface signatures
71- Test criteria"#,
72            node.goal, ctx.working_dir, node.context_files, node.output_targets,
73        )
74    }
75}
76
77#[async_trait]
78impl Agent for ArchitectAgent {
79    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
80        log::info!(
81            "[Architect] Processing node: {} with model {}",
82            node.node_id,
83            self.model
84        );
85
86        let prompt = self.build_planning_prompt(node, ctx);
87
88        let response = self
89            .provider
90            .generate_response_simple(&self.model, &prompt)
91            .await?;
92
93        Ok(AgentMessage::new(ModelTier::Architect, response))
94    }
95
96    fn name(&self) -> &str {
97        "Architect"
98    }
99
100    fn can_handle(&self, node: &SRBNNode) -> bool {
101        matches!(node.tier, ModelTier::Architect)
102    }
103
104    fn model(&self) -> &str {
105        &self.model
106    }
107
108    fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
109        self.build_planning_prompt(node, ctx)
110    }
111}
112
113/// Actuator agent - handles code generation
114pub struct ActuatorAgent {
115    model: String,
116    provider: Arc<GenAIProvider>,
117}
118
119impl ActuatorAgent {
120    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
121        Self {
122            model: model.unwrap_or_else(|| ModelTier::Actuator.default_model().to_string()),
123            provider,
124        }
125    }
126
127    pub fn build_coding_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
128        let contract = &node.contract;
129
130        // Determine target file from output_targets or generate default
131        let target_file = node
132            .output_targets
133            .first()
134            .map(|p| p.to_string_lossy().to_string())
135            .unwrap_or_else(|| "main.py".to_string());
136
137        format!(
138            r#"You are an Actuator agent responsible for implementing code.
139
140## Task
141Goal: {goal}
142
143## Behavioral Contract
144Interface Signature: {interface}
145Invariants: {invariants:?}
146Forbidden Patterns: {forbidden:?}
147
148## Context
149Working Directory: {working_dir:?}
150Files to Read: {context_files:?}
151Target Output File: {target_file}
152
153## Instructions
1541. Implement the required functionality
1552. Follow the interface signature exactly
1563. Maintain all specified invariants
1574. Avoid all forbidden patterns
1585. Write clean, well-documented, production-quality code
1596. Include proper imports at the top of the file
1607. Add type annotations if missing
1618. Import any missing modules
162
163## Output Format
164You MUST output the code in this EXACT format:
165
166File: {target_file}
167```python
168# Your complete implementation here
169# Include ALL necessary imports
170# Include the COMPLETE file, not just snippets
171```
172
173IMPORTANT:
174- The "File:" line MUST appear before the code block
175- Provide the COMPLETE corrected file, not just snippets
176- Include all imports at the top
177- Do not skip any functions or classes"#,
178            goal = node.goal,
179            interface = contract.interface_signature,
180            invariants = contract.invariants,
181            forbidden = contract.forbidden_patterns,
182            working_dir = ctx.working_dir,
183            context_files = node.context_files,
184            target_file = target_file,
185        )
186    }
187}
188
189#[async_trait]
190impl Agent for ActuatorAgent {
191    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
192        log::info!(
193            "[Actuator] Processing node: {} with model {}",
194            node.node_id,
195            self.model
196        );
197
198        let prompt = self.build_coding_prompt(node, ctx);
199
200        let response = self
201            .provider
202            .generate_response_simple(&self.model, &prompt)
203            .await?;
204
205        Ok(AgentMessage::new(ModelTier::Actuator, response))
206    }
207
208    fn name(&self) -> &str {
209        "Actuator"
210    }
211
212    fn can_handle(&self, node: &SRBNNode) -> bool {
213        matches!(node.tier, ModelTier::Actuator)
214    }
215
216    fn model(&self) -> &str {
217        &self.model
218    }
219
220    fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
221        self.build_coding_prompt(node, ctx)
222    }
223}
224
225/// Verifier agent - handles stability verification and contract checking
226pub struct VerifierAgent {
227    model: String,
228    provider: Arc<GenAIProvider>,
229}
230
231impl VerifierAgent {
232    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
233        Self {
234            model: model.unwrap_or_else(|| ModelTier::Verifier.default_model().to_string()),
235            provider,
236        }
237    }
238
239    pub fn build_verification_prompt(&self, node: &SRBNNode, implementation: &str) -> String {
240        let contract = &node.contract;
241
242        format!(
243            r#"You are a Verifier agent responsible for checking code correctness.
244
245## Task
246Verify the implementation satisfies the behavioral contract.
247
248## Behavioral Contract
249Interface Signature: {}
250Invariants: {:?}
251Forbidden Patterns: {:?}
252Weighted Tests: {:?}
253
254## Implementation
255{}
256
257## Verification Criteria
2581. Does the interface match the signature?
2592. Are all invariants satisfied?
2603. Are any forbidden patterns present?
2614. Would the weighted tests pass?
262
263## Output Format
264Provide:
265- PASS or FAIL status
266- Energy score (0.0 = perfect, 1.0 = total failure)
267- List of violations if any
268- Suggested fixes for each violation"#,
269            contract.interface_signature,
270            contract.invariants,
271            contract.forbidden_patterns,
272            contract.weighted_tests,
273            implementation,
274        )
275    }
276}
277
278#[async_trait]
279impl Agent for VerifierAgent {
280    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
281        log::info!(
282            "[Verifier] Processing node: {} with model {}",
283            node.node_id,
284            self.model
285        );
286
287        // In a real implementation, we would get the actual implementation from the context
288        let implementation = ctx
289            .history
290            .last()
291            .map(|m| m.content.as_str())
292            .unwrap_or("No implementation provided");
293
294        let prompt = self.build_verification_prompt(node, implementation);
295
296        let response = self
297            .provider
298            .generate_response_simple(&self.model, &prompt)
299            .await?;
300
301        Ok(AgentMessage::new(ModelTier::Verifier, response))
302    }
303
304    fn name(&self) -> &str {
305        "Verifier"
306    }
307
308    fn can_handle(&self, node: &SRBNNode) -> bool {
309        matches!(node.tier, ModelTier::Verifier)
310    }
311
312    fn model(&self) -> &str {
313        &self.model
314    }
315
316    fn build_prompt(&self, node: &SRBNNode, _ctx: &AgentContext) -> String {
317        // Verifier needs implementation context, use a placeholder
318        self.build_verification_prompt(node, "<implementation>")
319    }
320}
321
322/// Speculator agent - handles fast lookahead for exploration
323pub struct SpeculatorAgent {
324    model: String,
325    provider: Arc<GenAIProvider>,
326}
327
328impl SpeculatorAgent {
329    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
330        Self {
331            model: model.unwrap_or_else(|| ModelTier::Speculator.default_model().to_string()),
332            provider,
333        }
334    }
335}
336
337#[async_trait]
338impl Agent for SpeculatorAgent {
339    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
340        log::info!(
341            "[Speculator] Processing node: {} with model {}",
342            node.node_id,
343            self.model
344        );
345
346        let prompt = self.build_prompt(node, ctx);
347
348        let response = self
349            .provider
350            .generate_response_simple(&self.model, &prompt)
351            .await?;
352
353        Ok(AgentMessage::new(ModelTier::Speculator, response))
354    }
355
356    fn name(&self) -> &str {
357        "Speculator"
358    }
359
360    fn can_handle(&self, node: &SRBNNode) -> bool {
361        matches!(node.tier, ModelTier::Speculator)
362    }
363
364    fn model(&self) -> &str {
365        &self.model
366    }
367
368    fn build_prompt(&self, node: &SRBNNode, _ctx: &AgentContext) -> String {
369        format!("Briefly analyze potential issues for: {}", node.goal)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    // Note: Integration tests would require actual API keys
376    // These are unit tests for the prompt building logic
377
378    #[test]
379    fn test_architect_prompt_building() {
380        // Would need provider mock for full test
381    }
382}