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,128 @@
1
+ #!/usr/bin/env python3
2
+ import logging
3
+ from typing import Dict, List, Any, Optional
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ @dataclass
11
+ class ContextEntry:
12
+ key: str
13
+ value: Any
14
+ layer: str = "task"
15
+ source: str = ""
16
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
17
+ ttl: Optional[int] = None
18
+
19
+ def is_expired(self) -> bool:
20
+ if self.ttl is None:
21
+ return False
22
+ from datetime import timedelta
23
+ created = datetime.fromisoformat(self.timestamp)
24
+ return (datetime.now() - created).total_seconds() > self.ttl
25
+
26
+
27
+ class DualLayerContextManager:
28
+ """
29
+ Dual-layer context manager for DevSquad.
30
+
31
+ Manages two context layers:
32
+ - **Project layer**: Long-lived, project-wide context (architecture decisions, tech stack, conventions)
33
+ - **Task layer**: Short-lived, task-specific context (current task, worker results, scratchpad state)
34
+
35
+ This separation prevents task-specific noise from polluting project-level context,
36
+ and allows project context to persist across multiple task dispatches.
37
+ """
38
+
39
+ def __init__(self, max_project_entries: int = 100, max_task_entries: int = 50):
40
+ self.project_context: Dict[str, ContextEntry] = {}
41
+ self.task_context: Dict[str, ContextEntry] = {}
42
+ self.max_project = max_project_entries
43
+ self.max_task = max_task_entries
44
+
45
+ def set_project(self, key: str, value: Any, source: str = "", ttl: Optional[int] = None):
46
+ self.project_context[key] = ContextEntry(
47
+ key=key, value=value, layer="project", source=source, ttl=ttl,
48
+ )
49
+ self._evict_if_needed("project")
50
+
51
+ def get_project(self, key: str, default: Any = None) -> Any:
52
+ entry = self.project_context.get(key)
53
+ if entry and not entry.is_expired():
54
+ return entry.value
55
+ if entry and entry.is_expired():
56
+ del self.project_context[key]
57
+ return default
58
+
59
+ def set_task(self, key: str, value: Any, source: str = "", ttl: Optional[int] = None):
60
+ self.task_context[key] = ContextEntry(
61
+ key=key, value=value, layer="task", source=source, ttl=ttl,
62
+ )
63
+ self._evict_if_needed("task")
64
+
65
+ def get_task(self, key: str, default: Any = None) -> Any:
66
+ entry = self.task_context.get(key)
67
+ if entry and not entry.is_expired():
68
+ return entry.value
69
+ if entry and entry.is_expired():
70
+ del self.task_context[key]
71
+ return default
72
+
73
+ def get_combined(self, keys: Optional[List[str]] = None) -> Dict[str, Any]:
74
+ combined = {}
75
+ for k, v in self.project_context.items():
76
+ if not v.is_expired() and (keys is None or k in keys):
77
+ combined[k] = v.value
78
+ for k, v in self.task_context.items():
79
+ if not v.is_expired() and (keys is None or k in keys):
80
+ combined[k] = v.value
81
+ return combined
82
+
83
+ def build_prompt_context(self, role_id: str = "", task_description: str = "") -> str:
84
+ parts = []
85
+ if self.project_context:
86
+ parts.append("## Project Context")
87
+ for k, v in self.project_context.items():
88
+ if not v.is_expired():
89
+ parts.append(f"- **{k}**: {v.value}")
90
+
91
+ if self.task_context:
92
+ parts.append("\n## Task Context")
93
+ for k, v in self.task_context.items():
94
+ if not v.is_expired():
95
+ parts.append(f"- **{k}**: {v.value}")
96
+
97
+ return "\n".join(parts)
98
+
99
+ def clear_task_context(self):
100
+ self.task_context.clear()
101
+
102
+ def clear_all(self):
103
+ self.project_context.clear()
104
+ self.task_context.clear()
105
+
106
+ def cleanup_expired(self):
107
+ expired_project = [k for k, v in self.project_context.items() if v.is_expired()]
108
+ expired_task = [k for k, v in self.task_context.items() if v.is_expired()]
109
+ for k in expired_project:
110
+ del self.project_context[k]
111
+ for k in expired_task:
112
+ del self.task_context[k]
113
+ return len(expired_project) + len(expired_task)
114
+
115
+ def get_stats(self) -> Dict[str, Any]:
116
+ return {
117
+ "project_entries": len(self.project_context),
118
+ "task_entries": len(self.task_context),
119
+ "total_entries": len(self.project_context) + len(self.task_context),
120
+ }
121
+
122
+ def _evict_if_needed(self, layer: str):
123
+ if layer == "project" and len(self.project_context) > self.max_project:
124
+ oldest_key = min(self.project_context, key=lambda k: self.project_context[k].timestamp)
125
+ del self.project_context[oldest_key]
126
+ elif layer == "task" and len(self.task_context) > self.max_task:
127
+ oldest_key = min(self.task_context, key=lambda k: self.task_context[k].timestamp)
128
+ del self.task_context[oldest_key]
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Enhanced Worker Module
5
+
6
+ Extends the base Worker with:
7
+ - Agent briefing integration (personalization memory)
8
+ - Performance monitoring integration
9
+ - LLM cache integration
10
+ - Retry mechanism integration
11
+ - Protocol-based provider injection
12
+
13
+ Usage:
14
+ from scripts.collaboration.enhanced_worker import EnhancedWorker
15
+
16
+ worker = EnhancedWorker(
17
+ worker_id="architect-1",
18
+ role_id="architect",
19
+ cache_provider=llm_cache,
20
+ retry_provider=llm_retry,
21
+ monitor_provider=performance_monitor,
22
+ )
23
+
24
+ result = worker.execute(task)
25
+
26
+ Version: v1.0
27
+ Created: 2026-05-01
28
+ """
29
+
30
+ import re
31
+ import time
32
+ import logging
33
+ import unicodedata
34
+ from typing import Optional, Dict, Any, List
35
+ from dataclasses import dataclass, field
36
+
37
+ from .worker import Worker
38
+ from .models import TaskDefinition, WorkerResult, ROLE_REGISTRY
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ _SAFE_FILENAME_RE = re.compile(r'[^\w\-.]')
43
+ _MAX_RULE_TEXT_LENGTH = 500
44
+
45
+
46
+ def _is_available(provider) -> bool:
47
+ """Check provider availability, compatible with both property and method."""
48
+ if provider is None:
49
+ return False
50
+ val = provider.is_available
51
+ if callable(val):
52
+ return val()
53
+ return bool(val)
54
+
55
+
56
+ @dataclass
57
+ class AgentBriefingOutput:
58
+ """Compressed state passed between Agents (latent-briefing pattern)."""
59
+ task_summary: str = ""
60
+ key_decisions: List[str] = field(default_factory=list)
61
+ pending_items: List[str] = field(default_factory=list)
62
+ rules_applied: List[str] = field(default_factory=list)
63
+ result_summary: str = ""
64
+ confidence: float = 0.0
65
+ assumptions: List[str] = field(default_factory=list)
66
+
67
+
68
+ class EnhancedWorker(Worker):
69
+ """
70
+ Enhanced Worker with protocol-based provider injection.
71
+
72
+ Extends base Worker with:
73
+ - CacheProvider: LLM response caching
74
+ - RetryProvider: LLM call retry with fallback
75
+ - MonitorProvider: Performance monitoring
76
+ - Agent briefing: Personalization memory injection
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ worker_id: str,
82
+ role_id: str,
83
+ role_prompt: str = "",
84
+ scratchpad=None,
85
+ llm_backend=None,
86
+ stream: bool = False,
87
+ cache_provider=None,
88
+ retry_provider=None,
89
+ monitor_provider=None,
90
+ memory_provider=None,
91
+ ):
92
+ """
93
+ Initialize EnhancedWorker.
94
+
95
+ Args:
96
+ worker_id: Unique worker identifier
97
+ role_id: Role identifier (e.g., 'architect', 'coder')
98
+ role_prompt: Custom role prompt
99
+ scratchpad: Scratchpad instance for inter-agent communication
100
+ llm_backend: LLM backend instance
101
+ stream: Whether to enable streaming output
102
+ cache_provider: CacheProvider implementation (optional)
103
+ retry_provider: RetryProvider implementation (optional)
104
+ monitor_provider: MonitorProvider implementation (optional)
105
+ memory_provider: MemoryProvider implementation (optional, for rule injection)
106
+ """
107
+ super().__init__(
108
+ worker_id=worker_id,
109
+ role_id=role_id,
110
+ role_prompt=role_prompt,
111
+ scratchpad=scratchpad,
112
+ llm_backend=llm_backend,
113
+ stream=stream,
114
+ )
115
+
116
+ self.cache_provider = cache_provider
117
+ self.retry_provider = retry_provider
118
+ self.monitor_provider = monitor_provider
119
+ self.memory_provider = memory_provider
120
+
121
+ self._briefing = None
122
+ self._briefing_loaded = False
123
+ self._last_result: Optional[WorkerResult] = None
124
+ self._confidence_scorer = None
125
+ self._injected_rules: List[Dict[str, Any]] = []
126
+ self._rules_applied: List[str] = []
127
+ self._validator = None
128
+
129
+ try:
130
+ from .confidence_score import ConfidenceScorer
131
+ self._confidence_scorer = ConfidenceScorer()
132
+ except Exception:
133
+ pass
134
+
135
+ try:
136
+ from .input_validator import InputValidator
137
+ self._validator = InputValidator()
138
+ except Exception:
139
+ pass
140
+
141
+ @property
142
+ def briefing(self):
143
+ """Get agent briefing (lazy load)."""
144
+ if not self._briefing_loaded:
145
+ self._load_briefing()
146
+ self._briefing_loaded = True
147
+ return self._briefing
148
+
149
+ def _load_briefing(self):
150
+ """Load agent briefing from memory provider."""
151
+ try:
152
+ from .agent_briefing import AgentBriefing
153
+ self._briefing = AgentBriefing(agent_role=self.role_id)
154
+ except Exception as e:
155
+ logger.warning("Failed to load briefing for %s: %s", self.role_id, e)
156
+ self._briefing = None
157
+
158
+ def _do_work_with_briefing(self, task: TaskDefinition) -> WorkerResult:
159
+ """
160
+ Execute task with briefing context injection.
161
+
162
+ Args:
163
+ task: Task definition
164
+
165
+ Returns:
166
+ WorkerResult with briefing context
167
+ """
168
+ start_time = time.time()
169
+
170
+ try:
171
+ briefing_context = None
172
+ if self._briefing:
173
+ try:
174
+ briefing_context = self._briefing.get_briefing_context(task.description)
175
+ except Exception as e:
176
+ logger.warning("Failed to get briefing context: %s", e)
177
+
178
+ result = self._do_work(task)
179
+
180
+ if briefing_context and result.output:
181
+ if isinstance(result.output, dict):
182
+ result.output["briefing_context"] = briefing_context
183
+ elif isinstance(result.output, str):
184
+ result.output = f"{briefing_context}\n\n{result.output}"
185
+
186
+ duration = time.time() - start_time
187
+ self._record_monitor(task, duration, success=True)
188
+
189
+ return result
190
+
191
+ except Exception as e:
192
+ duration = time.time() - start_time
193
+ self._record_monitor(task, duration, success=False)
194
+ raise
195
+
196
+ def _record_monitor(self, task: TaskDefinition, duration: float, success: bool):
197
+ """Record execution metrics to monitor provider."""
198
+ if self.monitor_provider and self.monitor_provider.is_available():
199
+ try:
200
+ self.monitor_provider.record_agent_execution(
201
+ agent_role=self.role_id,
202
+ task=task.description[:100],
203
+ duration=duration,
204
+ success=success,
205
+ )
206
+ except Exception as e:
207
+ logger.warning("Monitor recording failed: %s", e)
208
+
209
+ def execute(self, task: TaskDefinition) -> WorkerResult:
210
+ """
211
+ Execute task with all enhanced features.
212
+
213
+ Args:
214
+ task: Task definition
215
+
216
+ Returns:
217
+ WorkerResult
218
+ """
219
+ self._inject_rules_from_provider(task)
220
+
221
+ if self.retry_provider and self.retry_provider.is_available():
222
+ try:
223
+ result = self.retry_provider.retry_with_fallback(
224
+ func=lambda: self._do_work_with_briefing(task),
225
+ max_attempts=3,
226
+ fallback=lambda: self._do_work(task),
227
+ )
228
+ except Exception:
229
+ result = self._do_work(task)
230
+ else:
231
+ result = self._do_work_with_briefing(task)
232
+
233
+ self._last_result = result
234
+
235
+ if self._confidence_scorer and result.success and result.output:
236
+ try:
237
+ output_text = result.output if isinstance(result.output, str) else str(result.output)
238
+ score = self._confidence_scorer.score_response(output_text)
239
+ if isinstance(result.output, dict):
240
+ result.output["confidence"] = score.overall_score
241
+ result.output["confidence_factors"] = {
242
+ "completeness": score.completeness_score,
243
+ "certainty": score.certainty_score,
244
+ "specificity": score.specificity_score,
245
+ }
246
+ if score.overall_score < 0.7:
247
+ result.output["low_confidence_warning"] = (
248
+ f"Low confidence ({score.overall_score:.2f}). "
249
+ "Assumptions may need verification by subsequent agents."
250
+ )
251
+ except Exception as e:
252
+ logger.warning("Confidence scoring failed: %s", e)
253
+
254
+ if self._injected_rules and result.success and result.output:
255
+ violations = self._check_forbid_violations(result)
256
+ if violations:
257
+ if isinstance(result.output, dict):
258
+ result.output["rule_violations"] = violations
259
+
260
+ return result
261
+
262
+ def _inject_rules_from_provider(self, task: TaskDefinition):
263
+ """Fetch and validate rules from MemoryProvider before task execution."""
264
+ if not self.memory_provider or not _is_available(self.memory_provider):
265
+ return
266
+
267
+ try:
268
+ if hasattr(self.memory_provider, 'match_rules'):
269
+ raw_rules = self.memory_provider.match_rules(
270
+ task_description=task.description,
271
+ user_id=getattr(task, 'user_id', None) or "default",
272
+ role=self.role_id,
273
+ max_rules=5
274
+ )
275
+ else:
276
+ rule_strings = self.memory_provider.get_rules(
277
+ user_id=getattr(task, 'user_id', None) or "default",
278
+ context={"task": task.description, "role": self.role_id}
279
+ )
280
+ raw_rules = []
281
+ for rs in rule_strings:
282
+ if isinstance(rs, str):
283
+ raw_rules.append({
284
+ "rule_type": "always",
285
+ "trigger": rs.lower(),
286
+ "action": rs,
287
+ "relevance_score": 0.0,
288
+ "rule_id": "",
289
+ "override": False,
290
+ })
291
+
292
+ safe_rules = self._validate_injected_rules(raw_rules)
293
+ self._injected_rules = safe_rules
294
+ self._rules_applied = [r.get("rule_id", r.get("action", "")[:50]) for r in safe_rules]
295
+ except Exception as e:
296
+ logger.warning("Rule injection from provider failed: %s", e)
297
+ self._injected_rules = []
298
+
299
+ def _validate_injected_rules(self, rules: List[Dict]) -> List[Dict]:
300
+ """Sanitize rules before prompt injection to prevent prompt injection attacks.
301
+
302
+ Two-layer defense:
303
+ 1. InputValidator check on rule text + Unicode NFKC normalization + length limit
304
+ 2. Length check in format_rules_as_prompt() (handled by MCEAdapter)
305
+
306
+ Args:
307
+ rules: List of rule dicts from match_rules()
308
+
309
+ Returns:
310
+ List of validated rule dicts (suspicious rules silently skipped)
311
+ """
312
+ safe_rules = []
313
+ for rule in rules:
314
+ if not isinstance(rule, dict):
315
+ continue
316
+
317
+ action = rule.get("action", "")
318
+ trigger = rule.get("trigger", "")
319
+
320
+ if isinstance(action, str):
321
+ action = unicodedata.normalize('NFKC', action)
322
+ rule["action"] = action
323
+ if isinstance(trigger, str):
324
+ trigger = unicodedata.normalize('NFKC', trigger)
325
+ rule["trigger"] = trigger
326
+
327
+ if len(action) > _MAX_RULE_TEXT_LENGTH:
328
+ rule["action"] = action[:_MAX_RULE_TEXT_LENGTH]
329
+ if len(trigger) > _MAX_RULE_TEXT_LENGTH:
330
+ rule["trigger"] = trigger[:_MAX_RULE_TEXT_LENGTH]
331
+
332
+ if self._validator:
333
+ try:
334
+ action_valid = self._validator.validate_task(action).valid
335
+ trigger_valid = self._validator.validate_task(trigger).valid if trigger else True
336
+ if not action_valid or not trigger_valid:
337
+ logger.warning("Skipping suspicious rule: action_valid=%s, trigger_valid=%s",
338
+ action_valid, trigger_valid)
339
+ continue
340
+ except Exception:
341
+ pass
342
+
343
+ safe_rules.append(rule)
344
+
345
+ return safe_rules
346
+
347
+ def _check_forbid_violations(self, result: WorkerResult) -> List[Dict[str, str]]:
348
+ """
349
+ Post-processing check: verify forbid rules were not violated in output.
350
+
351
+ Short-term: annotate violations as warnings (no auto-retry).
352
+ Mid-term: auto-retry with enhanced prompt on forbid violation.
353
+
354
+ Args:
355
+ result: Worker execution result
356
+
357
+ Returns:
358
+ List of violation dicts with rule_id and description
359
+ """
360
+ violations = []
361
+ forbid_rules = [r for r in self._injected_rules
362
+ if isinstance(r, dict) and r.get("rule_type") == "forbid"]
363
+
364
+ if not forbid_rules or not result.output:
365
+ return violations
366
+
367
+ output_text = result.output if isinstance(result.output, str) else str(result.output)
368
+ output_lower = output_text.lower()
369
+
370
+ for rule in forbid_rules:
371
+ trigger = rule.get("trigger", "").lower()
372
+ action = rule.get("action", "").lower()
373
+ rule_id = rule.get("rule_id", "unknown")
374
+
375
+ if trigger and trigger in output_lower:
376
+ violations.append({
377
+ "rule_id": rule_id,
378
+ "trigger": trigger,
379
+ "action": action,
380
+ "severity": "high" if rule.get("override") else "medium",
381
+ "message": f"Output may violate forbid rule '{rule_id}': "
382
+ f"trigger '{trigger}' found in output",
383
+ })
384
+
385
+ return violations
386
+
387
+ def get_briefing_summary(self) -> Dict[str, Any]:
388
+ """
389
+ Get agent briefing summary.
390
+
391
+ Returns:
392
+ Briefing summary dictionary
393
+ """
394
+ if not self._briefing:
395
+ return {"status": "unavailable", "role": self.role_id}
396
+
397
+ try:
398
+ return {
399
+ "status": "available",
400
+ "role": self.role_id,
401
+ "rules_count": len(self._briefing.get_rules()),
402
+ }
403
+ except Exception:
404
+ return {"status": "error", "role": self.role_id}
405
+
406
+ def export_briefing(self, output_dir: str = "output/briefings") -> Optional[str]:
407
+ """
408
+ Export agent briefing to file.
409
+
410
+ Args:
411
+ output_dir: Output directory path
412
+
413
+ Returns:
414
+ File path if successful, None otherwise
415
+ """
416
+ if not self._briefing:
417
+ return None
418
+
419
+ try:
420
+ import os
421
+ os.makedirs(output_dir, exist_ok=True)
422
+
423
+ safe_role = _SAFE_FILENAME_RE.sub('_', self.role_id)
424
+ output_path = os.path.join(output_dir, f"{safe_role}_briefing.json")
425
+
426
+ return self._briefing.export_briefing(output_path)
427
+ except Exception as e:
428
+ logger.warning("Failed to export briefing for %s: %s", self.role_id, e)
429
+ return None
430
+
431
+ def compress_to_briefing(self) -> AgentBriefingOutput:
432
+ """
433
+ Compress current execution result into a briefing for the next Agent.
434
+
435
+ Implements the latent-briefing pattern: instead of passing full message
436
+ history between Agents, pass a compressed state with key decisions,
437
+ pending items, and applied rules.
438
+
439
+ Returns:
440
+ AgentBriefingOutput: Compressed state for inter-Agent handoff
441
+ """
442
+ if not self._last_result:
443
+ return AgentBriefingOutput()
444
+
445
+ output_text = ""
446
+ if isinstance(self._last_result.output, dict):
447
+ output_text = self._last_result.output.get("finding_summary", "")
448
+ elif isinstance(self._last_result.output, str):
449
+ output_text = self._last_result.output[:500]
450
+
451
+ confidence = 0.0
452
+ if self._confidence_scorer and output_text:
453
+ try:
454
+ score = self._confidence_scorer.score_response(output_text)
455
+ confidence = score.overall_score
456
+ except Exception:
457
+ confidence = 0.5
458
+
459
+ return AgentBriefingOutput(
460
+ task_summary=self._last_result.task_id if hasattr(self._last_result, 'task_id') else "",
461
+ key_decisions=self._extract_decisions(output_text),
462
+ pending_items=self._extract_pending(output_text),
463
+ rules_applied=list(self._rules_applied),
464
+ result_summary=output_text[:200] if output_text else "",
465
+ confidence=confidence,
466
+ assumptions=[],
467
+ )
468
+
469
+ def receive_briefing(self, briefing: AgentBriefingOutput):
470
+ """
471
+ Receive compressed state from a preceding Agent.
472
+
473
+ Args:
474
+ briefing: Compressed state from the previous Agent
475
+ """
476
+ self._received_briefing = briefing
477
+
478
+ def _extract_decisions(self, text: str) -> List[str]:
479
+ """Extract key decisions from output text."""
480
+ decisions = []
481
+ markers = ["decision:", "decided:", "chosen:", "selected:", "conclusion:"]
482
+ for line in text.split('\n'):
483
+ lower = line.strip().lower()
484
+ for marker in markers:
485
+ if marker in lower:
486
+ decisions.append(line.strip()[:100])
487
+ break
488
+ if len(decisions) >= 5:
489
+ break
490
+ return decisions
491
+
492
+ def _extract_pending(self, text: str) -> List[str]:
493
+ """Extract pending items from output text."""
494
+ pending = []
495
+ markers = ["todo:", "pending:", "next:", "follow-up:", "remaining:"]
496
+ for line in text.split('\n'):
497
+ lower = line.strip().lower()
498
+ for marker in markers:
499
+ if marker in lower:
500
+ pending.append(line.strip()[:100])
501
+ break
502
+ if len(pending) >= 5:
503
+ break
504
+ return pending
505
+
506
+ def get_provider_status(self) -> Dict[str, Any]:
507
+ """
508
+ Get status of all injected providers.
509
+
510
+ Returns:
511
+ Provider status dictionary
512
+ """
513
+ return {
514
+ "worker_id": self.worker_id,
515
+ "role_id": self.role_id,
516
+ "cache": {
517
+ "available": _is_available(self.cache_provider),
518
+ "type": type(self.cache_provider).__name__ if self.cache_provider else "none",
519
+ },
520
+ "retry": {
521
+ "available": _is_available(self.retry_provider),
522
+ "type": type(self.retry_provider).__name__ if self.retry_provider else "none",
523
+ },
524
+ "monitor": {
525
+ "available": _is_available(self.monitor_provider),
526
+ "type": type(self.monitor_provider).__name__ if self.monitor_provider else "none",
527
+ },
528
+ "memory": {
529
+ "available": _is_available(self.memory_provider),
530
+ "type": type(self.memory_provider).__name__ if self.memory_provider else "none",
531
+ "rules_injected": len(self._injected_rules),
532
+ },
533
+ "briefing": {
534
+ "available": self._briefing is not None,
535
+ },
536
+ }
537
+
538
+
539
+ __all__ = ["EnhancedWorker", "AgentBriefingOutput"]