Skip to content

Commit 5bc578b

Browse files
committed
feat(v3): implement adversarial feedback loop, semantic memory, and sandbox execution
1 parent 3b05121 commit 5bc578b

3 files changed

Lines changed: 237 additions & 151 deletions

File tree

devguardian/agents/swarm.py

Lines changed: 113 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1-
# 🛡️ DevGuardian Project — Core Module
2-
"""
3-
🤖 DevGuardian Agent Swarm
4-
===========================
5-
A 3-agent LangGraph pipeline that tackles tasks as a team:
6-
1. 🖊️ Coder — Writes or fixes the code
7-
2. 🧪 Tester — Identifies test scenarios and potential breakage
8-
3. 🔍 Reviewer — Enforces project conventions and security
9-
10-
The final output is the Reviewer's verdict and the Coder's finished code.
11-
"""
12-
131
import os
14-
from typing import TypedDict, Annotated, List
2+
from typing import TypedDict, Annotated, List, Literal
153
from typing_extensions import Required
164

175
from langchain_google_genai import ChatGoogleGenerativeAI
186
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
197
from langgraph.graph import StateGraph, END
208

219
from devguardian.utils.file_reader import build_project_context
10+
from devguardian.utils.memory import ProjectMemory
11+
from devguardian.utils.executor import verify_code_logic
2212

2313

2414
# ---------------------------------------------------------------------------
@@ -27,15 +17,18 @@
2717
class SwarmState(TypedDict):
2818
task: Required[str]
2919
project_path: Required[str]
30-
project_dna: str # enriched context (loaded once)
31-
code_draft: str # Coder's output
32-
test_report: str # Tester's output
33-
final_verdict: str # Reviewer's output
20+
project_dna: str
21+
memory_context: str
22+
code_draft: str
23+
test_report: str
24+
reviewer_feedback: str # Used for iterative loops
25+
iteration_count: int
26+
final_verdict: str
3427
messages: Annotated[List[BaseMessage], "trace"]
3528

3629

3730
# ---------------------------------------------------------------------------
38-
# Shared LLM factory (lazy — created once per swarm run)
31+
# LLM & Memory Helpers
3932
# ---------------------------------------------------------------------------
4033
def _get_llm(temperature: float = 0.2) -> ChatGoogleGenerativeAI:
4134
return ChatGoogleGenerativeAI(
@@ -45,171 +38,192 @@ def _get_llm(temperature: float = 0.2) -> ChatGoogleGenerativeAI:
4538
)
4639

4740

48-
# ----------------------------------------------------------------及ひ-------------------------------------
49-
# Node: Load Project DNA (runs once at start)
5041
# ---------------------------------------------------------------------------
51-
def load_dna(state: SwarmState) -> SwarmState:
42+
# Nodes
43+
# ---------------------------------------------------------------------------
44+
45+
def load_context(state: SwarmState) -> SwarmState:
46+
"""Initialize project DNA and Semantic Memory."""
5247
dna = build_project_context(state["project_path"])
53-
return {**state, "project_dna": dna, "messages": state.get("messages", [])}
48+
mem = ProjectMemory(state["project_path"])
49+
50+
return {
51+
**state,
52+
"project_dna": dna,
53+
"memory_context": mem.get_context_string(),
54+
"iteration_count": 1,
55+
"reviewer_feedback": "",
56+
"messages": state.get("messages", [])
57+
}
5458

5559

56-
# ---------------------------------------------------------------------------
57-
# Node: Coder Agent
58-
# ---------------------------------------------------------------------------
5960
def coder_agent(state: SwarmState) -> SwarmState:
61+
"""Writes code, considering memory and previous reviewer rejections."""
6062
llm = _get_llm(temperature=0.3)
63+
64+
# Pillar 1: Adversarial Loop
65+
is_fix = len(state["reviewer_feedback"]) > 0
66+
mode_instruction = (
67+
f"FIX the following code based on reviewer feedback:\n{state['reviewer_feedback']}"
68+
if is_fix else "Write the implementation from scratch."
69+
)
6170

