loom-agent 0.0.1__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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/__init__.py +77 -0
- loom/agent.py +217 -0
- loom/agents/__init__.py +10 -0
- loom/agents/refs.py +28 -0
- loom/agents/registry.py +50 -0
- loom/builtin/compression/__init__.py +4 -0
- loom/builtin/compression/structured.py +79 -0
- loom/builtin/embeddings/__init__.py +9 -0
- loom/builtin/embeddings/openai_embedding.py +135 -0
- loom/builtin/embeddings/sentence_transformers_embedding.py +145 -0
- loom/builtin/llms/__init__.py +8 -0
- loom/builtin/llms/mock.py +34 -0
- loom/builtin/llms/openai.py +168 -0
- loom/builtin/llms/rule.py +102 -0
- loom/builtin/memory/__init__.py +5 -0
- loom/builtin/memory/in_memory.py +21 -0
- loom/builtin/memory/persistent_memory.py +278 -0
- loom/builtin/retriever/__init__.py +9 -0
- loom/builtin/retriever/chroma_store.py +265 -0
- loom/builtin/retriever/in_memory.py +106 -0
- loom/builtin/retriever/milvus_store.py +307 -0
- loom/builtin/retriever/pinecone_store.py +237 -0
- loom/builtin/retriever/qdrant_store.py +274 -0
- loom/builtin/retriever/vector_store.py +128 -0
- loom/builtin/retriever/vector_store_config.py +217 -0
- loom/builtin/tools/__init__.py +32 -0
- loom/builtin/tools/calculator.py +49 -0
- loom/builtin/tools/document_search.py +111 -0
- loom/builtin/tools/glob.py +27 -0
- loom/builtin/tools/grep.py +56 -0
- loom/builtin/tools/http_request.py +86 -0
- loom/builtin/tools/python_repl.py +73 -0
- loom/builtin/tools/read_file.py +32 -0
- loom/builtin/tools/task.py +158 -0
- loom/builtin/tools/web_search.py +64 -0
- loom/builtin/tools/write_file.py +31 -0
- loom/callbacks/base.py +9 -0
- loom/callbacks/logging.py +12 -0
- loom/callbacks/metrics.py +27 -0
- loom/callbacks/observability.py +248 -0
- loom/components/agent.py +107 -0
- loom/core/agent_executor.py +450 -0
- loom/core/circuit_breaker.py +178 -0
- loom/core/compression_manager.py +329 -0
- loom/core/context_retriever.py +185 -0
- loom/core/error_classifier.py +193 -0
- loom/core/errors.py +66 -0
- loom/core/message_queue.py +167 -0
- loom/core/permission_store.py +62 -0
- loom/core/permissions.py +69 -0
- loom/core/scheduler.py +125 -0
- loom/core/steering_control.py +47 -0
- loom/core/structured_logger.py +279 -0
- loom/core/subagent_pool.py +232 -0
- loom/core/system_prompt.py +141 -0
- loom/core/system_reminders.py +283 -0
- loom/core/tool_pipeline.py +113 -0
- loom/core/types.py +269 -0
- loom/interfaces/compressor.py +59 -0
- loom/interfaces/embedding.py +51 -0
- loom/interfaces/llm.py +33 -0
- loom/interfaces/memory.py +29 -0
- loom/interfaces/retriever.py +179 -0
- loom/interfaces/tool.py +27 -0
- loom/interfaces/vector_store.py +80 -0
- loom/llm/__init__.py +14 -0
- loom/llm/config.py +228 -0
- loom/llm/factory.py +111 -0
- loom/llm/model_health.py +235 -0
- loom/llm/model_pool_advanced.py +305 -0
- loom/llm/pool.py +170 -0
- loom/llm/registry.py +201 -0
- loom/mcp/__init__.py +4 -0
- loom/mcp/client.py +86 -0
- loom/mcp/registry.py +58 -0
- loom/mcp/tool_adapter.py +48 -0
- loom/observability/__init__.py +5 -0
- loom/patterns/__init__.py +5 -0
- loom/patterns/multi_agent.py +123 -0
- loom/patterns/rag.py +262 -0
- loom/plugins/registry.py +55 -0
- loom/resilience/__init__.py +5 -0
- loom/tooling.py +72 -0
- loom/utils/agent_loader.py +218 -0
- loom/utils/token_counter.py +19 -0
- loom_agent-0.0.1.dist-info/METADATA +457 -0
- loom_agent-0.0.1.dist-info/RECORD +89 -0
- loom_agent-0.0.1.dist-info/WHEEL +4 -0
- loom_agent-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""US7: Dynamic System Reminders
|
|
2
|
+
|
|
3
|
+
Provides contextual hints and reminders to agents based on runtime state.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
- "Warning: Memory usage is high (15,234 tokens). Consider using more concise responses."
|
|
7
|
+
- "Note: 3 stale todos detected. Consider cleaning up completed items."
|
|
8
|
+
- "Tip: Multiple file writes detected. Use batch operations for efficiency."
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import List, Optional, Callable, Any
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Reminder:
|
|
20
|
+
"""A system reminder."""
|
|
21
|
+
category: str # "memory", "performance", "todos", "errors"
|
|
22
|
+
severity: str # "info", "warning", "critical"
|
|
23
|
+
message: str
|
|
24
|
+
metadata: dict
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReminderRule:
|
|
28
|
+
"""Base class for reminder rules."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, category: str, severity: str = "info"):
|
|
31
|
+
self.category = category
|
|
32
|
+
self.severity = severity
|
|
33
|
+
|
|
34
|
+
def check(self, context: dict) -> Optional[Reminder]:
|
|
35
|
+
"""Check if reminder should be triggered.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
context: Runtime context dict
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Reminder if triggered, None otherwise
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HighMemoryRule(ReminderRule):
|
|
47
|
+
"""Remind when memory usage is high."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, threshold_tokens: int = 14000):
|
|
50
|
+
super().__init__("memory", "warning")
|
|
51
|
+
self.threshold_tokens = threshold_tokens
|
|
52
|
+
|
|
53
|
+
def check(self, context: dict) -> Optional[Reminder]:
|
|
54
|
+
current_tokens = context.get("current_tokens", 0)
|
|
55
|
+
max_tokens = context.get("max_tokens", 16000)
|
|
56
|
+
|
|
57
|
+
if current_tokens > self.threshold_tokens:
|
|
58
|
+
percentage = (current_tokens / max_tokens) * 100
|
|
59
|
+
return Reminder(
|
|
60
|
+
category=self.category,
|
|
61
|
+
severity=self.severity,
|
|
62
|
+
message=f"Memory usage is high ({current_tokens:,} tokens, {percentage:.1f}%). Consider using more concise responses or compressing context.",
|
|
63
|
+
metadata={
|
|
64
|
+
"current_tokens": current_tokens,
|
|
65
|
+
"max_tokens": max_tokens,
|
|
66
|
+
"percentage": percentage,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StaleTodosRule(ReminderRule):
|
|
73
|
+
"""Remind about stale todos."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, stale_threshold_hours: int = 24):
|
|
76
|
+
super().__init__("todos", "info")
|
|
77
|
+
self.stale_threshold = timedelta(hours=stale_threshold_hours)
|
|
78
|
+
|
|
79
|
+
def check(self, context: dict) -> Optional[Reminder]:
|
|
80
|
+
todos = context.get("todos", [])
|
|
81
|
+
now = datetime.now()
|
|
82
|
+
|
|
83
|
+
# Count stale completed todos
|
|
84
|
+
stale_count = 0
|
|
85
|
+
for todo in todos:
|
|
86
|
+
if todo.get("status") == "completed":
|
|
87
|
+
completed_at = todo.get("completed_at")
|
|
88
|
+
if completed_at and isinstance(completed_at, datetime):
|
|
89
|
+
if now - completed_at > self.stale_threshold:
|
|
90
|
+
stale_count += 1
|
|
91
|
+
|
|
92
|
+
if stale_count > 0:
|
|
93
|
+
return Reminder(
|
|
94
|
+
category=self.category,
|
|
95
|
+
severity=self.severity,
|
|
96
|
+
message=f"{stale_count} stale todo(s) detected (completed >24h ago). Consider cleaning up.",
|
|
97
|
+
metadata={"stale_count": stale_count}
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class HighErrorRateRule(ReminderRule):
|
|
103
|
+
"""Remind when error rate is high."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, threshold_percentage: float = 20.0):
|
|
106
|
+
super().__init__("errors", "warning")
|
|
107
|
+
self.threshold_percentage = threshold_percentage
|
|
108
|
+
|
|
109
|
+
def check(self, context: dict) -> Optional[Reminder]:
|
|
110
|
+
metrics = context.get("metrics", {})
|
|
111
|
+
total_ops = metrics.get("total_operations", 0)
|
|
112
|
+
errors = metrics.get("total_errors", 0)
|
|
113
|
+
|
|
114
|
+
if total_ops > 0:
|
|
115
|
+
error_rate = (errors / total_ops) * 100
|
|
116
|
+
if error_rate > self.threshold_percentage:
|
|
117
|
+
return Reminder(
|
|
118
|
+
category=self.category,
|
|
119
|
+
severity="critical" if error_rate > 50 else "warning",
|
|
120
|
+
message=f"High error rate detected: {errors}/{total_ops} operations failed ({error_rate:.1f}%). Check logs for details.",
|
|
121
|
+
metadata={
|
|
122
|
+
"error_rate": error_rate,
|
|
123
|
+
"errors": errors,
|
|
124
|
+
"total_ops": total_ops,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FrequentCompressionRule(ReminderRule):
|
|
131
|
+
"""Remind when compression happens too frequently."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, threshold_count: int = 5):
|
|
134
|
+
super().__init__("performance", "info")
|
|
135
|
+
self.threshold_count = threshold_count
|
|
136
|
+
|
|
137
|
+
def check(self, context: dict) -> Optional[Reminder]:
|
|
138
|
+
metrics = context.get("metrics", {})
|
|
139
|
+
compressions = metrics.get("compressions", 0)
|
|
140
|
+
|
|
141
|
+
if compressions >= self.threshold_count:
|
|
142
|
+
return Reminder(
|
|
143
|
+
category=self.category,
|
|
144
|
+
severity=self.severity,
|
|
145
|
+
message=f"Context compressed {compressions} times. Consider increasing max_context_tokens or using shorter prompts.",
|
|
146
|
+
metadata={"compression_count": compressions}
|
|
147
|
+
)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SystemReminderManager:
|
|
152
|
+
"""Manages system reminders and dynamic hint injection.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
manager = SystemReminderManager()
|
|
156
|
+
manager.add_rule(HighMemoryRule())
|
|
157
|
+
manager.add_rule(HighErrorRateRule())
|
|
158
|
+
|
|
159
|
+
# Check for reminders
|
|
160
|
+
context = {
|
|
161
|
+
"current_tokens": 15000,
|
|
162
|
+
"max_tokens": 16000,
|
|
163
|
+
"metrics": {"total_errors": 5, "total_operations": 10}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
reminders = manager.check_all(context)
|
|
167
|
+
if reminders:
|
|
168
|
+
print(manager.format_reminders(reminders))
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self):
|
|
172
|
+
"""Initialize reminder manager."""
|
|
173
|
+
self.rules: List[ReminderRule] = []
|
|
174
|
+
self._default_rules()
|
|
175
|
+
|
|
176
|
+
def _default_rules(self):
|
|
177
|
+
"""Add default reminder rules."""
|
|
178
|
+
self.add_rule(HighMemoryRule(threshold_tokens=14000))
|
|
179
|
+
self.add_rule(HighErrorRateRule(threshold_percentage=20.0))
|
|
180
|
+
self.add_rule(FrequentCompressionRule(threshold_count=5))
|
|
181
|
+
# StaleTodosRule disabled by default (requires todo tracking)
|
|
182
|
+
|
|
183
|
+
def add_rule(self, rule: ReminderRule) -> None:
|
|
184
|
+
"""Add a reminder rule.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
rule: ReminderRule instance
|
|
188
|
+
"""
|
|
189
|
+
self.rules.append(rule)
|
|
190
|
+
|
|
191
|
+
def remove_rule(self, category: str) -> None:
|
|
192
|
+
"""Remove all rules for a category.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
category: Category to remove
|
|
196
|
+
"""
|
|
197
|
+
self.rules = [r for r in self.rules if r.category != category]
|
|
198
|
+
|
|
199
|
+
def check_all(self, context: dict) -> List[Reminder]:
|
|
200
|
+
"""Check all rules and return triggered reminders.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
context: Runtime context
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of triggered reminders
|
|
207
|
+
"""
|
|
208
|
+
reminders = []
|
|
209
|
+
for rule in self.rules:
|
|
210
|
+
try:
|
|
211
|
+
reminder = rule.check(context)
|
|
212
|
+
if reminder:
|
|
213
|
+
reminders.append(reminder)
|
|
214
|
+
except Exception:
|
|
215
|
+
# Don't fail if a rule raises
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
return reminders
|
|
219
|
+
|
|
220
|
+
def format_reminders(self, reminders: List[Reminder]) -> str:
|
|
221
|
+
"""Format reminders as human-readable text.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
reminders: List of reminders
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Formatted reminder text
|
|
228
|
+
"""
|
|
229
|
+
if not reminders:
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
lines = ["=== System Reminders ==="]
|
|
233
|
+
|
|
234
|
+
# Group by severity
|
|
235
|
+
critical = [r for r in reminders if r.severity == "critical"]
|
|
236
|
+
warnings = [r for r in reminders if r.severity == "warning"]
|
|
237
|
+
info = [r for r in reminders if r.severity == "info"]
|
|
238
|
+
|
|
239
|
+
for severity_list, prefix in [
|
|
240
|
+
(critical, "🚨 CRITICAL"),
|
|
241
|
+
(warnings, "⚠️ WARNING"),
|
|
242
|
+
(info, "ℹ️ INFO"),
|
|
243
|
+
]:
|
|
244
|
+
for reminder in severity_list:
|
|
245
|
+
lines.append(f"{prefix}: {reminder.message}")
|
|
246
|
+
|
|
247
|
+
return "\n".join(lines)
|
|
248
|
+
|
|
249
|
+
def inject_into_context(
|
|
250
|
+
self,
|
|
251
|
+
context: dict,
|
|
252
|
+
existing_system_prompt: str = ""
|
|
253
|
+
) -> str:
|
|
254
|
+
"""Check reminders and inject into system prompt.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
context: Runtime context
|
|
258
|
+
existing_system_prompt: Existing system prompt
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
System prompt with reminders injected
|
|
262
|
+
"""
|
|
263
|
+
reminders = self.check_all(context)
|
|
264
|
+
|
|
265
|
+
if not reminders:
|
|
266
|
+
return existing_system_prompt
|
|
267
|
+
|
|
268
|
+
reminder_text = self.format_reminders(reminders)
|
|
269
|
+
|
|
270
|
+
# Inject at end of system prompt
|
|
271
|
+
if existing_system_prompt:
|
|
272
|
+
return f"{existing_system_prompt}\n\n{reminder_text}"
|
|
273
|
+
else:
|
|
274
|
+
return reminder_text
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Global instance
|
|
278
|
+
_default_manager = SystemReminderManager()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_reminder_manager() -> SystemReminderManager:
|
|
282
|
+
"""Get the global reminder manager."""
|
|
283
|
+
return _default_manager
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, AsyncGenerator, Dict, Iterable, List
|
|
6
|
+
|
|
7
|
+
from loom.core.errors import PermissionDeniedError, ToolNotFoundError, ToolValidationError
|
|
8
|
+
from loom.core.types import ToolCall, ToolResult
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
from loom.core.scheduler import Scheduler
|
|
11
|
+
from loom.core.permissions import PermissionManager, PermissionAction
|
|
12
|
+
from loom.callbacks.metrics import MetricsCollector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExecutionStage(str, Enum):
|
|
16
|
+
DISCOVER = "discover"
|
|
17
|
+
VALIDATE = "validate"
|
|
18
|
+
AUTHORIZE = "authorize"
|
|
19
|
+
CHECK_CANCEL = "check_cancel"
|
|
20
|
+
EXECUTE = "execute"
|
|
21
|
+
FORMAT = "format"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolExecutionPipeline:
|
|
25
|
+
"""最小可用的六阶段工具执行流水线(Discover→...→Format)。"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
tools: Dict[str, BaseTool],
|
|
30
|
+
scheduler: Scheduler | None = None,
|
|
31
|
+
permission_manager: PermissionManager | None = None,
|
|
32
|
+
metrics: MetricsCollector | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.tools = tools
|
|
35
|
+
self.scheduler = scheduler or Scheduler()
|
|
36
|
+
self._stage_metrics: Dict[str, float] = {}
|
|
37
|
+
self.permission_manager = permission_manager or PermissionManager(policy={"default": "allow"})
|
|
38
|
+
self.metrics = metrics or MetricsCollector()
|
|
39
|
+
|
|
40
|
+
async def execute_calls(self, tool_calls: Iterable[ToolCall]) -> AsyncGenerator[ToolResult, None]:
|
|
41
|
+
for call in tool_calls:
|
|
42
|
+
try:
|
|
43
|
+
tool = await self._stage_discover(call)
|
|
44
|
+
args = await self._stage_validate(tool, call)
|
|
45
|
+
await self._stage_authorize(tool, call, args)
|
|
46
|
+
# check_cancel: 由上层 EventBus 注入,此处留空
|
|
47
|
+
result = await self._stage_execute(tool, args, call)
|
|
48
|
+
tr = await self._stage_format(result, call)
|
|
49
|
+
self.metrics.metrics.tool_calls += 1
|
|
50
|
+
yield tr
|
|
51
|
+
except Exception as e: # 简化:归一化错误
|
|
52
|
+
self.metrics.metrics.tool_calls += 1
|
|
53
|
+
self.metrics.metrics.total_errors += 1
|
|
54
|
+
yield ToolResult(
|
|
55
|
+
tool_call_id=call.id,
|
|
56
|
+
status="error",
|
|
57
|
+
content=str(e),
|
|
58
|
+
metadata={"stage_breakdown": self._stage_metrics.copy()},
|
|
59
|
+
)
|
|
60
|
+
finally:
|
|
61
|
+
self._stage_metrics.clear()
|
|
62
|
+
|
|
63
|
+
async def _stage_discover(self, tool_call: ToolCall) -> BaseTool:
|
|
64
|
+
start = time.time()
|
|
65
|
+
if tool_call.name not in self.tools:
|
|
66
|
+
raise ToolNotFoundError(f"Tool '{tool_call.name}' not registered")
|
|
67
|
+
tool = self.tools[tool_call.name]
|
|
68
|
+
self._stage_metrics[ExecutionStage.DISCOVER.value] = time.time() - start
|
|
69
|
+
return tool
|
|
70
|
+
|
|
71
|
+
async def _stage_validate(self, tool: BaseTool, tool_call: ToolCall) -> Dict[str, Any]:
|
|
72
|
+
start = time.time()
|
|
73
|
+
if not getattr(tool, "args_schema", None):
|
|
74
|
+
args = tool_call.arguments
|
|
75
|
+
else:
|
|
76
|
+
try:
|
|
77
|
+
args_model = tool.args_schema(**tool_call.arguments)
|
|
78
|
+
args = args_model.model_dump()
|
|
79
|
+
except Exception as e: # pydantic 验证错误
|
|
80
|
+
raise ToolValidationError(str(e))
|
|
81
|
+
self._stage_metrics[ExecutionStage.VALIDATE.value] = time.time() - start
|
|
82
|
+
return args
|
|
83
|
+
|
|
84
|
+
async def _stage_authorize(self, tool: BaseTool, tool_call: ToolCall, args: Dict[str, Any]) -> None:
|
|
85
|
+
start = time.time()
|
|
86
|
+
action = self.permission_manager.check(tool.name, tool_call.arguments)
|
|
87
|
+
if action == PermissionAction.DENY:
|
|
88
|
+
raise PermissionDeniedError(f"Permission denied for tool '{tool.name}'")
|
|
89
|
+
if action == PermissionAction.ASK:
|
|
90
|
+
confirmed = self.permission_manager.confirm(tool.name, args)
|
|
91
|
+
if not confirmed:
|
|
92
|
+
raise PermissionDeniedError(f"User denied permission for tool '{tool.name}'")
|
|
93
|
+
self._stage_metrics[ExecutionStage.AUTHORIZE.value] = time.time() - start
|
|
94
|
+
|
|
95
|
+
async def _stage_execute(self, tool: BaseTool, args: Dict[str, Any], call: ToolCall) -> Any:
|
|
96
|
+
start = time.time()
|
|
97
|
+
# 使用调度器包装执行
|
|
98
|
+
result = None
|
|
99
|
+
async for r in self.scheduler.schedule_batch([(tool, args)]):
|
|
100
|
+
result = r
|
|
101
|
+
self._stage_metrics[ExecutionStage.EXECUTE.value] = time.time() - start
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
async def _stage_format(self, result: Any, call: ToolCall) -> ToolResult:
|
|
105
|
+
start = time.time()
|
|
106
|
+
tr = ToolResult(
|
|
107
|
+
tool_call_id=call.id,
|
|
108
|
+
status="success",
|
|
109
|
+
content=str(result) if result is not None else "",
|
|
110
|
+
metadata={"stage_breakdown": self._stage_metrics.copy()},
|
|
111
|
+
)
|
|
112
|
+
self._stage_metrics[ExecutionStage.FORMAT.value] = time.time() - start
|
|
113
|
+
return tr
|
loom/core/types.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Message:
|
|
14
|
+
role: str # user | assistant | system | tool
|
|
15
|
+
content: str
|
|
16
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
17
|
+
tool_call_id: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ToolCall:
|
|
22
|
+
id: str
|
|
23
|
+
name: str
|
|
24
|
+
arguments: Dict[str, Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ToolResult:
|
|
29
|
+
tool_call_id: str
|
|
30
|
+
status: str # success | error | warning
|
|
31
|
+
content: str
|
|
32
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class StreamEvent:
|
|
37
|
+
type: str # text_delta | tool_calls_start | tool_result | agent_finish | error | aborted
|
|
38
|
+
content: Optional[str] = None
|
|
39
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
40
|
+
result: Optional[ToolResult] = None
|
|
41
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Phase 2: Foundation Types for Claude Code Enhancements (v4.0.0)
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
# T011: MessageQueueItem - US1 Real-Time Steering
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
class MessageQueueItem(BaseModel):
|
|
53
|
+
"""Message queue item for h2A async message queue (US1)."""
|
|
54
|
+
|
|
55
|
+
id: UUID = Field(default_factory=uuid4, description="Unique message ID")
|
|
56
|
+
role: str = Field(..., description="Message role: user | assistant | system | tool")
|
|
57
|
+
content: str = Field(..., description="Message content")
|
|
58
|
+
priority: int = Field(default=5, ge=0, le=10, description="Priority 0-10 (10=highest)")
|
|
59
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Creation timestamp")
|
|
60
|
+
cancellable: bool = Field(default=True, description="Can this message be cancelled?")
|
|
61
|
+
correlation_id: Optional[str] = Field(default=None, description="Request correlation ID")
|
|
62
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# -----------------------------------------------------------------------------
|
|
66
|
+
# T012: CompressionMetadata - US2 Context Compression
|
|
67
|
+
# -----------------------------------------------------------------------------
|
|
68
|
+
class CompressionMetadata(BaseModel):
|
|
69
|
+
"""Metadata for AU2 8-segment compression (US2)."""
|
|
70
|
+
|
|
71
|
+
original_message_count: int = Field(..., ge=1, description="Messages before compression")
|
|
72
|
+
compressed_message_count: int = Field(..., ge=1, description="Messages after compression")
|
|
73
|
+
original_token_count: int = Field(..., ge=1, description="Tokens before compression")
|
|
74
|
+
compressed_token_count: int = Field(..., ge=1, description="Tokens after compression")
|
|
75
|
+
compression_ratio: float = Field(..., ge=0.0, le=1.0, description="Reduction ratio (0.75 = 75%)")
|
|
76
|
+
key_topics: List[str] = Field(default_factory=list, description="Extracted key topics")
|
|
77
|
+
compression_method: str = Field(default="au2_8segment", description="Algorithm used")
|
|
78
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Compression time")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# -----------------------------------------------------------------------------
|
|
82
|
+
# T013: SubAgentContext - US3 Sub-Agent Isolation
|
|
83
|
+
# -----------------------------------------------------------------------------
|
|
84
|
+
class SubAgentStatus(str, Enum):
|
|
85
|
+
"""Sub-agent execution status."""
|
|
86
|
+
|
|
87
|
+
PENDING = "pending"
|
|
88
|
+
RUNNING = "running"
|
|
89
|
+
COMPLETED = "completed"
|
|
90
|
+
FAILED = "failed"
|
|
91
|
+
TIMEOUT = "timeout"
|
|
92
|
+
CANCELLED = "cancelled"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SubAgentContext(BaseModel):
|
|
96
|
+
"""Context for I2A isolated sub-agent (US3)."""
|
|
97
|
+
|
|
98
|
+
agent_id: UUID = Field(default_factory=uuid4, description="Sub-agent unique ID")
|
|
99
|
+
parent_agent_id: Optional[UUID] = Field(default=None, description="Parent agent ID")
|
|
100
|
+
agent_type: str = Field(default="general", description="AgentSpec type reference")
|
|
101
|
+
tool_whitelist: Optional[List[str]] = Field(default=None, description="Allowed tools (None = all)")
|
|
102
|
+
max_iterations: int = Field(default=50, ge=1, le=100, description="Max loop iterations")
|
|
103
|
+
timeout_seconds: int = Field(default=300, ge=1, le=600, description="Max execution time")
|
|
104
|
+
status: SubAgentStatus = Field(default=SubAgentStatus.PENDING, description="Current status")
|
|
105
|
+
execution_depth: int = Field(default=0, ge=0, le=3, description="Nesting level")
|
|
106
|
+
parent_correlation_id: Optional[str] = Field(default=None, description="Parent's correlation ID")
|
|
107
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
108
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# -----------------------------------------------------------------------------
|
|
112
|
+
# T014: ToolExecutionStage - US4 Tool Pipeline
|
|
113
|
+
# -----------------------------------------------------------------------------
|
|
114
|
+
class ExecutionStage(str, Enum):
|
|
115
|
+
"""6-stage tool execution pipeline (MH1)."""
|
|
116
|
+
|
|
117
|
+
DISCOVER = "discover"
|
|
118
|
+
VALIDATE = "validate"
|
|
119
|
+
AUTHORIZE = "authorize"
|
|
120
|
+
CHECK_CANCEL = "check_cancel"
|
|
121
|
+
EXECUTE = "execute"
|
|
122
|
+
FORMAT = "format"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StageStatus(str, Enum):
|
|
126
|
+
"""Status of pipeline stage."""
|
|
127
|
+
|
|
128
|
+
PENDING = "pending"
|
|
129
|
+
SUCCESS = "success"
|
|
130
|
+
FAILED = "failed"
|
|
131
|
+
SKIPPED = "skipped"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ToolExecutionStageInfo(BaseModel):
|
|
135
|
+
"""Information about single pipeline stage."""
|
|
136
|
+
|
|
137
|
+
stage_name: ExecutionStage = Field(..., description="Stage identifier")
|
|
138
|
+
stage_status: StageStatus = Field(..., description="Stage result")
|
|
139
|
+
start_time: Optional[datetime] = Field(default=None, description="Stage start")
|
|
140
|
+
end_time: Optional[datetime] = Field(default=None, description="Stage end")
|
|
141
|
+
duration_ms: Optional[float] = Field(default=None, description="Duration in milliseconds")
|
|
142
|
+
error: Optional[str] = Field(default=None, description="Error message if failed")
|
|
143
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# -----------------------------------------------------------------------------
|
|
147
|
+
# T015: CircuitBreakerState - US5 Error Handling & Resilience
|
|
148
|
+
# -----------------------------------------------------------------------------
|
|
149
|
+
class CircuitState(str, Enum):
|
|
150
|
+
"""Circuit breaker state machine."""
|
|
151
|
+
|
|
152
|
+
CLOSED = "closed" # Normal operation
|
|
153
|
+
OPEN = "open" # Rejecting requests
|
|
154
|
+
HALF_OPEN = "half_open" # Testing recovery
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class CircuitBreakerState(BaseModel):
|
|
158
|
+
"""Circuit breaker state for service protection (US5)."""
|
|
159
|
+
|
|
160
|
+
service_name: str = Field(..., description="Protected service identifier")
|
|
161
|
+
state: CircuitState = Field(default=CircuitState.CLOSED, description="Current state")
|
|
162
|
+
failure_count: int = Field(default=0, ge=0, description="Consecutive failures")
|
|
163
|
+
success_count: int = Field(default=0, ge=0, description="Consecutive successes")
|
|
164
|
+
failure_threshold: int = Field(default=5, ge=1, description="Failures to open circuit")
|
|
165
|
+
success_threshold: int = Field(default=2, ge=1, description="Successes to close circuit")
|
|
166
|
+
timeout_seconds: int = Field(default=60, ge=1, description="Timeout in OPEN state")
|
|
167
|
+
last_failure_time: Optional[datetime] = Field(default=None)
|
|
168
|
+
last_success_time: Optional[datetime] = Field(default=None)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# -----------------------------------------------------------------------------
|
|
172
|
+
# T016: LongTermMemory - US6 Three-Tier Memory System
|
|
173
|
+
# -----------------------------------------------------------------------------
|
|
174
|
+
class LongTermMemory(BaseModel):
|
|
175
|
+
"""Long-term memory stored in .loom/CLAUDE.md (US6)."""
|
|
176
|
+
|
|
177
|
+
project_path: str = Field(..., description="Root project directory")
|
|
178
|
+
memory_file_path: str = Field(..., description="Path to .loom/CLAUDE.md")
|
|
179
|
+
sections: Dict[str, str] = Field(
|
|
180
|
+
default_factory=dict,
|
|
181
|
+
description="Markdown sections: code_style, architecture_decisions, user_preferences, etc.",
|
|
182
|
+
)
|
|
183
|
+
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
|
184
|
+
checksum: Optional[str] = Field(default=None, description="MD5 checksum for integrity")
|
|
185
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -----------------------------------------------------------------------------
|
|
189
|
+
# T017: AgentEvent Types - Enhanced Events for Callbacks
|
|
190
|
+
# -----------------------------------------------------------------------------
|
|
191
|
+
class AgentEventType(str, Enum):
|
|
192
|
+
"""Enhanced agent event types."""
|
|
193
|
+
|
|
194
|
+
TEXT_DELTA = "text_delta"
|
|
195
|
+
TOOL_CALL = "tool_call"
|
|
196
|
+
TOOL_RESULT = "tool_result"
|
|
197
|
+
COMPRESSION_START = "compression_start"
|
|
198
|
+
COMPRESSION_COMPLETE = "compression_complete"
|
|
199
|
+
SUBAGENT_SPAWNED = "subagent_spawned"
|
|
200
|
+
SUBAGENT_COMPLETED = "subagent_completed"
|
|
201
|
+
ERROR = "error"
|
|
202
|
+
COMPLETION = "completion"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class BaseAgentEvent(BaseModel):
|
|
206
|
+
"""Base class for all agent events."""
|
|
207
|
+
|
|
208
|
+
event_type: AgentEventType = Field(..., description="Event type identifier")
|
|
209
|
+
correlation_id: Optional[str] = Field(default=None, description="Request correlation ID")
|
|
210
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
211
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TextDeltaEvent(BaseAgentEvent):
|
|
215
|
+
"""Streaming text delta event."""
|
|
216
|
+
|
|
217
|
+
event_type: AgentEventType = Field(default=AgentEventType.TEXT_DELTA)
|
|
218
|
+
content: str = Field(..., description="Text delta content")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ToolCallEvent(BaseAgentEvent):
|
|
222
|
+
"""Tool call initiated event."""
|
|
223
|
+
|
|
224
|
+
event_type: AgentEventType = Field(default=AgentEventType.TOOL_CALL)
|
|
225
|
+
tool_call_id: str = Field(..., description="Tool call ID")
|
|
226
|
+
tool_name: str = Field(..., description="Tool name")
|
|
227
|
+
arguments: Dict[str, Any] = Field(..., description="Tool arguments")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ToolResultEvent(BaseAgentEvent):
|
|
231
|
+
"""Tool execution result event."""
|
|
232
|
+
|
|
233
|
+
event_type: AgentEventType = Field(default=AgentEventType.TOOL_RESULT)
|
|
234
|
+
tool_call_id: str = Field(..., description="Tool call ID")
|
|
235
|
+
status: str = Field(..., description="success | error | warning")
|
|
236
|
+
content: str = Field(..., description="Result content")
|
|
237
|
+
stages: List[ToolExecutionStageInfo] = Field(default_factory=list, description="Pipeline stages")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class CompressionEvent(BaseAgentEvent):
|
|
241
|
+
"""Context compression event."""
|
|
242
|
+
|
|
243
|
+
event_type: AgentEventType = Field(default=AgentEventType.COMPRESSION_COMPLETE)
|
|
244
|
+
compression_metadata: CompressionMetadata = Field(..., description="Compression details")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class SubAgentSpawnedEvent(BaseAgentEvent):
|
|
248
|
+
"""Sub-agent spawned event."""
|
|
249
|
+
|
|
250
|
+
event_type: AgentEventType = Field(default=AgentEventType.SUBAGENT_SPAWNED)
|
|
251
|
+
subagent_context: SubAgentContext = Field(..., description="Sub-agent details")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ErrorEvent(BaseAgentEvent):
|
|
255
|
+
"""Error event."""
|
|
256
|
+
|
|
257
|
+
event_type: AgentEventType = Field(default=AgentEventType.ERROR)
|
|
258
|
+
error_message: str = Field(..., description="Error description")
|
|
259
|
+
error_category: Optional[str] = Field(default=None, description="ErrorCategory enum value")
|
|
260
|
+
recoverable: bool = Field(default=False, description="Is error recoverable?")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class CompletionEvent(BaseAgentEvent):
|
|
264
|
+
"""Agent completion event."""
|
|
265
|
+
|
|
266
|
+
event_type: AgentEventType = Field(default=AgentEventType.COMPLETION)
|
|
267
|
+
final_content: str = Field(..., description="Final response")
|
|
268
|
+
model_used: Optional[str] = Field(default=None, description="Model identifier (if fallback occurred)")
|
|
269
|
+
|