devsquad 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,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"]
|