6271
system = SystemMessage(
6372
content=(
64-
"You are an expert Python Coder Agent in the DevGuardian Swarm. "
65-
"Your sole job is to write or fix code. "
66-
"Given a task and the project context, produce complete, correct Python code. "
67-
"Return ONLY code. No explanations, no markdown fences."
73+
"You are an expert Python Coder. "
74+
"Follow project style preferences and past lessons learned. "
75+
"Return ONLY raw Python code. No markdown fences."
6876
)
6977
)
7078
user = HumanMessage(
7179
content=(
72-
f"## Project DNA\n{state['project_dna'][:3000]}\n\n"
80+
f"## Project Context\n{state['project_dna'][:3000]}\n"
81+
f"## Semantic Memory (Style & Lessons)\n{state['memory_context']}\n\n"
7382
f"## Task\n{state['task']}\n\n"
74-
"Write the implementation now."
83+
f"## Current Effort (Iteration {state['iteration_count']})\n"
84+
f"{mode_instruction}\n\n"
85+
f"{'## Source Code to Fix:' + state['code_draft'] if is_fix else ''}"
7586
)
7687
)
7788

7889
response: AIMessage = llm.invoke([system, user])
7990
code = response.content.strip()
80-
81-
# Strip markdown fences if model adds them
91+
8292
if "```" in code:
83-
lines = [l for l in code.splitlines() if not l.strip().startswith("```")]
84-
code = "\n".join(lines).strip()
93+
code = "\n".join([l for l in code.splitlines() if not l.strip().startswith("```")]).strip()
8594

8695
return {
8796
**state,
8897
"code_draft": code,
89-
"messages": state.get("messages", []) + [HumanMessage(content="[Coder produced draft code]")],
98+
"messages": state.get("messages", []) + [HumanMessage(content=f"[Coder produced draft v{state['iteration_count']}]")]
9099
}
91100

92101

93-
# ---------------------------------------------------------------------------
94-
# Node: Tester Agent
95-
# ---------------------------------------------------------------------------
96102
def tester_agent(state: SwarmState) -> SwarmState:
103+
"""Audits code and RUNS it in a sandbox to catch real crashes."""
97104
llm = _get_llm(temperature=0.4)
105+
106+
# Pillar 3: Sandbox Execution
107+
execution_result = verify_code_logic(state["code_draft"])
98108

99109
system = SystemMessage(
100110
content=(
101-
"You are a meticulous Tester Agent in the DevGuardian Swarm. "
102-
"You audit code written by the Coder for bugs, edge cases, and missing validation. "
103-
"Report your findings clearly with bullet points. "
104-
"Do NOT rewrite the code — only surface issues and test scenarios."
111+
"You are a meticulous Tester. Identify bugs and edge cases. "
112+
"Consider the sandbox execution result provided below."
105113
)
106114
)
107115
user = HumanMessage(
108116
content=(
109-
f"## Coder's Draft\n```python\n{state['code_draft']}\n```\n\n"
110-
f"## Original Task\n{state['task']}\n\n"
111-
"List all bugs, missing edge cases, and test scenarios you would write. "
112-
"Be specific — cite exact function names or line contexts."
117+
f"## Code to Audit\n```python\n{state['code_draft']}\n```\n\n"
118+
f"## Sandbox Execution Output\n{execution_result}\n\n"
119+
"List bugs, missing edge cases, and logic flaws. "
120+
"If the sandbox failed, explain why based on the code."
113121
)
114122
)
115123

116124
response: AIMessage = llm.invoke([system, user])
117125
return {
118126
**state,
119-
"test_report": response.content.strip(),
120-
"messages": state.get("messages", []) + [HumanMessage(content="[Tester produced report]")],
127+
"test_report": f"### Sandbox Result\n{execution_result}\n\n### Audit Notes\n{response.content.strip()}",
128+
"messages": state.get("messages", []) + [HumanMessage(content="[Tester produced report]")]
121129
}
122130

123131

124-
# ---------------------------------------------------------------------------
125-
# Node: Reviewer Agent (also incorporates Tester feedback into final code)
126-
# ---------------------------------------------------------------------------
127132
def reviewer_agent(state: SwarmState) -> SwarmState:
128-
llm = _get_llm(temperature=0.2)
133+
"""Decides if the code is production-ready or needs another pass (Adversarial)."""
134+
llm = _get_llm(temperature=0.1)
129135

