stravinsky 0.1.12__py3-none-any.whl
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.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +5 -0
- mcp_bridge/auth/__init__.py +32 -0
- mcp_bridge/auth/cli.py +208 -0
- mcp_bridge/auth/oauth.py +418 -0
- mcp_bridge/auth/openai_oauth.py +350 -0
- mcp_bridge/auth/token_store.py +195 -0
- mcp_bridge/config/__init__.py +14 -0
- mcp_bridge/config/hooks.py +174 -0
- mcp_bridge/prompts/__init__.py +18 -0
- mcp_bridge/prompts/delphi.py +110 -0
- mcp_bridge/prompts/dewey.py +183 -0
- mcp_bridge/prompts/document_writer.py +155 -0
- mcp_bridge/prompts/explore.py +118 -0
- mcp_bridge/prompts/frontend.py +112 -0
- mcp_bridge/prompts/multimodal.py +58 -0
- mcp_bridge/prompts/stravinsky.py +329 -0
- mcp_bridge/server.py +866 -0
- mcp_bridge/tools/__init__.py +31 -0
- mcp_bridge/tools/agent_manager.py +665 -0
- mcp_bridge/tools/background_tasks.py +166 -0
- mcp_bridge/tools/code_search.py +301 -0
- mcp_bridge/tools/continuous_loop.py +67 -0
- mcp_bridge/tools/lsp/__init__.py +29 -0
- mcp_bridge/tools/lsp/tools.py +526 -0
- mcp_bridge/tools/model_invoke.py +233 -0
- mcp_bridge/tools/project_context.py +141 -0
- mcp_bridge/tools/session_manager.py +302 -0
- mcp_bridge/tools/skill_loader.py +212 -0
- mcp_bridge/tools/task_runner.py +97 -0
- mcp_bridge/utils/__init__.py +1 -0
- stravinsky-0.1.12.dist-info/METADATA +198 -0
- stravinsky-0.1.12.dist-info/RECORD +34 -0
- stravinsky-0.1.12.dist-info/WHEEL +4 -0
- stravinsky-0.1.12.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Tools module
|
|
2
|
+
from .model_invoke import invoke_gemini, invoke_openai
|
|
3
|
+
from .code_search import lsp_diagnostics, ast_grep_search, grep_search, glob_files
|
|
4
|
+
from .session_manager import list_sessions, read_session, search_sessions, get_session_info
|
|
5
|
+
from .skill_loader import list_skills, get_skill, create_skill
|
|
6
|
+
from .agent_manager import agent_spawn, agent_output, agent_cancel, agent_list, agent_progress
|
|
7
|
+
from .continuous_loop import enable_ralph_loop, disable_ralph_loop
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"invoke_gemini",
|
|
11
|
+
"invoke_openai",
|
|
12
|
+
"lsp_diagnostics",
|
|
13
|
+
"ast_grep_search",
|
|
14
|
+
"grep_search",
|
|
15
|
+
"glob_files",
|
|
16
|
+
"list_sessions",
|
|
17
|
+
"read_session",
|
|
18
|
+
"search_sessions",
|
|
19
|
+
"get_session_info",
|
|
20
|
+
"list_skills",
|
|
21
|
+
"get_skill",
|
|
22
|
+
"create_skill",
|
|
23
|
+
"agent_spawn",
|
|
24
|
+
"agent_output",
|
|
25
|
+
"agent_cancel",
|
|
26
|
+
"agent_list",
|
|
27
|
+
"agent_progress",
|
|
28
|
+
"enable_ralph_loop",
|
|
29
|
+
"disable_ralph_loop",
|
|
30
|
+
]
|
|
31
|
+
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Manager for Stravinsky.
|
|
3
|
+
|
|
4
|
+
Spawns background agents using Claude Code CLI with full tool access.
|
|
5
|
+
This replaces the simple model-only invocation with true agentic execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import signal
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
import threading
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AgentTask:
|
|
26
|
+
"""Represents a background agent task with full tool access."""
|
|
27
|
+
id: str
|
|
28
|
+
prompt: str
|
|
29
|
+
agent_type: str # explore, librarian, frontend, etc.
|
|
30
|
+
description: str
|
|
31
|
+
status: str # pending, running, completed, failed, cancelled
|
|
32
|
+
created_at: str
|
|
33
|
+
parent_session_id: Optional[str] = None
|
|
34
|
+
started_at: Optional[str] = None
|
|
35
|
+
completed_at: Optional[str] = None
|
|
36
|
+
result: Optional[str] = None
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
pid: Optional[int] = None
|
|
39
|
+
progress: Optional[Dict[str, Any]] = None # tool calls, last update
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class AgentProgress:
|
|
44
|
+
"""Progress tracking for a running agent."""
|
|
45
|
+
tool_calls: int = 0
|
|
46
|
+
last_tool: Optional[str] = None
|
|
47
|
+
last_message: Optional[str] = None
|
|
48
|
+
last_update: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentManager:
|
|
52
|
+
"""
|
|
53
|
+
Manages background agent execution using Claude Code CLI.
|
|
54
|
+
|
|
55
|
+
Key features:
|
|
56
|
+
- Spawns agents with full tool access via `claude -p`
|
|
57
|
+
- Tracks task status and progress
|
|
58
|
+
- Persists state to .stravinsky/agents.json
|
|
59
|
+
- Provides notification mechanism for task completion
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
CLAUDE_CLI = "/opt/homebrew/bin/claude"
|
|
63
|
+
|
|
64
|
+
def __init__(self, base_dir: Optional[str] = None):
|
|
65
|
+
if base_dir:
|
|
66
|
+
self.base_dir = Path(base_dir)
|
|
67
|
+
else:
|
|
68
|
+
self.base_dir = Path.cwd() / ".stravinsky"
|
|
69
|
+
|
|
70
|
+
self.agents_dir = self.base_dir / "agents"
|
|
71
|
+
self.state_file = self.base_dir / "agents.json"
|
|
72
|
+
|
|
73
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
self.agents_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
if not self.state_file.exists():
|
|
77
|
+
self._save_tasks({})
|
|
78
|
+
|
|
79
|
+
# In-memory tracking for running processes
|
|
80
|
+
self._processes: Dict[str, subprocess.Popen] = {}
|
|
81
|
+
self._notification_queue: Dict[str, List[AgentTask]] = {}
|
|
82
|
+
|
|
83
|
+
def _load_tasks(self) -> Dict[str, Any]:
|
|
84
|
+
"""Load tasks from persistent storage."""
|
|
85
|
+
try:
|
|
86
|
+
with open(self.state_file, "r") as f:
|
|
87
|
+
return json.load(f)
|
|
88
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
def _save_tasks(self, tasks: Dict[str, Any]):
|
|
92
|
+
"""Save tasks to persistent storage."""
|
|
93
|
+
with open(self.state_file, "w") as f:
|
|
94
|
+
json.dump(tasks, f, indent=2)
|
|
95
|
+
|
|
96
|
+
def _update_task(self, task_id: str, **kwargs):
|
|
97
|
+
"""Update a task's fields."""
|
|
98
|
+
tasks = self._load_tasks()
|
|
99
|
+
if task_id in tasks:
|
|
100
|
+
tasks[task_id].update(kwargs)
|
|
101
|
+
self._save_tasks(tasks)
|
|
102
|
+
|
|
103
|
+
def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:
|
|
104
|
+
"""Get a task by ID."""
|
|
105
|
+
tasks = self._load_tasks()
|
|
106
|
+
return tasks.get(task_id)
|
|
107
|
+
|
|
108
|
+
def list_tasks(self, parent_session_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
109
|
+
"""List all tasks, optionally filtered by parent session."""
|
|
110
|
+
tasks = self._load_tasks()
|
|
111
|
+
task_list = list(tasks.values())
|
|
112
|
+
|
|
113
|
+
if parent_session_id:
|
|
114
|
+
task_list = [t for t in task_list if t.get("parent_session_id") == parent_session_id]
|
|
115
|
+
|
|
116
|
+
return task_list
|
|
117
|
+
|
|
118
|
+
def spawn(
|
|
119
|
+
self,
|
|
120
|
+
prompt: str,
|
|
121
|
+
agent_type: str = "explore",
|
|
122
|
+
description: str = "",
|
|
123
|
+
parent_session_id: Optional[str] = None,
|
|
124
|
+
system_prompt: Optional[str] = None,
|
|
125
|
+
model: str = "gemini-3-flash",
|
|
126
|
+
thinking_budget: int = 0,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Spawn a new background agent.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
prompt: The task prompt for the agent
|
|
133
|
+
agent_type: Type of agent (explore, dewey, frontend, delphi)
|
|
134
|
+
description: Short description for status display
|
|
135
|
+
parent_session_id: Optional parent session for notifications
|
|
136
|
+
system_prompt: Optional custom system prompt
|
|
137
|
+
model: Model to use (gemini-3-flash, claude, etc.)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Task ID for tracking
|
|
141
|
+
"""
|
|
142
|
+
task_id = f"agent_{uuid.uuid4().hex[:8]}"
|
|
143
|
+
|
|
144
|
+
task = AgentTask(
|
|
145
|
+
id=task_id,
|
|
146
|
+
prompt=prompt,
|
|
147
|
+
agent_type=agent_type,
|
|
148
|
+
description=description or prompt[:50],
|
|
149
|
+
status="pending",
|
|
150
|
+
created_at=datetime.now().isoformat(),
|
|
151
|
+
parent_session_id=parent_session_id,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Persist task
|
|
155
|
+
tasks = self._load_tasks()
|
|
156
|
+
tasks[task_id] = asdict(task)
|
|
157
|
+
self._save_tasks(tasks)
|
|
158
|
+
|
|
159
|
+
# Start background execution
|
|
160
|
+
self._execute_agent(task_id, prompt, agent_type, system_prompt, model, thinking_budget)
|
|
161
|
+
|
|
162
|
+
return task_id
|
|
163
|
+
|
|
164
|
+
def _execute_agent(
|
|
165
|
+
self,
|
|
166
|
+
task_id: str,
|
|
167
|
+
prompt: str,
|
|
168
|
+
agent_type: str,
|
|
169
|
+
system_prompt: Optional[str] = None,
|
|
170
|
+
model: str = "gemini-3-flash",
|
|
171
|
+
thinking_budget: int = 0,
|
|
172
|
+
):
|
|
173
|
+
"""Execute agent in background thread."""
|
|
174
|
+
|
|
175
|
+
def run_agent():
|
|
176
|
+
log_file = self.agents_dir / f"{task_id}.log"
|
|
177
|
+
output_file = self.agents_dir / f"{task_id}.out"
|
|
178
|
+
|
|
179
|
+
self._update_task(
|
|
180
|
+
task_id,
|
|
181
|
+
status="running",
|
|
182
|
+
started_at=datetime.now().isoformat()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Route based on model type
|
|
187
|
+
if model.startswith("gemini"):
|
|
188
|
+
# Use Gemini via invoke_gemini
|
|
189
|
+
logger.info(f"[AgentManager] Spawning Gemini agent {task_id} with model {model}")
|
|
190
|
+
|
|
191
|
+
# Build full prompt with system context
|
|
192
|
+
full_prompt = prompt
|
|
193
|
+
if system_prompt:
|
|
194
|
+
full_prompt = f"{system_prompt}\n\n---\n\n{full_prompt}"
|
|
195
|
+
|
|
196
|
+
# Import and call invoke_gemini
|
|
197
|
+
import asyncio
|
|
198
|
+
from .model_invoke import invoke_gemini
|
|
199
|
+
|
|
200
|
+
# Run async in thread
|
|
201
|
+
loop = asyncio.new_event_loop()
|
|
202
|
+
asyncio.set_event_loop(loop)
|
|
203
|
+
try:
|
|
204
|
+
result = loop.run_until_complete(
|
|
205
|
+
invoke_gemini(
|
|
206
|
+
prompt=full_prompt,
|
|
207
|
+
model=model,
|
|
208
|
+
thinking_budget=thinking_budget
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
finally:
|
|
212
|
+
loop.close()
|
|
213
|
+
|
|
214
|
+
# Write output
|
|
215
|
+
with open(output_file, "w") as f:
|
|
216
|
+
f.write(result)
|
|
217
|
+
|
|
218
|
+
self._update_task(
|
|
219
|
+
task_id,
|
|
220
|
+
status="completed",
|
|
221
|
+
result=result,
|
|
222
|
+
completed_at=datetime.now().isoformat()
|
|
223
|
+
)
|
|
224
|
+
logger.info(f"[AgentManager] Gemini agent {task_id} completed successfully")
|
|
225
|
+
|
|
226
|
+
else:
|
|
227
|
+
# Use Claude CLI for Claude models
|
|
228
|
+
cmd = [
|
|
229
|
+
self.CLAUDE_CLI,
|
|
230
|
+
"-p", # Non-interactive print mode
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
# Add system prompt if provided
|
|
234
|
+
if system_prompt:
|
|
235
|
+
cmd.extend(["--system-prompt", system_prompt])
|
|
236
|
+
|
|
237
|
+
# Add the prompt
|
|
238
|
+
cmd.append(prompt)
|
|
239
|
+
|
|
240
|
+
logger.info(f"[AgentManager] Spawning Claude agent {task_id}: {' '.join(cmd[:5])}...")
|
|
241
|
+
|
|
242
|
+
# Run Claude CLI
|
|
243
|
+
process = subprocess.Popen(
|
|
244
|
+
cmd,
|
|
245
|
+
stdout=open(output_file, "w"),
|
|
246
|
+
stderr=open(log_file, "w"),
|
|
247
|
+
cwd=str(Path.cwd()),
|
|
248
|
+
start_new_session=True,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self._processes[task_id] = process
|
|
252
|
+
self._update_task(task_id, pid=process.pid)
|
|
253
|
+
|
|
254
|
+
# Wait for completion
|
|
255
|
+
return_code = process.wait()
|
|
256
|
+
|
|
257
|
+
# Read output
|
|
258
|
+
result = ""
|
|
259
|
+
if output_file.exists():
|
|
260
|
+
result = output_file.read_text()
|
|
261
|
+
|
|
262
|
+
if return_code == 0:
|
|
263
|
+
self._update_task(
|
|
264
|
+
task_id,
|
|
265
|
+
status="completed",
|
|
266
|
+
result=result,
|
|
267
|
+
completed_at=datetime.now().isoformat()
|
|
268
|
+
)
|
|
269
|
+
logger.info(f"[AgentManager] Agent {task_id} completed successfully")
|
|
270
|
+
else:
|
|
271
|
+
error_log = ""
|
|
272
|
+
if log_file.exists():
|
|
273
|
+
error_log = log_file.read_text()
|
|
274
|
+
self._update_task(
|
|
275
|
+
task_id,
|
|
276
|
+
status="failed",
|
|
277
|
+
error=f"Exit code {return_code}: {error_log}",
|
|
278
|
+
completed_at=datetime.now().isoformat()
|
|
279
|
+
)
|
|
280
|
+
logger.error(f"[AgentManager] Agent {task_id} failed: {error_log[:200]}")
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self._update_task(
|
|
284
|
+
task_id,
|
|
285
|
+
status="failed",
|
|
286
|
+
error=str(e),
|
|
287
|
+
completed_at=datetime.now().isoformat()
|
|
288
|
+
)
|
|
289
|
+
logger.exception(f"[AgentManager] Agent {task_id} exception")
|
|
290
|
+
finally:
|
|
291
|
+
self._processes.pop(task_id, None)
|
|
292
|
+
self._notify_completion(task_id)
|
|
293
|
+
|
|
294
|
+
# Run in background thread
|
|
295
|
+
thread = threading.Thread(target=run_agent, daemon=True)
|
|
296
|
+
thread.start()
|
|
297
|
+
|
|
298
|
+
def _notify_completion(self, task_id: str):
|
|
299
|
+
"""Queue notification for parent session."""
|
|
300
|
+
task = self.get_task(task_id)
|
|
301
|
+
if not task:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
parent_id = task.get("parent_session_id")
|
|
305
|
+
if parent_id:
|
|
306
|
+
if parent_id not in self._notification_queue:
|
|
307
|
+
self._notification_queue[parent_id] = []
|
|
308
|
+
|
|
309
|
+
self._notification_queue[parent_id].append(task)
|
|
310
|
+
logger.info(f"[AgentManager] Queued notification for {parent_id}: task {task_id}")
|
|
311
|
+
|
|
312
|
+
def get_pending_notifications(self, session_id: str) -> List[Dict[str, Any]]:
|
|
313
|
+
"""Get and clear pending notifications for a session."""
|
|
314
|
+
notifications = self._notification_queue.pop(session_id, [])
|
|
315
|
+
return notifications
|
|
316
|
+
|
|
317
|
+
def cancel(self, task_id: str) -> bool:
|
|
318
|
+
"""Cancel a running agent task."""
|
|
319
|
+
task = self.get_task(task_id)
|
|
320
|
+
if not task:
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
if task["status"] != "running":
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
process = self._processes.get(task_id)
|
|
327
|
+
if process:
|
|
328
|
+
try:
|
|
329
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
330
|
+
process.wait(timeout=5)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.warning(f"[AgentManager] Failed to kill process for {task_id}: {e}")
|
|
333
|
+
try:
|
|
334
|
+
process.kill()
|
|
335
|
+
except:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
self._update_task(
|
|
339
|
+
task_id,
|
|
340
|
+
status="cancelled",
|
|
341
|
+
completed_at=datetime.now().isoformat()
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def get_output(self, task_id: str, block: bool = False, timeout: float = 30.0) -> str:
|
|
347
|
+
"""
|
|
348
|
+
Get output from an agent task.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
task_id: The task ID
|
|
352
|
+
block: If True, wait for completion
|
|
353
|
+
timeout: Max seconds to wait if blocking
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Formatted task output/status
|
|
357
|
+
"""
|
|
358
|
+
task = self.get_task(task_id)
|
|
359
|
+
if not task:
|
|
360
|
+
return f"Task {task_id} not found."
|
|
361
|
+
|
|
362
|
+
if block and task["status"] == "running":
|
|
363
|
+
# Poll for completion
|
|
364
|
+
start = datetime.now()
|
|
365
|
+
while (datetime.now() - start).total_seconds() < timeout:
|
|
366
|
+
task = self.get_task(task_id)
|
|
367
|
+
if task["status"] != "running":
|
|
368
|
+
break
|
|
369
|
+
asyncio.sleep(0.5)
|
|
370
|
+
|
|
371
|
+
status = task["status"]
|
|
372
|
+
description = task.get("description", "")
|
|
373
|
+
agent_type = task.get("agent_type", "unknown")
|
|
374
|
+
|
|
375
|
+
if status == "completed":
|
|
376
|
+
result = task.get("result", "(no output)")
|
|
377
|
+
return f"""✅ Agent Task Completed
|
|
378
|
+
|
|
379
|
+
**Task ID**: {task_id}
|
|
380
|
+
**Agent**: {agent_type}
|
|
381
|
+
**Description**: {description}
|
|
382
|
+
|
|
383
|
+
**Result**:
|
|
384
|
+
{result}"""
|
|
385
|
+
|
|
386
|
+
elif status == "failed":
|
|
387
|
+
error = task.get("error", "(no error details)")
|
|
388
|
+
return f"""❌ Agent Task Failed
|
|
389
|
+
|
|
390
|
+
**Task ID**: {task_id}
|
|
391
|
+
**Agent**: {agent_type}
|
|
392
|
+
**Description**: {description}
|
|
393
|
+
|
|
394
|
+
**Error**:
|
|
395
|
+
{error}"""
|
|
396
|
+
|
|
397
|
+
elif status == "cancelled":
|
|
398
|
+
return f"""⚠️ Agent Task Cancelled
|
|
399
|
+
|
|
400
|
+
**Task ID**: {task_id}
|
|
401
|
+
**Agent**: {agent_type}
|
|
402
|
+
**Description**: {description}"""
|
|
403
|
+
|
|
404
|
+
else: # pending or running
|
|
405
|
+
pid = task.get("pid", "N/A")
|
|
406
|
+
started = task.get("started_at", "N/A")
|
|
407
|
+
return f"""⏳ Agent Task Running
|
|
408
|
+
|
|
409
|
+
**Task ID**: {task_id}
|
|
410
|
+
**Agent**: {agent_type}
|
|
411
|
+
**Description**: {description}
|
|
412
|
+
**PID**: {pid}
|
|
413
|
+
**Started**: {started}
|
|
414
|
+
|
|
415
|
+
Use `agent_output` with block=true to wait for completion."""
|
|
416
|
+
|
|
417
|
+
def get_progress(self, task_id: str, lines: int = 20) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Get real-time progress from a running agent's output.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
task_id: The task ID
|
|
423
|
+
lines: Number of lines to show from the end
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Recent output lines and status
|
|
427
|
+
"""
|
|
428
|
+
task = self.get_task(task_id)
|
|
429
|
+
if not task:
|
|
430
|
+
return f"Task {task_id} not found."
|
|
431
|
+
|
|
432
|
+
output_file = self.agents_dir / f"{task_id}.out"
|
|
433
|
+
log_file = self.agents_dir / f"{task_id}.log"
|
|
434
|
+
|
|
435
|
+
status = task["status"]
|
|
436
|
+
description = task.get("description", "")
|
|
437
|
+
agent_type = task.get("agent_type", "unknown")
|
|
438
|
+
|
|
439
|
+
# Read recent output
|
|
440
|
+
output_content = ""
|
|
441
|
+
if output_file.exists():
|
|
442
|
+
try:
|
|
443
|
+
full_content = output_file.read_text()
|
|
444
|
+
if full_content:
|
|
445
|
+
output_lines = full_content.strip().split("\n")
|
|
446
|
+
recent = output_lines[-lines:] if len(output_lines) > lines else output_lines
|
|
447
|
+
output_content = "\n".join(recent)
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
|
|
451
|
+
# Check log for errors
|
|
452
|
+
log_content = ""
|
|
453
|
+
if log_file.exists():
|
|
454
|
+
try:
|
|
455
|
+
log_content = log_file.read_text().strip()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
# Status emoji
|
|
460
|
+
status_emoji = {
|
|
461
|
+
"pending": "⏳",
|
|
462
|
+
"running": "🔄",
|
|
463
|
+
"completed": "✅",
|
|
464
|
+
"failed": "❌",
|
|
465
|
+
"cancelled": "⚠️",
|
|
466
|
+
}.get(status, "❓")
|
|
467
|
+
|
|
468
|
+
result = f"""{status_emoji} **Agent Progress**
|
|
469
|
+
|
|
470
|
+
**Task ID**: {task_id}
|
|
471
|
+
**Agent**: {agent_type}
|
|
472
|
+
**Description**: {description}
|
|
473
|
+
**Status**: {status}
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
if output_content:
|
|
477
|
+
result += f"\n**Recent Output** (last {lines} lines):\n```\n{output_content}\n```"
|
|
478
|
+
elif status == "running":
|
|
479
|
+
result += "\n*Agent is working... no output yet.*"
|
|
480
|
+
|
|
481
|
+
if log_content and status == "failed":
|
|
482
|
+
# Truncate log if too long
|
|
483
|
+
if len(log_content) > 500:
|
|
484
|
+
log_content = log_content[:500] + "..."
|
|
485
|
+
result += f"\n\n**Error Log**:\n```\n{log_content}\n```"
|
|
486
|
+
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# Global manager instance
|
|
491
|
+
_manager: Optional[AgentManager] = None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def get_manager() -> AgentManager:
|
|
495
|
+
"""Get or create the global AgentManager instance."""
|
|
496
|
+
global _manager
|
|
497
|
+
if _manager is None:
|
|
498
|
+
_manager = AgentManager()
|
|
499
|
+
return _manager
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# Tool interface functions
|
|
503
|
+
|
|
504
|
+
async def agent_spawn(
|
|
505
|
+
prompt: str,
|
|
506
|
+
agent_type: str = "explore",
|
|
507
|
+
description: str = "",
|
|
508
|
+
model: str = "gemini-3-flash",
|
|
509
|
+
thinking_budget: int = 0,
|
|
510
|
+
) -> str:
|
|
511
|
+
"""
|
|
512
|
+
Spawn a background agent.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
prompt: The task for the agent to perform
|
|
516
|
+
agent_type: Type of agent (explore, dewey, frontend, delphi)
|
|
517
|
+
description: Short description shown in status
|
|
518
|
+
model: Model to use (gemini-3-flash, gemini-2.0-flash, claude)
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Task ID and instructions
|
|
522
|
+
"""
|
|
523
|
+
manager = get_manager()
|
|
524
|
+
|
|
525
|
+
# Map agent types to system prompts
|
|
526
|
+
system_prompts = {
|
|
527
|
+
"explore": "You are a codebase exploration specialist. Find files, patterns, and answer 'where is X?' questions efficiently.",
|
|
528
|
+
"dewey": "You are a documentation and research specialist. Find implementation examples, official docs, and provide evidence-based answers.",
|
|
529
|
+
"frontend": """You are a Senior Frontend Architect & Avant-Garde UI Designer with 15+ years experience.
|
|
530
|
+
|
|
531
|
+
OPERATIONAL DIRECTIVES:
|
|
532
|
+
- Follow instructions. Execute immediately. No fluff.
|
|
533
|
+
- Output First: Prioritize code and visual solutions.
|
|
534
|
+
|
|
535
|
+
DESIGN PHILOSOPHY - "INTENTIONAL MINIMALISM":
|
|
536
|
+
- Anti-Generic: Reject standard "bootstrapped" layouts. If it looks like a template, it's wrong.
|
|
537
|
+
- Bespoke layouts, asymmetry, distinctive typography.
|
|
538
|
+
- Before placing any element, calculate its purpose. No purpose = delete it.
|
|
539
|
+
|
|
540
|
+
FRONTEND CODING STANDARDS:
|
|
541
|
+
- Library Discipline: If a UI library (Shadcn, Radix, MUI) is detected, YOU MUST USE IT.
|
|
542
|
+
- Do NOT build custom components if the library provides them.
|
|
543
|
+
- Stack: Modern (React/Vue/Svelte), Tailwind/Custom CSS, semantic HTML5.
|
|
544
|
+
- Focus on micro-interactions, perfect spacing, "invisible" UX.
|
|
545
|
+
|
|
546
|
+
RESPONSE FORMAT:
|
|
547
|
+
1. Rationale: (1 sentence on why elements were placed there)
|
|
548
|
+
2. The Code.
|
|
549
|
+
|
|
550
|
+
ULTRATHINK MODE (when user says "ULTRATHINK" or "think harder"):
|
|
551
|
+
1. Deep Reasoning Chain: Detailed breakdown of architectural and design decisions
|
|
552
|
+
2. Edge Case Analysis: What could go wrong and how we prevented it
|
|
553
|
+
3. The Code: Optimized, bespoke, production-ready, utilizing existing libraries""",
|
|
554
|
+
"delphi": "You are a strategic advisor. Provide architecture guidance, debugging assistance, and code review.",
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
system_prompt = system_prompts.get(agent_type, None)
|
|
558
|
+
|
|
559
|
+
task_id = manager.spawn(
|
|
560
|
+
prompt=prompt,
|
|
561
|
+
agent_type=agent_type,
|
|
562
|
+
description=description or prompt[:50],
|
|
563
|
+
system_prompt=system_prompt,
|
|
564
|
+
model=model,
|
|
565
|
+
thinking_budget=thinking_budget,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return f"""🚀 Background agent spawned successfully.
|
|
569
|
+
|
|
570
|
+
**Task ID**: {task_id}
|
|
571
|
+
**Agent Type**: {agent_type}
|
|
572
|
+
**Model**: {model}
|
|
573
|
+
**Thinking Budget**: {thinking_budget if thinking_budget > 0 else "N/A"}
|
|
574
|
+
**Description**: {description or prompt[:50]}
|
|
575
|
+
|
|
576
|
+
The agent is now running. Use:
|
|
577
|
+
- `agent_progress(task_id="{task_id}")` to monitor real-time progress
|
|
578
|
+
- `agent_output(task_id="{task_id}")` to get final result
|
|
579
|
+
- `agent_cancel(task_id="{task_id}")` to stop the agent"""
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
async def agent_output(task_id: str, block: bool = False) -> str:
|
|
583
|
+
"""
|
|
584
|
+
Get output from a background agent task.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
task_id: The task ID from agent_spawn
|
|
588
|
+
block: If True, wait for the task to complete (up to 30s)
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Task status and output
|
|
592
|
+
"""
|
|
593
|
+
manager = get_manager()
|
|
594
|
+
return manager.get_output(task_id, block=block)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
async def agent_cancel(task_id: str) -> str:
|
|
598
|
+
"""
|
|
599
|
+
Cancel a running background agent.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
task_id: The task ID to cancel
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Cancellation result
|
|
606
|
+
"""
|
|
607
|
+
manager = get_manager()
|
|
608
|
+
success = manager.cancel(task_id)
|
|
609
|
+
|
|
610
|
+
if success:
|
|
611
|
+
return f"✅ Agent task {task_id} has been cancelled."
|
|
612
|
+
else:
|
|
613
|
+
task = manager.get_task(task_id)
|
|
614
|
+
if not task:
|
|
615
|
+
return f"❌ Task {task_id} not found."
|
|
616
|
+
else:
|
|
617
|
+
return f"⚠️ Task {task_id} is not running (status: {task['status']}). Cannot cancel."
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
async def agent_list() -> str:
|
|
621
|
+
"""
|
|
622
|
+
List all background agent tasks.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Formatted list of tasks
|
|
626
|
+
"""
|
|
627
|
+
manager = get_manager()
|
|
628
|
+
tasks = manager.list_tasks()
|
|
629
|
+
|
|
630
|
+
if not tasks:
|
|
631
|
+
return "No background agent tasks found."
|
|
632
|
+
|
|
633
|
+
lines = ["**Background Agent Tasks**", ""]
|
|
634
|
+
|
|
635
|
+
for t in sorted(tasks, key=lambda x: x.get("created_at", ""), reverse=True):
|
|
636
|
+
status_emoji = {
|
|
637
|
+
"pending": "⏳",
|
|
638
|
+
"running": "🔄",
|
|
639
|
+
"completed": "✅",
|
|
640
|
+
"failed": "❌",
|
|
641
|
+
"cancelled": "⚠️",
|
|
642
|
+
}.get(t["status"], "❓")
|
|
643
|
+
|
|
644
|
+
desc = t.get("description", t.get("prompt", "")[:40])
|
|
645
|
+
lines.append(f"- {status_emoji} [{t['id']}] {t['agent_type']}: {desc}")
|
|
646
|
+
|
|
647
|
+
return "\n".join(lines)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
async def agent_progress(task_id: str, lines: int = 20) -> str:
|
|
651
|
+
"""
|
|
652
|
+
Get real-time progress from a running background agent.
|
|
653
|
+
|
|
654
|
+
Shows the most recent output lines from the agent, useful for
|
|
655
|
+
monitoring what the agent is currently doing.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
task_id: The task ID from agent_spawn
|
|
659
|
+
lines: Number of recent output lines to show (default 20)
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Recent agent output and status
|
|
663
|
+
"""
|
|
664
|
+
manager = get_manager()
|
|
665
|
+
return manager.get_progress(task_id, lines=lines)
|