code-puppy 0.0.348__py3-none-any.whl → 0.0.361__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.
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_manager.py +49 -0
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +17 -4
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/claude_cache_client.py +249 -34
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/config.py +66 -62
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_utils.py +54 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
- code_puppy/plugins/antigravity_oauth/transport.py +1 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/camoufox_manager.py +226 -64
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/RECORD +69 -38
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
code_puppy/messaging/messages.py
CHANGED
|
@@ -292,6 +292,31 @@ class SubAgentResponseMessage(BaseMessage):
|
|
|
292
292
|
)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
class SubAgentStatusMessage(BaseMessage):
|
|
296
|
+
"""Real-time status update for a running sub-agent."""
|
|
297
|
+
|
|
298
|
+
category: MessageCategory = MessageCategory.AGENT
|
|
299
|
+
session_id: str = Field(description="Unique session ID of the sub-agent")
|
|
300
|
+
agent_name: str = Field(description="Name of the agent (e.g., 'code-puppy')")
|
|
301
|
+
model_name: str = Field(description="Model being used by this agent")
|
|
302
|
+
status: Literal[
|
|
303
|
+
"starting", "running", "thinking", "tool_calling", "completed", "error"
|
|
304
|
+
] = Field(description="Current status of the agent")
|
|
305
|
+
tool_call_count: int = Field(
|
|
306
|
+
default=0, ge=0, description="Number of tools called so far"
|
|
307
|
+
)
|
|
308
|
+
token_count: int = Field(default=0, ge=0, description="Estimated tokens in context")
|
|
309
|
+
current_tool: Optional[str] = Field(
|
|
310
|
+
default=None, description="Name of tool currently being called"
|
|
311
|
+
)
|
|
312
|
+
elapsed_seconds: float = Field(
|
|
313
|
+
default=0.0, ge=0, description="Time since agent started"
|
|
314
|
+
)
|
|
315
|
+
error_message: Optional[str] = Field(
|
|
316
|
+
default=None, description="Error message if status is 'error'"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
295
320
|
# =============================================================================
|
|
296
321
|
# User Interaction Messages (Agent → User)
|
|
297
322
|
# =============================================================================
|
|
@@ -417,6 +442,7 @@ AnyMessage = Union[
|
|
|
417
442
|
AgentResponseMessage,
|
|
418
443
|
SubAgentInvocationMessage,
|
|
419
444
|
SubAgentResponseMessage,
|
|
445
|
+
SubAgentStatusMessage,
|
|
420
446
|
UserInputRequest,
|
|
421
447
|
ConfirmationRequest,
|
|
422
448
|
SelectionRequest,
|
|
@@ -458,6 +484,7 @@ __all__ = [
|
|
|
458
484
|
"AgentResponseMessage",
|
|
459
485
|
"SubAgentInvocationMessage",
|
|
460
486
|
"SubAgentResponseMessage",
|
|
487
|
+
"SubAgentStatusMessage",
|
|
461
488
|
# User interaction
|
|
462
489
|
"UserInputRequest",
|
|
463
490
|
"ConfirmationRequest",
|
|
@@ -239,7 +239,7 @@ class QueueConsole:
|
|
|
239
239
|
# Show the user's response in the chat as well
|
|
240
240
|
if user_response:
|
|
241
241
|
self.queue.emit_simple(
|
|
242
|
-
MessageType.
|
|
242
|
+
MessageType.INFO, f"User response: {user_response}"
|
|
243
243
|
)
|
|
244
244
|
|
|
245
245
|
return user_response
|
|
@@ -18,7 +18,9 @@ from rich.rule import Rule
|
|
|
18
18
|
# Note: Syntax import removed - file content not displayed, only header
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
|
|
21
|
+
from code_puppy.config import get_subagent_verbose
|
|
21
22
|
from code_puppy.tools.common import format_diff_with_colors
|
|
23
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
22
24
|
|
|
23
25
|
from .bus import MessageBus
|
|
24
26
|
from .commands import (
|
|
@@ -159,6 +161,14 @@ class RichConsoleRenderer:
|
|
|
159
161
|
color = self._get_banner_color(banner_name)
|
|
160
162
|
return f"[bold white on {color}] {text} [/bold white on {color}]"
|
|
161
163
|
|
|
164
|
+
def _should_suppress_subagent_output(self) -> bool:
|
|
165
|
+
"""Check if sub-agent output should be suppressed.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if we're in a sub-agent context and verbose mode is disabled
|
|
169
|
+
"""
|
|
170
|
+
return is_subagent() and not get_subagent_verbose()
|
|
171
|
+
|
|
162
172
|
# =========================================================================
|
|
163
173
|
# Lifecycle (Synchronous - for compatibility with main.py)
|
|
164
174
|
# =========================================================================
|
|
@@ -275,7 +285,8 @@ class RichConsoleRenderer:
|
|
|
275
285
|
elif isinstance(message, SubAgentInvocationMessage):
|
|
276
286
|
self._render_subagent_invocation(message)
|
|
277
287
|
elif isinstance(message, SubAgentResponseMessage):
|
|
278
|
-
|
|
288
|
+
# Skip rendering - we now display sub-agent responses via display_non_streamed_result
|
|
289
|
+
pass
|
|
279
290
|
elif isinstance(message, UserInputRequest):
|
|
280
291
|
# Can't handle async user input in sync context - skip
|
|
281
292
|
self._console.print("[dim]User input requested (requires async)[/dim]")
|
|
@@ -356,6 +367,10 @@ class RichConsoleRenderer:
|
|
|
356
367
|
- Total size
|
|
357
368
|
- Number of subdirectories
|
|
358
369
|
"""
|
|
370
|
+
# Skip for sub-agents unless verbose mode
|
|
371
|
+
if self._should_suppress_subagent_output():
|
|
372
|
+
return
|
|
373
|
+
|
|
359
374
|
import os
|
|
360
375
|
from collections import defaultdict
|
|
361
376
|
|
|
@@ -478,6 +493,10 @@ class RichConsoleRenderer:
|
|
|
478
493
|
|
|
479
494
|
The file content is for the LLM only, not for display in the UI.
|
|
480
495
|
"""
|
|
496
|
+
# Skip for sub-agents unless verbose mode
|
|
497
|
+
if self._should_suppress_subagent_output():
|
|
498
|
+
return
|
|
499
|
+
|
|
481
500
|
# Build line info
|
|
482
501
|
line_info = ""
|
|
483
502
|
if msg.start_line is not None and msg.num_lines is not None:
|
|
@@ -492,6 +511,10 @@ class RichConsoleRenderer:
|
|
|
492
511
|
|
|
493
512
|
def _render_grep_result(self, msg: GrepResultMessage) -> None:
|
|
494
513
|
"""Render grep results grouped by file matching old format."""
|
|
514
|
+
# Skip for sub-agents unless verbose mode
|
|
515
|
+
if self._should_suppress_subagent_output():
|
|
516
|
+
return
|
|
517
|
+
|
|
495
518
|
import re
|
|
496
519
|
|
|
497
520
|
# Header
|
|
@@ -572,6 +595,10 @@ class RichConsoleRenderer:
|
|
|
572
595
|
|
|
573
596
|
def _render_diff(self, msg: DiffMessage) -> None:
|
|
574
597
|
"""Render a diff with beautiful syntax highlighting."""
|
|
598
|
+
# Skip for sub-agents unless verbose mode
|
|
599
|
+
if self._should_suppress_subagent_output():
|
|
600
|
+
return
|
|
601
|
+
|
|
575
602
|
# Operation-specific styling
|
|
576
603
|
op_icons = {"create": "✨", "modify": "✏️", "delete": "🗑️"}
|
|
577
604
|
op_colors = {"create": "green", "modify": "yellow", "delete": "red"}
|
|
@@ -616,6 +643,10 @@ class RichConsoleRenderer:
|
|
|
616
643
|
|
|
617
644
|
def _render_shell_start(self, msg: ShellStartMessage) -> None:
|
|
618
645
|
"""Render shell command start notification."""
|
|
646
|
+
# Skip for sub-agents unless verbose mode
|
|
647
|
+
if self._should_suppress_subagent_output():
|
|
648
|
+
return
|
|
649
|
+
|
|
619
650
|
# Escape command to prevent Rich markup injection
|
|
620
651
|
safe_command = escape_rich_markup(msg.command)
|
|
621
652
|
# Header showing command is starting
|
|
@@ -700,6 +731,10 @@ class RichConsoleRenderer:
|
|
|
700
731
|
|
|
701
732
|
def _render_subagent_invocation(self, msg: SubAgentInvocationMessage) -> None:
|
|
702
733
|
"""Render sub-agent invocation header with nice formatting."""
|
|
734
|
+
# Skip for sub-agents unless verbose mode (avoid nested invocation banners)
|
|
735
|
+
if self._should_suppress_subagent_output():
|
|
736
|
+
return
|
|
737
|
+
|
|
703
738
|
# Header with agent name and session
|
|
704
739
|
session_type = (
|
|
705
740
|
"New session"
|
|
@@ -24,7 +24,16 @@ def unregister_spinner(spinner):
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def pause_all_spinners():
|
|
27
|
-
"""Pause all active spinners.
|
|
27
|
+
"""Pause all active spinners.
|
|
28
|
+
|
|
29
|
+
No-op when called from a sub-agent context to prevent
|
|
30
|
+
parallel sub-agents from interfering with the main spinner.
|
|
31
|
+
"""
|
|
32
|
+
# Lazy import to avoid circular dependency
|
|
33
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
34
|
+
|
|
35
|
+
if is_subagent():
|
|
36
|
+
return # Sub-agents don't control the main spinner
|
|
28
37
|
for spinner in _active_spinners:
|
|
29
38
|
try:
|
|
30
39
|
spinner.pause()
|
|
@@ -34,7 +43,16 @@ def pause_all_spinners():
|
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
def resume_all_spinners():
|
|
37
|
-
"""Resume all active spinners.
|
|
46
|
+
"""Resume all active spinners.
|
|
47
|
+
|
|
48
|
+
No-op when called from a sub-agent context to prevent
|
|
49
|
+
parallel sub-agents from interfering with the main spinner.
|
|
50
|
+
"""
|
|
51
|
+
# Lazy import to avoid circular dependency
|
|
52
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
53
|
+
|
|
54
|
+
if is_subagent():
|
|
55
|
+
return # Sub-agents don't control the main spinner
|
|
38
56
|
for spinner in _active_spinners:
|
|
39
57
|
try:
|
|
40
58
|
spinner.resume()
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""SubAgentConsoleManager - Aggregated display for parallel sub-agents.
|
|
2
|
+
|
|
3
|
+
Provides a Rich Live dashboard that shows real-time status of multiple
|
|
4
|
+
running sub-agents, each in its own panel with spinner animations,
|
|
5
|
+
status badges, and performance metrics.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
>>> manager = SubAgentConsoleManager.get_instance()
|
|
9
|
+
>>> manager.register_agent("session-123", "code-puppy", "gpt-4o")
|
|
10
|
+
>>> manager.update_agent("session-123", status="running", tool_call_count=5)
|
|
11
|
+
>>> manager.unregister_agent("session-123")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from rich.console import Console, Group
|
|
20
|
+
from rich.live import Live
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
|
|
25
|
+
from code_puppy.messaging.messages import SubAgentStatusMessage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Status Configuration
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
STATUS_STYLES = {
|
|
33
|
+
"starting": {"color": "cyan", "spinner": "dots", "emoji": "🚀"},
|
|
34
|
+
"running": {"color": "green", "spinner": "dots", "emoji": "🐕"},
|
|
35
|
+
"thinking": {"color": "magenta", "spinner": "dots", "emoji": "🤔"},
|
|
36
|
+
"tool_calling": {"color": "yellow", "spinner": "dots12", "emoji": "🔧"},
|
|
37
|
+
"completed": {"color": "green", "spinner": None, "emoji": "✅"},
|
|
38
|
+
"error": {"color": "red", "spinner": None, "emoji": "❌"},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DEFAULT_STYLE = {"color": "white", "spinner": "dots", "emoji": "⏳"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Agent State Tracking
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AgentState:
|
|
51
|
+
"""Internal state tracking for a single sub-agent.
|
|
52
|
+
|
|
53
|
+
Tracks all metrics needed for rendering the agent's status panel,
|
|
54
|
+
including timing, tool usage, and error information.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
session_id: str
|
|
58
|
+
agent_name: str
|
|
59
|
+
model_name: str
|
|
60
|
+
status: str = "starting"
|
|
61
|
+
tool_call_count: int = 0
|
|
62
|
+
token_count: int = 0
|
|
63
|
+
current_tool: Optional[str] = None
|
|
64
|
+
start_time: float = field(default_factory=time.time)
|
|
65
|
+
error_message: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
def elapsed_seconds(self) -> float:
|
|
68
|
+
"""Calculate elapsed time since agent started."""
|
|
69
|
+
return time.time() - self.start_time
|
|
70
|
+
|
|
71
|
+
def elapsed_formatted(self) -> str:
|
|
72
|
+
"""Format elapsed time as human-readable string."""
|
|
73
|
+
elapsed = self.elapsed_seconds()
|
|
74
|
+
if elapsed < 60:
|
|
75
|
+
return f"{elapsed:.1f}s"
|
|
76
|
+
minutes = int(elapsed // 60)
|
|
77
|
+
seconds = elapsed % 60
|
|
78
|
+
return f"{minutes}m {seconds:.1f}s"
|
|
79
|
+
|
|
80
|
+
def to_status_message(self) -> SubAgentStatusMessage:
|
|
81
|
+
"""Convert to a SubAgentStatusMessage for bus emission."""
|
|
82
|
+
return SubAgentStatusMessage(
|
|
83
|
+
session_id=self.session_id,
|
|
84
|
+
agent_name=self.agent_name,
|
|
85
|
+
model_name=self.model_name,
|
|
86
|
+
status=self.status, # type: ignore[arg-type]
|
|
87
|
+
tool_call_count=self.tool_call_count,
|
|
88
|
+
token_count=self.token_count,
|
|
89
|
+
current_tool=self.current_tool,
|
|
90
|
+
elapsed_seconds=self.elapsed_seconds(),
|
|
91
|
+
error_message=self.error_message,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# SubAgent Console Manager
|
|
97
|
+
# =============================================================================
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SubAgentConsoleManager:
|
|
101
|
+
"""Manager for displaying multiple parallel sub-agents in Rich Live panels.
|
|
102
|
+
|
|
103
|
+
This is a singleton that tracks all running sub-agents and renders them
|
|
104
|
+
in a unified Rich Live display. Each agent gets its own panel with:
|
|
105
|
+
- Agent name and session ID
|
|
106
|
+
- Model being used
|
|
107
|
+
- Status with spinner animation (for active states)
|
|
108
|
+
- Tool call count and current tool
|
|
109
|
+
- Token count
|
|
110
|
+
- Elapsed time
|
|
111
|
+
|
|
112
|
+
The display auto-starts when the first agent registers and auto-stops
|
|
113
|
+
when the last agent unregisters.
|
|
114
|
+
|
|
115
|
+
Thread-safe: All operations are protected by locks.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
_instance: Optional["SubAgentConsoleManager"] = None
|
|
119
|
+
_lock = threading.Lock()
|
|
120
|
+
|
|
121
|
+
def __init__(self, console: Optional[Console] = None):
|
|
122
|
+
"""Initialize the manager.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
console: Optional Rich Console instance. If not provided,
|
|
126
|
+
a new one will be created.
|
|
127
|
+
"""
|
|
128
|
+
self.console = console or Console()
|
|
129
|
+
self._agents: Dict[str, AgentState] = {}
|
|
130
|
+
self._agents_lock = threading.RLock() # Reentrant lock for agent operations
|
|
131
|
+
self._live: Optional[Live] = None
|
|
132
|
+
self._update_thread: Optional[threading.Thread] = None
|
|
133
|
+
self._stop_event = threading.Event()
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_instance(
|
|
137
|
+
cls, console: Optional[Console] = None
|
|
138
|
+
) -> "SubAgentConsoleManager":
|
|
139
|
+
"""Get or create the singleton instance.
|
|
140
|
+
|
|
141
|
+
Thread-safe singleton pattern using double-checked locking.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
console: Optional Rich Console to use. Only used when creating
|
|
145
|
+
the initial instance.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The singleton SubAgentConsoleManager instance.
|
|
149
|
+
"""
|
|
150
|
+
if cls._instance is None:
|
|
151
|
+
with cls._lock:
|
|
152
|
+
# Double-check inside lock
|
|
153
|
+
if cls._instance is None:
|
|
154
|
+
cls._instance = cls(console)
|
|
155
|
+
return cls._instance
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def reset_instance(cls) -> None:
|
|
159
|
+
"""Reset the singleton instance (primarily for testing).
|
|
160
|
+
|
|
161
|
+
Stops any running display and clears the singleton.
|
|
162
|
+
"""
|
|
163
|
+
with cls._lock:
|
|
164
|
+
if cls._instance is not None:
|
|
165
|
+
cls._instance._stop_display()
|
|
166
|
+
cls._instance = None
|
|
167
|
+
|
|
168
|
+
# =========================================================================
|
|
169
|
+
# Agent Registration
|
|
170
|
+
# =========================================================================
|
|
171
|
+
|
|
172
|
+
def register_agent(self, session_id: str, agent_name: str, model_name: str) -> None:
|
|
173
|
+
"""Register a new sub-agent and start display if needed.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
session_id: Unique identifier for this agent session.
|
|
177
|
+
agent_name: Name of the agent (e.g., 'code-puppy', 'qa-kitten').
|
|
178
|
+
model_name: Name of the model being used (e.g., 'gpt-4o').
|
|
179
|
+
"""
|
|
180
|
+
with self._agents_lock:
|
|
181
|
+
# Create new agent state
|
|
182
|
+
self._agents[session_id] = AgentState(
|
|
183
|
+
session_id=session_id,
|
|
184
|
+
agent_name=agent_name,
|
|
185
|
+
model_name=model_name,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Start display if this is the first agent
|
|
189
|
+
if len(self._agents) == 1:
|
|
190
|
+
self._start_display()
|
|
191
|
+
|
|
192
|
+
def update_agent(self, session_id: str, **kwargs) -> None:
|
|
193
|
+
"""Update status of an existing agent.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
session_id: The session ID of the agent to update.
|
|
197
|
+
**kwargs: Fields to update. Valid fields:
|
|
198
|
+
- status: Current status string
|
|
199
|
+
- tool_call_count: Number of tools called
|
|
200
|
+
- token_count: Tokens in context
|
|
201
|
+
- current_tool: Name of tool being called (or None)
|
|
202
|
+
- error_message: Error message if status is 'error'
|
|
203
|
+
"""
|
|
204
|
+
with self._agents_lock:
|
|
205
|
+
if session_id not in self._agents:
|
|
206
|
+
return # Silently ignore updates for unknown agents
|
|
207
|
+
|
|
208
|
+
agent = self._agents[session_id]
|
|
209
|
+
|
|
210
|
+
# Update only provided fields
|
|
211
|
+
if "status" in kwargs:
|
|
212
|
+
agent.status = kwargs["status"]
|
|
213
|
+
if "tool_call_count" in kwargs:
|
|
214
|
+
agent.tool_call_count = kwargs["tool_call_count"]
|
|
215
|
+
if "token_count" in kwargs:
|
|
216
|
+
agent.token_count = kwargs["token_count"]
|
|
217
|
+
if "current_tool" in kwargs:
|
|
218
|
+
agent.current_tool = kwargs["current_tool"]
|
|
219
|
+
if "error_message" in kwargs:
|
|
220
|
+
agent.error_message = kwargs["error_message"]
|
|
221
|
+
|
|
222
|
+
def unregister_agent(
|
|
223
|
+
self, session_id: str, final_status: str = "completed"
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Remove an agent from tracking.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
session_id: The session ID of the agent to remove.
|
|
229
|
+
final_status: Final status to set before removal (for display).
|
|
230
|
+
Defaults to 'completed'.
|
|
231
|
+
"""
|
|
232
|
+
with self._agents_lock:
|
|
233
|
+
if session_id in self._agents:
|
|
234
|
+
# Set final status
|
|
235
|
+
self._agents[session_id].status = final_status
|
|
236
|
+
# Remove from tracking
|
|
237
|
+
del self._agents[session_id]
|
|
238
|
+
|
|
239
|
+
# Stop display if no agents remain
|
|
240
|
+
if not self._agents:
|
|
241
|
+
self._stop_display()
|
|
242
|
+
|
|
243
|
+
def get_agent_state(self, session_id: str) -> Optional[AgentState]:
|
|
244
|
+
"""Get the current state of an agent.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
session_id: The session ID to look up.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
The AgentState if found, None otherwise.
|
|
251
|
+
"""
|
|
252
|
+
with self._agents_lock:
|
|
253
|
+
return self._agents.get(session_id)
|
|
254
|
+
|
|
255
|
+
def get_all_agents(self) -> List[AgentState]:
|
|
256
|
+
"""Get a list of all currently tracked agents.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of AgentState objects (copies to prevent mutation).
|
|
260
|
+
"""
|
|
261
|
+
with self._agents_lock:
|
|
262
|
+
return list(self._agents.values())
|
|
263
|
+
|
|
264
|
+
# =========================================================================
|
|
265
|
+
# Display Management
|
|
266
|
+
# =========================================================================
|
|
267
|
+
|
|
268
|
+
def _start_display(self) -> None:
|
|
269
|
+
"""Start the Rich Live display.
|
|
270
|
+
|
|
271
|
+
Creates the Live context and starts a background thread to
|
|
272
|
+
continuously refresh the display.
|
|
273
|
+
"""
|
|
274
|
+
if self._live is not None:
|
|
275
|
+
return # Already running
|
|
276
|
+
|
|
277
|
+
self._stop_event.clear()
|
|
278
|
+
|
|
279
|
+
# Create Live display
|
|
280
|
+
self._live = Live(
|
|
281
|
+
self._render_display(),
|
|
282
|
+
console=self.console,
|
|
283
|
+
refresh_per_second=10,
|
|
284
|
+
transient=True, # Clear when stopped
|
|
285
|
+
)
|
|
286
|
+
self._live.start()
|
|
287
|
+
|
|
288
|
+
# Start background update thread
|
|
289
|
+
self._update_thread = threading.Thread(
|
|
290
|
+
target=self._update_loop, daemon=True, name="SubAgentDisplayUpdater"
|
|
291
|
+
)
|
|
292
|
+
self._update_thread.start()
|
|
293
|
+
|
|
294
|
+
def _stop_display(self) -> None:
|
|
295
|
+
"""Stop the Rich Live display when no agents remain."""
|
|
296
|
+
# Signal stop
|
|
297
|
+
self._stop_event.set()
|
|
298
|
+
|
|
299
|
+
# Stop update thread
|
|
300
|
+
if self._update_thread is not None:
|
|
301
|
+
self._update_thread.join(timeout=1.0)
|
|
302
|
+
self._update_thread = None
|
|
303
|
+
|
|
304
|
+
# Stop Live display
|
|
305
|
+
if self._live is not None:
|
|
306
|
+
try:
|
|
307
|
+
self._live.stop()
|
|
308
|
+
except Exception:
|
|
309
|
+
pass # Ignore errors during cleanup
|
|
310
|
+
self._live = None
|
|
311
|
+
|
|
312
|
+
def _update_loop(self) -> None:
|
|
313
|
+
"""Background thread that refreshes the display."""
|
|
314
|
+
while not self._stop_event.is_set():
|
|
315
|
+
try:
|
|
316
|
+
if self._live is not None:
|
|
317
|
+
self._live.update(self._render_display())
|
|
318
|
+
except Exception:
|
|
319
|
+
pass # Ignore rendering errors, keep trying
|
|
320
|
+
|
|
321
|
+
# Sleep between updates (10 FPS)
|
|
322
|
+
time.sleep(0.1)
|
|
323
|
+
|
|
324
|
+
# =========================================================================
|
|
325
|
+
# Rendering
|
|
326
|
+
# =========================================================================
|
|
327
|
+
|
|
328
|
+
def _render_display(self) -> Group:
|
|
329
|
+
"""Render all agent panels as a Rich Group.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
A Group containing all agent panels stacked vertically.
|
|
333
|
+
"""
|
|
334
|
+
with self._agents_lock:
|
|
335
|
+
if not self._agents:
|
|
336
|
+
return Group(Text("No active sub-agents", style="dim"))
|
|
337
|
+
|
|
338
|
+
panels = [
|
|
339
|
+
self._render_agent_panel(agent) for agent in self._agents.values()
|
|
340
|
+
]
|
|
341
|
+
return Group(*panels)
|
|
342
|
+
|
|
343
|
+
def _render_agent_panel(self, agent: AgentState) -> Panel:
|
|
344
|
+
"""Render a single agent's status panel.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
agent: The AgentState to render.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
A Rich Panel containing the agent's status information.
|
|
351
|
+
"""
|
|
352
|
+
style_config = STATUS_STYLES.get(agent.status, DEFAULT_STYLE)
|
|
353
|
+
color = style_config["color"]
|
|
354
|
+
spinner_name = style_config["spinner"]
|
|
355
|
+
emoji = style_config["emoji"]
|
|
356
|
+
|
|
357
|
+
# Build the content table
|
|
358
|
+
table = Table.grid(padding=(0, 2))
|
|
359
|
+
table.add_column("label", style="dim")
|
|
360
|
+
table.add_column("value")
|
|
361
|
+
|
|
362
|
+
# Status row with spinner (if active)
|
|
363
|
+
status_text = Text()
|
|
364
|
+
status_text.append(f"{emoji} ", style=color)
|
|
365
|
+
if spinner_name:
|
|
366
|
+
# For active statuses, we add the status text
|
|
367
|
+
# The spinner is visual only in Rich Live
|
|
368
|
+
status_text.append(agent.status.upper(), style=f"bold {color}")
|
|
369
|
+
else:
|
|
370
|
+
status_text.append(agent.status.upper(), style=f"bold {color}")
|
|
371
|
+
|
|
372
|
+
table.add_row("Status:", status_text)
|
|
373
|
+
|
|
374
|
+
# Model
|
|
375
|
+
table.add_row("Model:", Text(agent.model_name, style="cyan"))
|
|
376
|
+
|
|
377
|
+
# Session ID (truncated for display)
|
|
378
|
+
session_display = agent.session_id
|
|
379
|
+
if len(session_display) > 24:
|
|
380
|
+
session_display = session_display[:21] + "..."
|
|
381
|
+
table.add_row("Session:", Text(session_display, style="dim"))
|
|
382
|
+
|
|
383
|
+
# Tool calls
|
|
384
|
+
tool_text = Text()
|
|
385
|
+
tool_text.append(str(agent.tool_call_count), style="bold yellow")
|
|
386
|
+
if agent.current_tool:
|
|
387
|
+
tool_text.append(" (calling: ", style="dim")
|
|
388
|
+
tool_text.append(agent.current_tool, style="yellow")
|
|
389
|
+
tool_text.append(")", style="dim")
|
|
390
|
+
table.add_row("Tools:", tool_text)
|
|
391
|
+
|
|
392
|
+
# Token count
|
|
393
|
+
token_display = f"{agent.token_count:,}" if agent.token_count else "0"
|
|
394
|
+
table.add_row("Tokens:", Text(token_display, style="blue"))
|
|
395
|
+
|
|
396
|
+
# Elapsed time
|
|
397
|
+
table.add_row("Elapsed:", Text(agent.elapsed_formatted(), style="magenta"))
|
|
398
|
+
|
|
399
|
+
# Error message (if any)
|
|
400
|
+
if agent.error_message:
|
|
401
|
+
error_text = Text(agent.error_message, style="red")
|
|
402
|
+
table.add_row("Error:", error_text)
|
|
403
|
+
|
|
404
|
+
# Build panel title with spinner for active states
|
|
405
|
+
title = Text()
|
|
406
|
+
title.append("🐕 ", style="bold")
|
|
407
|
+
title.append(agent.agent_name, style=f"bold {color}")
|
|
408
|
+
|
|
409
|
+
# Create panel
|
|
410
|
+
return Panel(
|
|
411
|
+
table,
|
|
412
|
+
title=title,
|
|
413
|
+
border_style=color,
|
|
414
|
+
padding=(0, 1),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# =========================================================================
|
|
418
|
+
# Context Manager Support
|
|
419
|
+
# =========================================================================
|
|
420
|
+
|
|
421
|
+
def __enter__(self) -> "SubAgentConsoleManager":
|
|
422
|
+
"""Support use as context manager."""
|
|
423
|
+
return self
|
|
424
|
+
|
|
425
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
426
|
+
"""Clean up on context exit."""
|
|
427
|
+
self._stop_display()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# =============================================================================
|
|
431
|
+
# Convenience Functions
|
|
432
|
+
# =============================================================================
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def get_subagent_console_manager(
|
|
436
|
+
console: Optional[Console] = None,
|
|
437
|
+
) -> SubAgentConsoleManager:
|
|
438
|
+
"""Get the singleton SubAgentConsoleManager instance.
|
|
439
|
+
|
|
440
|
+
Convenience function for accessing the manager.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
console: Optional Rich Console (only used on first call).
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
The singleton SubAgentConsoleManager.
|
|
447
|
+
"""
|
|
448
|
+
return SubAgentConsoleManager.get_instance(console)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# =============================================================================
|
|
452
|
+
# Exports
|
|
453
|
+
# =============================================================================
|
|
454
|
+
|
|
455
|
+
__all__ = [
|
|
456
|
+
"AgentState",
|
|
457
|
+
"SubAgentConsoleManager",
|
|
458
|
+
"get_subagent_console_manager",
|
|
459
|
+
"STATUS_STYLES",
|
|
460
|
+
"DEFAULT_STYLE",
|
|
461
|
+
]
|