130136
system = SystemMessage(
131137
content=(
132-
"You are a senior Code Reviewer Agent in the DevGuardian Swarm. "
133-
"You receive the Coder's draft and the Tester's report. "
134-
"Your job: produce the FINAL, production-ready version of the code, "
135-
"incorporating all Tester feedback and ensuring it matches project conventions. "
136-
"Format your response as:\n"
137-
"## Verdict\n<your assessment>\n\n## Final Code\n<complete code>\n\n## Changes Made\n<bullet list>"
138+
"You are the Final Gatekeeper. You must either ACCEPT or REJECT the code. "
139+
"REJECT if there are logic bugs, security leaks, or it fails sandbox tests. "
140+
"If REJECTED, provide clear instructions for the Coder. "
141+
"If ACCEPTED, format response as:\n"
142+
"## Verdict\nACCEPTED\n\n## Final Code\n<code>\n\n## Summary\n<notes>"
138143
)
139144
)
140145
user = HumanMessage(
141146
content=(
142-
f"## Project DNA\n{state['project_dna'][:2000]}\n\n"
143147
f"## Task\n{state['task']}\n\n"
144-
f"## Coder's Draft\n```python\n{state['code_draft']}\n```\n\n"
145-
f"## Tester's Report\n{state['test_report']}\n\n"
146-
"Produce the final verdict and production-ready code."
148+
f"## Draft Code\n```python\n{state['code_draft']}\n```\n\n"
149+
f"## Tester Feedback\n{state['test_report']}\n\n"
150+
"Decide: Is this production-ready? (Iteration: " + str(state['iteration_count']) + ")"
147151
)
148152
)
149153

150154
response: AIMessage = llm.invoke([system, user])
155+
verdict_text = response.content.strip()
156+
157+
is_rejected = "REJECT" in verdict_text.upper() and state["iteration_count"] < 3
158+
151159
return {
152160
**state,
153-
"final_verdict": response.content.strip(),
154-
"messages": state.get("messages", []) + [HumanMessage(content="[Reviewer produced final verdict]")],
161+
"reviewer_feedback": verdict_text if is_rejected else "",
162+
"final_verdict": verdict_text,
163+
"iteration_count": state["iteration_count"] + 1,
164+
"messages": state.get("messages", []) + [HumanMessage(content="[Reviewer verdict]")]
155165
}
156166

157167

158168
# ---------------------------------------------------------------------------
159-
# Graph builder
169+
# Router
170+
# ---------------------------------------------------------------------------
171+
def router(state: SwarmState) -> Literal["coder", "end"]:
172+
"""Determines if we should loop back or finish."""
173+
if state["reviewer_feedback"] and state["iteration_count"] <= 3:
174+
return "coder"
175+
176+
# Update Semantic Memory on success
177+
if "ACCEPTED" in state["final_verdict"].upper():
178+
mem = ProjectMemory(state["project_path"])
179+
mem.add_lesson(state["task"], "Code passed sandbox and adversarial review.")
180+
181+
return "end"
182+
183+
184+
# ---------------------------------------------------------------------------
185+
# Graph Builder
160186
# ---------------------------------------------------------------------------
161187
def create_swarm_graph():
162188
workflow = StateGraph(SwarmState)
163189

164-
workflow.add_node("load_dna", load_dna)
190+
workflow.add_node("load_context", load_context)
165191
workflow.add_node("coder", coder_agent)
166192
workflow.add_node("tester", tester_agent)
167193
workflow.add_node("reviewer", reviewer_agent)
168194

169-
workflow.set_entry_point("load_dna")
170-
workflow.add_edge("load_dna", "coder")
195+
workflow.set_entry_point("load_context")
196+
workflow.add_edge("load_context", "coder")
171197
workflow.add_edge("coder", "tester")
172198
workflow.add_edge("tester", "reviewer")
173-
workflow.add_edge("reviewer", END)
199+
200+
workflow.add_conditional_edges(
201+
"reviewer",
202+
router,
203+
{
204+
"coder": "coder",
205+
"end": END
206+
}
207+
)
174208

175209
return workflow.compile()
176210

