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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. 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