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,279 @@
|
|
|
1
|
+
"""US7: Structured Logging with Correlation IDs
|
|
2
|
+
|
|
3
|
+
Provides JSON-formatted structured logging for production observability.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Correlation ID tracking across requests
|
|
7
|
+
- JSON format for log aggregation tools (Datadog, CloudWatch, etc.)
|
|
8
|
+
- Context propagation
|
|
9
|
+
- Performance metrics
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from contextvars import ContextVar
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Context variable for correlation ID
|
|
23
|
+
_correlation_id: ContextVar[Optional[str]] = ContextVar('correlation_id', default=None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StructuredLogger:
|
|
27
|
+
"""Structured logger with correlation ID support.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
logger = StructuredLogger("my_agent")
|
|
31
|
+
|
|
32
|
+
# Set correlation ID for request
|
|
33
|
+
logger.set_correlation_id("req-123")
|
|
34
|
+
|
|
35
|
+
# All logs include correlation_id
|
|
36
|
+
logger.info("Processing request", user_id="user_456")
|
|
37
|
+
# Output: {"timestamp": "...", "level": "INFO", "message": "Processing request",
|
|
38
|
+
# "correlation_id": "req-123", "user_id": "user_456"}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
name: str,
|
|
44
|
+
level: int = logging.INFO,
|
|
45
|
+
include_timestamp: bool = True,
|
|
46
|
+
include_location: bool = True,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize structured logger.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name: Logger name (usually module or component name)
|
|
52
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
53
|
+
include_timestamp: Include ISO timestamp in logs
|
|
54
|
+
include_location: Include file location in logs
|
|
55
|
+
"""
|
|
56
|
+
self.name = name
|
|
57
|
+
self.level = level
|
|
58
|
+
self.include_timestamp = include_timestamp
|
|
59
|
+
self.include_location = include_location
|
|
60
|
+
self._logger = logging.getLogger(name)
|
|
61
|
+
self._logger.setLevel(level)
|
|
62
|
+
|
|
63
|
+
def set_correlation_id(self, correlation_id: str) -> None:
|
|
64
|
+
"""Set correlation ID for current context.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
correlation_id: Unique identifier for request/conversation
|
|
68
|
+
"""
|
|
69
|
+
_correlation_id.set(correlation_id)
|
|
70
|
+
|
|
71
|
+
def get_correlation_id(self) -> Optional[str]:
|
|
72
|
+
"""Get current correlation ID."""
|
|
73
|
+
return _correlation_id.get()
|
|
74
|
+
|
|
75
|
+
def clear_correlation_id(self) -> None:
|
|
76
|
+
"""Clear correlation ID from context."""
|
|
77
|
+
_correlation_id.set(None)
|
|
78
|
+
|
|
79
|
+
def _format_log(
|
|
80
|
+
self,
|
|
81
|
+
level: str,
|
|
82
|
+
message: str,
|
|
83
|
+
extra: Dict[str, Any],
|
|
84
|
+
exc_info: Optional[Exception] = None,
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
"""Format log entry as JSON-serializable dict.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
level: Log level string
|
|
90
|
+
message: Log message
|
|
91
|
+
extra: Additional context fields
|
|
92
|
+
exc_info: Exception info if available
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
JSON-serializable dict
|
|
96
|
+
"""
|
|
97
|
+
log_entry = {
|
|
98
|
+
"level": level,
|
|
99
|
+
"message": message,
|
|
100
|
+
"logger": self.name,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Add timestamp
|
|
104
|
+
if self.include_timestamp:
|
|
105
|
+
log_entry["timestamp"] = datetime.utcnow().isoformat() + "Z"
|
|
106
|
+
|
|
107
|
+
# Add correlation ID if available
|
|
108
|
+
correlation_id = self.get_correlation_id()
|
|
109
|
+
if correlation_id:
|
|
110
|
+
log_entry["correlation_id"] = correlation_id
|
|
111
|
+
|
|
112
|
+
# Add extra fields
|
|
113
|
+
if extra:
|
|
114
|
+
log_entry.update(extra)
|
|
115
|
+
|
|
116
|
+
# Add exception info
|
|
117
|
+
if exc_info:
|
|
118
|
+
log_entry["exception"] = {
|
|
119
|
+
"type": type(exc_info).__name__,
|
|
120
|
+
"message": str(exc_info),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return log_entry
|
|
124
|
+
|
|
125
|
+
def _log(
|
|
126
|
+
self,
|
|
127
|
+
level: int,
|
|
128
|
+
level_name: str,
|
|
129
|
+
message: str,
|
|
130
|
+
exc_info: Optional[Exception] = None,
|
|
131
|
+
**kwargs: Any,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Internal logging method.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
level: Logging level constant
|
|
137
|
+
level_name: Level name string
|
|
138
|
+
message: Log message
|
|
139
|
+
exc_info: Exception if available
|
|
140
|
+
**kwargs: Additional context fields
|
|
141
|
+
"""
|
|
142
|
+
if self._logger.isEnabledFor(level):
|
|
143
|
+
log_entry = self._format_log(level_name, message, kwargs, exc_info)
|
|
144
|
+
|
|
145
|
+
# Output as JSON
|
|
146
|
+
json_log = json.dumps(log_entry, default=str)
|
|
147
|
+
|
|
148
|
+
# Use standard logger
|
|
149
|
+
self._logger.log(level, json_log)
|
|
150
|
+
|
|
151
|
+
def debug(self, message: str, **kwargs: Any) -> None:
|
|
152
|
+
"""Log debug message."""
|
|
153
|
+
self._log(logging.DEBUG, "DEBUG", message, **kwargs)
|
|
154
|
+
|
|
155
|
+
def info(self, message: str, **kwargs: Any) -> None:
|
|
156
|
+
"""Log info message."""
|
|
157
|
+
self._log(logging.INFO, "INFO", message, **kwargs)
|
|
158
|
+
|
|
159
|
+
def warning(self, message: str, **kwargs: Any) -> None:
|
|
160
|
+
"""Log warning message."""
|
|
161
|
+
self._log(logging.WARNING, "WARNING", message, **kwargs)
|
|
162
|
+
|
|
163
|
+
def error(self, message: str, exc_info: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
164
|
+
"""Log error message."""
|
|
165
|
+
self._log(logging.ERROR, "ERROR", message, exc_info=exc_info, **kwargs)
|
|
166
|
+
|
|
167
|
+
def critical(self, message: str, exc_info: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
168
|
+
"""Log critical message."""
|
|
169
|
+
self._log(logging.CRITICAL, "CRITICAL", message, exc_info=exc_info, **kwargs)
|
|
170
|
+
|
|
171
|
+
def log_performance(
|
|
172
|
+
self,
|
|
173
|
+
operation: str,
|
|
174
|
+
duration_ms: float,
|
|
175
|
+
success: bool = True,
|
|
176
|
+
**kwargs: Any,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Log performance metrics.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
operation: Operation name
|
|
182
|
+
duration_ms: Duration in milliseconds
|
|
183
|
+
success: Whether operation succeeded
|
|
184
|
+
**kwargs: Additional metrics
|
|
185
|
+
"""
|
|
186
|
+
self.info(
|
|
187
|
+
"Performance metric",
|
|
188
|
+
operation=operation,
|
|
189
|
+
duration_ms=duration_ms,
|
|
190
|
+
success=success,
|
|
191
|
+
**kwargs,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PerformanceTimer:
|
|
196
|
+
"""Context manager for timing operations.
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
logger = StructuredLogger("my_agent")
|
|
200
|
+
|
|
201
|
+
with PerformanceTimer(logger, "llm_call") as timer:
|
|
202
|
+
result = await llm.generate(prompt)
|
|
203
|
+
|
|
204
|
+
# Automatically logs: {"operation": "llm_call", "duration_ms": 234.5, ...}
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
logger: StructuredLogger,
|
|
210
|
+
operation: str,
|
|
211
|
+
log_level: str = "info",
|
|
212
|
+
**extra_context: Any,
|
|
213
|
+
):
|
|
214
|
+
"""Initialize performance timer.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
logger: StructuredLogger instance
|
|
218
|
+
operation: Operation name
|
|
219
|
+
log_level: Log level for performance metric
|
|
220
|
+
**extra_context: Additional context to include
|
|
221
|
+
"""
|
|
222
|
+
self.logger = logger
|
|
223
|
+
self.operation = operation
|
|
224
|
+
self.log_level = log_level
|
|
225
|
+
self.extra_context = extra_context
|
|
226
|
+
self.start_time: Optional[float] = None
|
|
227
|
+
self.end_time: Optional[float] = None
|
|
228
|
+
|
|
229
|
+
def __enter__(self):
|
|
230
|
+
"""Start timer."""
|
|
231
|
+
self.start_time = time.time()
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
235
|
+
"""Stop timer and log performance."""
|
|
236
|
+
self.end_time = time.time()
|
|
237
|
+
|
|
238
|
+
if self.start_time is not None:
|
|
239
|
+
duration_ms = (self.end_time - self.start_time) * 1000
|
|
240
|
+
success = exc_type is None
|
|
241
|
+
|
|
242
|
+
self.logger.log_performance(
|
|
243
|
+
self.operation,
|
|
244
|
+
duration_ms,
|
|
245
|
+
success=success,
|
|
246
|
+
**self.extra_context,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return False # Don't suppress exceptions
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Global logger instance
|
|
253
|
+
_default_logger = StructuredLogger("loom")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_logger(name: str = "loom") -> StructuredLogger:
|
|
257
|
+
"""Get or create a structured logger.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
name: Logger name
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
StructuredLogger instance
|
|
264
|
+
"""
|
|
265
|
+
return StructuredLogger(name)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def set_correlation_id(correlation_id: str) -> None:
|
|
269
|
+
"""Set correlation ID for current context (convenience function).
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
correlation_id: Unique identifier
|
|
273
|
+
"""
|
|
274
|
+
_correlation_id.set(correlation_id)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_correlation_id() -> Optional[str]:
|
|
278
|
+
"""Get current correlation ID (convenience function)."""
|
|
279
|
+
return _correlation_id.get()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""SubAgentPool: I2A isolated sub-agent architecture (US3)
|
|
2
|
+
|
|
3
|
+
Spawns isolated sub-agents with independent tool permissions, message histories,
|
|
4
|
+
and fault boundaries. Enables concurrent execution via Python 3.11 TaskGroups.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Fault isolation (1 sub-agent failure doesn't affect others)
|
|
8
|
+
- Tool whitelist enforcement (independent permissions)
|
|
9
|
+
- Separate message histories (no cross-contamination)
|
|
10
|
+
- Execution depth limits (max 3 levels, prevent infinite recursion)
|
|
11
|
+
- Timeout and max_iterations enforcement
|
|
12
|
+
- Concurrent sub-agent execution via asyncio.TaskGroup
|
|
13
|
+
|
|
14
|
+
Architecture:
|
|
15
|
+
- Each sub-agent is a fully isolated Agent instance
|
|
16
|
+
- SubAgentPool manages lifecycle and resource limits
|
|
17
|
+
- Uses cancel_token (US1) for timeout enforcement
|
|
18
|
+
- Compatible with CompressionManager (US2)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
from typing import Dict, List, Optional
|
|
25
|
+
from uuid import uuid4
|
|
26
|
+
|
|
27
|
+
from loom.components.agent import Agent
|
|
28
|
+
from loom.core.types import Message
|
|
29
|
+
from loom.interfaces.llm import BaseLLM
|
|
30
|
+
from loom.interfaces.memory import BaseMemory
|
|
31
|
+
from loom.interfaces.tool import BaseTool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MaxDepthError(Exception):
|
|
35
|
+
"""Raised when sub-agent execution depth exceeds maximum."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SubAgentPool:
|
|
40
|
+
"""Manages isolated sub-agent spawning and execution.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
pool = SubAgentPool(max_depth=3)
|
|
44
|
+
|
|
45
|
+
# Spawn sub-agent with tool whitelist
|
|
46
|
+
result = await pool.spawn(
|
|
47
|
+
llm=llm,
|
|
48
|
+
prompt="Analyze dependencies",
|
|
49
|
+
tool_whitelist=["read_file", "glob"], # Only these tools
|
|
50
|
+
timeout_seconds=60,
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
max_depth: int = 3,
|
|
57
|
+
default_timeout: float = 300.0, # 5 minutes
|
|
58
|
+
default_max_iterations: int = 50,
|
|
59
|
+
):
|
|
60
|
+
"""Initialize SubAgentPool.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
max_depth: Maximum execution depth (prevent infinite recursion)
|
|
64
|
+
default_timeout: Default timeout in seconds for sub-agents
|
|
65
|
+
default_max_iterations: Default max iterations for sub-agents
|
|
66
|
+
"""
|
|
67
|
+
self.max_depth = max_depth
|
|
68
|
+
self.default_timeout = default_timeout
|
|
69
|
+
self.default_max_iterations = default_max_iterations
|
|
70
|
+
self._active_subagents: Dict[str, Agent] = {}
|
|
71
|
+
|
|
72
|
+
async def spawn(
|
|
73
|
+
self,
|
|
74
|
+
llm: BaseLLM,
|
|
75
|
+
prompt: str,
|
|
76
|
+
tools: Optional[List[BaseTool]] = None,
|
|
77
|
+
tool_whitelist: Optional[List[str]] = None,
|
|
78
|
+
memory: Optional[BaseMemory] = None,
|
|
79
|
+
execution_depth: int = 1,
|
|
80
|
+
timeout_seconds: Optional[float] = None,
|
|
81
|
+
max_iterations: Optional[int] = None,
|
|
82
|
+
system_instructions: Optional[str] = None,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Spawn an isolated sub-agent and execute task.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
llm: LLM instance for sub-agent
|
|
88
|
+
prompt: Task prompt for sub-agent
|
|
89
|
+
tools: Available tools (will be filtered by whitelist)
|
|
90
|
+
tool_whitelist: List of allowed tool names (None = all tools)
|
|
91
|
+
memory: Memory instance (if None, sub-agent gets fresh memory)
|
|
92
|
+
execution_depth: Current execution depth (for depth limit)
|
|
93
|
+
timeout_seconds: Timeout in seconds (None = default)
|
|
94
|
+
max_iterations: Max iterations (None = default)
|
|
95
|
+
system_instructions: System instructions for sub-agent
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Final response from sub-agent
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
MaxDepthError: If execution_depth > max_depth
|
|
102
|
+
asyncio.TimeoutError: If sub-agent exceeds timeout
|
|
103
|
+
"""
|
|
104
|
+
# Check depth limit
|
|
105
|
+
if execution_depth > self.max_depth:
|
|
106
|
+
raise MaxDepthError(
|
|
107
|
+
f"Execution depth {execution_depth} exceeds maximum {self.max_depth}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Filter tools by whitelist
|
|
111
|
+
filtered_tools = self._apply_tool_whitelist(tools, tool_whitelist)
|
|
112
|
+
|
|
113
|
+
# Create isolated sub-agent (compression always enabled in v4.0.0)
|
|
114
|
+
subagent_id = str(uuid4())
|
|
115
|
+
subagent = Agent(
|
|
116
|
+
llm=llm,
|
|
117
|
+
tools=filtered_tools,
|
|
118
|
+
memory=memory, # Separate memory instance
|
|
119
|
+
max_iterations=max_iterations or self.default_max_iterations,
|
|
120
|
+
system_instructions=system_instructions,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Register active sub-agent
|
|
124
|
+
self._active_subagents[subagent_id] = subagent
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Create cancel token for timeout enforcement
|
|
128
|
+
cancel_token = asyncio.Event()
|
|
129
|
+
timeout = timeout_seconds or self.default_timeout
|
|
130
|
+
|
|
131
|
+
# Execute sub-agent with timeout
|
|
132
|
+
task = asyncio.create_task(
|
|
133
|
+
subagent.run(prompt, cancel_token=cancel_token, correlation_id=subagent_id)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Wait with timeout
|
|
137
|
+
result = await asyncio.wait_for(task, timeout=timeout)
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
except asyncio.TimeoutError:
|
|
142
|
+
# Cancel sub-agent on timeout
|
|
143
|
+
cancel_token.set()
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
finally:
|
|
147
|
+
# Cleanup: Remove from active pool
|
|
148
|
+
self._active_subagents.pop(subagent_id, None)
|
|
149
|
+
|
|
150
|
+
def _apply_tool_whitelist(
|
|
151
|
+
self,
|
|
152
|
+
tools: Optional[List[BaseTool]],
|
|
153
|
+
whitelist: Optional[List[str]],
|
|
154
|
+
) -> Optional[List[BaseTool]]:
|
|
155
|
+
"""Filter tools by whitelist.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
tools: All available tools
|
|
159
|
+
whitelist: List of allowed tool names (None = all tools)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Filtered list of tools
|
|
163
|
+
"""
|
|
164
|
+
if tools is None or whitelist is None:
|
|
165
|
+
return tools
|
|
166
|
+
|
|
167
|
+
# Filter tools to only whitelisted ones
|
|
168
|
+
filtered = [tool for tool in tools if tool.name in whitelist]
|
|
169
|
+
|
|
170
|
+
return filtered if filtered else None
|
|
171
|
+
|
|
172
|
+
async def spawn_many(
|
|
173
|
+
self,
|
|
174
|
+
llm: BaseLLM,
|
|
175
|
+
prompts: List[str],
|
|
176
|
+
tools: Optional[List[BaseTool]] = None,
|
|
177
|
+
tool_whitelist: Optional[List[str]] = None,
|
|
178
|
+
timeout_seconds: Optional[float] = None,
|
|
179
|
+
return_exceptions: bool = True,
|
|
180
|
+
) -> List[str]:
|
|
181
|
+
"""Spawn multiple sub-agents concurrently.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
llm: LLM instance (shared across sub-agents)
|
|
185
|
+
prompts: List of task prompts
|
|
186
|
+
tools: Available tools
|
|
187
|
+
tool_whitelist: Tool whitelist (applied to all sub-agents)
|
|
188
|
+
timeout_seconds: Timeout per sub-agent
|
|
189
|
+
return_exceptions: If True, return exceptions instead of raising
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of results (or exceptions if return_exceptions=True)
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
prompts = ["Analyze file1.py", "Analyze file2.py", "Analyze file3.py"]
|
|
196
|
+
results = await pool.spawn_many(llm, prompts, tools=tools)
|
|
197
|
+
"""
|
|
198
|
+
tasks = [
|
|
199
|
+
self.spawn(
|
|
200
|
+
llm=llm,
|
|
201
|
+
prompt=prompt,
|
|
202
|
+
tools=tools,
|
|
203
|
+
tool_whitelist=tool_whitelist,
|
|
204
|
+
timeout_seconds=timeout_seconds,
|
|
205
|
+
)
|
|
206
|
+
for prompt in prompts
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
results = await asyncio.gather(*tasks, return_exceptions=return_exceptions)
|
|
210
|
+
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
def get_active_count(self) -> int:
|
|
214
|
+
"""Get number of currently active sub-agents."""
|
|
215
|
+
return len(self._active_subagents)
|
|
216
|
+
|
|
217
|
+
async def cancel_all(self) -> None:
|
|
218
|
+
"""Cancel all active sub-agents.
|
|
219
|
+
|
|
220
|
+
Note: This is a best-effort cancellation. Sub-agents may not
|
|
221
|
+
respond immediately to cancellation signals.
|
|
222
|
+
"""
|
|
223
|
+
for subagent_id, subagent in list(self._active_subagents.items()):
|
|
224
|
+
# Set cancel token (if sub-agent supports steering)
|
|
225
|
+
if hasattr(subagent.executor, "steering_control"):
|
|
226
|
+
subagent.executor.steering_control.abort()
|
|
227
|
+
|
|
228
|
+
# Wait a moment for cancellations to propagate
|
|
229
|
+
await asyncio.sleep(0.1)
|
|
230
|
+
|
|
231
|
+
# Clear active pool
|
|
232
|
+
self._active_subagents.clear()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""系统提示生成模块 - 对齐 Claude Code 的动态系统提示构建机制"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from loom.interfaces.tool import BaseTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SystemPromptBuilder:
|
|
9
|
+
"""动态生成系统提示,包含工具目录、风格指引与边界提醒"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
base_instructions: Optional[str] = None,
|
|
14
|
+
style_guide: Optional[str] = None,
|
|
15
|
+
boundary_reminders: Optional[List[str]] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.base_instructions = base_instructions or self._default_instructions()
|
|
18
|
+
self.style_guide = style_guide or self._default_style_guide()
|
|
19
|
+
self.boundary_reminders = boundary_reminders or self._default_boundary_reminders()
|
|
20
|
+
|
|
21
|
+
def build(
|
|
22
|
+
self,
|
|
23
|
+
tools: Dict[str, BaseTool],
|
|
24
|
+
context: Optional[Dict] = None,
|
|
25
|
+
) -> str:
|
|
26
|
+
"""构建完整的系统提示"""
|
|
27
|
+
sections = [
|
|
28
|
+
self.base_instructions,
|
|
29
|
+
self._build_tool_catalog(tools),
|
|
30
|
+
self.style_guide,
|
|
31
|
+
self._build_boundary_section(),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if context:
|
|
35
|
+
sections.insert(1, self._build_context_section(context))
|
|
36
|
+
|
|
37
|
+
return "\n\n".join(filter(None, sections))
|
|
38
|
+
|
|
39
|
+
def _build_tool_catalog(self, tools: Dict[str, BaseTool]) -> str:
|
|
40
|
+
"""生成工具目录"""
|
|
41
|
+
if not tools:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
catalog_lines = ["## Available Tools", ""]
|
|
45
|
+
for tool in tools.values():
|
|
46
|
+
catalog_lines.append(f"### {tool.name}")
|
|
47
|
+
catalog_lines.append(f"**Description**: {getattr(tool, 'description', 'No description')}")
|
|
48
|
+
|
|
49
|
+
# 提取参数 schema
|
|
50
|
+
if hasattr(tool, "args_schema"):
|
|
51
|
+
try:
|
|
52
|
+
schema = tool.args_schema.model_json_schema()
|
|
53
|
+
properties = schema.get("properties", {})
|
|
54
|
+
required = schema.get("required", [])
|
|
55
|
+
|
|
56
|
+
if properties:
|
|
57
|
+
catalog_lines.append("**Parameters**:")
|
|
58
|
+
for param_name, param_info in properties.items():
|
|
59
|
+
is_required = " (required)" if param_name in required else ""
|
|
60
|
+
param_type = param_info.get("type", "any")
|
|
61
|
+
param_desc = param_info.get("description", "")
|
|
62
|
+
catalog_lines.append(f"- `{param_name}` ({param_type}){is_required}: {param_desc}")
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# 并发安全性标记
|
|
67
|
+
if hasattr(tool, "is_concurrency_safe"):
|
|
68
|
+
safe = "✓" if tool.is_concurrency_safe else "✗"
|
|
69
|
+
catalog_lines.append(f"**Concurrency Safe**: {safe}")
|
|
70
|
+
|
|
71
|
+
catalog_lines.append("")
|
|
72
|
+
|
|
73
|
+
return "\n".join(catalog_lines)
|
|
74
|
+
|
|
75
|
+
def _build_context_section(self, context: Dict) -> str:
|
|
76
|
+
"""构建上下文信息段"""
|
|
77
|
+
lines = ["## Context Information", ""]
|
|
78
|
+
for key, value in context.items():
|
|
79
|
+
lines.append(f"**{key}**: {value}")
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
def _build_boundary_section(self) -> str:
|
|
83
|
+
"""构建边界提醒段"""
|
|
84
|
+
if not self.boundary_reminders:
|
|
85
|
+
return ""
|
|
86
|
+
lines = ["## Important Reminders", ""]
|
|
87
|
+
for reminder in self.boundary_reminders:
|
|
88
|
+
lines.append(f"- {reminder}")
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _default_instructions() -> str:
|
|
93
|
+
return """You are an intelligent AI agent powered by the Loom framework.
|
|
94
|
+
|
|
95
|
+
Your capabilities:
|
|
96
|
+
- Analyze user requests and break them down into actionable steps
|
|
97
|
+
- Use available tools to gather information and perform actions
|
|
98
|
+
- Think step-by-step through complex problems
|
|
99
|
+
- Provide clear, concise, and accurate responses
|
|
100
|
+
|
|
101
|
+
When using tools:
|
|
102
|
+
1. Carefully read the tool descriptions and parameters
|
|
103
|
+
2. Provide all required parameters with correct types
|
|
104
|
+
3. Handle tool results appropriately
|
|
105
|
+
4. If a tool fails, try alternative approaches or inform the user
|
|
106
|
+
|
|
107
|
+
Your responses should be:
|
|
108
|
+
- Clear and well-structured
|
|
109
|
+
- Accurate and factual
|
|
110
|
+
- Helpful and actionable
|
|
111
|
+
- Honest about limitations"""
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _default_style_guide() -> str:
|
|
115
|
+
return """## Response Style
|
|
116
|
+
|
|
117
|
+
- Use markdown formatting for better readability
|
|
118
|
+
- Break down complex explanations into bullet points or numbered lists
|
|
119
|
+
- When executing multiple steps, show your reasoning process
|
|
120
|
+
- If uncertain, acknowledge the uncertainty
|
|
121
|
+
- Cite tool results when making claims based on them"""
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _default_boundary_reminders() -> List[str]:
|
|
125
|
+
return [
|
|
126
|
+
"Always validate tool parameters before calling",
|
|
127
|
+
"Respect tool execution timeouts and handle failures gracefully",
|
|
128
|
+
"Do not make assumptions about tool results without verification",
|
|
129
|
+
"If a task requires multiple steps, break it down clearly",
|
|
130
|
+
"Stop and ask for clarification if the user's intent is ambiguous",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_system_prompt(
|
|
135
|
+
tools: Dict[str, BaseTool],
|
|
136
|
+
custom_instructions: Optional[str] = None,
|
|
137
|
+
context: Optional[Dict] = None,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""便捷函数:快速构建系统提示"""
|
|
140
|
+
builder = SystemPromptBuilder(base_instructions=custom_instructions)
|
|
141
|
+
return builder.build(tools, context)
|