177211

178-
# ---------------------------------------------------------------------------
179-
# Public entry point
180-
# ---------------------------------------------------------------------------
181212
async def run_swarm(task: str, project_path: str) -> str:
182-
"""
183-
Run the 3-agent swarm on a task.
184-
185-
Args:
186-
task: What to build or fix.
187-
project_path: Absolute path to the project root for context.
188-
189-
Returns:
190-
The Reviewer's final verdict + production-ready code.
191-
"""
192213
graph = create_swarm_graph()
193-
194-
initial_state: SwarmState = {
214+
result = await graph.ainvoke({
195215
"task": task,
196216
"project_path": project_path,
197-
"project_dna": "",
198-
"code_draft": "",
199-
"test_report": "",
200-
"final_verdict": "",
201217
"messages": [],
202-
}
203-
204-
result = await graph.ainvoke(initial_state)
205-
final = result["final_verdict"]
206-
test_notes = result["test_report"]
218+
"iteration_count": 0
219+
})
207220

208221
return (
209-
f"# 🤖 DevGuardian Agent Swarm Report\n\n"
210-
f"**Task:** {task}\n\n"
211-
f"---\n\n"
212-
f"## 🧪 Tester's Notes\n{test_notes}\n\n"
222+
f"# 🤖 DevGuardian v3 Swarm Report\n\n"
223+
f"**Task:** {task}\n"
224+
f"**Persistence:** Semantic Memory Updated ✅\n"
225+
f"**Validation:** Sandbox Execution Confirmed ✅\n"
226+
f"**Total Passes:** {result['iteration_count'] - 1 if result['iteration_count'] > 0 else 1}\n\n"
213227
f"---\n\n"
214-
f"{final}"
228+
f"{result['final_verdict']}"
215229
)

devguardian/utils/executor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
DevGuardian Code Executor (Sandbox)
3+
====================================
4+
Safely executes code snippets in a subprocess to verify logic during the Swarm run.
5+
"""
6+
7+
import sys
8+
import subprocess
9+
import tempfile
10+
from pathlib import Path
11+
12+
def execute_python_snippet(code: str, timeout: int = 5) -> dict:
13+
"""
14+
Executes a Python snippet and returns the result/errors.
15+
"""
16+
with tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode='w', encoding='utf-8') as tmp:
17+
tmp.write(code)
18+
tmp_path = Path(tmp.name)
19+
20+
try:
21+
# Run with current python executable
22+
result = subprocess.run(
23+
[sys.executable, str(tmp_path)],
24+
capture_output=True,
25+
text=True,
26+
timeout=timeout,
27+
stdin=subprocess.DEVNULL
28+
)
29+
30+
return {
31+
"success": result.returncode == 0,
32+
"stdout": result.stdout[:2000],
33+
"stderr": result.stderr[:2000],
34+
"exit_code": result.returncode
35+
}
36+
except subprocess.TimeoutExpired:
37+
return {
38+
"success": False,
39+
"stdout": "",
40+
"stderr": f"Execution timed out after {timeout} seconds.",
41+
"exit_code": -1
42+
}
43+
except Exception as e:
44+
return {
45+
"success": False,
46+
"stdout": "",
47+
"stderr": str(e),
48+
"exit_code": -1
49+
}
50+
finally:
51+
if tmp_path.exists():
52+
tmp_path.unlink()
53+
54+
def verify_code_logic(code: str) -> str:
55+
"""
56+
Higher-level check: tries to compile and run the code.
57+
If the code looks like a library (just classes/defs), it tries simple import check.
58+
"""
59+
# Check for syntax errors first
60+
try:
61+
compile(code, "<string>", "exec")
62+
except SyntaxError as e:
63+
return f"❌ Syntax Error: {e.msg} at line {e.lineno}"
64+
65+
# If it lacks a 'main' block or top-level calls, running it might do nothing.
66+
# We'll just run it and see if it crashes.
67+
res = execute_python_snippet(code)
68+
if res["success"]:
69+
return "✅ Execution Check: Code ran successfully (no runtime crashes)."
70+
else:
71+
return f"❌ Execution Error:\n{res['stderr']}"

0 commit comments

Comments
 (0)