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
1636. Output Format:
164   - For NEW files: Use 'File: {target_file}' followed by the full code.
165   - For EXISTING files: Use 'Diff: {target_file}' followed by a Unified Diff.
166
167## Output Format Examples
168
169### Creating a New File
170File: {target_file}
171```python
172import os
173
174def main():
175    print("Hello")
176```
177
178### Modifying an Existing File
179Diff: {target_file}
180```diff
181--- {target_file}
182+++ {target_file}
183@@ -10,2 +10,3 @@
184 def calculate(x):
185-    return x * 2
186+    return x * 3
187+    # Fixed calculation
188```
189
190IMPORTANT:
191- Use 'Diff:' for existing files to save tokens and apply changes safely.
192- Use 'File:' ONLY for new files or when rewriting the entire file is simpler.
193- For Diffs, include the standard header (---/+++) and @@ lines.
194- Do NOT output the full file contents if you are only changing a few lines."#,
195            goal = node.goal,
196            interface = contract.interface_signature,
197            invariants = contract.invariants,
198            forbidden = contract.forbidden_patterns,
199            working_dir = ctx.working_dir,
200            context_files = node.context_files,
201            target_file = target_file,
202        )
203    }
204}
205
206#[async_trait]
207impl Agent for ActuatorAgent {
208    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
209        log::info!(
210            "[Actuator] Processing node: {} with model {}",
211            node.node_id,
212            self.model
213        );
214
215        let prompt = self.build_coding_prompt(node, ctx);
216
217        let response = self
218            .provider
219            .generate_response_simple(&self.model, &prompt)
220            .await?;
221
222        Ok(AgentMessage::new(ModelTier::Actuator, response))
223    }
224
225    fn name(&self) -> &str {
226        "Actuator"
227    }
228
229    fn can_handle(&self, node: &SRBNNode) -> bool {
230        matches!(node.tier, ModelTier::Actuator)
231    }
232
233    fn model(&self) -> &str {
234        &self.model
235    }
236
237    fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String {
238        self.build_coding_prompt(node, ctx)
239    }
240}
241
242/// Verifier agent - handles stability verification and contract checking
243pub struct VerifierAgent {
244    model: String,
245    provider: Arc<GenAIProvider>,
246}
247
248impl VerifierAgent {
249    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
250        Self {
251            model: model.unwrap_or_else(|| ModelTier::Verifier.default_model().to_string()),
252            provider,
253        }
254    }
255
256    pub fn build_verification_prompt(&self, node: &SRBNNode, implementation: &str) -> String {
257        let contract = &node.contract;
258
259        format!(
260            r#"You are a Verifier agent responsible for checking code correctness.
261
262## Task
263Verify the implementation satisfies the behavioral contract.
264
265## Behavioral Contract
266Interface Signature: {}
267Invariants: {:?}
268Forbidden Patterns: {:?}
269Weighted Tests: {:?}
270
271## Implementation
272{}
273
274## Verification Criteria
2751. Does the interface match the signature?
2762. Are all invariants satisfied?
2773. Are any forbidden patterns present?
2784. Would the weighted tests pass?
279
280## Output Format
281Provide:
282- PASS or FAIL status
283- Energy score (0.0 = perfect, 1.0 = total failure)
284- List of violations if any
285- Suggested fixes for each violation"#,
286            contract.interface_signature,
287            contract.invariants,
288            contract.forbidden_patterns,
289            contract.weighted_tests,
290            implementation,
291        )
292    }
293}
294
295#[async_trait]
296impl Agent for VerifierAgent {
297    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
298        log::info!(
299            "[Verifier] Processing node: {} with model {}",
300            node.node_id,
301            self.model
302        );
303
304        // In a real implementation, we would get the actual implementation from the context
305        let implementation = ctx
306            .history
307            .last()
308            .map(|m| m.content.as_str())
309            .unwrap_or("No implementation provided");
310
311        let prompt = self.build_verification_prompt(node, implementation);
312
313        let response = self
314            .provider
315            .generate_response_simple(&self.model, &prompt)
316            .await?;
317
318        Ok(AgentMessage::new(ModelTier::Verifier, response))
319    }
320
321    fn name(&self) -> &str {
322        "Verifier"
323    }
324
325    fn can_handle(&self, node: &SRBNNode) -> bool {
326        matches!(node.tier, ModelTier::Verifier)
327    }
328
329    fn model(&self) -> &str {
330        &self.model
331    }
332
333    fn build_prompt(&self, node: &SRBNNode, _ctx: &AgentContext) -> String {
334        // Verifier needs implementation context, use a placeholder
335        self.build_verification_prompt(node, "<implementation>")
336    }
337}
338
339/// Speculator agent - handles fast lookahead for exploration
340pub struct SpeculatorAgent {
341    model: String,
342    provider: Arc<GenAIProvider>,
343}
344
345impl SpeculatorAgent {
346    pub fn new(provider: Arc<GenAIProvider>, model: Option<String>) -> Self {
347        Self {
348            model: model.unwrap_or_else(|| ModelTier::Speculator.default_model().to_string()),
349            provider,
350        }
351    }
352}
353
354#[async_trait]
355impl Agent for SpeculatorAgent {
356    async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage> {
357        log::info!(
358            "[Speculator] Processing node: {} with model {}",
359            node.node_id,
360            self.model
361        );
362
363        let prompt = self.build_prompt(node, ctx);
364
365        let response = self
366            .provider
367            .generate_response_simple(&self.model, &prompt)
368            .await?;
369
370        Ok(AgentMessage::new(ModelTier::Speculator, response))
371    }
372
373    fn name(&self) -> &str {
374        "Speculator"
375    }
376
377    fn can_handle(&self, node: &SRBNNode) -> bool {
378        matches!(node.tier, ModelTier::Speculator)
379    }
380
381    fn model(&self) -> &str {
382        &self.model
383    }
384
385    fn build_prompt(&self, node: &SRBNNode, _ctx: &AgentContext) -> String {
386        format!("Briefly analyze potential issues for: {}", node.goal)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    // Note: Integration tests would require actual API keys
393    // These are unit tests for the prompt building logic
394
395    #[test]
396    fn test_architect_prompt_building() {
397        // Would need provider mock for full test
398    }
399}