revona 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- revona-1.0.0/PKG-INFO +26 -0
- revona-1.0.0/agent/__init__.py +45 -0
- revona-1.0.0/agent/agent.py +462 -0
- revona-1.0.0/agent/agents.py +123 -0
- revona-1.0.0/agent/cli.py +411 -0
- revona-1.0.0/agent/client.py +15 -0
- revona-1.0.0/agent/config.py +33 -0
- revona-1.0.0/agent/context.py +51 -0
- revona-1.0.0/agent/memory.py +459 -0
- revona-1.0.0/agent/mission.py +124 -0
- revona-1.0.0/agent/models.py +67 -0
- revona-1.0.0/agent/progress.py +65 -0
- revona-1.0.0/agent/prompts.py +37 -0
- revona-1.0.0/agent/repo_intel.py +499 -0
- revona-1.0.0/agent/session.py +30 -0
- revona-1.0.0/agent/skills.py +304 -0
- revona-1.0.0/agent/terminal.py +104 -0
- revona-1.0.0/agent/tools.py +228 -0
- revona-1.0.0/agent/tui.py +467 -0
- revona-1.0.0/pyproject.toml +42 -0
- revona-1.0.0/revona.egg-info/PKG-INFO +26 -0
- revona-1.0.0/revona.egg-info/SOURCES.txt +25 -0
- revona-1.0.0/revona.egg-info/dependency_links.txt +1 -0
- revona-1.0.0/revona.egg-info/entry_points.txt +2 -0
- revona-1.0.0/revona.egg-info/requires.txt +4 -0
- revona-1.0.0/revona.egg-info/top_level.txt +1 -0
- revona-1.0.0/setup.cfg +4 -0
revona-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revona
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Autonomous AI Engineering Platform — built by LX Obsidian Labs
|
|
5
|
+
Author: LX Obsidian Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lx-obsidian-labs/revona-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/lx-obsidian-labs/revona-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/lx-obsidian-labs/revona-cli/issues
|
|
10
|
+
Keywords: ai,cli,coding,agent,engineering,automation,nvidia,revona
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: openai>=1.30
|
|
24
|
+
Requires-Dist: click>=8.1
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
AGENT_DIR = Path(".agent")
|
|
4
|
+
CONFIG_PATH = AGENT_DIR / "config.toml"
|
|
5
|
+
SESSIONS_DIR = AGENT_DIR / "sessions"
|
|
6
|
+
INDEX_PATH = AGENT_DIR / "index.txt"
|
|
7
|
+
MODELS_CACHE_PATH = AGENT_DIR / "models.json"
|
|
8
|
+
|
|
9
|
+
DEFAULT_MODEL = "deepseek-ai/deepseek-v4-pro"
|
|
10
|
+
BASE_URL = "https://integrate.api.nvidia.com/v1"
|
|
11
|
+
|
|
12
|
+
IGNORE_DIRS = {
|
|
13
|
+
".git", "node_modules", "__pycache__", ".venv", "venv",
|
|
14
|
+
"dist", "build", ".mypy_cache", ".pytest_cache", "target",
|
|
15
|
+
}
|
|
16
|
+
TEXT_EXTS = {
|
|
17
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".toml", ".md",
|
|
18
|
+
".txt", ".yaml", ".yml", ".html", ".css", ".sh", ".rs",
|
|
19
|
+
".go", ".java", ".c", ".cpp", ".h", ".sql", ".cfg", ".ini",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Brand name
|
|
23
|
+
APP_NAME = "Revona CLI"
|
|
24
|
+
COMPANY = "LX Obsidian Labs"
|
|
25
|
+
|
|
26
|
+
# Brand colours (Rich markup)
|
|
27
|
+
C_PRIMARY = "black"
|
|
28
|
+
C_ACCENT = "bright_blue"
|
|
29
|
+
C_SUCCESS = "bright_green"
|
|
30
|
+
C_WARNING = "bright_yellow"
|
|
31
|
+
C_ERROR = "bright_red"
|
|
32
|
+
C_DIM = "bright_black"
|
|
33
|
+
|
|
34
|
+
REVONA_ASCII = """\
|
|
35
|
+
██████╗ ███████╗██╗ ██╗ ██████╗ ███╗ ██╗ █████╗
|
|
36
|
+
██╔══██╗██╔════╝██║ ██║██╔═══██╗████╗ ██║██╔══██╗
|
|
37
|
+
██████╔╝█████╗ ██║ ██║██║ ██║██╔██╗ ██║███████║
|
|
38
|
+
██╔══██╗██╔══╝ ╚██╗ ██╔╝██║ ██║██║╚██╗██║██╔══██║
|
|
39
|
+
██║ ██║███████╗ ╚████╔╝ ╚██████╔╝██║ ╚████║██║ ██║
|
|
40
|
+
╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ensure_dirs() -> None:
|
|
44
|
+
AGENT_DIR.mkdir(exist_ok=True)
|
|
45
|
+
SESSIONS_DIR.mkdir(exist_ok=True)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from . import DEFAULT_MODEL
|
|
11
|
+
from .agents import AGENTS, AgentSpec, get as get_agent, resolve_tools
|
|
12
|
+
from .memory import IntelligenceEngine
|
|
13
|
+
from .mission import Mission, Task, TaskStatus
|
|
14
|
+
from .progress import ProgressEngine
|
|
15
|
+
from .prompts import SYSTEM_PROMPT
|
|
16
|
+
from .tools import TOOL_SCHEMAS, execute_tool
|
|
17
|
+
from .terminal import console
|
|
18
|
+
|
|
19
|
+
_intel = IntelligenceEngine()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Low-level agent loop
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def _run_agent_loop(
|
|
27
|
+
client,
|
|
28
|
+
model: str,
|
|
29
|
+
system_prompt: str,
|
|
30
|
+
user_prompt: str,
|
|
31
|
+
allowed_tools: list[dict],
|
|
32
|
+
max_iter: int = 30,
|
|
33
|
+
on_tool: Callable | None = None,
|
|
34
|
+
) -> tuple[str, list[dict]]:
|
|
35
|
+
"""Run a single agent with its tools. Returns (final_text, messages)."""
|
|
36
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
37
|
+
messages.append({"role": "user", "content": user_prompt})
|
|
38
|
+
|
|
39
|
+
for i in range(max_iter):
|
|
40
|
+
resp = client.chat.completions.create(
|
|
41
|
+
model=model,
|
|
42
|
+
messages=messages,
|
|
43
|
+
tools=allowed_tools or None,
|
|
44
|
+
stream=False,
|
|
45
|
+
temperature=0.2,
|
|
46
|
+
)
|
|
47
|
+
msg = resp.choices[0].message
|
|
48
|
+
|
|
49
|
+
if msg.tool_calls:
|
|
50
|
+
messages.append({
|
|
51
|
+
"role": "assistant",
|
|
52
|
+
"content": msg.content or "",
|
|
53
|
+
"tool_calls": [
|
|
54
|
+
{
|
|
55
|
+
"id": tc.id,
|
|
56
|
+
"type": "function",
|
|
57
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments},
|
|
58
|
+
}
|
|
59
|
+
for tc in msg.tool_calls
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
for tc in msg.tool_calls:
|
|
63
|
+
args = json.loads(tc.function.arguments or "{}")
|
|
64
|
+
if on_tool:
|
|
65
|
+
on_tool(tc.function.name, args)
|
|
66
|
+
result = execute_tool(tc.function.name, args)
|
|
67
|
+
messages.append({
|
|
68
|
+
"role": "tool",
|
|
69
|
+
"tool_call_id": tc.id,
|
|
70
|
+
"content": result[:12000],
|
|
71
|
+
})
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
final = msg.content or ""
|
|
75
|
+
messages.append({"role": "assistant", "content": final})
|
|
76
|
+
return final, messages
|
|
77
|
+
|
|
78
|
+
return "(max iterations reached)", messages
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Orchestrator
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def decompose_into_tasks(
|
|
86
|
+
client, model: str, request: str, context: str
|
|
87
|
+
) -> list[dict]:
|
|
88
|
+
"""Ask the planner to break a request into a structured task list."""
|
|
89
|
+
planner_spec = get_agent("planner")
|
|
90
|
+
if not planner_spec:
|
|
91
|
+
return _default_tasks(request)
|
|
92
|
+
|
|
93
|
+
prompt = (
|
|
94
|
+
f"## Repository context\n{context[:20000]}\n\n"
|
|
95
|
+
f"## Request\n{request}\n\n"
|
|
96
|
+
"Break this request into a list of numbered tasks that can be executed "
|
|
97
|
+
"sequentially. Each task should be a single, focused action. "
|
|
98
|
+
"Format as a JSON list:\n"
|
|
99
|
+
'[{"id": "1", "label": "short label", "description": "what to do", '
|
|
100
|
+
'"agent": "builder|tester|reviewer"}]\n'
|
|
101
|
+
"Output ONLY the JSON, no other text."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
text, _ = _run_agent_loop(
|
|
106
|
+
client, model,
|
|
107
|
+
planner_spec.system_prompt,
|
|
108
|
+
prompt,
|
|
109
|
+
resolve_tools(planner_spec.allowed_tools),
|
|
110
|
+
max_iter=5,
|
|
111
|
+
)
|
|
112
|
+
# Extract JSON from response
|
|
113
|
+
text = text.strip()
|
|
114
|
+
if "```json" in text:
|
|
115
|
+
text = text.split("```json")[1].split("```")[0].strip()
|
|
116
|
+
elif "```" in text:
|
|
117
|
+
text = text.split("```")[1].split("```")[0].strip()
|
|
118
|
+
tasks = json.loads(text)
|
|
119
|
+
if isinstance(tasks, list):
|
|
120
|
+
return tasks
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
return _default_tasks(request)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _default_tasks(request: str) -> list[dict]:
|
|
127
|
+
"""Fallback when decomposition fails."""
|
|
128
|
+
return [{"id": "1", "label": "Implement", "description": request, "agent": "builder"}]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def verify_changes(
|
|
132
|
+
client, model: str, mission: Mission,
|
|
133
|
+
) -> tuple[bool, str]:
|
|
134
|
+
"""Run the reviewer agent over changed files."""
|
|
135
|
+
reviewer = get_agent("reviewer")
|
|
136
|
+
if not reviewer:
|
|
137
|
+
return True, "no reviewer configured"
|
|
138
|
+
|
|
139
|
+
changed = list(mission.edited_files)
|
|
140
|
+
if not changed:
|
|
141
|
+
return True, "no files changed"
|
|
142
|
+
|
|
143
|
+
prompt = (
|
|
144
|
+
"Review these changed files for correctness, security, and style:\n"
|
|
145
|
+
+ "\n".join(f"- {f}" for f in changed)
|
|
146
|
+
+ "\n\nRun relevant lint/type-check commands. Report any issues."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
text, _ = _run_agent_loop(
|
|
150
|
+
client, model,
|
|
151
|
+
reviewer.system_prompt,
|
|
152
|
+
prompt,
|
|
153
|
+
resolve_tools(reviewer.allowed_tools),
|
|
154
|
+
max_iter=15,
|
|
155
|
+
)
|
|
156
|
+
approved = "APPROVED" in text
|
|
157
|
+
return approved, text
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_tests(
|
|
161
|
+
client, model: str, mission: Mission,
|
|
162
|
+
) -> tuple[bool, str]:
|
|
163
|
+
"""Run the tester agent."""
|
|
164
|
+
tester = get_agent("tester")
|
|
165
|
+
if not tester:
|
|
166
|
+
return True, "no tester configured"
|
|
167
|
+
|
|
168
|
+
prompt = (
|
|
169
|
+
"Discover and run the project's tests. "
|
|
170
|
+
"If none exist, check a few key files for correctness manually."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
text, _ = _run_agent_loop(
|
|
174
|
+
client, model,
|
|
175
|
+
tester.system_prompt,
|
|
176
|
+
prompt,
|
|
177
|
+
resolve_tools(tester.allowed_tools),
|
|
178
|
+
max_iter=15,
|
|
179
|
+
)
|
|
180
|
+
passing = "ALL TESTS PASSING" in text
|
|
181
|
+
return passing, text
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def execute_mission(
|
|
185
|
+
client,
|
|
186
|
+
model: str,
|
|
187
|
+
mission: Mission,
|
|
188
|
+
progress: ProgressEngine,
|
|
189
|
+
max_global_iter: int = 3,
|
|
190
|
+
) -> Mission:
|
|
191
|
+
"""Execute a mission: run each task with the right agent, verify, retry."""
|
|
192
|
+
for global_pass in range(max_global_iter):
|
|
193
|
+
if mission.all_done():
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
# Execute ready tasks
|
|
197
|
+
for task in mission.get_ready_tasks():
|
|
198
|
+
if task.status != TaskStatus.PENDING:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
task.status = TaskStatus.RUNNING
|
|
202
|
+
progress.start(task.id)
|
|
203
|
+
|
|
204
|
+
# Pick agent and model
|
|
205
|
+
agent_spec = get_agent(task.agent) or get_agent("builder")
|
|
206
|
+
task_model = task.model or model
|
|
207
|
+
|
|
208
|
+
tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
|
|
209
|
+
|
|
210
|
+
context = mission.context if global_pass == 0 else ""
|
|
211
|
+
prompt = (
|
|
212
|
+
f"## Task: {task.label}\n{task.description}\n\n"
|
|
213
|
+
f"## Repository context\n{context[:20000]}\n\n"
|
|
214
|
+
"Execute this task. Use the available tools."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _on_tool(name, args):
|
|
218
|
+
if name in ("write_file", "edit_file"):
|
|
219
|
+
fp = args.get("path", "")
|
|
220
|
+
if fp:
|
|
221
|
+
mission.edited_files.add(fp)
|
|
222
|
+
task.files_changed.append(fp)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
text, _ = _run_agent_loop(
|
|
226
|
+
client, task_model,
|
|
227
|
+
agent_spec.system_prompt,
|
|
228
|
+
prompt,
|
|
229
|
+
tools,
|
|
230
|
+
max_iter=30,
|
|
231
|
+
on_tool=_on_tool,
|
|
232
|
+
)
|
|
233
|
+
task.result = text[:2000]
|
|
234
|
+
|
|
235
|
+
# Auto-verify: run build/lint/test command
|
|
236
|
+
shell_result = execute_tool("run_shell", {"command": "python -m pytest --version 2>nul || pytest --version 2>nul || echo no test framework found"})
|
|
237
|
+
has_tests = "pytest" in shell_result.lower()
|
|
238
|
+
|
|
239
|
+
if has_tests:
|
|
240
|
+
test_out = execute_tool("run_shell", {"command": "python -m pytest -x -q 2>&1 || echo no tests to run"})
|
|
241
|
+
if "FAILED" in test_out or "ERROR" in test_out:
|
|
242
|
+
task.status = TaskStatus.FAILED
|
|
243
|
+
task.error = "Tests failed"
|
|
244
|
+
progress.fail(task.id, "tests failing")
|
|
245
|
+
else:
|
|
246
|
+
task.status = TaskStatus.DONE
|
|
247
|
+
progress.done(task.id)
|
|
248
|
+
else:
|
|
249
|
+
task.status = TaskStatus.DONE
|
|
250
|
+
progress.done(task.id)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
task.status = TaskStatus.FAILED
|
|
254
|
+
task.error = str(e)[:500]
|
|
255
|
+
progress.fail(task.id, str(e)[:80])
|
|
256
|
+
|
|
257
|
+
# Handle failures: retry or re-plan
|
|
258
|
+
for task in mission.tasks.values():
|
|
259
|
+
if task.status == TaskStatus.FAILED and task.retries < task.max_retries:
|
|
260
|
+
task.retries += 1
|
|
261
|
+
task.status = TaskStatus.PENDING
|
|
262
|
+
progress.add_step(f"{task.id}-retry{task.retries}", f"Retry {task.label} ({task.retries}/{task.max_retries})")
|
|
263
|
+
console.print(f"[yellow]Retrying {task.label}...[/]")
|
|
264
|
+
|
|
265
|
+
# Final review pass
|
|
266
|
+
if mission.edited_files:
|
|
267
|
+
progress.add_step("review", "Reviewing changes")
|
|
268
|
+
progress.start("review")
|
|
269
|
+
approved, review_text = verify_changes(client, model, mission)
|
|
270
|
+
if approved:
|
|
271
|
+
progress.done("review")
|
|
272
|
+
else:
|
|
273
|
+
progress.fail("review")
|
|
274
|
+
console.print(Panel(review_text[:2000], title="Review Issues"))
|
|
275
|
+
|
|
276
|
+
return mission
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def orchestrator_build(
|
|
280
|
+
client,
|
|
281
|
+
model: str,
|
|
282
|
+
request: str,
|
|
283
|
+
progress: ProgressEngine,
|
|
284
|
+
) -> Mission:
|
|
285
|
+
"""Full orchestrated build: decompose → execute → verify → reflect."""
|
|
286
|
+
global _intel
|
|
287
|
+
|
|
288
|
+
progress.add_step("brain", "Loading repository intelligence")
|
|
289
|
+
progress.start("brain")
|
|
290
|
+
enriched = _intel.load_all(request)
|
|
291
|
+
progress.done("brain")
|
|
292
|
+
|
|
293
|
+
progress.add_step("decompose", "Decomposing request into tasks")
|
|
294
|
+
progress.start("decompose")
|
|
295
|
+
|
|
296
|
+
task_list = decompose_into_tasks(client, model, request, enriched)
|
|
297
|
+
progress.done("decompose")
|
|
298
|
+
|
|
299
|
+
mission = Mission(request, enriched)
|
|
300
|
+
for t in task_list:
|
|
301
|
+
dep_ids = t.get("depends_on", [])
|
|
302
|
+
if isinstance(dep_ids, str):
|
|
303
|
+
dep_ids = [dep_ids]
|
|
304
|
+
mission.add_task(Task(
|
|
305
|
+
id=t["id"],
|
|
306
|
+
label=t.get("label", t["id"]),
|
|
307
|
+
description=t.get("description", ""),
|
|
308
|
+
agent=t.get("agent", "builder"),
|
|
309
|
+
model=model,
|
|
310
|
+
depends_on=dep_ids,
|
|
311
|
+
))
|
|
312
|
+
progress.add_step(t["id"], t.get("label", t["id"]))
|
|
313
|
+
|
|
314
|
+
mission = execute_mission(client, model, mission, progress)
|
|
315
|
+
mission.save()
|
|
316
|
+
progress.summary(mission.summary())
|
|
317
|
+
|
|
318
|
+
# Reflection phase
|
|
319
|
+
progress.add_step("reflect", "Reflecting on mission — extracting lessons")
|
|
320
|
+
progress.start("reflect")
|
|
321
|
+
_intel.after_mission(client, model, request, list(mission.tasks.values()), mission.edited_files)
|
|
322
|
+
progress.done("reflect")
|
|
323
|
+
|
|
324
|
+
return mission
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Keep backward-compatible wrappers
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def run_agent(client, model: str, prompt: str, messages: list | None = None, max_iter: int = 30, tui_state=None) -> list:
|
|
332
|
+
"""Simple chat loop (backward compatible). Optionally stream to a TUI state."""
|
|
333
|
+
agent_spec = get_agent("builder")
|
|
334
|
+
system = agent_spec.system_prompt if agent_spec else SYSTEM_PROMPT
|
|
335
|
+
tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
|
|
336
|
+
|
|
337
|
+
if messages is None:
|
|
338
|
+
messages = [{"role": "system", "content": system}]
|
|
339
|
+
messages.append({"role": "user", "content": prompt})
|
|
340
|
+
|
|
341
|
+
for i in range(max_iter):
|
|
342
|
+
resp = client.chat.completions.create(
|
|
343
|
+
model=model, messages=messages, tools=tools or None,
|
|
344
|
+
stream=False, temperature=0.2,
|
|
345
|
+
)
|
|
346
|
+
msg = resp.choices[0].message
|
|
347
|
+
|
|
348
|
+
if msg.tool_calls:
|
|
349
|
+
messages.append({
|
|
350
|
+
"role": "assistant", "content": msg.content or "",
|
|
351
|
+
"tool_calls": [
|
|
352
|
+
{"id": tc.id, "type": "function",
|
|
353
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments}}
|
|
354
|
+
for tc in msg.tool_calls
|
|
355
|
+
],
|
|
356
|
+
})
|
|
357
|
+
for tc in msg.tool_calls:
|
|
358
|
+
args = json.loads(tc.function.arguments or "{}")
|
|
359
|
+
detail = f"> {tc.function.name}({json.dumps(args)[:200]})"
|
|
360
|
+
if tui_state:
|
|
361
|
+
tui_state.add_activity(detail)
|
|
362
|
+
else:
|
|
363
|
+
console.print(f"[dim]{detail}[/]")
|
|
364
|
+
result = execute_tool(tc.function.name, args)
|
|
365
|
+
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result[:12000]})
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
stream = client.chat.completions.create(
|
|
369
|
+
model=model,
|
|
370
|
+
messages=messages + [{"role": "assistant", "content": msg.content}],
|
|
371
|
+
stream=True, temperature=0.2,
|
|
372
|
+
)
|
|
373
|
+
if tui_state:
|
|
374
|
+
tui_state.status_message = "Generating..."
|
|
375
|
+
parts = []
|
|
376
|
+
for chunk in stream:
|
|
377
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
378
|
+
parts.append(chunk.choices[0].delta.content)
|
|
379
|
+
tui_state.update_stream("".join(parts))
|
|
380
|
+
tui_state.status_message = "Ready"
|
|
381
|
+
else:
|
|
382
|
+
console.print("[bold cyan]agent>[/] ", end="")
|
|
383
|
+
parts = []
|
|
384
|
+
for chunk in stream:
|
|
385
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
386
|
+
console.print(chunk.choices[0].delta.content, end="", highlight=False)
|
|
387
|
+
parts.append(chunk.choices[0].delta.content)
|
|
388
|
+
console.print()
|
|
389
|
+
messages.append({"role": "assistant", "content": "".join(parts)})
|
|
390
|
+
return messages
|
|
391
|
+
|
|
392
|
+
msg = "[yellow]Reached max iterations.[/]"
|
|
393
|
+
if tui_state:
|
|
394
|
+
tui_state.add_activity(msg)
|
|
395
|
+
else:
|
|
396
|
+
console.print(msg)
|
|
397
|
+
return messages
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def plan(client, model: str, request: str, context: str) -> list:
|
|
401
|
+
"""Generate a plan (backward compatible)."""
|
|
402
|
+
planner_spec = get_agent("planner")
|
|
403
|
+
system = planner_spec.system_prompt if planner_spec else SYSTEM_PROMPT
|
|
404
|
+
messages = [{"role": "system", "content": system}]
|
|
405
|
+
messages.append({"role": "user", "content": f"## Repository context\n{context[:40000]}\n\n## Request\n{request}\n\nProduce a plan."})
|
|
406
|
+
resp = client.chat.completions.create(
|
|
407
|
+
model=model, messages=messages, stream=False, temperature=0.2,
|
|
408
|
+
)
|
|
409
|
+
plan_text = resp.choices[0].message.content or ""
|
|
410
|
+
messages.append({"role": "assistant", "content": plan_text})
|
|
411
|
+
console.print(Panel(Markdown(plan_text), title="Plan", border_style="green"))
|
|
412
|
+
return messages
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def build(client, model: str, plan_messages: list, user_request: str, max_iter: int = 50) -> list:
|
|
416
|
+
"""Simple build loop (backward compatible)."""
|
|
417
|
+
agent_spec = get_agent("builder")
|
|
418
|
+
system = agent_spec.system_prompt if agent_spec else SYSTEM_PROMPT
|
|
419
|
+
tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
|
|
420
|
+
plan_text = plan_messages[-1]["content"] if plan_messages[-1]["role"] == "assistant" else ""
|
|
421
|
+
messages = [{"role": "system", "content": f"{system}\n\n## Plan\n{plan_text}\n\nExecute this plan."}]
|
|
422
|
+
messages.append({"role": "user", "content": user_request})
|
|
423
|
+
|
|
424
|
+
for i in range(max_iter):
|
|
425
|
+
console.print(f"\n[bold blue]--- Step {i + 1}/{max_iter} ---[/]")
|
|
426
|
+
resp = client.chat.completions.create(
|
|
427
|
+
model=model, messages=messages, tools=tools, stream=False, temperature=0.2,
|
|
428
|
+
)
|
|
429
|
+
msg = resp.choices[0].message
|
|
430
|
+
|
|
431
|
+
if not msg.tool_calls:
|
|
432
|
+
stream = client.chat.completions.create(
|
|
433
|
+
model=model,
|
|
434
|
+
messages=messages + [{"role": "assistant", "content": msg.content}],
|
|
435
|
+
stream=True, temperature=0.2,
|
|
436
|
+
)
|
|
437
|
+
console.print("[bold cyan]builder>[/] ", end="")
|
|
438
|
+
parts = []
|
|
439
|
+
for chunk in stream:
|
|
440
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
441
|
+
console.print(chunk.choices[0].delta.content, end="", highlight=False)
|
|
442
|
+
parts.append(chunk.choices[0].delta.content)
|
|
443
|
+
console.print()
|
|
444
|
+
messages.append({"role": "assistant", "content": "".join(parts)})
|
|
445
|
+
return messages
|
|
446
|
+
|
|
447
|
+
messages.append({
|
|
448
|
+
"role": "assistant", "content": msg.content or "",
|
|
449
|
+
"tool_calls": [
|
|
450
|
+
{"id": tc.id, "type": "function",
|
|
451
|
+
"function": {"name": tc.function.name, "arguments": tc.function.arguments}}
|
|
452
|
+
for tc in msg.tool_calls
|
|
453
|
+
],
|
|
454
|
+
})
|
|
455
|
+
for tc in msg.tool_calls:
|
|
456
|
+
args = json.loads(tc.function.arguments or "{}")
|
|
457
|
+
console.print(f"[dim]> {tc.function.name}({json.dumps(args)[:200]})[/]")
|
|
458
|
+
result = execute_tool(tc.function.name, args)
|
|
459
|
+
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result[:12000]})
|
|
460
|
+
|
|
461
|
+
console.print("[yellow]Builder reached max iterations.[/]")
|
|
462
|
+
return messages
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AgentSpec:
|
|
8
|
+
name: str
|
|
9
|
+
description: str
|
|
10
|
+
system_prompt: str
|
|
11
|
+
allowed_tools: list[str] = field(default_factory=list)
|
|
12
|
+
default_model: str | None = None # None = use mission default
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
AGENTS: dict[str, AgentSpec] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register(spec: AgentSpec) -> AgentSpec:
|
|
19
|
+
AGENTS[spec.name.lower()] = spec
|
|
20
|
+
return spec
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get(name: str) -> AgentSpec | None:
|
|
24
|
+
return AGENTS.get(name.lower())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_tools(allowed: list[str]) -> list[dict]:
|
|
28
|
+
"""Return the tool schemas for an agent's allowed tools."""
|
|
29
|
+
from .tools import TOOL_SCHEMAS, TOOL_INDEX
|
|
30
|
+
|
|
31
|
+
return [TOOL_SCHEMAS[TOOL_INDEX[n]] for n in allowed if n in TOOL_INDEX]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Agent definitions
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
register(AgentSpec(
|
|
39
|
+
name="Planner",
|
|
40
|
+
description="Analyzes requests and produces structured plans. Read-only.",
|
|
41
|
+
allowed_tools=["read_file", "list_files", "grep_files", "web_fetch"],
|
|
42
|
+
system_prompt="""You are a software planning agent. Given a request and repository context,
|
|
43
|
+
produce a concrete, ordered implementation plan.
|
|
44
|
+
|
|
45
|
+
Rules:
|
|
46
|
+
- Use tools to explore the repo before planning.
|
|
47
|
+
- Never write code — your output is a plan only.
|
|
48
|
+
- Each step should specify: what file(s) to change, the approach, and verification.
|
|
49
|
+
- End with a "Risks" section.
|
|
50
|
+
- You cannot edit files or run shell commands.""",
|
|
51
|
+
))
|
|
52
|
+
|
|
53
|
+
register(AgentSpec(
|
|
54
|
+
name="Builder",
|
|
55
|
+
description="Writes and edits code to implement plans. Full tool access.",
|
|
56
|
+
allowed_tools=["read_file", "write_file", "edit_file", "list_files", "grep_files", "run_shell", "web_fetch"],
|
|
57
|
+
system_prompt="""You are a coding agent. You implement plans by writing and editing code.
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
- Read files before editing them. Never assume content.
|
|
61
|
+
- Make focused, minimal edits.
|
|
62
|
+
- After each file change, run the appropriate build/lint/test command.
|
|
63
|
+
- If a step fails, diagnose and fix it before moving on.
|
|
64
|
+
- Do not expose secrets. Do not run destructive commands (rm -rf, git push --force).
|
|
65
|
+
- When done, summarize what was built.""",
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
register(AgentSpec(
|
|
69
|
+
name="Reviewer",
|
|
70
|
+
description="Reviews code for bugs, security issues, and style problems. Read + git only.",
|
|
71
|
+
allowed_tools=["read_file", "list_files", "grep_files", "run_shell"],
|
|
72
|
+
system_prompt="""You are a code reviewer. You inspect changes and report issues.
|
|
73
|
+
|
|
74
|
+
Rules:
|
|
75
|
+
- Read the changed files and review for: correctness, security, performance, style.
|
|
76
|
+
- Run lint/type-check commands to find issues.
|
|
77
|
+
- Report findings with file:line references.
|
|
78
|
+
- Do not edit files.
|
|
79
|
+
- If no issues, say "APPROVED".
|
|
80
|
+
- If issues found, list them clearly so a Builder can fix them.""",
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
register(AgentSpec(
|
|
84
|
+
name="Tester",
|
|
85
|
+
description="Writes and runs tests. Has test framework + shell access.",
|
|
86
|
+
allowed_tools=["read_file", "write_file", "edit_file", "list_files", "grep_files", "run_shell"],
|
|
87
|
+
system_prompt="""You are a testing agent. You write and run tests for the code in this repository.
|
|
88
|
+
|
|
89
|
+
Rules:
|
|
90
|
+
- Discover the test framework from existing tests or pyproject.toml.
|
|
91
|
+
- Write tests that cover the implemented functionality.
|
|
92
|
+
- Run tests after writing. Fix any failures.
|
|
93
|
+
- If tests pass, say "ALL TESTS PASSING".
|
|
94
|
+
- If tests fail, fix the code or tests until they pass.""",
|
|
95
|
+
))
|
|
96
|
+
|
|
97
|
+
register(AgentSpec(
|
|
98
|
+
name="Researcher",
|
|
99
|
+
description="Searches the web for documentation, APIs, and solutions.",
|
|
100
|
+
allowed_tools=["web_fetch", "list_files", "read_file"],
|
|
101
|
+
system_prompt="""You are a research agent. You find documentation and solutions from the web.
|
|
102
|
+
|
|
103
|
+
Rules:
|
|
104
|
+
- Use web_fetch to search for and retrieve documentation.
|
|
105
|
+
- Summarize findings concisely.
|
|
106
|
+
- Provide code examples when relevant.
|
|
107
|
+
- Always cite the source URL.
|
|
108
|
+
- Do not edit any files.""",
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
register(AgentSpec(
|
|
112
|
+
name="Learner",
|
|
113
|
+
description="Reflects on completed missions, extracts lessons, rates code quality, and updates organizational knowledge.",
|
|
114
|
+
allowed_tools=["read_file", "list_files", "grep_files", "run_shell"],
|
|
115
|
+
system_prompt="""You are a learning and reflection agent. After every mission, you analyze what happened.
|
|
116
|
+
|
|
117
|
+
Rules:
|
|
118
|
+
- Read the important files that were changed.
|
|
119
|
+
- Reflect on: what worked, what failed, why, and whether the solution can be reused.
|
|
120
|
+
- Rate code quality and identify security issues.
|
|
121
|
+
- Output a structured JSON reflection (never edit files).
|
|
122
|
+
- Focus on extracting *reusable* lessons, not just summaries.""",
|
|
123
|
+
))
|