daveloop 1.4.0__py3-none-any.whl → 1.5.1__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.
- daveloop-1.5.1.dist-info/METADATA +403 -0
- daveloop-1.5.1.dist-info/RECORD +7 -0
- daveloop.py +740 -18
- daveloop-1.4.0.dist-info/METADATA +0 -391
- daveloop-1.4.0.dist-info/RECORD +0 -7
- {daveloop-1.4.0.dist-info → daveloop-1.5.1.dist-info}/WHEEL +0 -0
- {daveloop-1.4.0.dist-info → daveloop-1.5.1.dist-info}/entry_points.txt +0 -0
- {daveloop-1.4.0.dist-info → daveloop-1.5.1.dist-info}/top_level.txt +0 -0
daveloop.py
CHANGED
|
@@ -14,6 +14,8 @@ import itertools
|
|
|
14
14
|
import json
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from collections import deque
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
19
|
|
|
18
20
|
# Configuration
|
|
19
21
|
MAX_ITERATIONS = 20
|
|
@@ -29,6 +31,12 @@ SIGNAL_RESOLVED = "[DAVELOOP:RESOLVED]"
|
|
|
29
31
|
SIGNAL_BLOCKED = "[DAVELOOP:BLOCKED]"
|
|
30
32
|
SIGNAL_CLARIFY = "[DAVELOOP:CLARIFY]"
|
|
31
33
|
|
|
34
|
+
# Allowed tools for Claude Code CLI
|
|
35
|
+
# Default: no Task tool (prevents recursive sub-agent spawning)
|
|
36
|
+
ALLOWED_TOOLS_DEFAULT = "Bash,Read,Write,Edit,Glob,Grep"
|
|
37
|
+
# Swarm mode: Task tool enabled for controlled sub-agent spawning
|
|
38
|
+
ALLOWED_TOOLS_SWARM = "Bash,Read,Write,Edit,Glob,Grep,Task"
|
|
39
|
+
|
|
32
40
|
# ============================================================================
|
|
33
41
|
# ANSI Color Codes
|
|
34
42
|
# ============================================================================
|
|
@@ -268,6 +276,508 @@ class TaskQueue:
|
|
|
268
276
|
print()
|
|
269
277
|
|
|
270
278
|
|
|
279
|
+
# ============================================================================
|
|
280
|
+
# Swarm Budget
|
|
281
|
+
# ============================================================================
|
|
282
|
+
class SwarmBudget:
|
|
283
|
+
"""Tracks and enforces sub-agent spawn budget for swarm mode."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, max_spawns: int = 5, max_depth: int = 1):
|
|
286
|
+
self.max_spawns = max_spawns
|
|
287
|
+
self.max_depth = max_depth
|
|
288
|
+
self.spawn_count = 0
|
|
289
|
+
self.active_agents = 0
|
|
290
|
+
self.completed_agents = 0
|
|
291
|
+
|
|
292
|
+
def can_spawn(self) -> bool:
|
|
293
|
+
"""Check if spawning another sub-agent is within budget."""
|
|
294
|
+
return self.spawn_count < self.max_spawns
|
|
295
|
+
|
|
296
|
+
def record_spawn(self, description: str):
|
|
297
|
+
"""Record a sub-agent spawn."""
|
|
298
|
+
self.spawn_count += 1
|
|
299
|
+
self.active_agents += 1
|
|
300
|
+
print(f" {C.BRIGHT_CYAN}[Swarm]{C.RESET} Sub-agent {self.spawn_count}/{self.max_spawns}: {description}")
|
|
301
|
+
|
|
302
|
+
def record_completion(self):
|
|
303
|
+
"""Record a sub-agent completion."""
|
|
304
|
+
self.active_agents -= 1
|
|
305
|
+
self.completed_agents += 1
|
|
306
|
+
|
|
307
|
+
def budget_exhausted_message(self) -> str:
|
|
308
|
+
"""Return message when budget is exhausted."""
|
|
309
|
+
return (
|
|
310
|
+
f"Sub-agent budget exhausted ({self.spawn_count}/{self.max_spawns}). "
|
|
311
|
+
f"Complete remaining work directly without spawning more sub-agents."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def summary(self) -> dict:
|
|
315
|
+
"""Return budget tracking summary."""
|
|
316
|
+
return {
|
|
317
|
+
"total_spawned": self.spawn_count,
|
|
318
|
+
"completed": self.completed_agents,
|
|
319
|
+
"budget": self.max_spawns,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ============================================================================
|
|
324
|
+
# Turbo Dashboard (Rich-based split-pane UI)
|
|
325
|
+
# ============================================================================
|
|
326
|
+
class TurboDashboard:
|
|
327
|
+
"""Rich-based split-pane terminal dashboard for parallel DaveLoop instances.
|
|
328
|
+
|
|
329
|
+
Shows one panel per instance with: instance ID, current task name,
|
|
330
|
+
and a scrolling window of the last ~20 lines of output.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
PANEL_COLORS = ["cyan", "green", "yellow", "magenta", "blue", "red"]
|
|
334
|
+
|
|
335
|
+
def __init__(self, instance_count: int):
|
|
336
|
+
from rich.live import Live
|
|
337
|
+
from rich.layout import Layout
|
|
338
|
+
from rich.panel import Panel
|
|
339
|
+
from rich.text import Text
|
|
340
|
+
from rich.console import Console
|
|
341
|
+
|
|
342
|
+
self._Live = Live
|
|
343
|
+
self._Layout = Layout
|
|
344
|
+
self._Panel = Panel
|
|
345
|
+
self._Text = Text
|
|
346
|
+
self._console = Console()
|
|
347
|
+
|
|
348
|
+
self.instance_count = instance_count
|
|
349
|
+
self._lock = threading.Lock()
|
|
350
|
+
|
|
351
|
+
# Per-instance state
|
|
352
|
+
self.task_names = ["Waiting..."] * instance_count
|
|
353
|
+
self.statuses = ["pending"] * instance_count # pending, running, done, failed
|
|
354
|
+
self.output_buffers = [deque(maxlen=20) for _ in range(instance_count)]
|
|
355
|
+
self.iterations = [0] * instance_count
|
|
356
|
+
self.max_iterations = [0] * instance_count
|
|
357
|
+
self.start_times = [None] * instance_count
|
|
358
|
+
|
|
359
|
+
self._live = None
|
|
360
|
+
|
|
361
|
+
def _build_layout(self):
|
|
362
|
+
"""Build the Rich Layout with panels for each instance."""
|
|
363
|
+
layout = self._Layout()
|
|
364
|
+
|
|
365
|
+
# Create rows of 2 panels each
|
|
366
|
+
rows = []
|
|
367
|
+
for i in range(0, self.instance_count, 2):
|
|
368
|
+
row_name = f"row_{i // 2}"
|
|
369
|
+
row = self._Layout(name=row_name)
|
|
370
|
+
left = self._Layout(name=f"inst_{i}")
|
|
371
|
+
|
|
372
|
+
if i + 1 < self.instance_count:
|
|
373
|
+
right = self._Layout(name=f"inst_{i + 1}")
|
|
374
|
+
row.split_row(left, right)
|
|
375
|
+
else:
|
|
376
|
+
row.split_row(left)
|
|
377
|
+
|
|
378
|
+
rows.append(row)
|
|
379
|
+
|
|
380
|
+
if rows:
|
|
381
|
+
layout.split_column(*rows)
|
|
382
|
+
|
|
383
|
+
return layout
|
|
384
|
+
|
|
385
|
+
def _render_panel(self, idx: int):
|
|
386
|
+
"""Render a single instance panel."""
|
|
387
|
+
color = self.PANEL_COLORS[idx % len(self.PANEL_COLORS)]
|
|
388
|
+
status = self.statuses[idx]
|
|
389
|
+
|
|
390
|
+
# Status indicator
|
|
391
|
+
if status == "running":
|
|
392
|
+
status_icon = "[bold bright_green]\u25cf RUNNING[/]"
|
|
393
|
+
elif status == "done":
|
|
394
|
+
status_icon = "[bold bright_green]\u2713 RESOLVED[/]"
|
|
395
|
+
elif status == "failed":
|
|
396
|
+
status_icon = "[bold bright_red]\u2717 FAILED[/]"
|
|
397
|
+
else:
|
|
398
|
+
status_icon = "[dim]\u25cb PENDING[/]"
|
|
399
|
+
|
|
400
|
+
# Iteration info
|
|
401
|
+
iter_info = ""
|
|
402
|
+
if self.max_iterations[idx] > 0:
|
|
403
|
+
iter_info = f" | Iter {self.iterations[idx]}/{self.max_iterations[idx]}"
|
|
404
|
+
|
|
405
|
+
# Elapsed time
|
|
406
|
+
elapsed = ""
|
|
407
|
+
if self.start_times[idx]:
|
|
408
|
+
secs = int(time.time() - self.start_times[idx])
|
|
409
|
+
mins, secs = divmod(secs, 60)
|
|
410
|
+
elapsed = f" | {mins}m{secs:02d}s"
|
|
411
|
+
|
|
412
|
+
# Build subtitle
|
|
413
|
+
subtitle = f"{status_icon}{iter_info}{elapsed}"
|
|
414
|
+
|
|
415
|
+
# Build output text
|
|
416
|
+
lines = list(self.output_buffers[idx])
|
|
417
|
+
if not lines:
|
|
418
|
+
lines = ["[dim]Waiting for output...[/]"]
|
|
419
|
+
|
|
420
|
+
output_text = "\n".join(lines)
|
|
421
|
+
|
|
422
|
+
task_name = self.task_names[idx]
|
|
423
|
+
if len(task_name) > 50:
|
|
424
|
+
task_name = task_name[:47] + "..."
|
|
425
|
+
|
|
426
|
+
panel = self._Panel(
|
|
427
|
+
output_text,
|
|
428
|
+
title=f"[bold {color}] Instance {idx + 1} [/] {task_name}",
|
|
429
|
+
subtitle=subtitle,
|
|
430
|
+
border_style=color if status == "running" else "dim" if status == "pending" else "green" if status == "done" else "red",
|
|
431
|
+
padding=(0, 1),
|
|
432
|
+
)
|
|
433
|
+
return panel
|
|
434
|
+
|
|
435
|
+
def _refresh(self):
|
|
436
|
+
"""Rebuild and update the live display."""
|
|
437
|
+
layout = self._build_layout()
|
|
438
|
+
for i in range(self.instance_count):
|
|
439
|
+
try:
|
|
440
|
+
layout[f"inst_{i}"].update(self._render_panel(i))
|
|
441
|
+
except KeyError:
|
|
442
|
+
pass
|
|
443
|
+
return layout
|
|
444
|
+
|
|
445
|
+
def start(self):
|
|
446
|
+
"""Start the live dashboard."""
|
|
447
|
+
self._live = self._Live(
|
|
448
|
+
self._refresh(),
|
|
449
|
+
console=self._console,
|
|
450
|
+
refresh_per_second=4,
|
|
451
|
+
screen=True,
|
|
452
|
+
)
|
|
453
|
+
self._live.start()
|
|
454
|
+
|
|
455
|
+
def stop(self):
|
|
456
|
+
"""Stop the live dashboard."""
|
|
457
|
+
if self._live:
|
|
458
|
+
self._live.stop()
|
|
459
|
+
|
|
460
|
+
def update_task(self, idx: int, task_name: str, max_iterations: int):
|
|
461
|
+
"""Set the task name and max iterations for an instance."""
|
|
462
|
+
with self._lock:
|
|
463
|
+
self.task_names[idx] = task_name
|
|
464
|
+
self.max_iterations[idx] = max_iterations
|
|
465
|
+
self.statuses[idx] = "running"
|
|
466
|
+
self.start_times[idx] = time.time()
|
|
467
|
+
if self._live:
|
|
468
|
+
self._live.update(self._refresh())
|
|
469
|
+
|
|
470
|
+
def update_iteration(self, idx: int, iteration: int):
|
|
471
|
+
"""Update the current iteration number for an instance."""
|
|
472
|
+
with self._lock:
|
|
473
|
+
self.iterations[idx] = iteration
|
|
474
|
+
if self._live:
|
|
475
|
+
self._live.update(self._refresh())
|
|
476
|
+
|
|
477
|
+
def append_output(self, idx: int, line: str):
|
|
478
|
+
"""Append a line to an instance's output buffer."""
|
|
479
|
+
with self._lock:
|
|
480
|
+
# Strip ANSI codes for cleaner display in Rich panels
|
|
481
|
+
clean = _strip_ansi(line)
|
|
482
|
+
if clean.strip():
|
|
483
|
+
self.output_buffers[idx].append(clean)
|
|
484
|
+
if self._live:
|
|
485
|
+
self._live.update(self._refresh())
|
|
486
|
+
|
|
487
|
+
def mark_done(self, idx: int, outcome: str = "done"):
|
|
488
|
+
"""Mark an instance as completed."""
|
|
489
|
+
with self._lock:
|
|
490
|
+
self.statuses[idx] = outcome
|
|
491
|
+
if self._live:
|
|
492
|
+
self._live.update(self._refresh())
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _strip_ansi(text: str) -> str:
|
|
496
|
+
"""Remove ANSI escape sequences from a string."""
|
|
497
|
+
import re
|
|
498
|
+
return re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', text)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def run_claude_code_turbo(prompt: str, working_dir: str, instance_idx: int,
|
|
502
|
+
dashboard: TurboDashboard, timeout: int = DEFAULT_TIMEOUT,
|
|
503
|
+
swarm_mode: bool = False, swarm_budget_max: int = 5,
|
|
504
|
+
swarm_depth_max: int = 1) -> str:
|
|
505
|
+
"""Execute Claude Code CLI for turbo mode, streaming output to a dashboard panel.
|
|
506
|
+
|
|
507
|
+
Similar to run_claude_code but routes output to a TurboDashboard panel
|
|
508
|
+
instead of printing directly to stdout.
|
|
509
|
+
"""
|
|
510
|
+
claude_cmd = find_claude_cli()
|
|
511
|
+
if not claude_cmd:
|
|
512
|
+
return "[DAVELOOP:ERROR] Claude CLI not found"
|
|
513
|
+
|
|
514
|
+
cmd = [claude_cmd]
|
|
515
|
+
allowed = ALLOWED_TOOLS_SWARM if swarm_mode else ALLOWED_TOOLS_DEFAULT
|
|
516
|
+
cmd.extend(["-p", "--verbose", "--output-format", "stream-json", "--allowedTools", allowed])
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
process = subprocess.Popen(
|
|
520
|
+
cmd,
|
|
521
|
+
stdin=subprocess.PIPE,
|
|
522
|
+
stdout=subprocess.PIPE,
|
|
523
|
+
stderr=subprocess.STDOUT,
|
|
524
|
+
text=True,
|
|
525
|
+
encoding='utf-8',
|
|
526
|
+
errors='replace',
|
|
527
|
+
cwd=working_dir,
|
|
528
|
+
bufsize=1
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
process.stdin.write(prompt)
|
|
532
|
+
process.stdin.close()
|
|
533
|
+
|
|
534
|
+
full_text = []
|
|
535
|
+
|
|
536
|
+
for line in process.stdout:
|
|
537
|
+
line = line.strip()
|
|
538
|
+
if not line:
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
data = json.loads(line)
|
|
543
|
+
msg_type = data.get("type", "")
|
|
544
|
+
|
|
545
|
+
if msg_type == "assistant":
|
|
546
|
+
content = data.get("message", {}).get("content", [])
|
|
547
|
+
for block in content:
|
|
548
|
+
if block.get("type") == "text":
|
|
549
|
+
text = block.get("text", "")
|
|
550
|
+
for text_line in text.split('\n'):
|
|
551
|
+
if text_line.strip():
|
|
552
|
+
dashboard.append_output(instance_idx, text_line)
|
|
553
|
+
full_text.append(text)
|
|
554
|
+
elif block.get("type") == "tool_use":
|
|
555
|
+
tool_name = block.get("name", "unknown")
|
|
556
|
+
tool_input = block.get("input", {})
|
|
557
|
+
tool_desc = _format_tool_short(tool_name, tool_input)
|
|
558
|
+
dashboard.append_output(instance_idx, f"\u25b6 {tool_desc}")
|
|
559
|
+
|
|
560
|
+
elif msg_type == "content_block_delta":
|
|
561
|
+
delta = data.get("delta", {})
|
|
562
|
+
if delta.get("type") == "text_delta":
|
|
563
|
+
text = delta.get("text", "")
|
|
564
|
+
full_text.append(text)
|
|
565
|
+
# Only push meaningful chunks to dashboard
|
|
566
|
+
for text_line in text.split('\n'):
|
|
567
|
+
if text_line.strip():
|
|
568
|
+
dashboard.append_output(instance_idx, text_line)
|
|
569
|
+
|
|
570
|
+
elif msg_type == "tool_use":
|
|
571
|
+
tool_name = data.get("name", "unknown")
|
|
572
|
+
tool_input = data.get("input", {})
|
|
573
|
+
tool_desc = _format_tool_short(tool_name, tool_input)
|
|
574
|
+
dashboard.append_output(instance_idx, f"\u25b6 {tool_desc}")
|
|
575
|
+
|
|
576
|
+
elif msg_type == "tool_result":
|
|
577
|
+
dashboard.append_output(instance_idx, "\u2514\u2500 \u2713 done")
|
|
578
|
+
|
|
579
|
+
elif msg_type == "result":
|
|
580
|
+
text = data.get("result", "")
|
|
581
|
+
if text:
|
|
582
|
+
full_text.append(text)
|
|
583
|
+
|
|
584
|
+
elif msg_type == "error":
|
|
585
|
+
error_msg = data.get("error", {}).get("message", "Unknown error")
|
|
586
|
+
dashboard.append_output(instance_idx, f"\u2717 ERROR: {error_msg}")
|
|
587
|
+
|
|
588
|
+
except json.JSONDecodeError:
|
|
589
|
+
dashboard.append_output(instance_idx, line)
|
|
590
|
+
full_text.append(line)
|
|
591
|
+
|
|
592
|
+
process.wait(timeout=timeout)
|
|
593
|
+
return '\n'.join(full_text)
|
|
594
|
+
|
|
595
|
+
except subprocess.TimeoutExpired:
|
|
596
|
+
return f"[DAVELOOP:TIMEOUT] Claude Code timed out after {timeout // 60} minutes"
|
|
597
|
+
except FileNotFoundError:
|
|
598
|
+
return "[DAVELOOP:ERROR] Claude Code CLI not found"
|
|
599
|
+
except Exception as e:
|
|
600
|
+
return f"[DAVELOOP:ERROR] {str(e)}"
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _format_tool_short(tool_name: str, tool_input: dict) -> str:
|
|
604
|
+
"""Format a tool call for compact dashboard display."""
|
|
605
|
+
if tool_name == "Bash":
|
|
606
|
+
cmd = tool_input.get("command", "")
|
|
607
|
+
return f"Bash({cmd[:40]}{'...' if len(cmd) > 40 else ''})"
|
|
608
|
+
elif tool_name in ("Read", "Write", "Edit"):
|
|
609
|
+
fp = tool_input.get("file_path", "")
|
|
610
|
+
fname = fp.split("\\")[-1].split("/")[-1]
|
|
611
|
+
return f"{tool_name}({fname})"
|
|
612
|
+
elif tool_name == "Grep":
|
|
613
|
+
pat = tool_input.get("pattern", "")
|
|
614
|
+
return f"Grep({pat[:25]}{'...' if len(pat) > 25 else ''})"
|
|
615
|
+
elif tool_name == "Glob":
|
|
616
|
+
pat = tool_input.get("pattern", "")
|
|
617
|
+
return f"Glob({pat})"
|
|
618
|
+
elif tool_name == "Task":
|
|
619
|
+
desc = tool_input.get("description", "")
|
|
620
|
+
return f"Task({desc[:30]}{'...' if len(desc) > 30 else ''})"
|
|
621
|
+
return tool_name
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def run_turbo_task(task_desc: str, task_idx: int, system_prompt: str,
|
|
625
|
+
working_dir: str, max_iterations: int, timeout: int,
|
|
626
|
+
dashboard: TurboDashboard, swarm_mode: bool = False,
|
|
627
|
+
swarm_budget_max: int = 5, swarm_depth_max: int = 1) -> dict:
|
|
628
|
+
"""Run a single DaveLoop task inside the turbo dashboard.
|
|
629
|
+
|
|
630
|
+
Returns a dict with outcome, iterations, and output.
|
|
631
|
+
"""
|
|
632
|
+
dashboard.update_task(task_idx, task_desc[:50], max_iterations)
|
|
633
|
+
|
|
634
|
+
context = f"""
|
|
635
|
+
## Bug Report
|
|
636
|
+
|
|
637
|
+
{task_desc}
|
|
638
|
+
|
|
639
|
+
## Instructions
|
|
640
|
+
|
|
641
|
+
Analyze this bug. Gather whatever logs/information you need to understand it.
|
|
642
|
+
Then fix it. Use the reasoning protocol before each action.
|
|
643
|
+
"""
|
|
644
|
+
full_output = []
|
|
645
|
+
|
|
646
|
+
for iteration in range(1, max_iterations + 1):
|
|
647
|
+
dashboard.update_iteration(task_idx, iteration)
|
|
648
|
+
dashboard.append_output(task_idx, f"--- Iteration {iteration}/{max_iterations} ---")
|
|
649
|
+
|
|
650
|
+
if iteration == 1:
|
|
651
|
+
full_prompt = f"{system_prompt}\n\n---\n\n{context}"
|
|
652
|
+
else:
|
|
653
|
+
full_prompt = context
|
|
654
|
+
|
|
655
|
+
output = run_claude_code_turbo(
|
|
656
|
+
full_prompt, working_dir,
|
|
657
|
+
instance_idx=task_idx,
|
|
658
|
+
dashboard=dashboard,
|
|
659
|
+
timeout=timeout,
|
|
660
|
+
swarm_mode=swarm_mode,
|
|
661
|
+
swarm_budget_max=swarm_budget_max,
|
|
662
|
+
swarm_depth_max=swarm_depth_max,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
full_output.append(output)
|
|
666
|
+
|
|
667
|
+
signal, should_exit = check_exit_condition(output)
|
|
668
|
+
|
|
669
|
+
if should_exit:
|
|
670
|
+
if signal == "RESOLVED":
|
|
671
|
+
dashboard.append_output(task_idx, "\u2713 BUG RESOLVED!")
|
|
672
|
+
dashboard.mark_done(task_idx, "done")
|
|
673
|
+
return {"outcome": "RESOLVED", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
674
|
+
elif signal == "BLOCKED":
|
|
675
|
+
dashboard.append_output(task_idx, "\u2717 BLOCKED - needs human help")
|
|
676
|
+
dashboard.mark_done(task_idx, "failed")
|
|
677
|
+
return {"outcome": "BLOCKED", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
678
|
+
elif signal == "CLARIFY":
|
|
679
|
+
dashboard.append_output(task_idx, "\u2717 NEEDS CLARIFICATION")
|
|
680
|
+
dashboard.mark_done(task_idx, "failed")
|
|
681
|
+
return {"outcome": "CLARIFY", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
682
|
+
else:
|
|
683
|
+
dashboard.append_output(task_idx, f"\u2717 Error: {signal}")
|
|
684
|
+
dashboard.mark_done(task_idx, "failed")
|
|
685
|
+
return {"outcome": signal, "iterations": iteration, "output": '\n'.join(full_output)}
|
|
686
|
+
|
|
687
|
+
# Prepare next iteration context
|
|
688
|
+
context = f"""
|
|
689
|
+
## Iteration {iteration + 1}
|
|
690
|
+
|
|
691
|
+
The bug is NOT yet resolved. You have full context from previous iterations.
|
|
692
|
+
|
|
693
|
+
Continue debugging. Analyze what happened, determine next steps, and proceed.
|
|
694
|
+
Use the reasoning protocol before each action.
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
# Max iterations reached
|
|
698
|
+
dashboard.append_output(task_idx, f"\u2717 Max iterations ({max_iterations}) reached")
|
|
699
|
+
dashboard.mark_done(task_idx, "failed")
|
|
700
|
+
return {"outcome": "MAX_ITERATIONS", "iterations": max_iterations, "output": '\n'.join(full_output)}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# ============================================================================
|
|
704
|
+
# Token Tracker
|
|
705
|
+
# ============================================================================
|
|
706
|
+
class TokenTracker:
|
|
707
|
+
"""Tracks token usage across API turns in a DaveLoop session."""
|
|
708
|
+
|
|
709
|
+
def __init__(self):
|
|
710
|
+
self.total_input = 0
|
|
711
|
+
self.total_output = 0
|
|
712
|
+
self.turn_count = 0
|
|
713
|
+
self.peak_input = 0
|
|
714
|
+
self.peak_output = 0
|
|
715
|
+
self.peak_total = 0
|
|
716
|
+
self.per_tool = {} # tool_name -> {"input": int, "output": int, "count": int}
|
|
717
|
+
self._current_tool = None # Track which tool is active for per-tool attribution
|
|
718
|
+
self._turn_input = 0 # Accumulate within a turn for per-tool attribution
|
|
719
|
+
self._turn_output = 0
|
|
720
|
+
|
|
721
|
+
def set_current_tool(self, tool_name: str):
|
|
722
|
+
"""Set the currently active tool for per-tool token attribution."""
|
|
723
|
+
self._current_tool = tool_name
|
|
724
|
+
|
|
725
|
+
def record_usage(self, input_tokens: int, output_tokens: int):
|
|
726
|
+
"""Record token usage from an API turn."""
|
|
727
|
+
self.total_input += input_tokens
|
|
728
|
+
self.total_output += output_tokens
|
|
729
|
+
self.turn_count += 1
|
|
730
|
+
|
|
731
|
+
turn_total = input_tokens + output_tokens
|
|
732
|
+
if turn_total > self.peak_total:
|
|
733
|
+
self.peak_total = turn_total
|
|
734
|
+
self.peak_input = input_tokens
|
|
735
|
+
self.peak_output = output_tokens
|
|
736
|
+
|
|
737
|
+
# Attribute to current tool if one is active
|
|
738
|
+
if self._current_tool:
|
|
739
|
+
if self._current_tool not in self.per_tool:
|
|
740
|
+
self.per_tool[self._current_tool] = {"input": 0, "output": 0, "count": 0}
|
|
741
|
+
self.per_tool[self._current_tool]["input"] += input_tokens
|
|
742
|
+
self.per_tool[self._current_tool]["output"] += output_tokens
|
|
743
|
+
self.per_tool[self._current_tool]["count"] += 1
|
|
744
|
+
|
|
745
|
+
@property
|
|
746
|
+
def total_tokens(self) -> int:
|
|
747
|
+
return self.total_input + self.total_output
|
|
748
|
+
|
|
749
|
+
def summary(self) -> dict:
|
|
750
|
+
"""Return a dict with all token stats."""
|
|
751
|
+
return {
|
|
752
|
+
"input_tokens": self.total_input,
|
|
753
|
+
"output_tokens": self.total_output,
|
|
754
|
+
"total_tokens": self.total_tokens,
|
|
755
|
+
"turn_count": self.turn_count,
|
|
756
|
+
"peak_turn": {
|
|
757
|
+
"input": self.peak_input,
|
|
758
|
+
"output": self.peak_output,
|
|
759
|
+
"total": self.peak_total,
|
|
760
|
+
},
|
|
761
|
+
"per_tool": dict(self.per_tool),
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
def summary_line(self) -> str:
|
|
765
|
+
"""Return a one-line summary string for display."""
|
|
766
|
+
return (
|
|
767
|
+
f"Tokens: {self.total_input:,} in / {self.total_output:,} out / "
|
|
768
|
+
f"{self.total_tokens:,} total ({self.turn_count} turns)"
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
def verbose_turn_line(self, input_tokens: int, output_tokens: int) -> str:
|
|
772
|
+
"""Return a per-turn detail line for --show-tokens mode."""
|
|
773
|
+
total = input_tokens + output_tokens
|
|
774
|
+
tool_info = f" [{self._current_tool}]" if self._current_tool else ""
|
|
775
|
+
return (
|
|
776
|
+
f" Turn {self.turn_count}: {input_tokens:,} in / {output_tokens:,} out / "
|
|
777
|
+
f"{total:,} total{tool_info}"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
271
781
|
# ============================================================================
|
|
272
782
|
# Session Memory
|
|
273
783
|
# ============================================================================
|
|
@@ -294,16 +804,21 @@ def save_history(working_dir: str, history_data: dict):
|
|
|
294
804
|
history_file.write_text(json.dumps(history_data, indent=2), encoding="utf-8")
|
|
295
805
|
|
|
296
806
|
|
|
297
|
-
def summarize_session(bug: str, outcome: str, iterations: int) -> dict:
|
|
807
|
+
def summarize_session(bug: str, outcome: str, iterations: int, token_tracker: "TokenTracker" = None) -> dict:
|
|
298
808
|
"""Return a dict summarizing a session."""
|
|
299
809
|
now = datetime.now()
|
|
300
|
-
|
|
810
|
+
entry = {
|
|
301
811
|
"session_id": now.strftime("%Y%m%d_%H%M%S"),
|
|
302
812
|
"bug": bug,
|
|
303
813
|
"outcome": outcome,
|
|
304
814
|
"iterations": iterations,
|
|
305
815
|
"timestamp": now.isoformat()
|
|
306
816
|
}
|
|
817
|
+
if token_tracker and token_tracker.turn_count > 0:
|
|
818
|
+
entry["tokens_in"] = token_tracker.total_input
|
|
819
|
+
entry["tokens_out"] = token_tracker.total_output
|
|
820
|
+
entry["tokens_total"] = token_tracker.total_tokens
|
|
821
|
+
return entry
|
|
307
822
|
|
|
308
823
|
|
|
309
824
|
def format_history_context(sessions: list) -> str:
|
|
@@ -329,10 +844,12 @@ def print_history_box(sessions: list):
|
|
|
329
844
|
outcome = s.get("outcome", "UNKNOWN")
|
|
330
845
|
bug = s.get("bug", "unknown")[:55]
|
|
331
846
|
iters = s.get("iterations", "?")
|
|
847
|
+
tokens_total = s.get("tokens_total")
|
|
848
|
+
token_str = f" · {tokens_total:,} tok" if tokens_total else ""
|
|
332
849
|
if outcome == "RESOLVED":
|
|
333
|
-
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
850
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter{token_str}){C.RESET}")
|
|
334
851
|
else:
|
|
335
|
-
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
852
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter{token_str}){C.RESET}")
|
|
336
853
|
print()
|
|
337
854
|
|
|
338
855
|
|
|
@@ -426,7 +943,7 @@ class InputMonitor:
|
|
|
426
943
|
Call resume_reading() after the main thread is done with input().
|
|
427
944
|
"""
|
|
428
945
|
|
|
429
|
-
VALID_COMMANDS = ("wait", "pause", "add", "done")
|
|
946
|
+
VALID_COMMANDS = ("wait", "pause", "add", "done", "stop")
|
|
430
947
|
|
|
431
948
|
def __init__(self):
|
|
432
949
|
self._command = None
|
|
@@ -554,12 +1071,17 @@ def find_claude_cli():
|
|
|
554
1071
|
return None
|
|
555
1072
|
|
|
556
1073
|
|
|
557
|
-
def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT, input_monitor=None) -> str:
|
|
1074
|
+
def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT, input_monitor=None, swarm_mode: bool = False, swarm_budget_max: int = 5, swarm_depth_max: int = 1, token_tracker: "TokenTracker" = None, show_tokens: bool = False) -> str:
|
|
558
1075
|
"""Execute Claude Code CLI with the given prompt.
|
|
559
1076
|
|
|
560
1077
|
If stream=True, output is printed in real-time and also returned.
|
|
561
1078
|
timeout is in seconds (default 600 = 10 minutes).
|
|
562
1079
|
input_monitor: optional InputMonitor to check for user commands during execution.
|
|
1080
|
+
swarm_mode: if True, enables Task tool for sub-agent spawning.
|
|
1081
|
+
swarm_budget_max: max sub-agents per session in swarm mode.
|
|
1082
|
+
swarm_depth_max: max sub-agent depth in swarm mode.
|
|
1083
|
+
token_tracker: optional TokenTracker to accumulate token usage from the stream.
|
|
1084
|
+
show_tokens: if True, print per-turn token usage during execution.
|
|
563
1085
|
"""
|
|
564
1086
|
claude_cmd = find_claude_cli()
|
|
565
1087
|
if not claude_cmd:
|
|
@@ -578,7 +1100,8 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
578
1100
|
if continue_session:
|
|
579
1101
|
cmd.append("--continue")
|
|
580
1102
|
|
|
581
|
-
|
|
1103
|
+
allowed = ALLOWED_TOOLS_SWARM if swarm_mode else ALLOWED_TOOLS_DEFAULT
|
|
1104
|
+
cmd.extend(["-p", "--verbose", "--output-format", "stream-json", "--allowedTools", allowed])
|
|
582
1105
|
|
|
583
1106
|
try:
|
|
584
1107
|
if stream:
|
|
@@ -602,6 +1125,9 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
602
1125
|
# Track start time
|
|
603
1126
|
start_time = time.time()
|
|
604
1127
|
|
|
1128
|
+
# Swarm budget tracking (only active in swarm mode)
|
|
1129
|
+
swarm_budget = SwarmBudget(max_spawns=swarm_budget_max, max_depth=swarm_depth_max) if swarm_mode else None
|
|
1130
|
+
|
|
605
1131
|
# Read and display JSON stream output
|
|
606
1132
|
output_lines = []
|
|
607
1133
|
full_text = []
|
|
@@ -617,6 +1143,19 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
617
1143
|
msg_type = data.get("type", "")
|
|
618
1144
|
|
|
619
1145
|
|
|
1146
|
+
# Extract token usage from any message that has it
|
|
1147
|
+
if token_tracker:
|
|
1148
|
+
usage = (data.get("message", {}).get("usage")
|
|
1149
|
+
or data.get("usage")
|
|
1150
|
+
or None)
|
|
1151
|
+
if usage and isinstance(usage, dict):
|
|
1152
|
+
inp = usage.get("input_tokens", 0)
|
|
1153
|
+
outp = usage.get("output_tokens", 0)
|
|
1154
|
+
if inp or outp:
|
|
1155
|
+
token_tracker.record_usage(inp, outp)
|
|
1156
|
+
if show_tokens:
|
|
1157
|
+
print(f" {C.DIM}{token_tracker.verbose_turn_line(inp, outp)}{C.RESET}")
|
|
1158
|
+
|
|
620
1159
|
# Handle different message types
|
|
621
1160
|
if msg_type == "assistant":
|
|
622
1161
|
# Assistant text message
|
|
@@ -631,6 +1170,8 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
631
1170
|
elif block.get("type") == "tool_use":
|
|
632
1171
|
# Tool being called - show what Claude is doing
|
|
633
1172
|
tool_name = block.get("name", "unknown")
|
|
1173
|
+
if token_tracker:
|
|
1174
|
+
token_tracker.set_current_tool(tool_name)
|
|
634
1175
|
tool_input = block.get("input", {})
|
|
635
1176
|
|
|
636
1177
|
# Format tool call based on type
|
|
@@ -660,6 +1201,18 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
660
1201
|
elif tool_name == "Task":
|
|
661
1202
|
desc = tool_input.get("description", "")
|
|
662
1203
|
tool_display = f"{C.BRIGHT_BLUE}Task{C.RESET}({C.WHITE}{desc}{C.RESET})"
|
|
1204
|
+
# Swarm budget enforcement
|
|
1205
|
+
if swarm_budget:
|
|
1206
|
+
if not swarm_budget.can_spawn():
|
|
1207
|
+
print(f" {C.BRIGHT_YELLOW}[Swarm] Budget exhausted. Terminating to restart without Task tool.{C.RESET}")
|
|
1208
|
+
process.terminate()
|
|
1209
|
+
try:
|
|
1210
|
+
process.wait(timeout=10)
|
|
1211
|
+
except Exception:
|
|
1212
|
+
process.kill()
|
|
1213
|
+
return '\n'.join(full_text) + "\n[DAVELOOP:SWARM_BUDGET_EXHAUSTED]"
|
|
1214
|
+
else:
|
|
1215
|
+
swarm_budget.record_spawn(desc)
|
|
663
1216
|
else:
|
|
664
1217
|
tool_display = f"{C.BRIGHT_BLUE}{tool_name}{C.RESET}"
|
|
665
1218
|
|
|
@@ -677,6 +1230,8 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
677
1230
|
elif msg_type == "tool_use":
|
|
678
1231
|
# Tool being used - show what Claude is doing
|
|
679
1232
|
tool_name = data.get("name", "unknown")
|
|
1233
|
+
if token_tracker:
|
|
1234
|
+
token_tracker.set_current_tool(tool_name)
|
|
680
1235
|
tool_input = data.get("input", {})
|
|
681
1236
|
|
|
682
1237
|
# Format tool call based on type
|
|
@@ -706,6 +1261,18 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
706
1261
|
elif tool_name == "Task":
|
|
707
1262
|
desc = tool_input.get("description", "")
|
|
708
1263
|
tool_display = f"{C.BRIGHT_BLUE}Task{C.RESET}({C.WHITE}{desc}{C.RESET})"
|
|
1264
|
+
# Swarm budget enforcement
|
|
1265
|
+
if swarm_budget:
|
|
1266
|
+
if not swarm_budget.can_spawn():
|
|
1267
|
+
print(f" {C.BRIGHT_YELLOW}[Swarm] Budget exhausted. Terminating to restart without Task tool.{C.RESET}")
|
|
1268
|
+
process.terminate()
|
|
1269
|
+
try:
|
|
1270
|
+
process.wait(timeout=10)
|
|
1271
|
+
except Exception:
|
|
1272
|
+
process.kill()
|
|
1273
|
+
return '\n'.join(full_text) + "\n[DAVELOOP:SWARM_BUDGET_EXHAUSTED]"
|
|
1274
|
+
else:
|
|
1275
|
+
swarm_budget.record_spawn(desc)
|
|
709
1276
|
else:
|
|
710
1277
|
tool_display = f"{C.BRIGHT_BLUE}{tool_name}{C.RESET}"
|
|
711
1278
|
|
|
@@ -859,6 +1426,18 @@ def main():
|
|
|
859
1426
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
860
1427
|
parser.add_argument("--maestro", action="store_true", help="Enable Maestro mobile testing mode")
|
|
861
1428
|
parser.add_argument("--web", action="store_true", help="Enable Playwright web UI testing mode")
|
|
1429
|
+
parser.add_argument("--swarm", action="store_true",
|
|
1430
|
+
help="Enable swarm mode: DaveLoop can spawn sub-agents via Task tool")
|
|
1431
|
+
parser.add_argument("--swarm-budget", type=int, default=5,
|
|
1432
|
+
help="Max sub-agents per DaveLoop worker in swarm mode (default: 5)")
|
|
1433
|
+
parser.add_argument("--swarm-depth", type=int, default=1, choices=[1, 2],
|
|
1434
|
+
help="Max sub-agent depth in swarm mode (default: 1, no recursive spawning)")
|
|
1435
|
+
parser.add_argument("--show-tokens", action="store_true",
|
|
1436
|
+
help="Show verbose per-turn token usage during execution")
|
|
1437
|
+
parser.add_argument("--turbo", action="store_true",
|
|
1438
|
+
help="Run all tasks in parallel with a Rich split-pane dashboard")
|
|
1439
|
+
parser.add_argument("--turbo-workers", type=int, default=None,
|
|
1440
|
+
help="Max parallel workers in turbo mode (default: number of tasks)")
|
|
862
1441
|
|
|
863
1442
|
args = parser.parse_args()
|
|
864
1443
|
|
|
@@ -908,6 +1487,14 @@ def main():
|
|
|
908
1487
|
print_status("Tasks", str(len(bug_descriptions)), C.WHITE)
|
|
909
1488
|
mode_name = "Maestro Mobile Testing" if args.maestro else "Playwright Web Testing" if args.web else "Autonomous"
|
|
910
1489
|
print_status("Mode", mode_name, C.WHITE)
|
|
1490
|
+
if args.swarm:
|
|
1491
|
+
print_status("Swarm", f"ENABLED (budget: {args.swarm_budget}, depth: {args.swarm_depth})", C.BRIGHT_CYAN)
|
|
1492
|
+
print_status("Tools", ALLOWED_TOOLS_SWARM, C.WHITE)
|
|
1493
|
+
else:
|
|
1494
|
+
print_status("Tools", ALLOWED_TOOLS_DEFAULT, C.WHITE)
|
|
1495
|
+
if args.turbo:
|
|
1496
|
+
workers = args.turbo_workers or len(bug_descriptions)
|
|
1497
|
+
print_status("Turbo", f"ENABLED ({workers} parallel workers)", C.BRIGHT_MAGENTA)
|
|
911
1498
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
912
1499
|
|
|
913
1500
|
# Build task queue
|
|
@@ -917,18 +1504,103 @@ def main():
|
|
|
917
1504
|
|
|
918
1505
|
# Print controls hint
|
|
919
1506
|
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}┌─ CONTROLS {'─' * 58}┐{C.RESET}")
|
|
920
|
-
print(f"{C.BRIGHT_BLUE}│{C.RESET} Type while running: {C.BRIGHT_WHITE}wait{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}pause{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}add{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}done{C.RESET}
|
|
1507
|
+
print(f"{C.BRIGHT_BLUE}│{C.RESET} Type while running: {C.BRIGHT_WHITE}wait{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}pause{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}add{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}done{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}stop{C.RESET} {C.BRIGHT_BLUE}│{C.RESET}")
|
|
921
1508
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
922
1509
|
|
|
923
|
-
# Start input monitor
|
|
924
|
-
input_monitor = InputMonitor()
|
|
925
|
-
input_monitor.start()
|
|
926
|
-
|
|
927
1510
|
# Build history context for initial prompt
|
|
928
1511
|
history_context = ""
|
|
929
1512
|
if history_data["sessions"]:
|
|
930
1513
|
history_context = "\n\n" + format_history_context(history_data["sessions"])
|
|
931
1514
|
|
|
1515
|
+
# ========================================================================
|
|
1516
|
+
# TURBO MODE: parallel execution with Rich dashboard
|
|
1517
|
+
# ========================================================================
|
|
1518
|
+
if args.turbo and len(bug_descriptions) >= 1:
|
|
1519
|
+
num_workers = args.turbo_workers or len(bug_descriptions)
|
|
1520
|
+
num_workers = min(num_workers, len(bug_descriptions))
|
|
1521
|
+
|
|
1522
|
+
print(f"\n {C.BRIGHT_MAGENTA}{C.BOLD}◆ Launching Turbo Mode with {num_workers} parallel workers...{C.RESET}\n")
|
|
1523
|
+
time.sleep(1) # Brief pause so user sees the message before dashboard takes over
|
|
1524
|
+
|
|
1525
|
+
dashboard = TurboDashboard(len(bug_descriptions))
|
|
1526
|
+
system_prompt_full = system_prompt + history_context
|
|
1527
|
+
|
|
1528
|
+
# Launch all tasks in parallel
|
|
1529
|
+
results = [None] * len(bug_descriptions)
|
|
1530
|
+
|
|
1531
|
+
dashboard.start()
|
|
1532
|
+
try:
|
|
1533
|
+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
1534
|
+
futures = {}
|
|
1535
|
+
for idx, desc in enumerate(bug_descriptions):
|
|
1536
|
+
future = executor.submit(
|
|
1537
|
+
run_turbo_task,
|
|
1538
|
+
task_desc=desc,
|
|
1539
|
+
task_idx=idx,
|
|
1540
|
+
system_prompt=system_prompt_full,
|
|
1541
|
+
working_dir=working_dir,
|
|
1542
|
+
max_iterations=args.max_iterations,
|
|
1543
|
+
timeout=args.timeout,
|
|
1544
|
+
dashboard=dashboard,
|
|
1545
|
+
swarm_mode=args.swarm,
|
|
1546
|
+
swarm_budget_max=args.swarm_budget,
|
|
1547
|
+
swarm_depth_max=args.swarm_depth,
|
|
1548
|
+
)
|
|
1549
|
+
futures[future] = idx
|
|
1550
|
+
|
|
1551
|
+
for future in as_completed(futures):
|
|
1552
|
+
idx = futures[future]
|
|
1553
|
+
try:
|
|
1554
|
+
results[idx] = future.result()
|
|
1555
|
+
except Exception as e:
|
|
1556
|
+
results[idx] = {"outcome": "ERROR", "iterations": 0, "output": str(e)}
|
|
1557
|
+
dashboard.append_output(idx, f"\u2717 Exception: {e}")
|
|
1558
|
+
dashboard.mark_done(idx, "failed")
|
|
1559
|
+
finally:
|
|
1560
|
+
# Keep dashboard visible for a moment so user can see final state
|
|
1561
|
+
time.sleep(2)
|
|
1562
|
+
dashboard.stop()
|
|
1563
|
+
|
|
1564
|
+
# Print final summary
|
|
1565
|
+
print(f"\n{BANNER}\n")
|
|
1566
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ TURBO MODE COMPLETE{C.RESET}")
|
|
1567
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
1568
|
+
for idx, desc in enumerate(bug_descriptions):
|
|
1569
|
+
r = results[idx]
|
|
1570
|
+
outcome = r["outcome"] if r else "ERROR"
|
|
1571
|
+
iters = r["iterations"] if r else 0
|
|
1572
|
+
desc_short = desc[:55]
|
|
1573
|
+
if outcome == "RESOLVED":
|
|
1574
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc_short}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
1575
|
+
else:
|
|
1576
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc_short}{C.RESET} {C.DIM}({outcome}, {iters} iter){C.RESET}")
|
|
1577
|
+
|
|
1578
|
+
# Save to history
|
|
1579
|
+
session_entry = summarize_session(desc, outcome, iters)
|
|
1580
|
+
history_data["sessions"].append(session_entry)
|
|
1581
|
+
|
|
1582
|
+
# Save log
|
|
1583
|
+
save_log(idx + 1, r.get("output", "") if r else "", session_id)
|
|
1584
|
+
|
|
1585
|
+
save_history(working_dir, history_data)
|
|
1586
|
+
|
|
1587
|
+
print(f"\n {C.DIM}Session: {session_id}{C.RESET}")
|
|
1588
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1589
|
+
|
|
1590
|
+
all_resolved = all(r and r["outcome"] == "RESOLVED" for r in results)
|
|
1591
|
+
return 0 if all_resolved else 1
|
|
1592
|
+
|
|
1593
|
+
# ========================================================================
|
|
1594
|
+
# SEQUENTIAL MODE (original behavior)
|
|
1595
|
+
# ========================================================================
|
|
1596
|
+
|
|
1597
|
+
# Start input monitor
|
|
1598
|
+
input_monitor = InputMonitor()
|
|
1599
|
+
input_monitor.start()
|
|
1600
|
+
|
|
1601
|
+
# Session-wide token tracking (aggregates across all tasks)
|
|
1602
|
+
session_token_tracker = TokenTracker()
|
|
1603
|
+
|
|
932
1604
|
# === OUTER LOOP: iterate over tasks ===
|
|
933
1605
|
while True:
|
|
934
1606
|
task = task_queue.next()
|
|
@@ -1004,6 +1676,7 @@ Then fix it. Use the reasoning protocol before each action.
|
|
|
1004
1676
|
"""
|
|
1005
1677
|
|
|
1006
1678
|
iteration_history = []
|
|
1679
|
+
task_token_tracker = TokenTracker()
|
|
1007
1680
|
|
|
1008
1681
|
# === INNER LOOP: iterations for current task ===
|
|
1009
1682
|
for iteration in range(1, args.max_iterations + 1):
|
|
@@ -1027,11 +1700,20 @@ Then fix it. Use the reasoning protocol before each action.
|
|
|
1027
1700
|
full_prompt, working_dir,
|
|
1028
1701
|
continue_session=continue_session,
|
|
1029
1702
|
stream=True, timeout=args.timeout,
|
|
1030
|
-
input_monitor=input_monitor
|
|
1703
|
+
input_monitor=input_monitor,
|
|
1704
|
+
swarm_mode=args.swarm,
|
|
1705
|
+
swarm_budget_max=args.swarm_budget,
|
|
1706
|
+
swarm_depth_max=args.swarm_depth,
|
|
1707
|
+
token_tracker=task_token_tracker,
|
|
1708
|
+
show_tokens=args.show_tokens
|
|
1031
1709
|
)
|
|
1032
1710
|
|
|
1033
1711
|
print(f"\n{C.BRIGHT_BLUE} {'─' * 70}{C.RESET}")
|
|
1034
1712
|
|
|
1713
|
+
# Print token usage summary for this iteration
|
|
1714
|
+
if task_token_tracker.turn_count > 0:
|
|
1715
|
+
print(f" {C.BRIGHT_CYAN}⊛ {task_token_tracker.summary_line()}{C.RESET}")
|
|
1716
|
+
|
|
1035
1717
|
# Save log
|
|
1036
1718
|
save_log(iteration, output, session_id)
|
|
1037
1719
|
iteration_history.append(output)
|
|
@@ -1085,22 +1767,34 @@ Continue the current debugging task. Use the reasoning protocol before each acti
|
|
|
1085
1767
|
elif user_cmd == "done":
|
|
1086
1768
|
# Clean exit
|
|
1087
1769
|
input_monitor.stop()
|
|
1088
|
-
session_entry = summarize_session(bug_input, "DONE_BY_USER", iteration)
|
|
1770
|
+
session_entry = summarize_session(bug_input, "DONE_BY_USER", iteration, task_token_tracker)
|
|
1089
1771
|
history_data["sessions"].append(session_entry)
|
|
1090
1772
|
save_history(working_dir, history_data)
|
|
1091
1773
|
print(f"\n {C.GREEN}✓{C.RESET} Session saved. Exiting by user request.")
|
|
1092
1774
|
return 0
|
|
1093
1775
|
|
|
1776
|
+
elif user_cmd == "stop":
|
|
1777
|
+
# Boris-commanded stop - terminate this iteration immediately
|
|
1778
|
+
print(f"\n {C.BRIGHT_RED}{C.BOLD} ■ STOPPED BY BORIS{C.RESET}")
|
|
1779
|
+
print(f"{C.BRIGHT_RED} {'─' * 70}{C.RESET}")
|
|
1780
|
+
input_monitor.stop()
|
|
1781
|
+
session_entry = summarize_session(bug_input, "STOPPED_BY_BORIS", iteration, task_token_tracker)
|
|
1782
|
+
history_data["sessions"].append(session_entry)
|
|
1783
|
+
save_history(working_dir, history_data)
|
|
1784
|
+
return 1
|
|
1785
|
+
|
|
1094
1786
|
# Check exit condition
|
|
1095
1787
|
signal, should_exit = check_exit_condition(output)
|
|
1096
1788
|
|
|
1097
1789
|
if should_exit:
|
|
1098
1790
|
if signal == "RESOLVED":
|
|
1099
1791
|
print_success_box("")
|
|
1792
|
+
if task_token_tracker.turn_count > 0:
|
|
1793
|
+
print(f" {C.BRIGHT_CYAN}⊛ {task_token_tracker.summary_line()}{C.RESET}")
|
|
1100
1794
|
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1101
1795
|
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1102
1796
|
task_queue.mark_done()
|
|
1103
|
-
session_entry = summarize_session(bug_input, "RESOLVED", iteration)
|
|
1797
|
+
session_entry = summarize_session(bug_input, "RESOLVED", iteration, task_token_tracker)
|
|
1104
1798
|
history_data["sessions"].append(session_entry)
|
|
1105
1799
|
save_history(working_dir, history_data)
|
|
1106
1800
|
break # Move to next task
|
|
@@ -1126,14 +1820,14 @@ Continue debugging with this information. Use the reasoning protocol before each
|
|
|
1126
1820
|
print_status("Logs", str(LOG_DIR), C.WHITE)
|
|
1127
1821
|
print()
|
|
1128
1822
|
task_queue.mark_failed()
|
|
1129
|
-
session_entry = summarize_session(bug_input, "BLOCKED", iteration)
|
|
1823
|
+
session_entry = summarize_session(bug_input, "BLOCKED", iteration, task_token_tracker)
|
|
1130
1824
|
history_data["sessions"].append(session_entry)
|
|
1131
1825
|
save_history(working_dir, history_data)
|
|
1132
1826
|
break # Move to next task
|
|
1133
1827
|
else:
|
|
1134
1828
|
print_error_box(f"Error occurred: {signal}")
|
|
1135
1829
|
task_queue.mark_failed()
|
|
1136
|
-
session_entry = summarize_session(bug_input, "ERROR", iteration)
|
|
1830
|
+
session_entry = summarize_session(bug_input, "ERROR", iteration, task_token_tracker)
|
|
1137
1831
|
history_data["sessions"].append(session_entry)
|
|
1138
1832
|
save_history(working_dir, history_data)
|
|
1139
1833
|
break # Move to next task
|
|
@@ -1173,15 +1867,33 @@ Use the reasoning protocol before each action.
|
|
|
1173
1867
|
# Max iterations reached for this task (for-else)
|
|
1174
1868
|
print_warning_box(f"Max iterations ({args.max_iterations}) reached for current task")
|
|
1175
1869
|
task_queue.mark_failed()
|
|
1176
|
-
session_entry = summarize_session(bug_input, "MAX_ITERATIONS", args.max_iterations)
|
|
1870
|
+
session_entry = summarize_session(bug_input, "MAX_ITERATIONS", args.max_iterations, task_token_tracker)
|
|
1177
1871
|
history_data["sessions"].append(session_entry)
|
|
1178
1872
|
save_history(working_dir, history_data)
|
|
1179
1873
|
|
|
1874
|
+
# Aggregate task tokens into session-level tracker
|
|
1875
|
+
if task_token_tracker.turn_count > 0:
|
|
1876
|
+
session_token_tracker.total_input += task_token_tracker.total_input
|
|
1877
|
+
session_token_tracker.total_output += task_token_tracker.total_output
|
|
1878
|
+
session_token_tracker.turn_count += task_token_tracker.turn_count
|
|
1879
|
+
if task_token_tracker.peak_total > session_token_tracker.peak_total:
|
|
1880
|
+
session_token_tracker.peak_total = task_token_tracker.peak_total
|
|
1881
|
+
session_token_tracker.peak_input = task_token_tracker.peak_input
|
|
1882
|
+
session_token_tracker.peak_output = task_token_tracker.peak_output
|
|
1883
|
+
for tool, stats in task_token_tracker.per_tool.items():
|
|
1884
|
+
if tool not in session_token_tracker.per_tool:
|
|
1885
|
+
session_token_tracker.per_tool[tool] = {"input": 0, "output": 0, "count": 0}
|
|
1886
|
+
session_token_tracker.per_tool[tool]["input"] += stats["input"]
|
|
1887
|
+
session_token_tracker.per_tool[tool]["output"] += stats["output"]
|
|
1888
|
+
session_token_tracker.per_tool[tool]["count"] += stats["count"]
|
|
1889
|
+
|
|
1180
1890
|
# Save iteration summary for this task
|
|
1181
1891
|
LOG_DIR.mkdir(exist_ok=True)
|
|
1182
1892
|
summary = f"# DaveLoop Session {session_id}\n\n"
|
|
1183
1893
|
summary += f"Bug: {bug_input[:200]}...\n\n"
|
|
1184
1894
|
summary += f"Iterations: {len(iteration_history)}\n\n"
|
|
1895
|
+
if task_token_tracker.turn_count > 0:
|
|
1896
|
+
summary += f"Token Usage: {task_token_tracker.summary_line()}\n\n"
|
|
1185
1897
|
summary += "## Iteration History\n\n"
|
|
1186
1898
|
for i, hist in enumerate(iteration_history, 1):
|
|
1187
1899
|
summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
|
|
@@ -1203,6 +1915,16 @@ Use the reasoning protocol before each action.
|
|
|
1203
1915
|
print(f" {C.DIM}○ {desc}{C.RESET}")
|
|
1204
1916
|
print()
|
|
1205
1917
|
|
|
1918
|
+
# Print session-wide token usage
|
|
1919
|
+
if session_token_tracker.turn_count > 0:
|
|
1920
|
+
print(f" {C.BRIGHT_CYAN}⊛ {session_token_tracker.summary_line()}{C.RESET}")
|
|
1921
|
+
if session_token_tracker.per_tool:
|
|
1922
|
+
print(f" {C.DIM} Per tool:{C.RESET}")
|
|
1923
|
+
for tool, stats in sorted(session_token_tracker.per_tool.items(), key=lambda x: x[1]["input"] + x[1]["output"], reverse=True):
|
|
1924
|
+
tool_total = stats["input"] + stats["output"]
|
|
1925
|
+
print(f" {C.DIM} {tool}: {stats['input']:,} in / {stats['output']:,} out / {tool_total:,} total ({stats['count']} calls){C.RESET}")
|
|
1926
|
+
print()
|
|
1927
|
+
|
|
1206
1928
|
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1207
1929
|
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1208
1930
|
|