1use 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#[async_trait]
17pub trait Agent: Send + Sync {
18 async fn process(&self, node: &SRBNNode, ctx: &AgentContext) -> Result<AgentMessage>;
20
21 fn name(&self) -> &str;
23
24 fn can_handle(&self, node: &SRBNNode) -> bool;
26
27 fn model(&self) -> &str;
29
30 fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String;
32}
33
34pub 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
113pub 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 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
225pub 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 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 self.build_verification_prompt(node, "<implementation>")
319 }
320}
321
322pub 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 #[test]
379 fn test_architect_prompt_building() {
380 }
382}