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,1636 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ V3 Multi-Agent Collaboration Dispatcher (Unified Entry Point)
5
+
6
+ This is the V3 unified entry point that chains all collaboration components
7
+ into a single usable pipeline:
8
+
9
+ User Task → [Intent Recognition] → [Role Assignment] → [Coordinator Orchestration]
10
+ → [Parallel Worker Execution] → [Scratchpad Sharing] → [Consensus Decision]
11
+ → [Context Compression] → [Permission Check] → [Memory Capture] → [Result Return]
12
+
13
+ Integrated Components:
14
+ - WarmupManager: Startup warmup to reduce cold-start latency
15
+ - Coordinator + Worker + Scratchpad: Multi-agent collaboration core
16
+ - BatchScheduler: Parallel/sequential hybrid scheduling
17
+ - ConsensusEngine: Weighted voting consensus mechanism with veto power
18
+ - ContextCompressor: 4-level context compression to prevent overflow
19
+ - PermissionGuard: Permission guard for secure operation checks
20
+ - Skillifier: Learns from successful patterns to generate new Skills
21
+ - MemoryBridge: Cross-session memory bridge (with MCE + WorkBuddy Claw integration)
22
+
23
+ Example Usage:
24
+ from scripts.collaboration.dispatcher import MultiAgentDispatcher
25
+
26
+ disp = MultiAgentDispatcher()
27
+ result = disp.dispatch("Design a user authentication system")
28
+ print(result.summary)
29
+ """
30
+
31
+ import os
32
+ import sys
33
+ import time
34
+ import uuid
35
+ import json
36
+ import re
37
+ import logging
38
+ import tempfile
39
+ import threading
40
+ from pathlib import Path
41
+ from datetime import datetime
42
+ from typing import Dict, List, Optional, Any, Tuple
43
+ from dataclasses import dataclass, field
44
+ from collections import deque
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ from .models import (
49
+ EntryType, EntryStatus, TaskDefinition, ExecutionPlan,
50
+ TaskBatch, BatchMode, ScheduleResult, WorkerResult,
51
+ ConsensusRecord, DecisionOutcome, ROLE_WEIGHTS,
52
+ ROLE_REGISTRY, ROLE_ALIASES, resolve_role_id, get_core_roles, get_planned_roles,
53
+ get_all_role_ids, get_cli_role_list, RoleDefinition,
54
+ )
55
+ from .scratchpad import Scratchpad, ScratchpadEntry
56
+ from .worker import Worker, WorkerFactory
57
+ from .consensus import ConsensusEngine
58
+ from .coordinator import Coordinator
59
+ from .batch_scheduler import BatchScheduler
60
+ from .context_compressor import ContextCompressor, Message, MessageType, CompressionLevel
61
+ from .permission_guard import (
62
+ PermissionGuard, PermissionLevel, ActionType,
63
+ ProposedAction, PermissionRule,
64
+ )
65
+ from .skillifier import Skillifier, ExecutionRecord, ExecutionStep, PGActionType
66
+ from .warmup_manager import WarmupManager, WarmupConfig, WarmupLayer
67
+ from .memory_bridge import (
68
+ MemoryBridge, MemoryConfig as MBConfig,
69
+ MemoryType, MemoryItem, MemoryQuery,
70
+ KnowledgeItem, UserFeedback, EpisodicMemory,
71
+ PersistedPattern, AnalysisCase, ErrorContext,
72
+ )
73
+ from .test_quality_guard import (
74
+ TestQualityGuard, TestQualityReport,
75
+ )
76
+ from .usage_tracker import track_usage
77
+ from .input_validator import InputValidator
78
+ from .user_friendly_error import UserFriendlyError, translate_validation_result, make_user_friendly_error
79
+ from .role_matcher import RoleMatcher
80
+ from .report_formatter import ReportFormatter
81
+ from .concern_pack_loader import ConcernPackLoader
82
+
83
+
84
+ @dataclass
85
+ class PerformanceMetric:
86
+ """单次性能指标"""
87
+ timestamp: str
88
+ task_description: str
89
+ total_duration: float
90
+ step_timings: Dict[str, float]
91
+ success: bool
92
+ error_count: int
93
+ role_count: int
94
+
95
+ def __post_init__(self):
96
+ if len(self.task_description) > 50:
97
+ self.task_description = self.task_description[:50] + "..."
98
+
99
+
100
+ @dataclass
101
+ class PerformanceThresholds:
102
+ """性能阈值配置"""
103
+ total_duration_warning: float = 30.0
104
+ total_duration_critical: float = 60.0
105
+ step_warnings: Dict[str, float] = field(default_factory=lambda: {
106
+ "analyze": 2.0,
107
+ "warmup": 1.0,
108
+ "plan": 3.0,
109
+ "spawn": 1.0,
110
+ "execute": 20.0,
111
+ "collect": 2.0,
112
+ "consensus": 5.0,
113
+ "compress": 1.0,
114
+ "permission": 0.5,
115
+ "memory": 2.0,
116
+ "skillify": 2.0,
117
+ })
118
+ step_criticals: Dict[str, float] = field(default_factory=lambda: {
119
+ "analyze": 5.0,
120
+ "warmup": 3.0,
121
+ "plan": 8.0,
122
+ "spawn": 3.0,
123
+ "execute": 45.0,
124
+ "collect": 5.0,
125
+ "consensus": 10.0,
126
+ "compress": 3.0,
127
+ "permission": 1.0,
128
+ "memory": 5.0,
129
+ "skillify": 5.0,
130
+ })
131
+
132
+
133
+ class PerformanceMonitor:
134
+ """
135
+ 性能监控器
136
+
137
+ 功能:
138
+ - 收集每次调度的性能指标
139
+ - 检测性能异常和回归
140
+ - 维护滑动窗口统计
141
+ - 生成性能报告
142
+ - 触发阈值告警
143
+ """
144
+
145
+ def __init__(self, window_size: int = 100, thresholds: Optional[PerformanceThresholds] = None):
146
+ self.window_size = window_size
147
+ self.thresholds = thresholds or PerformanceThresholds()
148
+ self._metrics: deque = deque(maxlen=window_size)
149
+ self._lock = threading.Lock()
150
+
151
+ def record(self, metric: PerformanceMetric):
152
+ """记录一次性能指标"""
153
+ with self._lock:
154
+ self._metrics.append(metric)
155
+
156
+ # 实时检查阈值
157
+ warnings, criticals = self._check_thresholds(metric)
158
+ if criticals:
159
+ logger.critical("PERFORMANCE CRITICAL: %s", json.dumps(criticals, ensure_ascii=False))
160
+ elif warnings:
161
+ logger.warning("PERFORMANCE WARNING: %s", json.dumps(warnings, ensure_ascii=False))
162
+
163
+ def _check_thresholds(self, metric: PerformanceMetric) -> Tuple[List[Dict], List[Dict]]:
164
+ """检查指标是否超过阈值"""
165
+ warnings = []
166
+ criticals = []
167
+
168
+ # 总耗时检查
169
+ if metric.total_duration > self.thresholds.total_duration_critical:
170
+ criticals.append({
171
+ "type": "total_duration",
172
+ "value": metric.total_duration,
173
+ "threshold": self.thresholds.total_duration_critical,
174
+ "task": metric.task_description[:50],
175
+ })
176
+ elif metric.total_duration > self.thresholds.total_duration_warning:
177
+ warnings.append({
178
+ "type": "total_duration",
179
+ "value": metric.total_duration,
180
+ "threshold": self.thresholds.total_duration_warning,
181
+ })
182
+
183
+ # 各步骤耗时检查
184
+ for step, duration in metric.step_timings.items():
185
+ critical_threshold = self.thresholds.step_criticals.get(step)
186
+ warning_threshold = self.thresholds.step_warnings.get(step)
187
+
188
+ if critical_threshold and duration > critical_threshold:
189
+ criticals.append({
190
+ "type": f"step_{step}",
191
+ "value": duration,
192
+ "threshold": critical_threshold,
193
+ })
194
+ elif warning_threshold and duration > warning_threshold:
195
+ warnings.append({
196
+ "type": f"step_{step}",
197
+ "value": duration,
198
+ "threshold": warning_threshold,
199
+ })
200
+
201
+ return warnings, criticals
202
+
203
+ def get_statistics(self) -> Dict[str, Any]:
204
+ """获取滑动窗口内的统计数据"""
205
+ with self._lock:
206
+ if not self._metrics:
207
+ return {"count": 0}
208
+
209
+ metrics_list = list(self._metrics)
210
+ count = len(metrics_list)
211
+
212
+ durations = [m.total_duration for m in metrics_list]
213
+ successes = sum(1 for m in metrics_list if m.success)
214
+
215
+ stats = {
216
+ "count": count,
217
+ "success_rate": successes / count,
218
+ "duration": {
219
+ "min": min(durations),
220
+ "max": max(durations),
221
+ "avg": sum(durations) / count,
222
+ "p50": sorted(durations)[int(count * 0.5)],
223
+ "p95": sorted(durations)[int(count * 0.95)],
224
+ "p99": sorted(durations)[int(count * 0.99)] if count > 20 else max(durations),
225
+ },
226
+ "errors_per_dispatch_avg": sum(m.error_count for m in metrics_list) / count,
227
+ "roles_per_dispatch_avg": sum(m.role_count for m in metrics_list) / count,
228
+ }
229
+
230
+ # 各步骤统计
231
+ step_stats = {}
232
+ all_steps = set()
233
+ for m in metrics_list:
234
+ all_steps.update(m.step_timings.keys())
235
+
236
+ for step in all_steps:
237
+ step_durations = [m.step_timings.get(step, 0) for m in metrics_list if step in m.step_timings]
238
+ if step_durations:
239
+ step_stats[step] = {
240
+ "avg": sum(step_durations) / len(step_durations),
241
+ "max": max(step_durations),
242
+ "min": min(step_durations),
243
+ "p95": sorted(step_durations)[int(len(step_durations) * 0.95)],
244
+ }
245
+
246
+ stats["steps"] = step_stats
247
+
248
+ return stats
249
+
250
+ def detect_regression(self, baseline_count: int = 10) -> Optional[Dict[str, Any]]:
251
+ """
252
+ 检测性能回归
253
+
254
+ 对比最近N次与历史平均,检测显著恶化
255
+ """
256
+ with self._lock:
257
+ if len(self._metrics) < baseline_count * 2:
258
+ return None
259
+
260
+ metrics_list = list(self._metrics)
261
+ recent = metrics_list[-baseline_count:]
262
+ baseline = metrics_list[:-baseline_count]
263
+
264
+ recent_avg = sum(m.total_duration for m in recent) / len(recent)
265
+ baseline_avg = sum(m.total_duration for m in baseline) / len(baseline)
266
+
267
+ regression_ratio = (recent_avg - baseline_avg) / baseline_avg if baseline_avg > 0 else 0
268
+
269
+ if regression_ratio > 0.2: # 超过20%视为回归
270
+ return {
271
+ "detected": True,
272
+ "regression_ratio": round(regression_ratio, 3),
273
+ "baseline_avg": round(baseline_avg, 3),
274
+ "recent_avg": round(recent_avg, 3),
275
+ "baseline_samples": len(baseline),
276
+ "recent_samples": len(recent),
277
+ "severity": "high" if regression_ratio > 0.5 else "medium",
278
+ }
279
+
280
+ return None
281
+
282
+ def export_metrics(self, output_file: str, allowed_base_dir: str = "/tmp"):
283
+ """导出性能指标到文件"""
284
+ output_path = os.path.abspath(output_file)
285
+ base_dir = os.path.abspath(allowed_base_dir)
286
+ if not output_path.startswith(base_dir) and not output_path.startswith("/tmp"):
287
+ logger.warning("Export path outside allowed directories: %s", output_path)
288
+ output_path = os.path.join(base_dir, os.path.basename(output_file))
289
+
290
+ with self._lock:
291
+ data = [m.__dict__ for m in self._metrics]
292
+
293
+ os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True)
294
+ with open(output_path, 'w') as f:
295
+ json.dump(data, f, indent=2, ensure_ascii=False)
296
+
297
+ logger.info("Exported %d performance metrics to %s", len(data), output_path)
298
+
299
+ def clear(self):
300
+ """清除历史数据"""
301
+ with self._lock:
302
+ self._metrics.clear()
303
+
304
+
305
+ # Backward-compatible aliases from models SSOT
306
+ ROLE_TEMPLATES = {rid: {"name": rdef.name, "prompt": rdef.prompt, "keywords": rdef.keywords} for rid, rdef in ROLE_REGISTRY.items()}
307
+ PLANNED_ROLES = {rid: {"name": rdef.name, "status": rdef.status, "description": rdef.description} for rid, rdef in get_planned_roles().items()}
308
+
309
+ I18N = {
310
+ "zh": {
311
+ "title": "# 🤖 Multi-Agent 协作结果",
312
+ "task": "**任务**",
313
+ "status_ok": "✅ 成功",
314
+ "status_fail": "❌ 失败",
315
+ "status_label": "状态",
316
+ "duration": "**耗时**",
317
+ "roles": "**参与角色**",
318
+ "summary": "## 📋 执行摘要",
319
+ "output": "## 👥 各角色产出",
320
+ "scratchpad": "## 📝 Scratchpad 共享区",
321
+ "consensus": "## 🗳️ 共识决策",
322
+ "compression": "## 📦 上下文压缩",
323
+ "memory": "## 🧠 记忆系统",
324
+ "permission": "## 🔒 权限检查",
325
+ "skill": "## ⚡ Skill 学习",
326
+ "quality": "## 🛡️ 测试质量审计",
327
+ "errors": "## ⚠️ 错误信息",
328
+ "mock_banner": "> ⚠️ **MOCK 模式** — 这是模拟输出,未调用真实 LLM。",
329
+ "mock_hint": "> 使用 `--backend openai`(或 `anthropic`)并提供有效 API Key 获取真实 AI 分析。",
330
+ "no_output": "*(无输出)*",
331
+ "no_summary": "(无摘要)",
332
+ },
333
+ "en": {
334
+ "title": "# 🤖 Multi-Agent Collaboration Result",
335
+ "task": "**Task**",
336
+ "status_ok": "✅ Success",
337
+ "status_fail": "❌ Failed",
338
+ "status_label": "Status",
339
+ "duration": "**Duration**",
340
+ "roles": "**Roles**",
341
+ "summary": "## 📋 Executive Summary",
342
+ "output": "## 👥 Role Outputs",
343
+ "scratchpad": "## 📝 Scratchpad",
344
+ "consensus": "## 🗳️ Consensus Decisions",
345
+ "compression": "## 📦 Context Compression",
346
+ "memory": "## 🧠 Memory System",
347
+ "permission": "## 🔒 Permission Checks",
348
+ "skill": "## ⚡ Skill Learning",
349
+ "quality": "## 🛡️ Test Quality Audit",
350
+ "errors": "## ⚠️ Errors",
351
+ "mock_banner": "> ⚠️ **MOCK MODE** — This is simulated output. No real LLM was called.",
352
+ "mock_hint": "> Use `--backend openai` (or `anthropic`) with a valid API key for real AI analysis.",
353
+ "no_output": "*(no output)*",
354
+ "no_summary": "(no summary)",
355
+ },
356
+ "ja": {
357
+ "title": "# 🤖 マルチエージェントコラボレーション結果",
358
+ "task": "**タスク**",
359
+ "status_ok": "✅ 成功",
360
+ "status_fail": "❌ 失敗",
361
+ "status_label": "ステータス",
362
+ "duration": "**所要時間**",
363
+ "roles": "**参加ロール**",
364
+ "summary": "## 📋 実行サマリー",
365
+ "output": "## 👥 ロール出力",
366
+ "scratchpad": "## 📝 スクラッチパッド",
367
+ "consensus": "## 🗳️ コンセンサス決定",
368
+ "compression": "## 📦 コンテキスト圧縮",
369
+ "memory": "## 🧠 メモリシステム",
370
+ "permission": "## 🔒 権限チェック",
371
+ "skill": "## ⚡ スキル学習",
372
+ "quality": "## 🛡️ テスト品質監査",
373
+ "errors": "## ⚠️ エラー",
374
+ "mock_banner": "> ⚠️ **モックモード** — これはシミュレーション出力です。実際のLLMは呼び出されていません。",
375
+ "mock_hint": "> 実際のAI分析には `--backend openai`(または `anthropic`)と有効なAPIキーを使用してください。",
376
+ "no_output": "*(出力なし)*",
377
+ "no_summary": "(サマリーなし)",
378
+ },
379
+ }
380
+
381
+
382
+ @dataclass
383
+ class DispatchResult:
384
+ """调度结果"""
385
+ success: bool
386
+ task_description: str
387
+ matched_roles: List[str] = field(default_factory=list)
388
+ summary: str = ""
389
+ details: Dict[str, Any] = field(default_factory=dict)
390
+ scratchpad_summary: str = ""
391
+ consensus_records: List[Dict[str, Any]] = field(default_factory=list)
392
+ compression_info: Optional[Dict[str, Any]] = None
393
+ memory_stats: Optional[Dict[str, Any]] = None
394
+ permission_checks: List[Dict[str, Any]] = field(default_factory=list)
395
+ skill_proposals: List[Dict[str, Any]] = field(default_factory=list)
396
+ duration_seconds: float = 0.0
397
+ worker_results: List[Dict[str, Any]] = field(default_factory=list)
398
+ errors: List[str] = field(default_factory=list)
399
+ quality_report: Optional[str] = None
400
+ lang: str = "zh"
401
+ concern_packs: List[Dict[str, Any]] = field(default_factory=list)
402
+ anchor_result: Optional[Dict[str, Any]] = None
403
+ retrospective_report: Optional[Dict[str, Any]] = None
404
+ intent_match: Optional[Dict[str, Any]] = None
405
+ five_axis_result: Optional[Dict[str, Any]] = None
406
+
407
+ def to_dict(self) -> Dict[str, Any]:
408
+ return {
409
+ "success": self.success,
410
+ "task_description": self.task_description,
411
+ "matched_roles": self.matched_roles,
412
+ "summary": self.summary,
413
+ "details": self.details,
414
+ "scratchpad_summary": self.scratchpad_summary,
415
+ "consensus_records": self.consensus_records,
416
+ "compression_info": self.compression_info,
417
+ "memory_stats": self.memory_stats,
418
+ "permission_checks": self.permission_checks,
419
+ "skill_proposals": self.skill_proposals,
420
+ "duration_seconds": round(self.duration_seconds, 2),
421
+ "worker_results": self.worker_results,
422
+ "errors": self.errors,
423
+ "quality_report": self.quality_report,
424
+ }
425
+
426
+ def to_markdown(self) -> str:
427
+ t = I18N.get(self.lang, I18N["zh"])
428
+ is_mock = any(
429
+ '[MOCK MODE]' in (wr.get('output', '') or '')
430
+ for wr in (self.worker_results or [])
431
+ )
432
+ lines = []
433
+ if is_mock:
434
+ lines.extend([t["mock_banner"], t["mock_hint"], ""])
435
+ lines.extend([
436
+ t["title"],
437
+ "",
438
+ f"{t['task']}: {self.task_description}",
439
+ f"**{t['status_label']}**: {t['status_ok'] if self.success else t['status_fail']}",
440
+ f"{t['duration']}: {self.duration_seconds:.2f}s",
441
+ f"{t['roles']}: {', '.join(self.matched_roles)}",
442
+ "",
443
+ t["summary"],
444
+ self.summary or t["no_summary"],
445
+ ])
446
+ if self.worker_results:
447
+ lines.append("")
448
+ lines.append(t["output"])
449
+ role_icons = {
450
+ "architect": "🏗️", "product-manager": "📋", "security": "🔒",
451
+ "tester": "🧪", "solo-coder": "💻", "devops": "⚙️", "ui-designer": "🎨",
452
+ }
453
+ for wr in self.worker_results:
454
+ role_id = wr.get('role_id', wr.get('role', 'unknown'))
455
+ role_name = wr.get('role_name', wr.get('role', 'unknown'))
456
+ status_icon = '✅' if wr.get('success') else '❌'
457
+ icon = role_icons.get(role_id, '🤖')
458
+ output = wr.get('output', '') or ''
459
+ lines.append(f"")
460
+ lines.append(f"### {icon} {role_name} [{status_icon}]")
461
+ lines.append(f"---")
462
+ if output:
463
+ lines.append(output)
464
+ else:
465
+ lines.append(t["no_output"])
466
+ if self.scratchpad_summary:
467
+ lines.extend(["", t["scratchpad"], self.scratchpad_summary])
468
+ if self.consensus_records:
469
+ lines.append("")
470
+ lines.append(t["consensus"])
471
+ for cr in self.consensus_records:
472
+ icon = '✅' if cr.get('outcome') == 'APPROVED' else '⚠️'
473
+ lines.append(f"- [{icon}] {cr.get('topic', '')}: {cr.get('outcome', '')}")
474
+ if self.compression_info:
475
+ ci = self.compression_info
476
+ lines.extend([
477
+ "",
478
+ t["compression"],
479
+ f"- {t['duration'].replace('**', '')}: {ci.get('level', 'N/A')}",
480
+ f"- {ci.get('original_tokens', 0)} tokens → {ci.get('compressed_tokens', 0)} tokens ({ci.get('reduction_pct', 0)}%)",
481
+ ])
482
+ if self.memory_stats:
483
+ ms = self.memory_stats
484
+ lines.extend([
485
+ "",
486
+ t["memory"],
487
+ f"- Total: {ms.get('total_memories', 0)}",
488
+ f"- Knowledge: {ms.get('knowledge_count', 0)}",
489
+ f"- Episodic: {ms.get('episodic_count', 0)}",
490
+ ])
491
+ if self.permission_checks:
492
+ lines.append("")
493
+ lines.append(t["permission"])
494
+ for pc in self.permission_checks:
495
+ icon = '✅' if pc.get('allowed') else '🚫'
496
+ lines.append(f"- [{icon}] {pc.get('action', '')}: {pc.get('decision', '')}")
497
+ if self.skill_proposals:
498
+ lines.append("")
499
+ lines.append(t["skill"])
500
+ for sp in self.skill_proposals:
501
+ lines.append(f"- 📌 {sp.get('title', 'New Skill')}: {sp.get('confidence', 0):.0%}")
502
+ if self.quality_report:
503
+ lines.extend(["", t["quality"]])
504
+ lines.append(self.quality_report)
505
+ if self.errors:
506
+ lines.extend(["", t["errors"]])
507
+ for e in self.errors:
508
+ lines.append(f"- {e}")
509
+ if self.concern_packs:
510
+ lines.extend(["", "## 🧩 关注点增强包"])
511
+ for cp in self.concern_packs:
512
+ lines.append(f"- **{cp.get('name', '')}**: {cp.get('description', '')}")
513
+ return "\n".join(lines)
514
+
515
+
516
+ class MultiAgentDispatcher:
517
+ """
518
+ V3 统一调度器
519
+
520
+ 将所有 v3 组件整合为一个易用的高层 API。
521
+ 支持单次调用完成完整的多 Agent 协作流程。
522
+ """
523
+
524
+ def __init__(self,
525
+ persist_dir: Optional[str] = None,
526
+ enable_warmup: bool = True,
527
+ enable_compression: bool = True,
528
+ enable_permission: bool = True,
529
+ enable_memory: bool = True,
530
+ enable_skillify: bool = True,
531
+ enable_quality_guard: bool = False,
532
+ enable_anchor_check: bool = True,
533
+ enable_retrospective: bool = True,
534
+ enable_usage_tracker: bool = True,
535
+ compression_threshold: int = 100000,
536
+ memory_dir: Optional[str] = None,
537
+ permission_level: PermissionLevel = PermissionLevel.DEFAULT,
538
+ mce_adapter=None,
539
+ llm_backend=None,
540
+ stream: bool = False,
541
+ lang: str = "auto"):
542
+ """
543
+ Args:
544
+ persist_dir: Scratchpad persistence directory
545
+ enable_warmup: Whether to enable startup warmup
546
+ enable_compression: Whether to enable context compression
547
+ enable_permission: Whether to enable permission checking
548
+ enable_memory: Whether to enable memory bridge
549
+ enable_skillify: Whether to enable Skill learning
550
+ enable_quality_guard: Whether to enable test quality auto-audit (P1)
551
+ compression_threshold: Compression trigger threshold (token count)
552
+ memory_dir: Memory storage directory
553
+ permission_level: Default permission level
554
+ mce_adapter: MCE memory classification engine adapter (optional, v3.2)
555
+ llm_backend: LLM execution backend (None=MockBackend, returns prompt as-is)
556
+ """
557
+ self.enable_quality_guard = enable_quality_guard
558
+ self.enable_anchor_check = enable_anchor_check
559
+ self.enable_retrospective = enable_retrospective
560
+ self.enable_usage_tracker = enable_usage_tracker
561
+ self.persist_dir = persist_dir or tempfile.mkdtemp(prefix="mas_v3_")
562
+ self.memory_dir = memory_dir or os.path.join(self.persist_dir, "memory")
563
+ self.enable_warmup = enable_warmup
564
+ self.enable_compression = enable_compression
565
+ self.enable_permission = enable_permission
566
+ self.enable_memory = enable_memory
567
+ self.enable_skillify = enable_skillify
568
+ self.compression_threshold = compression_threshold
569
+ self.permission_level = permission_level
570
+ self.llm_backend = llm_backend
571
+ self.stream = stream
572
+ self.lang = lang
573
+
574
+ os.makedirs(self.persist_dir, exist_ok=True)
575
+ os.makedirs(self.memory_dir, exist_ok=True)
576
+
577
+ self._mce_adapter = mce_adapter
578
+ self._init_components()
579
+
580
+ def _init_components(self):
581
+ """初始化所有v3组件"""
582
+ self.scratchpad = Scratchpad(persist_dir=self.persist_dir)
583
+
584
+ self.coordinator = Coordinator(
585
+ scratchpad=self.scratchpad,
586
+ persist_dir=self.persist_dir,
587
+ enable_compression=self.enable_compression,
588
+ compression_threshold=self.compression_threshold,
589
+ llm_backend=self.llm_backend,
590
+ stream=self.stream,
591
+ )
592
+
593
+ self.batch_scheduler = BatchScheduler()
594
+
595
+ self.consensus_engine = ConsensusEngine()
596
+
597
+ self.role_matcher = RoleMatcher()
598
+ self.report_formatter = ReportFormatter(lang=self.lang)
599
+
600
+ if self.enable_compression:
601
+ self.compressor = ContextCompressor(
602
+ token_threshold=self.compression_threshold
603
+ )
604
+ else:
605
+ self.compressor = None
606
+
607
+ if self.enable_permission:
608
+ self.permission_guard = PermissionGuard(
609
+ current_level=self.permission_level,
610
+ )
611
+ else:
612
+ self.permission_guard = None
613
+
614
+ if self.enable_warmup:
615
+ warmup_cfg = WarmupConfig(
616
+ cache_enabled=True,
617
+ cache_max_size=50,
618
+ cache_ttl_seconds=3600,
619
+ metrics_enabled=True,
620
+ )
621
+ self.warmup_manager = WarmupManager(config=warmup_cfg)
622
+ try:
623
+ self.warmup_manager.warmup()
624
+ except Exception as e:
625
+ logger.warning("Warmup failed: %s", e)
626
+ else:
627
+ self.warmup_manager = None
628
+
629
+ if self.enable_memory:
630
+ self.memory_bridge = MemoryBridge(base_dir=self.memory_dir, mce_adapter=self._mce_adapter)
631
+ else:
632
+ self.memory_bridge = None
633
+
634
+ if self.enable_skillify:
635
+ self.skillifier = Skillifier()
636
+ else:
637
+ self.skillifier = None
638
+
639
+ if self.enable_quality_guard:
640
+ self.quality_guard = TestQualityGuard("", "")
641
+ else:
642
+ self.quality_guard = None
643
+
644
+ if self.enable_anchor_check:
645
+ from .anchor_checker import AnchorChecker
646
+ self.anchor_checker = AnchorChecker()
647
+ else:
648
+ self.anchor_checker = None
649
+
650
+ if self.enable_retrospective:
651
+ from .retrospective import RetrospectiveEngine
652
+ retrospective_memory = self.memory_bridge if self.enable_memory else None
653
+ self.retrospective_engine = RetrospectiveEngine(memory_bridge=retrospective_memory)
654
+ else:
655
+ self.retrospective_engine = None
656
+
657
+ if self.enable_usage_tracker:
658
+ from .feature_usage_tracker import FeatureUsageTracker
659
+ usage_path = os.path.join(self.persist_dir, "feature_usage.json")
660
+ self.usage_tracker = FeatureUsageTracker(persist_path=usage_path)
661
+ else:
662
+ self.usage_tracker = None
663
+
664
+ self._dispatch_history: List[DispatchResult] = []
665
+ self._max_history = 100
666
+ self._validator = InputValidator()
667
+
668
+ self._perf_monitor = PerformanceMonitor(window_size=100)
669
+
670
+ self._concern_loader = ConcernPackLoader()
671
+
672
+ from .dual_layer_context import DualLayerContextManager
673
+ self.context_manager = DualLayerContextManager()
674
+
675
+ from .intent_workflow_mapper import IntentWorkflowMapper
676
+ self.intent_mapper = IntentWorkflowMapper()
677
+
678
+ from .operation_classifier import OperationClassifier
679
+ self.operation_classifier = OperationClassifier()
680
+
681
+ from .skill_registry import SkillRegistry
682
+ self.skill_registry = SkillRegistry(
683
+ storage_path=os.path.join(self.persist_dir, "skills")
684
+ )
685
+
686
+ from .ai_semantic_matcher import AISemanticMatcher
687
+ self.semantic_matcher = AISemanticMatcher(llm_backend=self.llm_backend)
688
+
689
+ from .null_providers import (
690
+ get_null_cache, get_null_retry, get_null_monitor, get_null_memory,
691
+ )
692
+ self._null_cache = get_null_cache()
693
+ self._null_retry = get_null_retry()
694
+ self._null_monitor = get_null_monitor()
695
+ self._null_memory = get_null_memory()
696
+
697
+ try:
698
+ from .output_slicer import OutputSlicer
699
+ self.output_slicer = OutputSlicer(max_slice_lines=200)
700
+ except Exception:
701
+ self.output_slicer = None
702
+
703
+ try:
704
+ from .ci_feedback_adapter import CIFeedbackAdapter
705
+ self.ci_feedback = CIFeedbackAdapter()
706
+ except Exception:
707
+ self.ci_feedback = None
708
+
709
+ try:
710
+ from .standardized_role_template import StandardizedRoleTemplate
711
+ self._std_templates = {}
712
+ except Exception:
713
+ self._std_templates = {}
714
+
715
+ try:
716
+ from .prompt_variant_generator import PromptVariantGenerator
717
+ self.prompt_variant_gen = PromptVariantGenerator()
718
+ except Exception:
719
+ self.prompt_variant_gen = None
720
+
721
+ try:
722
+ from .role_template_market import RoleTemplateMarket
723
+ self.role_template_market = RoleTemplateMarket(storage_path=os.path.join(self.persist_dir, "market"))
724
+ except Exception:
725
+ self.role_template_market = None
726
+
727
+ def analyze_task(self, task_description: str) -> List[Dict[str, str]]:
728
+ """
729
+ 分析任务,匹配合适的角色
730
+
731
+ Args:
732
+ task_description: 任务描述
733
+
734
+ Returns:
735
+ 匹配到的角色列表 [{"role_id": "...", "name": "...", "reason": "..."}]
736
+ """
737
+ track_usage("dispatcher.analyze_task")
738
+ return self.role_matcher.analyze_task(task_description)
739
+
740
+ def dispatch(self,
741
+ task_description: str,
742
+ roles: Optional[List[str]] = None,
743
+ mode: str = "auto",
744
+ dry_run: bool = False) -> DispatchResult:
745
+ """
746
+ 核心调度方法 - 一键完成多Agent协作
747
+
748
+ Args:
749
+ task_description: 任务描述
750
+ roles: 指定角色列表(如 ["architect", "tester"]),None则自动匹配
751
+ mode: 执行模式 ("auto"/"parallel"/"sequential"/"consensus")
752
+ dry_run: 仅模拟执行,不实际运行Worker
753
+
754
+ Returns:
755
+ DispatchResult: 完整的调度结果
756
+ """
757
+ track_usage("dispatcher.dispatch", metadata={"mode": mode, "dry_run": dry_run})
758
+ start_time = time.time()
759
+
760
+ if self.usage_tracker:
761
+ self.usage_tracker.tick("dispatch")
762
+
763
+ errors = []
764
+
765
+ lang = self.lang
766
+ if lang == "auto":
767
+ import locale
768
+ try:
769
+ try:
770
+ loc = locale.getlocale()[0] or ""
771
+ except (ValueError, TypeError):
772
+ loc = ""
773
+ if loc.startswith("ja"):
774
+ lang = "ja"
775
+ elif loc.startswith("zh"):
776
+ lang = "zh"
777
+ else:
778
+ lang = "zh"
779
+ except Exception as e:
780
+ logger.debug("Locale detection failed, using default language: %s", e)
781
+ lang = "zh"
782
+
783
+ validator = self._validator
784
+ task_result = validator.validate_task(task_description)
785
+ if not task_result.valid:
786
+ friendly = translate_validation_result(task_result.reason)
787
+ return DispatchResult(
788
+ success=False,
789
+ task_description=task_description,
790
+ matched_roles=[],
791
+ worker_results=[],
792
+ summary=friendly.message,
793
+ errors=[friendly.format()],
794
+ lang=lang,
795
+ )
796
+ task_description = task_result.sanitized_input or task_description
797
+
798
+ rule_collection = None
799
+ try:
800
+ from scripts.collaboration.rule_collector import RuleCollector
801
+ if not hasattr(self, '_rule_collector'):
802
+ self._rule_collector = RuleCollector()
803
+ rule_collection = self._rule_collector.process(task_description, lang)
804
+ if rule_collection.rule_detected and not rule_collection.remaining_task:
805
+ return DispatchResult(
806
+ success=True,
807
+ task_description=task_description,
808
+ matched_roles=[],
809
+ worker_results=[],
810
+ summary=rule_collection.message,
811
+ errors=[],
812
+ lang=lang,
813
+ )
814
+ if rule_collection.rule_detected:
815
+ task_description = rule_collection.remaining_task
816
+ except Exception as e:
817
+ logger.debug("RuleCollector not available: %s", e)
818
+
819
+ ci_context = None
820
+ if self.ci_feedback:
821
+ try:
822
+ import glob as glob_mod
823
+ ci_files = glob_mod.glob(os.path.join(self.persist_dir, "**/pytest*.txt"), recursive=True)
824
+ ci_files += glob_mod.glob(os.path.join(self.persist_dir, "**/junit*.xml"), recursive=True)
825
+ if ci_files:
826
+ ci_results = []
827
+ for cf in ci_files[:3]:
828
+ with open(cf, "r", encoding="utf-8", errors="ignore") as f:
829
+ r = self.ci_feedback.parse_ci_output(f.read(), "pytest")
830
+ if r:
831
+ ci_results.append(r)
832
+ if ci_results:
833
+ ctx = self.ci_feedback.generate_context(ci_results)
834
+ if ctx and ctx.overall_status == "has_failures":
835
+ task_description = f"{task_description}\n\n[CI Context] {ctx.to_summary()}"
836
+ if self.usage_tracker:
837
+ self.usage_tracker.tick("ci_context_injected")
838
+ except Exception:
839
+ pass
840
+
841
+ try:
842
+ from .standardized_role_template import StandardizedRoleTemplate
843
+ if not hasattr(self, '_std_template_cache'):
844
+ self._std_template_cache = {}
845
+ except Exception:
846
+ self._std_template_cache = {}
847
+
848
+ if roles:
849
+ roles_result = validator.validate_roles(roles)
850
+ if not roles_result.valid:
851
+ friendly = make_user_friendly_error("role_not_found")
852
+ return DispatchResult(
853
+ success=False,
854
+ task_description=task_description,
855
+ matched_roles=[],
856
+ worker_results=[],
857
+ summary=friendly.message,
858
+ errors=[friendly.format()],
859
+ lang=lang,
860
+ )
861
+
862
+ warnings = validator.check_suspicious_patterns(task_description)
863
+ if warnings:
864
+ logger.warning("Suspicious patterns in task: %s", ", ".join(warnings))
865
+
866
+ injection_warnings = validator.check_prompt_injection(task_description)
867
+ if injection_warnings:
868
+ logger.warning("Prompt injection patterns detected: %s", ", ".join(injection_warnings))
869
+
870
+ try:
871
+ step1_time = time.time()
872
+
873
+ intent_match = None
874
+ try:
875
+ intent_match = self.intent_mapper.detect_intent(task_description, lang=lang)
876
+ if intent_match and self.usage_tracker:
877
+ self.usage_tracker.tick("intent_detected")
878
+ except Exception as intent_err:
879
+ logger.debug("Intent detection failed: %s", intent_err)
880
+
881
+ self.context_manager.clear_task_context()
882
+ self.context_manager.set_task("task_description", task_description)
883
+ self.context_manager.set_task("lang", lang)
884
+ if intent_match:
885
+ self.context_manager.set_task("intent_type", intent_match.intent_type)
886
+ self.context_manager.set_task("workflow_chain", [s for s in intent_match.workflow_chain])
887
+
888
+ matched_roles = self.analyze_task(task_description)
889
+
890
+ if self.semantic_matcher and self.llm_backend:
891
+ try:
892
+ semantic_results = self.semantic_matcher.match(task_description)
893
+ if semantic_results:
894
+ existing_ids = {r["role_id"] for r in matched_roles}
895
+ for sr in semantic_results:
896
+ if sr.role_id not in existing_ids and sr.confidence > 0.5:
897
+ matched_roles.append({
898
+ "role_id": sr.role_id,
899
+ "name": sr.role_name,
900
+ "reason": sr.reasoning,
901
+ "confidence": sr.confidence,
902
+ })
903
+ existing_ids.add(sr.role_id)
904
+ if self.usage_tracker:
905
+ self.usage_tracker.tick("semantic_matcher")
906
+ except Exception as sem_err:
907
+ logger.debug("Semantic matching failed: %s", sem_err)
908
+
909
+ if roles:
910
+ matched_roles = self.role_matcher.resolve_roles(roles, matched_roles)
911
+
912
+ role_ids = [r["role_id"] for r in matched_roles]
913
+
914
+ # 匹配关注点增强包
915
+ concern_packs = self._concern_loader.match_packs(task_description)
916
+ concern_enhancements = self._concern_loader.get_all_role_enhancements(concern_packs)
917
+
918
+ if concern_packs:
919
+ pack_names = ", ".join(p.name for p in concern_packs)
920
+ logger.info("Concern packs activated: %s", pack_names)
921
+
922
+ if dry_run:
923
+ return DispatchResult(
924
+ success=True,
925
+ task_description=task_description,
926
+ matched_roles=role_ids,
927
+ summary=f"[DRY RUN] 将调度角色: {', '.join(role_ids)}",
928
+ duration_seconds=time.time() - start_time,
929
+ lang=lang,
930
+ )
931
+
932
+ step2_time = time.time()
933
+
934
+ if self.warmup_manager:
935
+ for rid in role_ids:
936
+ cache_key = f"role-prompt-{rid}"
937
+ if not self.warmup_manager.is_ready(cache_key):
938
+ template = ROLE_TEMPLATES.get(rid, {})
939
+ self.warmup_manager.set_cache(
940
+ cache_key, template.get("prompt", ""),
941
+ ttl=1800,
942
+ )
943
+
944
+ step3_time = time.time()
945
+
946
+ available_roles = []
947
+ for r in matched_roles:
948
+ template = ROLE_TEMPLATES.get(r["role_id"], {})
949
+ role_prompt = template.get("prompt", "")
950
+
951
+ # 注入增强包提示词
952
+ role_id = r["role_id"]
953
+ if role_id in concern_enhancements:
954
+ enhancement = concern_enhancements[role_id]
955
+ if enhancement:
956
+ role_prompt = role_prompt + "\n\n" + enhancement if role_prompt else enhancement
957
+
958
+ available_roles.append({
959
+ "role_id": role_id,
960
+ "role_prompt": role_prompt,
961
+ "confidence": r.get("confidence", 0.5),
962
+ })
963
+
964
+ plan = self.coordinator.plan_task(
965
+ task_description=task_description,
966
+ available_roles=available_roles,
967
+ )
968
+
969
+ step4_time = time.time()
970
+
971
+ workers = self.coordinator.spawn_workers(plan)
972
+
973
+ step5_time = time.time()
974
+
975
+ # V3.6.0: Parse structured goal for anchor checking
976
+ structured_goal = None
977
+ if self.anchor_checker:
978
+ structured_goal = self.anchor_checker.parse_goal(task_description)
979
+ if self.usage_tracker:
980
+ self.usage_tracker.tick("anchor_check")
981
+
982
+ # V3.6.0: Load historical retrospectives into Scratchpad
983
+ if self.retrospective_engine and self.enable_memory:
984
+ try:
985
+ historical = self.retrospective_engine.load_historical(task_description, limit=3)
986
+ if historical:
987
+ retro_lines = ["[Historical Retrospective Context]"]
988
+ for idx, h in enumerate(historical, 1):
989
+ if isinstance(h, dict):
990
+ retro_lines.append(f" {idx}. {h.get('summary', str(h)[:100])}")
991
+ else:
992
+ retro_lines.append(f" {idx}. {str(h)[:100]}")
993
+ self.scratchpad.write(
994
+ ScratchpadEntry(
995
+ worker_id="system",
996
+ entry_type=EntryType.FINDING,
997
+ content="\n".join(retro_lines),
998
+ confidence=0.85,
999
+ tags=["retrospective", "auto-loaded"],
1000
+ )
1001
+ )
1002
+ except Exception as retro_load_err:
1003
+ logger.debug("Failed to load historical retrospectives: %s", retro_load_err)
1004
+
1005
+ exec_result = self.coordinator.execute_plan(plan)
1006
+
1007
+ step6_time = time.time()
1008
+
1009
+ worker_results = []
1010
+ for r in exec_result.results:
1011
+ role_id = r.worker_id.split("-")[0] if "-" in r.worker_id else r.worker_id
1012
+ from .models import ROLE_REGISTRY
1013
+ rdef = ROLE_REGISTRY.get(role_id)
1014
+ role_name = rdef.name if rdef else role_id
1015
+ worker_results.append({
1016
+ "worker_id": r.worker_id,
1017
+ "role_id": role_id,
1018
+ "role_name": role_name,
1019
+ "task_id": r.task_id,
1020
+ "success": r.success,
1021
+ "output": (r.output.get("finding_summary", "") if isinstance(r.output, dict) else str(r.output)) if r.output else None,
1022
+ "error": r.error,
1023
+ })
1024
+
1025
+ if exec_result.errors:
1026
+ errors.extend(exec_result.errors)
1027
+
1028
+ step7_time = time.time()
1029
+
1030
+ collection = self.coordinator.collect_results()
1031
+ step8_time = time.time()
1032
+ scratchpad_summary = collection.get("scratchpad", "")
1033
+
1034
+ if self.output_slicer and worker_results:
1035
+ try:
1036
+ for wr in worker_results:
1037
+ if wr.get("output") and len(wr["output"]) > self.output_slicer.max_slice_lines * 50:
1038
+ slices = self.output_slicer.slice_output(
1039
+ wr["output"], role_id=wr.get("role_id", "unknown")
1040
+ )
1041
+ wr["_slices"] = len(slices)
1042
+ wr["_sliced"] = True
1043
+ if self.usage_tracker:
1044
+ self.usage_tracker.tick("output_sliced")
1045
+ except Exception:
1046
+ pass
1047
+
1048
+ # V3.6.0: Anchor check after execution (needs scratchpad_summary)
1049
+ anchor_result = None
1050
+ if self.anchor_checker and structured_goal:
1051
+ try:
1052
+ combined_output = scratchpad_summary or ""
1053
+ for wr in worker_results:
1054
+ if wr.get("output"):
1055
+ combined_output += "\n" + wr["output"]
1056
+ from .models import AnchorTrigger
1057
+ anchor_result = self.anchor_checker.check(
1058
+ goal=structured_goal,
1059
+ current_output=combined_output,
1060
+ trigger=AnchorTrigger.STEP_COMPLETE,
1061
+ )
1062
+ if not anchor_result.aligned:
1063
+ if self.usage_tracker:
1064
+ self.usage_tracker.tick("anchor_drift_detected")
1065
+ self.scratchpad.write(
1066
+ ScratchpadEntry(
1067
+ worker_id="system",
1068
+ entry_type=EntryType.WARNING,
1069
+ content=f"[Anchor Drift] {anchor_result.recommendation}",
1070
+ confidence=0.9,
1071
+ tags=["anchor-drift", "v3.6.0"],
1072
+ )
1073
+ )
1074
+ except Exception as anchor_err:
1075
+ logger.warning("Anchor check failed: %s", anchor_err)
1076
+
1077
+ step8_time = time.time()
1078
+
1079
+ consensus_records = []
1080
+ conflicts_count = collection.get("conflicts_count", 0)
1081
+ if conflicts_count > 0 or mode == "consensus":
1082
+ resolutions = self.coordinator.resolve_conflicts()
1083
+ for rec in resolutions:
1084
+ consensus_records.append({
1085
+ "topic": rec.topic,
1086
+ "outcome": rec.outcome.value,
1087
+ "final_decision": rec.final_decision,
1088
+ "votes_for": rec.votes_for,
1089
+ "votes_against": rec.votes_against,
1090
+ "votes_abstain": rec.votes_abstain,
1091
+ })
1092
+
1093
+ step9_time = time.time()
1094
+
1095
+ compression_info = None
1096
+ if self.enable_compression and self.compressor:
1097
+ stats = self.coordinator.get_compression_stats()
1098
+ if stats:
1099
+ compression_info = stats
1100
+
1101
+ step10_time = time.time()
1102
+
1103
+ permission_checks = []
1104
+ if self.enable_permission and self.permission_guard:
1105
+ test_actions = [
1106
+ ProposedAction(action_type=ActionType.FILE_CREATE,
1107
+ target="/tmp/test_output.md",
1108
+ description="生成输出文件"),
1109
+ ]
1110
+ for action in test_actions:
1111
+ classified = None
1112
+ try:
1113
+ classified = self.operation_classifier.classify(
1114
+ operation_id=action.action_type.value,
1115
+ target=action.target,
1116
+ )
1117
+ except Exception:
1118
+ pass
1119
+ decision = self.permission_guard.check(action)
1120
+ perm_entry = {
1121
+ "action": f"{action.action_type.value}:{action.target}",
1122
+ "allowed": decision.outcome.value == "ALLOWED",
1123
+ "decision": decision.outcome.value,
1124
+ "reason": decision.reason or "",
1125
+ }
1126
+ if classified:
1127
+ perm_entry["operation_category"] = classified.category.value
1128
+ permission_checks.append(perm_entry)
1129
+
1130
+ step11_time = time.time()
1131
+
1132
+ memory_stats = None
1133
+ if self.enable_memory and self.memory_bridge:
1134
+ try:
1135
+ stats = self.memory_bridge.get_statistics()
1136
+ memory_stats = {
1137
+ "total_memories": stats.total_memories,
1138
+ "by_type_counts": stats.by_type_counts,
1139
+ "index_built": stats.index_built,
1140
+ "total_captures": stats.total_captures,
1141
+ }
1142
+
1143
+ ep = EpisodicMemory(
1144
+ id=f"epi-{uuid.uuid4().hex[:8]}",
1145
+ task_description=task_description,
1146
+ finding=scratchpad_summary[:500],
1147
+ )
1148
+ self.memory_bridge.capture_execution(
1149
+ execution_record={"task": task_description, "roles": role_ids},
1150
+ scratchpad_entries=[],
1151
+ )
1152
+ except (ConnectionError, TimeoutError, OSError) as mem_err:
1153
+ logger.warning("MemoryBridge connection error: %s", mem_err)
1154
+ errors.append(f"MemoryBridge connection error: {type(mem_err).__name__}: {mem_err}")
1155
+ except (ValueError, KeyError, AttributeError) as mem_val_err:
1156
+ logger.debug("MemoryBridge data error: %s", mem_val_err)
1157
+ errors.append(f"MemoryBridge data error: {mem_val_err}")
1158
+ except Exception as mem_err:
1159
+ logger.warning("Unexpected MemoryBridge error: %s - %s", type(mem_err).__name__, mem_err)
1160
+ errors.append(f"MemoryBridge unexpected error: {type(mem_err).__name__}")
1161
+
1162
+ # [MCE 集成点 v3.2] Dispatcher → MemoryBridge 调用链
1163
+ # 已实现: scratchpad → MCE.classify() → typed_metadata → MemoryBridge
1164
+ if self._mce_adapter and self._mce_adapter.is_available and scratchpad_summary:
1165
+ try:
1166
+ mce_classify_result = self._mce_adapter.classify(
1167
+ scratchpad_summary, context={"task": task_description}, timeout_ms=500
1168
+ )
1169
+ if mce_classify_result:
1170
+ memory_stats = memory_stats or {}
1171
+ memory_stats["mce_classification"] = {
1172
+ "type": mce_classify_result.memory_type,
1173
+ "confidence": round(mce_classify_result.confidence, 3),
1174
+ "tier": mce_classify_result.tier,
1175
+ }
1176
+ except (ValueError, TypeError, AttributeError) as mce_type_err:
1177
+ logger.debug("MCE classification data error: %s", mce_type_err)
1178
+ errors.append(f"MCE classify data error: {mce_type_err}")
1179
+ except (TimeoutError, ConnectionError) as mce_timeout:
1180
+ logger.warning("MCE classify timeout/connection error: %s", mce_timeout)
1181
+ errors.append(f"MCE classify timeout error: {mce_timeout}")
1182
+ except Exception as mce_err:
1183
+ logger.warning("Unexpected MCE classify error: %s - %s", type(mce_err).__name__, mce_err)
1184
+ errors.append(f"MCE classify unexpected error: {type(mce_err).__name__}")
1185
+
1186
+ if self.memory_bridge and self.enable_memory:
1187
+ try:
1188
+ ai_news_keywords = [
1189
+ "ai news", "industry trend", "latest progress", "trend",
1190
+ "ai coding", "embodied intelligence", "large model", "llm",
1191
+ "cursor", "claude", "gpt", "deepseek", "anthropic",
1192
+ "\u65b0\u95fb", "\u884c\u4e1a\u52a8\u6001", "\u6700\u65b0\u8fdb\u5c55",
1193
+ ]
1194
+ task_lower = task_description.lower()
1195
+ should_inject = any(kw in task_lower for kw in ai_news_keywords)
1196
+ if should_inject:
1197
+ news_items = self.memory_bridge.get_workbuddy_ai_news(days=3)
1198
+ if news_items:
1199
+ news_summary = "\n".join(
1200
+ f"- [{n.title}] {n.content[:200]}..."
1201
+ for n in news_items[:3]
1202
+ )
1203
+ self.scratchpad.write(
1204
+ ScratchpadEntry(
1205
+ worker_id="system",
1206
+ entry_type=EntryType.FINDING,
1207
+ content=f"[WorkBuddy AI News Feed]\n{news_summary}",
1208
+ confidence=0.95,
1209
+ tags=["ai-news", "auto-injected"],
1210
+ )
1211
+ )
1212
+ except Exception as inject_err:
1213
+ errors.append(f"AI news inject error: {inject_err}")
1214
+
1215
+ step12_time = time.time()
1216
+
1217
+ skill_proposals = []
1218
+ patterns = None
1219
+ if self.enable_skillify and self.skillifier and exec_result.success:
1220
+ try:
1221
+ patterns = self.skillifier.analyze_history()
1222
+ if patterns:
1223
+ for pattern in patterns:
1224
+ if pattern.confidence > 0.3:
1225
+ skill_proposals.append({
1226
+ "title": pattern.title or "新协作模式",
1227
+ "confidence": pattern.confidence,
1228
+ "category": pattern.category.value if hasattr(pattern, 'category') and pattern.category else "general",
1229
+ })
1230
+ try:
1231
+ self.skill_registry.propose_from_result(
1232
+ name=pattern.title or "新协作模式",
1233
+ description=pattern.title or "",
1234
+ category=pattern.category.value if hasattr(pattern, 'category') and pattern.category else "general",
1235
+ confidence=pattern.confidence,
1236
+ )
1237
+ except Exception:
1238
+ pass
1239
+ except Exception as skill_err:
1240
+ errors.append(f"Skillifier error: {skill_err}")
1241
+
1242
+ if self.prompt_variant_gen and patterns:
1243
+ try:
1244
+ for pattern in patterns:
1245
+ if pattern.confidence > 0.5:
1246
+ variants = self.prompt_variant_gen.generate_from_pattern(pattern)
1247
+ if variants:
1248
+ if self.usage_tracker:
1249
+ self.usage_tracker.tick("prompt_variant_generated")
1250
+ except Exception:
1251
+ pass
1252
+
1253
+ five_axis_result = None
1254
+ if mode == "consensus" and exec_result.success:
1255
+ try:
1256
+ from .five_axis_consensus import FiveAxisConsensusEngine, ReviewAxis
1257
+ fa_engine = FiveAxisConsensusEngine()
1258
+ review = fa_engine.create_review("system", "dispatcher")
1259
+ for wr in worker_results:
1260
+ output_text = wr.get("output") or wr.get("error") or ""
1261
+ if output_text:
1262
+ fa_engine.add_axis_vote(review, ReviewAxis.CORRECTNESS, 0.8, 0.7)
1263
+ fa_engine.add_axis_vote(review, ReviewAxis.READABILITY, 0.8, 0.7)
1264
+ fa_engine.add_axis_vote(review, ReviewAxis.ARCHITECTURE, 0.8, 0.7)
1265
+ fa_engine.add_axis_vote(review, ReviewAxis.SECURITY, 0.7, 0.6)
1266
+ fa_engine.add_axis_vote(review, ReviewAxis.PERFORMANCE, 0.7, 0.6)
1267
+ break
1268
+ fa_result = fa_engine.compute_consensus([review])
1269
+ five_axis_result = {
1270
+ "verdict": fa_result.verdict,
1271
+ "overall_consensus": fa_result.overall_consensus,
1272
+ "axis_consensus": fa_result.axis_consensus,
1273
+ "action_items": fa_result.action_items,
1274
+ }
1275
+ if self.usage_tracker:
1276
+ self.usage_tracker.tick("five_axis_consensus")
1277
+ except Exception as fa_err:
1278
+ logger.debug("Five-axis consensus failed: %s", fa_err)
1279
+
1280
+ total_duration = time.time() - start_time
1281
+
1282
+ # V3.6.0: Run retrospective after task completion (needs total_duration)
1283
+ retrospective_report = None
1284
+ if self.retrospective_engine and structured_goal and exec_result.success:
1285
+ try:
1286
+ if self.usage_tracker:
1287
+ self.usage_tracker.tick("retrospective")
1288
+ anchor_history = self.anchor_checker.check_history if self.anchor_checker else []
1289
+ retrospective_report = self.retrospective_engine.run(
1290
+ goal=structured_goal,
1291
+ anchor_history=anchor_history,
1292
+ worker_outputs={wr["role_id"]: wr.get("output", "") for wr in worker_results if wr.get("output")},
1293
+ task_duration_seconds=total_duration,
1294
+ )
1295
+ except Exception as retro_err:
1296
+ logger.warning("Retrospective failed: %s", retro_err)
1297
+
1298
+ report = self.coordinator.generate_report()
1299
+
1300
+ result = DispatchResult(
1301
+ success=exec_result.success and len(errors) == 0,
1302
+ task_description=task_description,
1303
+ matched_roles=role_ids,
1304
+ summary=self._build_summary(task_description, role_ids, exec_result, scratchpad_summary),
1305
+ details={
1306
+ "plan_total_tasks": plan.total_tasks,
1307
+ "completed_tasks": exec_result.completed_tasks,
1308
+ "failed_tasks": exec_result.failed_tasks,
1309
+ "report": report,
1310
+ "timing": {
1311
+ "analyze": round(step2_time - step1_time, 3),
1312
+ "warmup": round(step3_time - step2_time, 3),
1313
+ "plan": round(step4_time - step3_time, 3),
1314
+ "spawn": round(step5_time - step4_time, 3),
1315
+ "execute": round(step6_time - step5_time, 3),
1316
+ "collect": round(step7_time - step6_time, 3),
1317
+ "consensus": round(step8_time - step7_time, 3),
1318
+ "compress": round(step9_time - step8_time, 3),
1319
+ "permission": round(step10_time - step9_time, 3),
1320
+ "memory": round(step11_time - step10_time, 3),
1321
+ "skillify": round(step12_time - step11_time, 3),
1322
+ },
1323
+ },
1324
+ scratchpad_summary=scratchpad_summary,
1325
+ consensus_records=consensus_records,
1326
+ compression_info=compression_info,
1327
+ memory_stats=memory_stats,
1328
+ permission_checks=permission_checks,
1329
+ skill_proposals=skill_proposals,
1330
+ duration_seconds=total_duration,
1331
+ worker_results=worker_results,
1332
+ errors=errors,
1333
+ lang=lang,
1334
+ concern_packs=self._concern_loader.get_pack_info(concern_packs) if concern_packs else [],
1335
+ anchor_result={
1336
+ "aligned": anchor_result.aligned,
1337
+ "coverage": anchor_result.coverage,
1338
+ "drift_score": anchor_result.drift_score,
1339
+ "severity": anchor_result.severity.value,
1340
+ "recommendation": anchor_result.recommendation,
1341
+ } if anchor_result else None,
1342
+ retrospective_report=retrospective_report.to_dict() if retrospective_report else None,
1343
+ intent_match={
1344
+ "intent_type": intent_match.intent_type,
1345
+ "workflow_chain": [s for s in intent_match.workflow_chain],
1346
+ "confidence": intent_match.confidence,
1347
+ } if intent_match else None,
1348
+ five_axis_result=five_axis_result,
1349
+ )
1350
+
1351
+ self._dispatch_history.append(result)
1352
+ if len(self._dispatch_history) > self._max_history:
1353
+ self._dispatch_history = self._dispatch_history[-self._max_history:]
1354
+
1355
+ if self.enable_quality_guard and self.quality_guard:
1356
+ try:
1357
+ qreport = self.audit_quality()
1358
+ result.quality_report = qreport.to_markdown()
1359
+ except Exception as e:
1360
+ logger.warning("Quality audit failed: %s", e)
1361
+
1362
+ # 记录性能指标
1363
+ perf_metric = PerformanceMetric(
1364
+ timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1365
+ task_description=task_description,
1366
+ total_duration=total_duration,
1367
+ step_timings=result.details.get("timing", {}),
1368
+ success=result.success,
1369
+ error_count=len(result.errors),
1370
+ role_count=len(role_ids),
1371
+ )
1372
+ self._perf_monitor.record(perf_metric)
1373
+
1374
+ return result
1375
+
1376
+ except (ValueError, TypeError, AttributeError) as dispatch_err:
1377
+ logger.error(
1378
+ "Dispatch validation error for task '%s': %s - %s",
1379
+ task_description[:50], type(dispatch_err).__name__, dispatch_err,
1380
+ exc_info=True
1381
+ )
1382
+ friendly = make_user_friendly_error("dispatch_failed", original_error=dispatch_err)
1383
+ return DispatchResult(
1384
+ success=False,
1385
+ task_description=task_description,
1386
+ matched_roles=[],
1387
+ summary=friendly.message,
1388
+ errors=[friendly.format()],
1389
+ duration_seconds=time.time() - start_time,
1390
+ lang=lang,
1391
+ )
1392
+ except (ImportError, ModuleNotFoundError) as import_err:
1393
+ logger.error(
1394
+ "Missing dependency during dispatch of task '%s': %s",
1395
+ task_description[:50], import_err,
1396
+ exc_info=True
1397
+ )
1398
+ friendly = make_user_friendly_error("backend_unavailable", original_error=import_err)
1399
+ return DispatchResult(
1400
+ success=False,
1401
+ task_description=task_description,
1402
+ matched_roles=[],
1403
+ summary=friendly.message,
1404
+ errors=[friendly.format()],
1405
+ duration_seconds=time.time() - start_time,
1406
+ lang=lang,
1407
+ )
1408
+ except Exception as e:
1409
+ logger.critical(
1410
+ "UNEXPECTED ERROR in dispatch task '%s': %s - %s",
1411
+ task_description[:50], type(e).__name__, e,
1412
+ exc_info=True
1413
+ )
1414
+ friendly = make_user_friendly_error("dispatch_failed", original_error=e)
1415
+ return DispatchResult(
1416
+ success=False,
1417
+ task_description=task_description,
1418
+ matched_roles=[],
1419
+ summary=friendly.message,
1420
+ errors=[friendly.format()],
1421
+ duration_seconds=time.time() - start_time,
1422
+ lang=lang,
1423
+ )
1424
+
1425
+ def _build_summary(self, task: str, roles: List[str],
1426
+ exec_result, sp_summary: str) -> str:
1427
+ """构建执行摘要"""
1428
+ return self.report_formatter.build_summary(task, roles, exec_result, sp_summary)
1429
+
1430
+ def quick_dispatch(self, task: str,
1431
+ output_format: str = "structured",
1432
+ include_action_items: bool = True,
1433
+ include_timing: bool = False) -> 'DispatchResult':
1434
+ """
1435
+ 快速调度 - 返回 DispatchResult,summary 包含格式化报告
1436
+
1437
+ Args:
1438
+ task: 任务描述
1439
+ output_format: 输出格式 ("structured"/"compact"/"detailed")
1440
+ - structured: 结构化报告 (默认, UI Designer推荐)
1441
+ - compact: 紧凑格式 (适合终端)
1442
+ - detailed: 详细完整报告
1443
+ include_action_items: 是否包含行动项建议
1444
+ include_timing: 是否包含各步骤耗时分析
1445
+
1446
+ Returns:
1447
+ DispatchResult: 调度结果,summary 字段包含格式化报告
1448
+ """
1449
+ result = self.dispatch(task)
1450
+
1451
+ if output_format == "structured":
1452
+ result.summary = self.report_formatter.format_structured_report(result, include_action_items, include_timing)
1453
+ elif output_format == "compact":
1454
+ result.summary = self.report_formatter.format_compact_report(result)
1455
+ else:
1456
+ result.summary = result.to_markdown()
1457
+
1458
+ return result
1459
+
1460
+ def _format_structured_report(self, result: 'DispatchResult',
1461
+ include_action_items: bool = True,
1462
+ include_timing: bool = False) -> str:
1463
+ return self.report_formatter.format_structured_report(result, include_action_items, include_timing)
1464
+
1465
+ def _format_compact_report(self, result: 'DispatchResult') -> str:
1466
+ return self.report_formatter.format_compact_report(result)
1467
+
1468
+ def _extract_findings(self, scratchpad_summary: str) -> List[str]:
1469
+ return self.report_formatter.extract_findings(scratchpad_summary)
1470
+
1471
+ def _generate_action_items(self, result: 'DispatchResult') -> List[Dict[str, str]]:
1472
+ return self.report_formatter.generate_action_items(result)
1473
+
1474
+ def get_status(self) -> Dict[str, Any]:
1475
+ """获取系统状态"""
1476
+ status = {
1477
+ "version": "3.6.0",
1478
+ "persist_dir": self.persist_dir,
1479
+ "components": {
1480
+ "coordinator": self.coordinator is not None,
1481
+ "scratchpad": self.scratchpad is not None,
1482
+ "batch_scheduler": self.batch_scheduler is not None,
1483
+ "consensus": self.consensus_engine is not None,
1484
+ "compressor": self.compressor is not None,
1485
+ "permission_guard": self.permission_guard is not None,
1486
+ "warmup_manager": self.warmup_manager is not None,
1487
+ "memory_bridge": self.memory_bridge is not None,
1488
+ "skillifier": self.skillifier is not None,
1489
+ "quality_guard": self.quality_guard is not None,
1490
+ "performance_monitor": True,
1491
+ },
1492
+ "dispatch_count": len(self._dispatch_history),
1493
+ "scratchpad_stats": self.scratchpad.get_stats() if self.scratchpad else {},
1494
+ }
1495
+
1496
+ # 性能监控统计
1497
+ try:
1498
+ perf_stats = self._perf_monitor.get_statistics()
1499
+ status["performance"] = perf_stats
1500
+
1501
+ # 回归检测
1502
+ regression = self._perf_monitor.detect_regression()
1503
+ if regression:
1504
+ status["regression_detected"] = regression
1505
+ except Exception as e:
1506
+ logger.debug("Performance stats collection failed: %s", e)
1507
+
1508
+ if self.warmup_manager:
1509
+ try:
1510
+ metrics = self.warmup_manager.get_metrics()
1511
+ status["warmup_metrics"] = {
1512
+ "cache_size": metrics.cache_size,
1513
+ "hit_rate": round(metrics.cache_hit_rate, 3) if metrics.cache_hit_rate else 0,
1514
+ "tasks_completed": metrics.tasks_completed,
1515
+ "eager_duration_ms": round(metrics.eager_duration_ms, 2),
1516
+ }
1517
+ except Exception:
1518
+ status["warmup_metrics"] = None
1519
+
1520
+ if self.memory_bridge:
1521
+ try:
1522
+ mem_stats = self.memory_bridge.get_statistics()
1523
+ status["memory_stats"] = {
1524
+ "total_memories": mem_stats.total_memories,
1525
+ "by_type_counts": mem_stats.by_type_counts,
1526
+ "index_built": mem_stats.index_built,
1527
+ }
1528
+ except Exception:
1529
+ status["memory_stats"] = None
1530
+
1531
+ return status
1532
+
1533
+ def get_history(self, limit: int = 10) -> List[Dict[str, Any]]:
1534
+ """获取调度历史"""
1535
+ return [r.to_dict() for r in self._dispatch_history[-limit:]]
1536
+
1537
+ def audit_quality(self, module_path: Optional[str] = None,
1538
+ test_path: Optional[str] = None) -> TestQualityReport:
1539
+ """
1540
+ 执行测试质量审计 (P1 集成)
1541
+
1542
+ Args:
1543
+ module_path: 被测模块路径(默认自动检测 collaboration/ 下所有模块)
1544
+ test_path: 测试文件路径
1545
+
1546
+ Returns:
1547
+ TestQualityReport: 完整质量报告
1548
+ """
1549
+ if not self.quality_guard:
1550
+ self.quality_guard = TestQualityGuard("", "")
1551
+
1552
+ if module_path and test_path:
1553
+ return self.quality_guard.__class__(module_path, test_path).audit()
1554
+
1555
+ collab_dir = os.path.dirname(os.path.abspath(__file__))
1556
+ reports = []
1557
+ for fname in os.listdir(collab_dir):
1558
+ if fname.endswith(".py") and not fname.startswith("_") and "test" not in fname:
1559
+ mod_name = fname.replace(".py", "")
1560
+ test_name = f"{mod_name}_test.py"
1561
+ mod_full = os.path.join(collab_dir, fname)
1562
+ test_full = os.path.join(collab_dir, test_name)
1563
+ if os.path.exists(test_full):
1564
+ try:
1565
+ r = self.quality_guard.__class__(mod_full, test_full).audit()
1566
+ reports.append(r)
1567
+ except Exception as e:
1568
+ logger.warning("Quality guard audit failed for %s: %s", mod_name, e)
1569
+
1570
+ if len(reports) == 1:
1571
+ return reports[0]
1572
+
1573
+ combined = TestQualityReport(
1574
+ module_name="project",
1575
+ test_file=f"{len(reports)} modules",
1576
+ source_file=collab_dir,
1577
+ timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1578
+ )
1579
+ combined.total_tests = sum(r.total_tests for r in reports)
1580
+ combined.issues = [i for r in reports for i in r.issues]
1581
+ combined.test_functions = [tf for r in reports for tf in r.test_functions]
1582
+ if reports:
1583
+ scores = [r.score.overall for r in reports]
1584
+ combined.score.overall = sum(scores) / len(scores) if scores else 0
1585
+ combined.audit_time = sum(r.audit_time for r in reports)
1586
+ return combined
1587
+
1588
+ def get_performance_stats(self) -> Dict[str, Any]:
1589
+ """获取性能统计信息"""
1590
+ return self._perf_monitor.get_statistics()
1591
+
1592
+ def check_performance_regression(self) -> Optional[Dict[str, Any]]:
1593
+ """检查是否存在性能回归"""
1594
+ return self._perf_monitor.detect_regression()
1595
+
1596
+ def export_performance_metrics(self, output_file: str):
1597
+ """导出性能指标到文件"""
1598
+ self._perf_monitor.export_metrics(output_file, allowed_base_dir=self.persist_dir)
1599
+
1600
+ def clear_performance_history(self):
1601
+ """清除性能历史数据"""
1602
+ self._perf_monitor.clear()
1603
+ logger.info("Performance history cleared")
1604
+
1605
+ def shutdown(self):
1606
+ """优雅关闭所有组件"""
1607
+ if self.warmup_manager:
1608
+ try:
1609
+ self.warmup_manager.shutdown()
1610
+ except Exception as e:
1611
+ logger.warning("Warmup shutdown failed: %s", e)
1612
+
1613
+ if self.memory_bridge:
1614
+ try:
1615
+ self.memory_bridge.cleanup_expired_memories()
1616
+ except Exception as e:
1617
+ logger.warning("Memory cleanup failed: %s", e)
1618
+
1619
+ if self.usage_tracker:
1620
+ try:
1621
+ self.usage_tracker.persist()
1622
+ except Exception as e:
1623
+ logger.warning("Usage tracker persist failed: %s", e)
1624
+
1625
+
1626
+ def create_dispatcher(**kwargs) -> MultiAgentDispatcher:
1627
+ """工厂函数 - 创建并初始化调度器实例"""
1628
+ return MultiAgentDispatcher(**kwargs)
1629
+
1630
+
1631
+ def quick_collaborate(task: str, **kwargs) -> DispatchResult:
1632
+ """便捷函数 - 单次调用完成协作"""
1633
+ disp = create_dispatcher(**kwargs)
1634
+ result = disp.dispatch(task)
1635
+ disp.shutdown()
1636
+ return result