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
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
242pub 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 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 self.build_verification_prompt(node, "<implementation>")
336 }
337}
338
339pub 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 #[test]
396 fn test_architect_prompt_building() {
397 }
399}