devsquad 3.6.0__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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Coordinator - Global Orchestrator
|
|
5
|
+
|
|
6
|
+
Core component for multi-Worker collaboration:
|
|
7
|
+
1. Decompose tasks into parallel Worker plans
|
|
8
|
+
2. Create and schedule Workers
|
|
9
|
+
3. Collect results and shared state
|
|
10
|
+
4. Resolve conflicts and reach consensus
|
|
11
|
+
5. Generate final collaboration report
|
|
12
|
+
6. Inter-Agent briefing handoff (latent-briefing pattern)
|
|
13
|
+
7. Rule pre-check via MemoryProvider (optional)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
19
|
+
|
|
20
|
+
from .models import (
|
|
21
|
+
EntryType,
|
|
22
|
+
TaskDefinition,
|
|
23
|
+
ExecutionPlan,
|
|
24
|
+
TaskBatch,
|
|
25
|
+
BatchMode,
|
|
26
|
+
ScheduleResult,
|
|
27
|
+
WorkerResult,
|
|
28
|
+
ConsensusRecord,
|
|
29
|
+
DecisionOutcome,
|
|
30
|
+
)
|
|
31
|
+
from .scratchpad import Scratchpad
|
|
32
|
+
from .worker import Worker, WorkerFactory
|
|
33
|
+
from .consensus import ConsensusEngine
|
|
34
|
+
from .context_compressor import ContextCompressor, Message, MessageType, CompressionLevel, CompressedContext
|
|
35
|
+
from .usage_tracker import track_usage
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Coordinator:
|
|
39
|
+
"""
|
|
40
|
+
全局协调者 - 多 Agent 协作的核心编排组件
|
|
41
|
+
|
|
42
|
+
职责:
|
|
43
|
+
1. 接收用户任务,分解为可并行的 Worker 计划 (plan_task)
|
|
44
|
+
2. 根据计划创建和调度 Worker 实例 (spawn_workers)
|
|
45
|
+
3. 按批次执行任务,支持并行/串行混合模式 (execute_plan)
|
|
46
|
+
4. 从 Scratchpad 收集所有 Worker 的结果和状态 (collect_results)
|
|
47
|
+
5. 检测并解决 Worker 间的冲突 (resolve_conflicts)
|
|
48
|
+
6. 生成完整的协作报告 (generate_report)
|
|
49
|
+
|
|
50
|
+
与其他组件的关系:
|
|
51
|
+
- Scratchpad: 共享黑板,Worker 间交换信息的媒介
|
|
52
|
+
- Worker: 执行具体任务的 Agent 实例
|
|
53
|
+
- ConsensusEngine: 解决冲突时的共识决策引擎
|
|
54
|
+
- ContextCompressor: 长任务中的上下文压缩管理
|
|
55
|
+
|
|
56
|
+
使用示例:
|
|
57
|
+
coord = Coordinator(scratchpad=scratchpad)
|
|
58
|
+
plan = coord.plan_task("设计系统架构", available_roles=[...])
|
|
59
|
+
workers = coord.spawn_workers(plan)
|
|
60
|
+
result = coord.execute_plan(plan)
|
|
61
|
+
report = coord.generate_report()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, scratchpad: Optional[Scratchpad] = None,
|
|
65
|
+
persist_dir: Optional[str] = None,
|
|
66
|
+
enable_compression: bool = True,
|
|
67
|
+
compression_threshold: int = 100000,
|
|
68
|
+
llm_backend=None,
|
|
69
|
+
stream: bool = False,
|
|
70
|
+
memory_provider=None,
|
|
71
|
+
briefing_mode: bool = True):
|
|
72
|
+
"""
|
|
73
|
+
Initialize Coordinator.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
scratchpad: Shared scratchpad instance (auto-created if not provided)
|
|
77
|
+
persist_dir: Scratchpad persistence directory
|
|
78
|
+
enable_compression: Enable context compression (prevent overflow in long tasks)
|
|
79
|
+
compression_threshold: Compression trigger threshold (token count), default 100000
|
|
80
|
+
llm_backend: LLM execution backend (None=MockBackend, returns prompt text)
|
|
81
|
+
memory_provider: MemoryProvider implementation (optional, for rule pre-check)
|
|
82
|
+
briefing_mode: Enable inter-Agent briefing handoff (default True)
|
|
83
|
+
"""
|
|
84
|
+
self.scratchpad = scratchpad or Scratchpad(persist_dir=persist_dir)
|
|
85
|
+
self.consensus = ConsensusEngine()
|
|
86
|
+
self.workers: Dict[str, Worker] = {}
|
|
87
|
+
self._execution_history: List[Dict[str, Any]] = []
|
|
88
|
+
self.coordinator_id = f"coord-{uuid.uuid4().hex[:8]}"
|
|
89
|
+
self.enable_compression = enable_compression
|
|
90
|
+
self.compressor = ContextCompressor(token_threshold=compression_threshold) if enable_compression else None
|
|
91
|
+
self._message_buffer: List[Message] = []
|
|
92
|
+
self.llm_backend = llm_backend
|
|
93
|
+
self.stream = stream
|
|
94
|
+
self.memory_provider = memory_provider
|
|
95
|
+
self.briefing_mode = briefing_mode
|
|
96
|
+
self._briefing_chain: List[Any] = []
|
|
97
|
+
|
|
98
|
+
def plan_task(self, task_description: str,
|
|
99
|
+
available_roles: List[Dict[str, str]],
|
|
100
|
+
stage_id: Optional[str] = None) -> ExecutionPlan:
|
|
101
|
+
"""
|
|
102
|
+
将用户任务分解为可并行的 Worker 执行计划
|
|
103
|
+
|
|
104
|
+
为每个可用角色创建一个 TaskDefinition,打包为并行批次。
|
|
105
|
+
当前实现为简单的一对一映射(每个角色一个任务),
|
|
106
|
+
未来可扩展为智能任务拆分(如将大任务拆为子任务分配给多个角色)。
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
task_description: 用户原始任务描述
|
|
110
|
+
available_roles: 可用角色列表,每项含 role_id 和 role_prompt
|
|
111
|
+
stage_id: 阶段标识(可选,用于多阶段工作流)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ExecutionPlan: 包含批次列表、总任务数、并行度估计
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> plan = coord.plan_task(
|
|
118
|
+
... "设计用户认证系统",
|
|
119
|
+
... [{"role_id": "architect", "role_prompt": "..."}]
|
|
120
|
+
... )
|
|
121
|
+
>>> plan.total_tasks
|
|
122
|
+
1
|
|
123
|
+
"""
|
|
124
|
+
tasks = []
|
|
125
|
+
for role_cfg in available_roles:
|
|
126
|
+
task = TaskDefinition(
|
|
127
|
+
description=task_description,
|
|
128
|
+
role_id=role_cfg["role_id"],
|
|
129
|
+
role_prompt=role_cfg.get("role_prompt", ""),
|
|
130
|
+
stage_id=stage_id,
|
|
131
|
+
is_read_only=True,
|
|
132
|
+
)
|
|
133
|
+
tasks.append(task)
|
|
134
|
+
|
|
135
|
+
parallel_batch = TaskBatch(
|
|
136
|
+
mode=BatchMode.PARALLEL,
|
|
137
|
+
tasks=tasks,
|
|
138
|
+
max_concurrency=len(tasks),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
plan = ExecutionPlan(
|
|
142
|
+
batches=[parallel_batch],
|
|
143
|
+
total_tasks=len(tasks),
|
|
144
|
+
estimated_parallelism=1.0 if len(tasks) > 1 else 0.0,
|
|
145
|
+
)
|
|
146
|
+
track_usage("coordinator.plan_task", success=True, metadata={
|
|
147
|
+
"num_roles": len(available_roles),
|
|
148
|
+
"total_tasks": len(tasks)
|
|
149
|
+
})
|
|
150
|
+
return plan
|
|
151
|
+
|
|
152
|
+
def spawn_workers(self, plan: ExecutionPlan,
|
|
153
|
+
registry=None) -> List[Worker]:
|
|
154
|
+
"""
|
|
155
|
+
根据执行计划创建 Worker 实例
|
|
156
|
+
|
|
157
|
+
遍历计划中的所有任务,为每个任务创建对应的 Worker。
|
|
158
|
+
如提供 registry(PromptRegistry),会自动加载角色的 prompt 模板。
|
|
159
|
+
创建的 Worker 会自动关联到本协调器的 Scratchpad。
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
plan: 执行计划(由 plan_task 生成)
|
|
163
|
+
registry: 可选的 PromptRegistry 实例,用于加载角色 prompt
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List[Worker]: 创建的 Worker 实例列表
|
|
167
|
+
"""
|
|
168
|
+
self.workers.clear()
|
|
169
|
+
all_tasks = []
|
|
170
|
+
for batch in plan.batches:
|
|
171
|
+
all_tasks.extend(batch.tasks)
|
|
172
|
+
|
|
173
|
+
for task in all_tasks:
|
|
174
|
+
worker_id = f"{task.role_id}-{uuid.uuid4().hex[:6]}"
|
|
175
|
+
role_prompt = task.role_prompt or ""
|
|
176
|
+
if not role_prompt and registry:
|
|
177
|
+
from prompts.registry import PromptRegistry
|
|
178
|
+
if isinstance(registry, PromptRegistry):
|
|
179
|
+
info = registry.get_role_prompt(task.role_id)
|
|
180
|
+
if info:
|
|
181
|
+
role_prompt = info.prompt_content[:2000]
|
|
182
|
+
|
|
183
|
+
worker = WorkerFactory.create(
|
|
184
|
+
worker_id=worker_id,
|
|
185
|
+
role_id=task.role_id,
|
|
186
|
+
role_prompt=role_prompt,
|
|
187
|
+
scratchpad=self.scratchpad,
|
|
188
|
+
llm_backend=self.llm_backend,
|
|
189
|
+
stream=getattr(self, 'stream', False),
|
|
190
|
+
)
|
|
191
|
+
self.workers[worker_id] = worker
|
|
192
|
+
return list(self.workers.values())
|
|
193
|
+
|
|
194
|
+
def execute_plan(self, plan: ExecutionPlan) -> ScheduleResult:
|
|
195
|
+
"""
|
|
196
|
+
执行完整的协作计划
|
|
197
|
+
|
|
198
|
+
按批次顺序执行计划中的所有任务。对于每个批次:
|
|
199
|
+
- PARALLEL 模式: 并行执行所有任务
|
|
200
|
+
- SEQUENTIAL 模式: 串行逐个执行
|
|
201
|
+
执行过程中自动进行上下文压缩(如启用)。
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
plan: 执行计划(由 plan_task + spawn_workers 准备)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
ScheduleResult: 包含成功/失败统计、各 Worker 结果、耗时、错误列表
|
|
208
|
+
"""
|
|
209
|
+
start_time = time.time()
|
|
210
|
+
results = []
|
|
211
|
+
errors = []
|
|
212
|
+
|
|
213
|
+
for batch_idx, batch in enumerate(plan.batches):
|
|
214
|
+
batch_results, batch_errors = self._execute_batch(batch)
|
|
215
|
+
results.extend(batch_results)
|
|
216
|
+
errors.extend(batch_errors)
|
|
217
|
+
|
|
218
|
+
if self.compressor and batch_idx < len(plan.batches) - 1:
|
|
219
|
+
self._buffer_worker_messages(batch_results)
|
|
220
|
+
compressed = self.compressor.check_and_compress(self._message_buffer)
|
|
221
|
+
if compressed.compression_level != CompressionLevel.NONE:
|
|
222
|
+
self._execution_history.append({
|
|
223
|
+
"timestamp": time.time(),
|
|
224
|
+
"compression": {
|
|
225
|
+
"level": compressed.compression_level.value,
|
|
226
|
+
"original_tokens": compressed.original_token_count,
|
|
227
|
+
"compressed_tokens": compressed.compressed_token_count,
|
|
228
|
+
"reduction_pct": round(compressed.reduction_percent, 1),
|
|
229
|
+
"summary": compressed.summary[:200],
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
duration = time.time() - start_time
|
|
234
|
+
success_count = sum(1 for r in results if r.success)
|
|
235
|
+
|
|
236
|
+
result = ScheduleResult(
|
|
237
|
+
success=len(errors) == 0,
|
|
238
|
+
total_tasks=sum(len(b.tasks) for b in plan.batches),
|
|
239
|
+
completed_tasks=success_count,
|
|
240
|
+
failed_tasks=len(errors),
|
|
241
|
+
results=results,
|
|
242
|
+
duration_seconds=duration,
|
|
243
|
+
errors=errors,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self._record_execution(result)
|
|
247
|
+
self._message_buffer.clear()
|
|
248
|
+
track_usage("coordinator.execute_plan", success=result.success, metadata={
|
|
249
|
+
"total_tasks": result.total_tasks,
|
|
250
|
+
"completed": result.completed_tasks,
|
|
251
|
+
"failed": result.failed_tasks,
|
|
252
|
+
"duration": round(duration, 2)
|
|
253
|
+
})
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def _buffer_worker_messages(self, batch_results: List[WorkerResult]):
|
|
257
|
+
for r in batch_results:
|
|
258
|
+
if r.output:
|
|
259
|
+
self._message_buffer.append(Message(
|
|
260
|
+
role=r.worker_id,
|
|
261
|
+
content=str(r.output)[:2000],
|
|
262
|
+
msg_type=MessageType.ASSISTANT,
|
|
263
|
+
metadata={"task_id": r.task_id, "success": r.success},
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
def compress_context(self, force_level=None) -> Optional[CompressedContext]:
|
|
267
|
+
"""
|
|
268
|
+
手动触发上下文压缩
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
force_level: 强制指定压缩级别(None=自动判断)
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
CompressedContext: 压缩结果,含级别/原始token/压缩后token/摘要
|
|
275
|
+
如未启用压缩则返回 None
|
|
276
|
+
"""
|
|
277
|
+
if not self.compressor:
|
|
278
|
+
return None
|
|
279
|
+
return self.compressor.check_and_compress(self._message_buffer, force_level=force_level)
|
|
280
|
+
|
|
281
|
+
def get_compression_stats(self) -> Optional[Dict[str, Any]]:
|
|
282
|
+
"""
|
|
283
|
+
获取上下文压缩统计信息
|
|
284
|
+
|
|
285
|
+
从执行历史中提取所有压缩事件的聚合统计数据,
|
|
286
|
+
包括总压缩次数、平均节省率、最近一次压缩详情等。
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dict[str, Any]: 统计信息字典,包含:
|
|
290
|
+
- total_compressions: 总压缩次数
|
|
291
|
+
- avg_reduction_pct: 平均压缩率(%)
|
|
292
|
+
- last_compression: 最近一次压缩的详细信息
|
|
293
|
+
- total_original_tokens: 原始token总数
|
|
294
|
+
- total_compressed_tokens: 压缩后token总数
|
|
295
|
+
如未启用压缩则返回 None
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
>>> stats = coord.get_compression_stats()
|
|
299
|
+
>>> if stats:
|
|
300
|
+
... print(f"平均节省 {stats['avg_reduction_pct']}%")
|
|
301
|
+
"""
|
|
302
|
+
if not self.compressor:
|
|
303
|
+
return None
|
|
304
|
+
compression_events = [
|
|
305
|
+
e["compression"] for e in self._execution_history
|
|
306
|
+
if "compression" in e
|
|
307
|
+
]
|
|
308
|
+
if not compression_events:
|
|
309
|
+
return {
|
|
310
|
+
"total_compressions": 0,
|
|
311
|
+
"avg_reduction_pct": 0.0,
|
|
312
|
+
"last_compression": None,
|
|
313
|
+
"total_original_tokens": 0,
|
|
314
|
+
"total_compressed_tokens": 0,
|
|
315
|
+
}
|
|
316
|
+
total_original = sum(e.get("original_tokens", 0) for e in compression_events)
|
|
317
|
+
total_compressed = sum(e.get("compressed_tokens", 0) for e in compression_events)
|
|
318
|
+
avg_reduction = (
|
|
319
|
+
sum(e.get("reduction_pct", 0) for e in compression_events) / len(compression_events)
|
|
320
|
+
)
|
|
321
|
+
return {
|
|
322
|
+
"total_compressions": len(compression_events),
|
|
323
|
+
"avg_reduction_pct": round(avg_reduction, 1),
|
|
324
|
+
"last_compression": compression_events[-1],
|
|
325
|
+
"total_original_tokens": total_original,
|
|
326
|
+
"total_compressed_tokens": total_compressed,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def get_session_memory(self, category=None, limit=50):
|
|
330
|
+
"""
|
|
331
|
+
获取会话记忆(从 ContextCompressor 的 SessionMemory 中提取)
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
category: 记忆类别过滤(可选)
|
|
335
|
+
limit: 返回条数上限
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List[Dict]: 提取的记忆条目列表
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def _execute_batch(self, batch: TaskBatch) -> Tuple[List[WorkerResult], List[str]]:
|
|
342
|
+
results = []
|
|
343
|
+
errors = []
|
|
344
|
+
|
|
345
|
+
if batch.mode == BatchMode.PARALLEL:
|
|
346
|
+
results = self._execute_parallel(batch)
|
|
347
|
+
else:
|
|
348
|
+
for task in batch.tasks:
|
|
349
|
+
try:
|
|
350
|
+
worker = self._get_worker_for_task(task)
|
|
351
|
+
if worker:
|
|
352
|
+
if self.briefing_mode and self._briefing_chain:
|
|
353
|
+
self._inject_briefing_to_worker(worker)
|
|
354
|
+
r = worker.execute(task)
|
|
355
|
+
results.append(r)
|
|
356
|
+
if self.briefing_mode:
|
|
357
|
+
self._collect_briefing_from_worker(worker)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
errors.append(f"Task {task.task_id} failed: {e}")
|
|
360
|
+
|
|
361
|
+
return results, errors
|
|
362
|
+
|
|
363
|
+
def _execute_parallel(self, batch: TaskBatch) -> List[WorkerResult]:
|
|
364
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
365
|
+
results = []
|
|
366
|
+
max_workers = min(batch.max_concurrency or len(batch.tasks), len(batch.tasks))
|
|
367
|
+
if max_workers <= 0:
|
|
368
|
+
return results
|
|
369
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
370
|
+
futures = {}
|
|
371
|
+
for task in batch.tasks:
|
|
372
|
+
worker = self._get_worker_for_task(task)
|
|
373
|
+
if worker:
|
|
374
|
+
future = executor.submit(worker.execute, task)
|
|
375
|
+
futures[future] = task.task_id
|
|
376
|
+
for future in as_completed(futures):
|
|
377
|
+
try:
|
|
378
|
+
results.append(future.result())
|
|
379
|
+
except Exception as e:
|
|
380
|
+
results.append(WorkerResult(
|
|
381
|
+
worker_id="unknown",
|
|
382
|
+
task_id=futures[future],
|
|
383
|
+
success=False,
|
|
384
|
+
error=str(e),
|
|
385
|
+
))
|
|
386
|
+
return results
|
|
387
|
+
|
|
388
|
+
def _inject_briefing_to_worker(self, worker):
|
|
389
|
+
"""Inject compressed briefing from preceding Agents into the next Worker."""
|
|
390
|
+
try:
|
|
391
|
+
from .enhanced_worker import EnhancedWorker, AgentBriefingOutput
|
|
392
|
+
if isinstance(worker, EnhancedWorker) and self._briefing_chain:
|
|
393
|
+
merged = self._merge_briefings(self._briefing_chain)
|
|
394
|
+
worker.receive_briefing(merged)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def _collect_briefing_from_worker(self, worker):
|
|
399
|
+
"""Collect compressed briefing from a Worker after execution."""
|
|
400
|
+
try:
|
|
401
|
+
from .enhanced_worker import EnhancedWorker
|
|
402
|
+
if isinstance(worker, EnhancedWorker):
|
|
403
|
+
briefing = worker.compress_to_briefing()
|
|
404
|
+
if briefing and briefing.result_summary:
|
|
405
|
+
self._briefing_chain.append(briefing)
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
def _merge_briefings(self, briefings: List[Any]) -> Any:
|
|
410
|
+
"""Merge multiple Agent briefings into a single composite briefing."""
|
|
411
|
+
from .enhanced_worker import AgentBriefingOutput
|
|
412
|
+
if not briefings:
|
|
413
|
+
return AgentBriefingOutput()
|
|
414
|
+
|
|
415
|
+
all_decisions = []
|
|
416
|
+
all_pending = []
|
|
417
|
+
all_rules = []
|
|
418
|
+
summaries = []
|
|
419
|
+
min_confidence = 1.0
|
|
420
|
+
|
|
421
|
+
for b in briefings:
|
|
422
|
+
all_decisions.extend(b.key_decisions[:3])
|
|
423
|
+
all_pending.extend(b.pending_items[:3])
|
|
424
|
+
all_rules.extend(b.rules_applied[:5])
|
|
425
|
+
if b.result_summary:
|
|
426
|
+
summaries.append(f"[{b.task_summary}] {b.result_summary}")
|
|
427
|
+
min_confidence = min(min_confidence, b.confidence)
|
|
428
|
+
|
|
429
|
+
return AgentBriefingOutput(
|
|
430
|
+
task_summary="; ".join(summaries[:3]),
|
|
431
|
+
key_decisions=all_decisions[:5],
|
|
432
|
+
pending_items=all_pending[:5],
|
|
433
|
+
rules_applied=list(set(all_rules))[:5],
|
|
434
|
+
result_summary=" | ".join(summaries[:3]),
|
|
435
|
+
confidence=min_confidence,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def preload_rules(self, task_description: str, user_id: str = "default") -> Dict[str, List[dict]]:
|
|
439
|
+
"""
|
|
440
|
+
Pre-load rules from MemoryProvider for all active Workers.
|
|
441
|
+
|
|
442
|
+
Called before execute_plan() to pre-check rules at the orchestrator level,
|
|
443
|
+
avoiding repeated queries from individual Agents.
|
|
444
|
+
|
|
445
|
+
Uses match_rules() when available (CarryMem v0.2.9+), falls back to
|
|
446
|
+
get_rules() for backward compatibility.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
task_description: Task description for rule matching
|
|
450
|
+
user_id: User identifier for rule lookup
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Dict mapping role_id -> list of matched rules
|
|
454
|
+
"""
|
|
455
|
+
if not self.memory_provider or not self.memory_provider.is_available():
|
|
456
|
+
return {}
|
|
457
|
+
|
|
458
|
+
role_rules: Dict[str, List[dict]] = {}
|
|
459
|
+
for wid, worker in self.workers.items():
|
|
460
|
+
try:
|
|
461
|
+
if hasattr(self.memory_provider, 'match_rules'):
|
|
462
|
+
rules = self.memory_provider.match_rules(
|
|
463
|
+
task_description=task_description,
|
|
464
|
+
user_id=user_id,
|
|
465
|
+
role=worker.role_id,
|
|
466
|
+
max_rules=5
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
rule_strings = self.memory_provider.get_rules(
|
|
470
|
+
user_id=user_id,
|
|
471
|
+
context={"task": task_description, "role": worker.role_id}
|
|
472
|
+
)
|
|
473
|
+
rules = []
|
|
474
|
+
for rs in rule_strings:
|
|
475
|
+
if isinstance(rs, str):
|
|
476
|
+
rules.append({
|
|
477
|
+
"rule_type": "always",
|
|
478
|
+
"trigger": rs.lower(),
|
|
479
|
+
"action": rs,
|
|
480
|
+
"relevance_score": 0.0,
|
|
481
|
+
"rule_id": "",
|
|
482
|
+
"override": False,
|
|
483
|
+
})
|
|
484
|
+
elif isinstance(rs, dict):
|
|
485
|
+
rules.append(rs)
|
|
486
|
+
if rules:
|
|
487
|
+
role_rules[worker.role_id] = rules if isinstance(rules, list) else []
|
|
488
|
+
except Exception:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
return role_rules
|
|
492
|
+
|
|
493
|
+
def _get_worker_for_task(self, task: TaskDefinition) -> Optional[Worker]:
|
|
494
|
+
for wid, w in self.workers.items():
|
|
495
|
+
if w.role_id == task.role_id:
|
|
496
|
+
return w
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
def collect_results(self) -> Dict[str, Any]:
|
|
500
|
+
"""
|
|
501
|
+
从 Scratchpad 收集所有 Worker 的执行结果和共享状态
|
|
502
|
+
|
|
503
|
+
汇总当前会话中的所有协作数据,包括:
|
|
504
|
+
- Scratchpad 全局摘要和统计
|
|
505
|
+
- 各类型的条目计数(发现/决策/冲突)
|
|
506
|
+
- 所有 Worker 的待处理通知(跨Worker消息)
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Dict[str, Any]: 结果集合,包含:
|
|
510
|
+
- coordinator_id: 协调器唯一标识
|
|
511
|
+
- scratchpad: Scratchpad 文本摘要
|
|
512
|
+
- scratchpad_stats: 详细统计(按类型/状态/Worker分布)
|
|
513
|
+
- findings_count: 发现条目数
|
|
514
|
+
- decisions_count: 决策条目数
|
|
515
|
+
- conflicts_count: 冲突条目数
|
|
516
|
+
- notifications: 待处理通知列表 (TaskNotification)
|
|
517
|
+
- workers: 当前活跃 Worker ID 列表
|
|
518
|
+
"""
|
|
519
|
+
scratchpad_summary = self.scratchpad.get_summary()
|
|
520
|
+
stats = self.scratchpad.get_stats()
|
|
521
|
+
|
|
522
|
+
findings = self.scratchpad.read(entry_type=EntryType.FINDING)
|
|
523
|
+
decisions = self.scratchpad.read(entry_type=EntryType.DECISION)
|
|
524
|
+
conflicts = self.scratchpad.get_conflicts()
|
|
525
|
+
|
|
526
|
+
notifications = []
|
|
527
|
+
for w in self.workers.values():
|
|
528
|
+
notifications.extend(w.get_pending_notifications())
|
|
529
|
+
|
|
530
|
+
track_usage("coordinator.collect_results", success=True, metadata={
|
|
531
|
+
"findings": len(findings),
|
|
532
|
+
"decisions": len(decisions),
|
|
533
|
+
"conflicts": len(conflicts)
|
|
534
|
+
})
|
|
535
|
+
return {
|
|
536
|
+
"coordinator_id": self.coordinator_id,
|
|
537
|
+
"scratchpad": scratchpad_summary,
|
|
538
|
+
"scratchpad_stats": stats,
|
|
539
|
+
"findings_count": len(findings),
|
|
540
|
+
"decisions_count": len(decisions),
|
|
541
|
+
"conflicts_count": len(conflicts),
|
|
542
|
+
"notifications": notifications,
|
|
543
|
+
"workers": list(self.workers.keys()),
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def resolve_conflicts(self) -> List[ConsensusRecord]:
|
|
547
|
+
"""
|
|
548
|
+
检测并解决 Scratchpad 中的所有活跃冲突
|
|
549
|
+
|
|
550
|
+
对每个 CONFLICT 类型的条目发起共识投票流程:
|
|
551
|
+
1. 通过 ConsensusEngine 创建提案(含4个选项:接受A/接受B/合并/升级人工)
|
|
552
|
+
2. 收集所有 Worker 的投票
|
|
553
|
+
3. 调用 reach_consensus() 生成最终决策
|
|
554
|
+
4. 根据决策结果更新冲突条目状态为 RESOLVED
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
List[ConsensusRecord]: 共识记录列表,每条包含:
|
|
558
|
+
- topic: 冲突主题
|
|
559
|
+
- outcome: 最终决策结果 (APPROVED/REJECTED/TIE)
|
|
560
|
+
- final_decision: 决策描述文本
|
|
561
|
+
- votes_for/against/abstain: 各选项票数
|
|
562
|
+
|
|
563
|
+
Note:
|
|
564
|
+
此方法会修改 Scratchpad 中冲突条目的状态。
|
|
565
|
+
解决后的条目会被标记为 RESOLVED 并附加解决方案说明。
|
|
566
|
+
"""
|
|
567
|
+
conflicts = self.scratchpad.get_conflicts()
|
|
568
|
+
resolutions = []
|
|
569
|
+
|
|
570
|
+
for conflict in conflicts:
|
|
571
|
+
proposal = self.consensus.create_proposal(
|
|
572
|
+
topic=f"解决冲突: {conflict.content[:80]}",
|
|
573
|
+
proposer_id=self.coordinator_id,
|
|
574
|
+
content=f"冲突详情: {conflict.content}",
|
|
575
|
+
options=["接受A", "接受B", "合并方案", "升级人工"],
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
for wid, w in self.workers.items():
|
|
579
|
+
vote_result = w.vote_on_proposal(proposal.proposal_id, decision=True,
|
|
580
|
+
reason="默认赞成待讨论")
|
|
581
|
+
vote_obj = vote_result.get("vote", vote_result)
|
|
582
|
+
self.consensus.cast_vote(proposal.proposal_id, vote_obj)
|
|
583
|
+
|
|
584
|
+
record = self.consensus.reach_consensus(proposal.proposal_id)
|
|
585
|
+
resolutions.append(record)
|
|
586
|
+
|
|
587
|
+
if record.outcome != DecisionOutcome.APPROVED:
|
|
588
|
+
self.scratchpad.resolve(conflict.entry_id,
|
|
589
|
+
resolution=f"[共识:{record.outcome.value}] {record.final_decision}")
|
|
590
|
+
else:
|
|
591
|
+
self.scratchpad.resolve(conflict.entry_id,
|
|
592
|
+
resolution=f"已通过共识解决")
|
|
593
|
+
|
|
594
|
+
track_usage("coordinator.resolve_conflicts", success=True, metadata={
|
|
595
|
+
"conflicts_resolved": len(resolutions)
|
|
596
|
+
})
|
|
597
|
+
return resolutions
|
|
598
|
+
|
|
599
|
+
def generate_report(self) -> str:
|
|
600
|
+
"""
|
|
601
|
+
生成完整的协作会话报告(Markdown格式)
|
|
602
|
+
|
|
603
|
+
汇聚所有协作组件的数据,生成结构化的 Markdown 报告,
|
|
604
|
+
包含以下章节:
|
|
605
|
+
- 协作概要(协调器ID、参与Worker、耗时)
|
|
606
|
+
- Scratchpad 概况(发现/决策/冲突统计)
|
|
607
|
+
- Worker 间消息通知(如有)
|
|
608
|
+
- 共识决策记录(如有)
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
str: Markdown 格式的完整报告文本
|
|
612
|
+
|
|
613
|
+
Example:
|
|
614
|
+
>>> report = coord.generate_report()
|
|
615
|
+
>>> print(report)
|
|
616
|
+
# 多角色协作报告
|
|
617
|
+
**协调器ID**: coord-a1b2c3d4
|
|
618
|
+
**参与Worker**: architect-abc123, tester-def456
|
|
619
|
+
...
|
|
620
|
+
"""
|
|
621
|
+
collection = self.collect_results()
|
|
622
|
+
lines = [
|
|
623
|
+
"# 多角色协作报告",
|
|
624
|
+
"",
|
|
625
|
+
f"**协调器ID**: {collection['coordinator_id']}",
|
|
626
|
+
f"**参与Worker**: {', '.join(collection['workers'])}",
|
|
627
|
+
f"**总耗时**: {self._get_last_duration():.1f}s",
|
|
628
|
+
"",
|
|
629
|
+
"## Scratchpad 概况",
|
|
630
|
+
collection["scratchpad"],
|
|
631
|
+
"",
|
|
632
|
+
f"- 发现: {collection['findings_count']} 条",
|
|
633
|
+
f"- 决策: {collection['decisions_count']} 条",
|
|
634
|
+
f"- 冲突: {collection['conflicts_count']} 条",
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
if collection["notifications"]:
|
|
638
|
+
lines.append("\n## Worker 间消息")
|
|
639
|
+
for n in collection["notifications"][:10]:
|
|
640
|
+
lines.append(f"- **{n.from_worker}** → {', '.join(n.to_workers)}: {n.summary}")
|
|
641
|
+
|
|
642
|
+
consensus_records = self.consensus.get_all_records()
|
|
643
|
+
if consensus_records:
|
|
644
|
+
lines.append("\n## 共识记录")
|
|
645
|
+
for cr in consensus_records:
|
|
646
|
+
icon = "✅" if cr.outcome == DecisionOutcome.APPROVED else \
|
|
647
|
+
"❌" if cr.outcome == DecisionOutcome.REJECTED else \
|
|
648
|
+
"⚠️"
|
|
649
|
+
lines.append(f"- [{icon}] {cr.topic}: {cr.outcome.value}")
|
|
650
|
+
|
|
651
|
+
return "\n".join(lines)
|
|
652
|
+
|
|
653
|
+
def _record_execution(self, result: ScheduleResult):
|
|
654
|
+
self._execution_history.append({
|
|
655
|
+
"timestamp": time.time(),
|
|
656
|
+
"result": {
|
|
657
|
+
"success": result.success,
|
|
658
|
+
"total": result.total_tasks,
|
|
659
|
+
"completed": result.completed_tasks,
|
|
660
|
+
"failed": result.failed_tasks,
|
|
661
|
+
"duration": result.duration_seconds,
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
def _get_last_duration(self) -> float:
|
|
666
|
+
if self._execution_history:
|
|
667
|
+
return self._execution_history[-1]["result"]["duration"]
|
|
668
|
+
return 0.0
|