kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
"""Agent Orchestrator Plugin for spawning and managing parallel kollab sub-agents.
|
|
2
|
+
|
|
3
|
+
This plugin enables the LLM to spawn sub-agents via XML commands in its responses.
|
|
4
|
+
Sub-agents run in tmux sessions and are monitored for completion via MD5 hashing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Any, Optional
|
|
16
|
+
|
|
17
|
+
from core.plugins.base import BasePlugin
|
|
18
|
+
from core.events.models import EventType, Hook, HookPriority
|
|
19
|
+
from core.io.visual_effects import ColorPalette
|
|
20
|
+
from core.io.message_renderer import DisplayFilterRegistry, MessageType
|
|
21
|
+
|
|
22
|
+
from .xml_parser import XMLCommandParser
|
|
23
|
+
from .orchestrator import AgentOrchestrator
|
|
24
|
+
from .activity_monitor import ActivityMonitor
|
|
25
|
+
from .message_injector import MessageInjector
|
|
26
|
+
from .models import AgentTask
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Agent orchestration instructions for the LLM
|
|
31
|
+
AGENT_INSTRUCTIONS = """
|
|
32
|
+
## Agent Orchestration
|
|
33
|
+
|
|
34
|
+
You can spawn parallel sub-agents to work on tasks concurrently. Each agent runs in a separate tmux session.
|
|
35
|
+
|
|
36
|
+
IMPORTANT: When using agent commands, output ONLY the XML tag. Do NOT add any explanation text before or after the command. The system will display the command execution result automatically.
|
|
37
|
+
|
|
38
|
+
### Spawn Agents
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<agent>
|
|
42
|
+
<agent-name>
|
|
43
|
+
<task>
|
|
44
|
+
objective: What to accomplish
|
|
45
|
+
|
|
46
|
+
context:
|
|
47
|
+
- Relevant constraints
|
|
48
|
+
- Background info
|
|
49
|
+
|
|
50
|
+
todo:
|
|
51
|
+
[ ] Step 1
|
|
52
|
+
[ ] Step 2
|
|
53
|
+
|
|
54
|
+
success: How to verify completion
|
|
55
|
+
</task>
|
|
56
|
+
<files>
|
|
57
|
+
<file>path/to/file.py</file>
|
|
58
|
+
</files>
|
|
59
|
+
</agent-name>
|
|
60
|
+
</agent>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Other Commands
|
|
64
|
+
|
|
65
|
+
```xml
|
|
66
|
+
<message to="agent-name">Send instruction to running agent</message>
|
|
67
|
+
<capture>agent-name 200</capture>
|
|
68
|
+
<stop>agent-name</stop>
|
|
69
|
+
<status></status>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Command Behavior
|
|
73
|
+
|
|
74
|
+
These commands work like tool calls:
|
|
75
|
+
1. You output the XML command (no additional text)
|
|
76
|
+
2. System executes and shows: `⏺ spawn_agent(name)` or `⏺ status()`
|
|
77
|
+
3. Result is injected back to you
|
|
78
|
+
4. You can then respond to the result if needed
|
|
79
|
+
|
|
80
|
+
Do NOT say things like "I'll spawn an agent" or "I've spawned..." - just output the XML.
|
|
81
|
+
|
|
82
|
+
Files in `<files>` are auto-attached to agent context.
|
|
83
|
+
Agents complete when idle for 6 seconds (MD5 hash unchanged).
|
|
84
|
+
""".strip()
|
|
85
|
+
|
|
86
|
+
# Keywords that trigger instruction injection
|
|
87
|
+
TRIGGER_KEYWORDS = [
|
|
88
|
+
"spawn agent",
|
|
89
|
+
"spawn agents",
|
|
90
|
+
"spawn an agent",
|
|
91
|
+
"spawn one agent",
|
|
92
|
+
"spawn multiple",
|
|
93
|
+
"parallel agent",
|
|
94
|
+
"parallel agents",
|
|
95
|
+
"sub-agent",
|
|
96
|
+
"sub-agents",
|
|
97
|
+
"subagent",
|
|
98
|
+
"subagents",
|
|
99
|
+
"<agent>",
|
|
100
|
+
"tmux agent",
|
|
101
|
+
"agent orchestrat",
|
|
102
|
+
"launch agent",
|
|
103
|
+
"create agent",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AgentOrchestratorPlugin(BasePlugin):
|
|
108
|
+
"""Plugin for spawning and managing parallel kollab sub-agents."""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
name: str = "agent_orchestrator",
|
|
113
|
+
event_bus=None,
|
|
114
|
+
renderer=None,
|
|
115
|
+
config=None,
|
|
116
|
+
):
|
|
117
|
+
"""Initialize the agent orchestrator plugin.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
name: Plugin name.
|
|
121
|
+
event_bus: Event bus for hook registration.
|
|
122
|
+
renderer: Terminal renderer.
|
|
123
|
+
config: Configuration manager.
|
|
124
|
+
"""
|
|
125
|
+
self.name = name
|
|
126
|
+
self.version = "1.0.0"
|
|
127
|
+
self.description = "Spawn and manage parallel kollab sub-agents"
|
|
128
|
+
self.enabled = True
|
|
129
|
+
|
|
130
|
+
self.event_bus = event_bus
|
|
131
|
+
self.renderer = renderer
|
|
132
|
+
self.config = config
|
|
133
|
+
self.command_registry = None
|
|
134
|
+
self.conversation_manager = None
|
|
135
|
+
|
|
136
|
+
# Components (initialized in initialize())
|
|
137
|
+
self.xml_parser = XMLCommandParser()
|
|
138
|
+
self.orchestrator: Optional[AgentOrchestrator] = None
|
|
139
|
+
self.activity_monitor: Optional[ActivityMonitor] = None
|
|
140
|
+
self.message_injector: Optional[MessageInjector] = None
|
|
141
|
+
|
|
142
|
+
self._monitor_task: Optional[asyncio.Task] = None
|
|
143
|
+
self._args: Optional[argparse.Namespace] = None
|
|
144
|
+
|
|
145
|
+
# Tracking for keyword trigger mode
|
|
146
|
+
self._last_injection_time: float = 0.0
|
|
147
|
+
self._message_count_since_injection: int = 0
|
|
148
|
+
|
|
149
|
+
self.logger = logger
|
|
150
|
+
|
|
151
|
+
# -------------------------------------------------------------------------
|
|
152
|
+
# CLI Args (static, called before app init)
|
|
153
|
+
# -------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def register_cli_args(parser: argparse.ArgumentParser) -> None:
|
|
157
|
+
"""Register CLI arguments for agent management."""
|
|
158
|
+
group = parser.add_argument_group("Agent Orchestrator")
|
|
159
|
+
group.add_argument(
|
|
160
|
+
"--session",
|
|
161
|
+
type=str,
|
|
162
|
+
metavar="NAME",
|
|
163
|
+
help="Agent session name to interact with",
|
|
164
|
+
)
|
|
165
|
+
group.add_argument(
|
|
166
|
+
"--capture",
|
|
167
|
+
type=int,
|
|
168
|
+
metavar="LINES",
|
|
169
|
+
default=None,
|
|
170
|
+
help="Capture N lines from session (requires --session)",
|
|
171
|
+
)
|
|
172
|
+
group.add_argument(
|
|
173
|
+
"--list-agents",
|
|
174
|
+
action="store_true",
|
|
175
|
+
help="List all active agents and exit",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def handle_early_args(args: argparse.Namespace) -> bool:
|
|
180
|
+
"""Handle args that exit before app starts."""
|
|
181
|
+
project_name = Path.cwd().name
|
|
182
|
+
|
|
183
|
+
if getattr(args, "list_agents", False):
|
|
184
|
+
orchestrator = AgentOrchestrator(project_name=project_name)
|
|
185
|
+
agents = orchestrator.list_agents()
|
|
186
|
+
if agents:
|
|
187
|
+
print("[agents]")
|
|
188
|
+
for agent in agents:
|
|
189
|
+
print(f" {agent.name:<20} {agent.status:<10} {agent.duration}")
|
|
190
|
+
else:
|
|
191
|
+
print("No active agents")
|
|
192
|
+
return True # exit
|
|
193
|
+
|
|
194
|
+
session = getattr(args, "session", None)
|
|
195
|
+
capture = getattr(args, "capture", None)
|
|
196
|
+
|
|
197
|
+
if session and capture:
|
|
198
|
+
orchestrator = AgentOrchestrator(project_name=project_name)
|
|
199
|
+
output = orchestrator.capture_output(session, capture)
|
|
200
|
+
print(output)
|
|
201
|
+
return True # exit
|
|
202
|
+
|
|
203
|
+
return False # continue normal startup
|
|
204
|
+
|
|
205
|
+
# -------------------------------------------------------------------------
|
|
206
|
+
# Plugin Lifecycle
|
|
207
|
+
# -------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
async def initialize(self, args: argparse.Namespace = None, **kwargs) -> None:
|
|
210
|
+
"""Initialize plugin components.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
args: Parsed CLI arguments.
|
|
214
|
+
**kwargs: Additional initialization parameters including:
|
|
215
|
+
- event_bus: Event bus for hook registration
|
|
216
|
+
- config: Configuration manager
|
|
217
|
+
- command_registry: Command registry for slash commands
|
|
218
|
+
- conversation_manager: Conversation manager instance
|
|
219
|
+
"""
|
|
220
|
+
self._args = args
|
|
221
|
+
|
|
222
|
+
# Get dependencies from kwargs
|
|
223
|
+
if "event_bus" in kwargs:
|
|
224
|
+
self.event_bus = kwargs["event_bus"]
|
|
225
|
+
if "config" in kwargs:
|
|
226
|
+
self.config = kwargs["config"]
|
|
227
|
+
if "command_registry" in kwargs:
|
|
228
|
+
self.command_registry = kwargs["command_registry"]
|
|
229
|
+
if "conversation_manager" in kwargs:
|
|
230
|
+
self.conversation_manager = kwargs["conversation_manager"]
|
|
231
|
+
|
|
232
|
+
# Initialize orchestrator
|
|
233
|
+
self.orchestrator = AgentOrchestrator(project_name=Path.cwd().name)
|
|
234
|
+
|
|
235
|
+
# Initialize message injector if we have conversation manager
|
|
236
|
+
if self.conversation_manager and self.event_bus:
|
|
237
|
+
self.message_injector = MessageInjector(
|
|
238
|
+
event_bus=self.event_bus,
|
|
239
|
+
conversation_manager=self.conversation_manager,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Get config values
|
|
243
|
+
poll_interval = 2
|
|
244
|
+
idle_threshold = 3
|
|
245
|
+
capture_lines = 500
|
|
246
|
+
|
|
247
|
+
if self.config:
|
|
248
|
+
poll_interval = self.config.get("plugins.agent_orchestrator.poll_interval", 2)
|
|
249
|
+
idle_threshold = self.config.get("plugins.agent_orchestrator.idle_threshold", 3)
|
|
250
|
+
capture_lines = self.config.get("plugins.agent_orchestrator.capture_lines", 500)
|
|
251
|
+
|
|
252
|
+
# Initialize activity monitor
|
|
253
|
+
self.activity_monitor = ActivityMonitor(
|
|
254
|
+
orchestrator=self.orchestrator,
|
|
255
|
+
on_agent_complete=self._on_agent_complete,
|
|
256
|
+
poll_interval=poll_interval,
|
|
257
|
+
idle_threshold=idle_threshold,
|
|
258
|
+
capture_lines=capture_lines,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Register display filter to strip orchestrator XML from displayed messages
|
|
262
|
+
DisplayFilterRegistry.register(
|
|
263
|
+
name="agent_orchestrator",
|
|
264
|
+
filter_fn=self._strip_orchestrator_xml,
|
|
265
|
+
message_types=[MessageType.ASSISTANT],
|
|
266
|
+
priority=100,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
logger.info("Agent orchestrator plugin initialized")
|
|
270
|
+
|
|
271
|
+
async def start_monitor(self) -> None:
|
|
272
|
+
"""Start the activity monitor background task."""
|
|
273
|
+
if self.activity_monitor and not self._monitor_task:
|
|
274
|
+
self._monitor_task = asyncio.create_task(self.activity_monitor.start())
|
|
275
|
+
logger.info("Activity monitor started")
|
|
276
|
+
|
|
277
|
+
def shutdown(self) -> None:
|
|
278
|
+
"""Cleanup on shutdown."""
|
|
279
|
+
if self._monitor_task:
|
|
280
|
+
self._monitor_task.cancel()
|
|
281
|
+
try:
|
|
282
|
+
# Can't await in sync method, just cancel
|
|
283
|
+
pass
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# Unregister display filter
|
|
288
|
+
DisplayFilterRegistry.unregister("agent_orchestrator")
|
|
289
|
+
|
|
290
|
+
logger.info("Agent orchestrator plugin shutdown")
|
|
291
|
+
|
|
292
|
+
# -------------------------------------------------------------------------
|
|
293
|
+
# Display Filter
|
|
294
|
+
# -------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
def _strip_orchestrator_xml(self, content: str, message_type: MessageType) -> str:
|
|
297
|
+
"""Strip orchestrator XML commands from content before display.
|
|
298
|
+
|
|
299
|
+
These commands are displayed separately as tool indicators,
|
|
300
|
+
so we strip them from the prose to avoid duplication.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
content: Message content to filter.
|
|
304
|
+
message_type: Type of message (only ASSISTANT messages filtered).
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Content with orchestrator XML stripped.
|
|
308
|
+
"""
|
|
309
|
+
# Strip <agent>...</agent> blocks
|
|
310
|
+
content = re.sub(r"<agent>.*?</agent>\s*", "", content, flags=re.DOTALL)
|
|
311
|
+
|
|
312
|
+
# Strip <status></status>
|
|
313
|
+
content = re.sub(r"<status>\s*</status>\s*", "", content)
|
|
314
|
+
|
|
315
|
+
# Strip <capture>...</capture>
|
|
316
|
+
content = re.sub(r"<capture>.*?</capture>\s*", "", content, flags=re.DOTALL)
|
|
317
|
+
|
|
318
|
+
# Strip <stop>...</stop>
|
|
319
|
+
content = re.sub(r"<stop>.*?</stop>\s*", "", content, flags=re.DOTALL)
|
|
320
|
+
|
|
321
|
+
# Strip <message to="...">...</message>
|
|
322
|
+
content = re.sub(
|
|
323
|
+
r'<message\s+to=["\'][^"\']+["\']>.*?</message>\s*',
|
|
324
|
+
"",
|
|
325
|
+
content,
|
|
326
|
+
flags=re.DOTALL,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Strip <clone>...</clone>
|
|
330
|
+
content = re.sub(r"<clone>.*?</clone>\s*", "", content, flags=re.DOTALL)
|
|
331
|
+
|
|
332
|
+
# Strip <team ...>...</team>
|
|
333
|
+
content = re.sub(r"<team\s+[^>]*>.*?</team>\s*", "", content, flags=re.DOTALL)
|
|
334
|
+
|
|
335
|
+
# Strip <broadcast ...>...</broadcast>
|
|
336
|
+
content = re.sub(
|
|
337
|
+
r"<broadcast\s+[^>]*>.*?</broadcast>\s*", "", content, flags=re.DOTALL
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return content
|
|
341
|
+
|
|
342
|
+
# -------------------------------------------------------------------------
|
|
343
|
+
# Hook Registration
|
|
344
|
+
# -------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
async def register_hooks(self) -> None:
|
|
347
|
+
"""Register hooks for LLM response processing and keyword triggers."""
|
|
348
|
+
if not self.event_bus:
|
|
349
|
+
logger.warning("No event bus available for hook registration")
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
# Hook for processing XML commands in LLM responses
|
|
353
|
+
response_hook = Hook(
|
|
354
|
+
name="agent_orchestrator_response",
|
|
355
|
+
plugin_name=self.name,
|
|
356
|
+
event_type=EventType.LLM_RESPONSE_POST,
|
|
357
|
+
callback=self._on_llm_response,
|
|
358
|
+
priority=HookPriority.POSTPROCESSING.value,
|
|
359
|
+
)
|
|
360
|
+
await self.event_bus.register_hook(response_hook)
|
|
361
|
+
|
|
362
|
+
# Hook for keyword-triggered instruction injection
|
|
363
|
+
keyword_hook = Hook(
|
|
364
|
+
name="agent_orchestrator_keyword_trigger",
|
|
365
|
+
plugin_name=self.name,
|
|
366
|
+
event_type=EventType.USER_INPUT_PRE,
|
|
367
|
+
callback=self._on_user_input,
|
|
368
|
+
priority=HookPriority.PREPROCESSING.value,
|
|
369
|
+
)
|
|
370
|
+
await self.event_bus.register_hook(keyword_hook)
|
|
371
|
+
|
|
372
|
+
logger.info("Registered agent orchestration hooks")
|
|
373
|
+
|
|
374
|
+
# -------------------------------------------------------------------------
|
|
375
|
+
# Default Config
|
|
376
|
+
# -------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def get_default_config() -> Dict[str, Any]:
|
|
380
|
+
"""Get default configuration for this plugin."""
|
|
381
|
+
return {
|
|
382
|
+
"plugins": {
|
|
383
|
+
"agent_orchestrator": {
|
|
384
|
+
"enabled": True,
|
|
385
|
+
"poll_interval": 2,
|
|
386
|
+
"idle_threshold": 3,
|
|
387
|
+
"capture_lines": 500,
|
|
388
|
+
"max_concurrent": 10,
|
|
389
|
+
# How to enable agent instructions for LLM:
|
|
390
|
+
# "startup" = inject into system prompt at startup
|
|
391
|
+
# "keyword" = inject when keywords detected in user input
|
|
392
|
+
# "disabled" = don't inject (LLM won't know about agents)
|
|
393
|
+
"enable_mode": "keyword",
|
|
394
|
+
# Trigger delay for keyword mode:
|
|
395
|
+
# "0" = inject once (first trigger only)
|
|
396
|
+
# "60s" = inject every 60 seconds
|
|
397
|
+
# "5m" = inject every 5 messages/turns
|
|
398
|
+
"trigger_delay": "0",
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def get_system_prompt_addition(self) -> Optional[str]:
|
|
404
|
+
"""Get system prompt addition if enable_mode is 'startup'.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Agent instructions string, or None if not startup mode.
|
|
408
|
+
"""
|
|
409
|
+
if not self.config:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
enable_mode = self.config.get("plugins.agent_orchestrator.enable_mode", "keyword")
|
|
413
|
+
if enable_mode == "startup":
|
|
414
|
+
return AGENT_INSTRUCTIONS
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
def _parse_trigger_delay(self, delay_str: str) -> tuple:
|
|
418
|
+
"""Parse trigger delay string.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
delay_str: Delay string like "0", "60s", "5m"
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Tuple of (delay_type, delay_value) where:
|
|
425
|
+
- ("once", 0) for "0"
|
|
426
|
+
- ("seconds", N) for "Ns"
|
|
427
|
+
- ("messages", N) for "Nm"
|
|
428
|
+
"""
|
|
429
|
+
delay_str = delay_str.strip().lower()
|
|
430
|
+
|
|
431
|
+
if delay_str == "0":
|
|
432
|
+
return ("once", 0)
|
|
433
|
+
|
|
434
|
+
if delay_str.endswith("s"):
|
|
435
|
+
try:
|
|
436
|
+
return ("seconds", int(delay_str[:-1]))
|
|
437
|
+
except ValueError:
|
|
438
|
+
return ("once", 0)
|
|
439
|
+
|
|
440
|
+
if delay_str.endswith("m"):
|
|
441
|
+
try:
|
|
442
|
+
return ("messages", int(delay_str[:-1]))
|
|
443
|
+
except ValueError:
|
|
444
|
+
return ("once", 0)
|
|
445
|
+
|
|
446
|
+
return ("once", 0)
|
|
447
|
+
|
|
448
|
+
def _should_inject(self) -> bool:
|
|
449
|
+
"""Check if we should inject instructions based on trigger_delay.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
True if injection should happen.
|
|
453
|
+
"""
|
|
454
|
+
import time
|
|
455
|
+
|
|
456
|
+
trigger_delay = self.config.get("plugins.agent_orchestrator.trigger_delay", "0") if self.config else "0"
|
|
457
|
+
delay_type, delay_value = self._parse_trigger_delay(trigger_delay)
|
|
458
|
+
|
|
459
|
+
if delay_type == "once":
|
|
460
|
+
# Only inject if never injected before
|
|
461
|
+
return self._last_injection_time == 0.0
|
|
462
|
+
|
|
463
|
+
elif delay_type == "seconds":
|
|
464
|
+
# Inject if enough time has passed
|
|
465
|
+
elapsed = time.time() - self._last_injection_time
|
|
466
|
+
return elapsed >= delay_value
|
|
467
|
+
|
|
468
|
+
elif delay_type == "messages":
|
|
469
|
+
# Inject if enough messages have passed
|
|
470
|
+
return self._message_count_since_injection >= delay_value
|
|
471
|
+
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
async def _on_user_input(self, data: dict, event) -> dict:
|
|
475
|
+
"""Check for trigger keywords and inject instructions.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
data: Event data with user input.
|
|
479
|
+
event: Event object.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Modified data with injected instructions.
|
|
483
|
+
"""
|
|
484
|
+
logger.info(f"[AGENT_ORCH] _on_user_input called with data keys: {list(data.keys())}")
|
|
485
|
+
context = data # Alias for compatibility
|
|
486
|
+
import time
|
|
487
|
+
|
|
488
|
+
if not self.config:
|
|
489
|
+
logger.info("[AGENT_ORCH] No config, returning early")
|
|
490
|
+
return context
|
|
491
|
+
|
|
492
|
+
enable_mode = self.config.get("plugins.agent_orchestrator.enable_mode", "keyword")
|
|
493
|
+
if enable_mode != "keyword":
|
|
494
|
+
return context
|
|
495
|
+
|
|
496
|
+
# Increment message count for tracking
|
|
497
|
+
self._message_count_since_injection += 1
|
|
498
|
+
|
|
499
|
+
# The event data uses "message" key, not "input"
|
|
500
|
+
user_input = context.get("message", context.get("input", "")).lower()
|
|
501
|
+
logger.info(f"[AGENT_ORCH] User input: {user_input[:50]}...")
|
|
502
|
+
|
|
503
|
+
# Check for trigger keywords
|
|
504
|
+
triggered = any(kw in user_input for kw in TRIGGER_KEYWORDS)
|
|
505
|
+
logger.info(f"[AGENT_ORCH] Triggered: {triggered}")
|
|
506
|
+
|
|
507
|
+
if triggered and self._should_inject():
|
|
508
|
+
# Inject instructions as sys_msg (hidden from user display)
|
|
509
|
+
original_input = context.get("message", context.get("input", ""))
|
|
510
|
+
injected = f"""<sys_msg>
|
|
511
|
+
{AGENT_INSTRUCTIONS}
|
|
512
|
+
</sys_msg>
|
|
513
|
+
|
|
514
|
+
{original_input}"""
|
|
515
|
+
context["message"] = injected
|
|
516
|
+
if "input" in context:
|
|
517
|
+
context["input"] = injected
|
|
518
|
+
|
|
519
|
+
# Update tracking
|
|
520
|
+
self._last_injection_time = time.time()
|
|
521
|
+
self._message_count_since_injection = 0
|
|
522
|
+
|
|
523
|
+
logger.info("Injected agent orchestration instructions (keyword trigger)")
|
|
524
|
+
|
|
525
|
+
return context
|
|
526
|
+
|
|
527
|
+
# -------------------------------------------------------------------------
|
|
528
|
+
# Core Logic
|
|
529
|
+
# -------------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
async def _on_llm_response(self, data: dict, event) -> dict:
|
|
532
|
+
"""Process LLM response for agent XML commands.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
data: Event data with response data.
|
|
536
|
+
event: Event object.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Modified data.
|
|
540
|
+
"""
|
|
541
|
+
context = data # Alias for compatibility
|
|
542
|
+
response_text = context.get("response_text", "")
|
|
543
|
+
if not response_text:
|
|
544
|
+
response_text = context.get("content", "")
|
|
545
|
+
|
|
546
|
+
if not response_text:
|
|
547
|
+
return context
|
|
548
|
+
|
|
549
|
+
# Parse XML commands from response
|
|
550
|
+
commands = self.xml_parser.parse(response_text)
|
|
551
|
+
|
|
552
|
+
if not commands:
|
|
553
|
+
return context # no agent commands
|
|
554
|
+
|
|
555
|
+
logger.info(f"Found {len(commands)} agent command(s) in LLM response")
|
|
556
|
+
|
|
557
|
+
results = []
|
|
558
|
+
|
|
559
|
+
for cmd in commands:
|
|
560
|
+
try:
|
|
561
|
+
# Display tool indicator before execution (like real tool calls)
|
|
562
|
+
self._display_tool_indicator(cmd)
|
|
563
|
+
|
|
564
|
+
result = await self._execute_command(cmd)
|
|
565
|
+
if result:
|
|
566
|
+
results.append(result)
|
|
567
|
+
# No result display - matches tool call pattern where only indicator shows
|
|
568
|
+
# Result gets injected to conversation for LLM to process
|
|
569
|
+
except Exception as e:
|
|
570
|
+
logger.error(f"Error executing agent command: {e}")
|
|
571
|
+
results.append(f"[error: {e}]")
|
|
572
|
+
# Only show errors to the user
|
|
573
|
+
self._display_tool_result(cmd, f"[error: {e}]", is_error=True)
|
|
574
|
+
|
|
575
|
+
# Inject results back to LLM (like tool call results)
|
|
576
|
+
if results:
|
|
577
|
+
result_text = "\n".join(results)
|
|
578
|
+
|
|
579
|
+
# Check if any result is an error (don't continue on errors to prevent loops)
|
|
580
|
+
has_error = any(r.startswith("[error") or "already exists" in r.lower() for r in results)
|
|
581
|
+
|
|
582
|
+
# Inject to conversation history for LLM context
|
|
583
|
+
if self.message_injector:
|
|
584
|
+
await self.message_injector.inject(
|
|
585
|
+
source="agent_orchestrator",
|
|
586
|
+
content=result_text,
|
|
587
|
+
trigger_llm=False, # we'll force continue via context flag
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Force continuation for all successful results (spawn, status, capture, etc.)
|
|
591
|
+
# LLM will respond naturally to the tool result
|
|
592
|
+
# Only skip continuation on errors to prevent infinite loops
|
|
593
|
+
if not has_error:
|
|
594
|
+
context["force_continue"] = True
|
|
595
|
+
logger.info("Forcing continuation for orchestrator result")
|
|
596
|
+
|
|
597
|
+
return context
|
|
598
|
+
|
|
599
|
+
def _display_tool_indicator(self, cmd) -> None:
|
|
600
|
+
"""Display tool call indicator like real tool calls.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
cmd: Parsed command object.
|
|
604
|
+
"""
|
|
605
|
+
if not self.renderer:
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
indicator = f"{ColorPalette.BRIGHT_LIME}⏺{ColorPalette.RESET}"
|
|
609
|
+
|
|
610
|
+
if cmd.type == "agent":
|
|
611
|
+
# Get agent names from the agents list
|
|
612
|
+
names = [a.name for a in cmd.agents] if cmd.agents else []
|
|
613
|
+
args = ", ".join(names) if names else ""
|
|
614
|
+
tool_line = f"{indicator} spawn_agent({args})"
|
|
615
|
+
elif cmd.type == "status":
|
|
616
|
+
tool_line = f"{indicator} status()"
|
|
617
|
+
elif cmd.type == "capture":
|
|
618
|
+
tool_line = f"{indicator} capture({cmd.target}, {cmd.lines})"
|
|
619
|
+
elif cmd.type == "stop":
|
|
620
|
+
targets = ", ".join(cmd.targets) if cmd.targets else ""
|
|
621
|
+
tool_line = f"{indicator} stop({targets})"
|
|
622
|
+
elif cmd.type == "message":
|
|
623
|
+
tool_line = f"{indicator} message({cmd.target})"
|
|
624
|
+
elif cmd.type == "clone":
|
|
625
|
+
names = [a.name for a in cmd.agents] if cmd.agents else []
|
|
626
|
+
tool_line = f"{indicator} clone({names[0] if names else ''})"
|
|
627
|
+
elif cmd.type == "team":
|
|
628
|
+
tool_line = f"{indicator} spawn_team({cmd.lead})"
|
|
629
|
+
elif cmd.type == "broadcast":
|
|
630
|
+
tool_line = f"{indicator} broadcast({cmd.pattern})"
|
|
631
|
+
else:
|
|
632
|
+
tool_line = f"{indicator} {cmd.type}()"
|
|
633
|
+
|
|
634
|
+
# Display using message coordinator
|
|
635
|
+
self.renderer.message_coordinator.display_message_sequence([
|
|
636
|
+
("system", tool_line, {})
|
|
637
|
+
])
|
|
638
|
+
|
|
639
|
+
def _display_tool_result(self, cmd, result: str, is_error: bool = False) -> None:
|
|
640
|
+
"""Display tool execution result like real tool calls.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
cmd: Parsed command object.
|
|
644
|
+
result: Result string from execution.
|
|
645
|
+
is_error: Whether this is an error result.
|
|
646
|
+
"""
|
|
647
|
+
if not self.renderer:
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
if is_error:
|
|
651
|
+
result_line = f"\033[31m ▮ {result}\033[0m"
|
|
652
|
+
else:
|
|
653
|
+
# Format based on command type
|
|
654
|
+
if cmd.type == "agent":
|
|
655
|
+
result_line = f"\033[32m ▮ {result}\033[0m"
|
|
656
|
+
elif cmd.type == "status":
|
|
657
|
+
# Status returns multi-line, show summary
|
|
658
|
+
line_count = result.count('\n') + 1
|
|
659
|
+
result_line = f"\033[32m ▮ Retrieved status ({line_count} lines)\033[0m"
|
|
660
|
+
elif cmd.type == "capture":
|
|
661
|
+
line_count = result.count('\n') + 1
|
|
662
|
+
result_line = f"\033[32m ▮ Captured {line_count} lines\033[0m"
|
|
663
|
+
else:
|
|
664
|
+
result_line = f"\033[32m ▮ {result}\033[0m"
|
|
665
|
+
|
|
666
|
+
self.renderer.message_coordinator.display_message_sequence([
|
|
667
|
+
("system", result_line, {})
|
|
668
|
+
])
|
|
669
|
+
|
|
670
|
+
async def _execute_command(self, cmd) -> str:
|
|
671
|
+
"""Execute a parsed agent command.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
cmd: ParsedCommand instance.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Result string.
|
|
678
|
+
"""
|
|
679
|
+
if cmd.type == "agent":
|
|
680
|
+
return await self._spawn_agents(cmd.agents)
|
|
681
|
+
elif cmd.type == "message":
|
|
682
|
+
return await self._message_agent(cmd.target, cmd.content)
|
|
683
|
+
elif cmd.type == "stop":
|
|
684
|
+
return await self._stop_agents(cmd.targets)
|
|
685
|
+
elif cmd.type == "status":
|
|
686
|
+
return await self._get_status()
|
|
687
|
+
elif cmd.type == "capture":
|
|
688
|
+
return await self._capture_output(cmd.target, cmd.lines)
|
|
689
|
+
elif cmd.type == "clone":
|
|
690
|
+
return await self._clone_agent(cmd.agents[0] if cmd.agents else None)
|
|
691
|
+
elif cmd.type == "team":
|
|
692
|
+
return await self._spawn_team(cmd.lead, cmd.workers, cmd.agents)
|
|
693
|
+
elif cmd.type == "broadcast":
|
|
694
|
+
return await self._broadcast(cmd.pattern, cmd.content)
|
|
695
|
+
else:
|
|
696
|
+
return f"[error: unknown command type '{cmd.type}']"
|
|
697
|
+
|
|
698
|
+
async def _spawn_agents(self, agents: list) -> str:
|
|
699
|
+
"""Spawn multiple agents.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
agents: List of AgentTask instances.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
Result string.
|
|
706
|
+
"""
|
|
707
|
+
if not self.orchestrator:
|
|
708
|
+
return "[error: orchestrator not initialized]"
|
|
709
|
+
|
|
710
|
+
spawned = []
|
|
711
|
+
|
|
712
|
+
for agent in agents:
|
|
713
|
+
success = await self.orchestrator.spawn(
|
|
714
|
+
name=agent.name, task=agent.task, files=agent.files
|
|
715
|
+
)
|
|
716
|
+
if success:
|
|
717
|
+
spawned.append(agent.name)
|
|
718
|
+
if self.activity_monitor:
|
|
719
|
+
self.activity_monitor.track(agent.name)
|
|
720
|
+
|
|
721
|
+
# Emit plugin-specific event (string literal, not in core EventType)
|
|
722
|
+
if self.event_bus:
|
|
723
|
+
await self.event_bus.emit_with_hooks(
|
|
724
|
+
"agent_spawned",
|
|
725
|
+
{"name": agent.name, "task": agent.task},
|
|
726
|
+
"agent_orchestrator"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
if spawned:
|
|
730
|
+
return f"[spawned: {', '.join(spawned)}]"
|
|
731
|
+
return "[error: no agents spawned]"
|
|
732
|
+
|
|
733
|
+
async def _message_agent(self, target: str, content: str) -> str:
|
|
734
|
+
"""Send message to agent.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
target: Agent name.
|
|
738
|
+
content: Message content.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Result string.
|
|
742
|
+
"""
|
|
743
|
+
if not self.orchestrator:
|
|
744
|
+
return "[error: orchestrator not initialized]"
|
|
745
|
+
|
|
746
|
+
success = await self.orchestrator.message(target, content)
|
|
747
|
+
|
|
748
|
+
# Reset activity state so monitor waits for new activity
|
|
749
|
+
if success and self.activity_monitor:
|
|
750
|
+
self.activity_monitor.reset_agent_state(target)
|
|
751
|
+
|
|
752
|
+
if success:
|
|
753
|
+
return f"[message sent: {target}]"
|
|
754
|
+
return f"[error: agent {target} not found]"
|
|
755
|
+
|
|
756
|
+
async def _stop_agents(self, targets: list) -> str:
|
|
757
|
+
"""Stop agents and capture final output.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
targets: List of agent names to stop.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Result string.
|
|
764
|
+
"""
|
|
765
|
+
if not self.orchestrator:
|
|
766
|
+
return "[error: orchestrator not initialized]"
|
|
767
|
+
|
|
768
|
+
results = []
|
|
769
|
+
|
|
770
|
+
for target in targets:
|
|
771
|
+
if self.activity_monitor:
|
|
772
|
+
self.activity_monitor.untrack(target)
|
|
773
|
+
|
|
774
|
+
output, duration = await self.orchestrator.stop(target)
|
|
775
|
+
|
|
776
|
+
# Emit plugin-specific event (string literal, not in core EventType)
|
|
777
|
+
if self.event_bus:
|
|
778
|
+
await self.event_bus.emit_with_hooks(
|
|
779
|
+
"agent_stopped",
|
|
780
|
+
{"name": target, "duration": duration, "output": output},
|
|
781
|
+
"agent_orchestrator"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Truncate output for display
|
|
785
|
+
output_lines = output.strip().split("\n")
|
|
786
|
+
if len(output_lines) > 10:
|
|
787
|
+
output_preview = "\n".join(output_lines[-10:])
|
|
788
|
+
else:
|
|
789
|
+
output_preview = output.strip()
|
|
790
|
+
|
|
791
|
+
results.append(f"[stopped: {target} @ {duration}]\n{output_preview}")
|
|
792
|
+
|
|
793
|
+
return "\n".join(results)
|
|
794
|
+
|
|
795
|
+
async def _get_status(self) -> str:
|
|
796
|
+
"""Get status of all agents.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
Status string.
|
|
800
|
+
"""
|
|
801
|
+
if not self.orchestrator:
|
|
802
|
+
return "[error: orchestrator not initialized]"
|
|
803
|
+
|
|
804
|
+
agents = self.orchestrator.list_agents()
|
|
805
|
+
|
|
806
|
+
if not agents:
|
|
807
|
+
return "[agents]\n (none active)"
|
|
808
|
+
|
|
809
|
+
lines = ["[agents]"]
|
|
810
|
+
for agent in agents:
|
|
811
|
+
lines.append(f" {agent.name:<20} {agent.status:<10} {agent.duration}")
|
|
812
|
+
|
|
813
|
+
return "\n".join(lines)
|
|
814
|
+
|
|
815
|
+
async def _capture_output(self, target: str, lines: int) -> str:
|
|
816
|
+
"""Capture output from agent.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
target: Agent name.
|
|
820
|
+
lines: Number of lines to capture.
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Captured output string.
|
|
824
|
+
"""
|
|
825
|
+
if not self.orchestrator:
|
|
826
|
+
return "[error: orchestrator not initialized]"
|
|
827
|
+
|
|
828
|
+
output = self.orchestrator.capture_output(target, lines)
|
|
829
|
+
agent = self.orchestrator.get_agent(target)
|
|
830
|
+
duration = agent.duration if agent else "?"
|
|
831
|
+
|
|
832
|
+
return f"[capture: {target} @ {duration}, {lines} lines]\n{output}"
|
|
833
|
+
|
|
834
|
+
async def _clone_agent(self, agent: AgentTask) -> str:
|
|
835
|
+
"""Clone agent with conversation context.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
agent: AgentTask to clone.
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Result string.
|
|
842
|
+
"""
|
|
843
|
+
if not agent:
|
|
844
|
+
return "[error: no agent specified for clone]"
|
|
845
|
+
|
|
846
|
+
if not self.orchestrator:
|
|
847
|
+
return "[error: orchestrator not initialized]"
|
|
848
|
+
|
|
849
|
+
# Export conversation
|
|
850
|
+
conv_file = await self._export_conversation()
|
|
851
|
+
if not conv_file:
|
|
852
|
+
return "[error: could not export conversation for clone]"
|
|
853
|
+
|
|
854
|
+
success = await self.orchestrator.spawn_clone(
|
|
855
|
+
name=agent.name,
|
|
856
|
+
task=agent.task,
|
|
857
|
+
files=agent.files,
|
|
858
|
+
conversation_file=conv_file,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
if success:
|
|
862
|
+
if self.activity_monitor:
|
|
863
|
+
self.activity_monitor.track(agent.name)
|
|
864
|
+
return f"[cloned: {agent.name} with conversation context]"
|
|
865
|
+
return f"[error: failed to clone {agent.name}]"
|
|
866
|
+
|
|
867
|
+
async def _spawn_team(self, lead: str, workers: int, agents: list) -> str:
|
|
868
|
+
"""Spawn team lead agent.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
lead: Lead agent name.
|
|
872
|
+
workers: Max number of workers.
|
|
873
|
+
agents: List of agent tasks (should have one for the lead).
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
Result string.
|
|
877
|
+
"""
|
|
878
|
+
if not self.orchestrator:
|
|
879
|
+
return "[error: orchestrator not initialized]"
|
|
880
|
+
|
|
881
|
+
task = agents[0] if agents else AgentTask(name=lead, task="", files=[])
|
|
882
|
+
|
|
883
|
+
success = await self.orchestrator.spawn_team_lead(
|
|
884
|
+
lead_name=lead, max_workers=workers, task=task
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
if success:
|
|
888
|
+
if self.activity_monitor:
|
|
889
|
+
self.activity_monitor.track(lead)
|
|
890
|
+
return f"[team spawned: {lead} (max {workers} workers)]"
|
|
891
|
+
return f"[error: failed to spawn team {lead}]"
|
|
892
|
+
|
|
893
|
+
async def _broadcast(self, pattern: str, content: str) -> str:
|
|
894
|
+
"""Broadcast message to agents matching pattern.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
pattern: Glob pattern for matching agents.
|
|
898
|
+
content: Message content.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Result string.
|
|
902
|
+
"""
|
|
903
|
+
if not self.orchestrator:
|
|
904
|
+
return "[error: orchestrator not initialized]"
|
|
905
|
+
|
|
906
|
+
targets = self.orchestrator.find_agents(pattern)
|
|
907
|
+
count = 0
|
|
908
|
+
|
|
909
|
+
for target in targets:
|
|
910
|
+
if await self.orchestrator.message(target, content):
|
|
911
|
+
count += 1
|
|
912
|
+
# Reset activity state
|
|
913
|
+
if self.activity_monitor:
|
|
914
|
+
self.activity_monitor.reset_agent_state(target)
|
|
915
|
+
|
|
916
|
+
return f"[broadcast: sent to {count} agents matching '{pattern}']"
|
|
917
|
+
|
|
918
|
+
# -------------------------------------------------------------------------
|
|
919
|
+
# Callbacks
|
|
920
|
+
# -------------------------------------------------------------------------
|
|
921
|
+
|
|
922
|
+
async def _on_agent_complete(
|
|
923
|
+
self, name: str, duration: str, output: str
|
|
924
|
+
) -> None:
|
|
925
|
+
"""Called when activity monitor detects agent completion.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
name: Agent name.
|
|
929
|
+
duration: Duration string.
|
|
930
|
+
output: Captured output.
|
|
931
|
+
"""
|
|
932
|
+
logger.info(f"Agent completed: {name} @ {duration}")
|
|
933
|
+
|
|
934
|
+
# Emit plugin-specific event (string literal, not in core EventType)
|
|
935
|
+
if self.event_bus:
|
|
936
|
+
await self.event_bus.emit_with_hooks(
|
|
937
|
+
"agent_completed",
|
|
938
|
+
{"name": name, "duration": duration, "output": output},
|
|
939
|
+
"agent_orchestrator"
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Inject completion message
|
|
943
|
+
if self.message_injector:
|
|
944
|
+
# Truncate output for summary
|
|
945
|
+
output_lines = output.strip().split("\n")
|
|
946
|
+
if len(output_lines) > 20:
|
|
947
|
+
summary = "\n".join(output_lines[-20:])
|
|
948
|
+
else:
|
|
949
|
+
summary = output.strip()
|
|
950
|
+
|
|
951
|
+
await self.message_injector.inject(
|
|
952
|
+
source=name,
|
|
953
|
+
content=f"[done: {name} @ {duration}]\n{summary}",
|
|
954
|
+
trigger_llm=True, # auto-continue conversation
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
async def _export_conversation(self) -> Optional[str]:
|
|
958
|
+
"""Export current conversation to temp file.
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
Path to temp file, or None on error.
|
|
962
|
+
"""
|
|
963
|
+
if not self.conversation_manager:
|
|
964
|
+
return None
|
|
965
|
+
|
|
966
|
+
try:
|
|
967
|
+
messages = self.conversation_manager.get_messages()
|
|
968
|
+
|
|
969
|
+
fd, path = tempfile.mkstemp(suffix=".json", prefix="conv-")
|
|
970
|
+
with open(path, "w") as f:
|
|
971
|
+
json.dump(messages, f)
|
|
972
|
+
|
|
973
|
+
return path
|
|
974
|
+
except Exception as e:
|
|
975
|
+
logger.error(f"Failed to export conversation: {e}")
|
|
976
|
+
return None
|