loom-agent 0.0.1__py3-none-any.whl → 0.0.2__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/builtin/tools/calculator.py +4 -0
- loom/builtin/tools/document_search.py +5 -0
- loom/builtin/tools/glob.py +4 -0
- loom/builtin/tools/grep.py +4 -0
- loom/builtin/tools/http_request.py +5 -0
- loom/builtin/tools/python_repl.py +5 -0
- loom/builtin/tools/read_file.py +4 -0
- loom/builtin/tools/task.py +5 -0
- loom/builtin/tools/web_search.py +4 -0
- loom/builtin/tools/write_file.py +4 -0
- loom/components/agent.py +121 -5
- loom/core/agent_executor.py +505 -320
- loom/core/compression_manager.py +17 -10
- loom/core/context_assembly.py +329 -0
- loom/core/events.py +414 -0
- loom/core/execution_context.py +119 -0
- loom/core/tool_orchestrator.py +383 -0
- loom/core/turn_state.py +188 -0
- loom/core/types.py +15 -4
- loom/interfaces/event_producer.py +172 -0
- loom/interfaces/tool.py +22 -1
- loom/security/__init__.py +13 -0
- loom/security/models.py +85 -0
- loom/security/path_validator.py +128 -0
- loom/security/validator.py +346 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
- loom/tasks/README.md +109 -0
- loom/tasks/__init__.py +11 -0
- loom/tasks/sql_placeholder.py +100 -0
- loom_agent-0.0.2.dist-info/METADATA +295 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/RECORD +37 -19
- loom_agent-0.0.1.dist-info/METADATA +0 -457
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/WHEEL +0 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/licenses/LICENSE +0 -0
loom/core/compression_manager.py
CHANGED
|
@@ -151,8 +151,8 @@ Now compress the conversation above following this exact structure:"""
|
|
|
151
151
|
return messages, CompressionMetadata(
|
|
152
152
|
original_message_count=0,
|
|
153
153
|
compressed_message_count=0,
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
original_token_count=0,
|
|
155
|
+
compressed_token_count=0,
|
|
156
156
|
compression_ratio=0.0,
|
|
157
157
|
key_topics=[],
|
|
158
158
|
)
|
|
@@ -163,11 +163,12 @@ Now compress the conversation above following this exact structure:"""
|
|
|
163
163
|
|
|
164
164
|
if not compressible:
|
|
165
165
|
# No messages to compress, return as-is
|
|
166
|
+
token_count = count_messages_tokens(messages)
|
|
166
167
|
return messages, CompressionMetadata(
|
|
167
168
|
original_message_count=len(messages),
|
|
168
169
|
compressed_message_count=len(messages),
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
original_token_count=token_count,
|
|
171
|
+
compressed_token_count=token_count,
|
|
171
172
|
compression_ratio=1.0,
|
|
172
173
|
key_topics=[],
|
|
173
174
|
)
|
|
@@ -201,13 +202,16 @@ Now compress the conversation above following this exact structure:"""
|
|
|
201
202
|
windowed_messages = self.sliding_window_fallback(compressible, self.sliding_window_size)
|
|
202
203
|
final_messages = system_messages + windowed_messages
|
|
203
204
|
compressed_tokens = count_messages_tokens(windowed_messages)
|
|
205
|
+
ratio = compressed_tokens / original_tokens if original_tokens > 0 else 0.0
|
|
206
|
+
# Ensure ratio is clamped to [0.0, 1.0]
|
|
207
|
+
ratio = min(max(ratio, 0.0), 1.0)
|
|
204
208
|
|
|
205
209
|
metadata = CompressionMetadata(
|
|
206
210
|
original_message_count=len(compressible),
|
|
207
211
|
compressed_message_count=len(windowed_messages),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
compression_ratio=
|
|
212
|
+
original_token_count=original_tokens,
|
|
213
|
+
compressed_token_count=compressed_tokens,
|
|
214
|
+
compression_ratio=ratio,
|
|
211
215
|
key_topics=["fallback"],
|
|
212
216
|
)
|
|
213
217
|
return final_messages, metadata
|
|
@@ -223,13 +227,16 @@ Now compress the conversation above following this exact structure:"""
|
|
|
223
227
|
|
|
224
228
|
# Combine system messages + compressed summary
|
|
225
229
|
final_messages = system_messages + [compressed_message]
|
|
230
|
+
ratio = compressed_tokens / original_tokens if original_tokens > 0 else 0.0
|
|
231
|
+
# Ensure ratio is clamped to [0.0, 1.0]
|
|
232
|
+
ratio = min(max(ratio, 0.0), 1.0)
|
|
226
233
|
|
|
227
234
|
metadata = CompressionMetadata(
|
|
228
235
|
original_message_count=len(compressible),
|
|
229
236
|
compressed_message_count=1,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
compression_ratio=
|
|
237
|
+
original_token_count=original_tokens,
|
|
238
|
+
compressed_token_count=compressed_tokens,
|
|
239
|
+
compression_ratio=ratio,
|
|
233
240
|
key_topics=key_topics,
|
|
234
241
|
)
|
|
235
242
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Assembly Module
|
|
3
|
+
|
|
4
|
+
Provides intelligent context assembly with priority-based component management
|
|
5
|
+
and token budget constraints.
|
|
6
|
+
|
|
7
|
+
This module fixes the RAG Context Bug where retrieved documents were being
|
|
8
|
+
overwritten by system prompts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Dict, List, Optional, Callable
|
|
15
|
+
from enum import IntEnum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ComponentPriority(IntEnum):
|
|
19
|
+
"""
|
|
20
|
+
Component priority levels for context assembly.
|
|
21
|
+
|
|
22
|
+
Higher values = higher priority = less likely to be truncated.
|
|
23
|
+
"""
|
|
24
|
+
CRITICAL = 100 # Base instructions (must be included)
|
|
25
|
+
HIGH = 90 # RAG context, important configurations
|
|
26
|
+
MEDIUM = 70 # Tool definitions
|
|
27
|
+
LOW = 50 # Examples, additional hints
|
|
28
|
+
OPTIONAL = 30 # Optional content
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ContextComponent:
|
|
33
|
+
"""
|
|
34
|
+
A single component of the context.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: Component identifier (e.g., "base_instructions", "retrieved_docs")
|
|
38
|
+
content: The actual content text
|
|
39
|
+
priority: Priority level (0-100)
|
|
40
|
+
token_count: Estimated number of tokens
|
|
41
|
+
truncatable: Whether this component can be truncated
|
|
42
|
+
"""
|
|
43
|
+
name: str
|
|
44
|
+
content: str
|
|
45
|
+
priority: int
|
|
46
|
+
token_count: int
|
|
47
|
+
truncatable: bool = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ContextAssembler:
|
|
51
|
+
"""
|
|
52
|
+
Intelligent context assembler with priority-based management.
|
|
53
|
+
|
|
54
|
+
Features:
|
|
55
|
+
- Priority-based component ordering
|
|
56
|
+
- Token budget management
|
|
57
|
+
- Smart truncation of low-priority components
|
|
58
|
+
- Guarantee high-priority component integrity
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```python
|
|
62
|
+
assembler = ContextAssembler(max_tokens=4000)
|
|
63
|
+
|
|
64
|
+
# Add components with priorities
|
|
65
|
+
assembler.add_component(
|
|
66
|
+
"base_instructions",
|
|
67
|
+
"You are a helpful assistant.",
|
|
68
|
+
priority=ComponentPriority.CRITICAL,
|
|
69
|
+
truncatable=False
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assembler.add_component(
|
|
73
|
+
"retrieved_docs",
|
|
74
|
+
doc_context,
|
|
75
|
+
priority=ComponentPriority.HIGH,
|
|
76
|
+
truncatable=True
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Assemble final context
|
|
80
|
+
final_prompt = assembler.assemble()
|
|
81
|
+
```
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
max_tokens: int = 16000,
|
|
87
|
+
token_counter: Optional[Callable[[str], int]] = None,
|
|
88
|
+
token_buffer: float = 0.9 # Use 90% of budget for safety
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize the context assembler.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
max_tokens: Maximum token budget
|
|
95
|
+
token_counter: Custom token counting function (defaults to simple estimation)
|
|
96
|
+
token_buffer: Safety buffer ratio (0.9 = use 90% of max_tokens)
|
|
97
|
+
"""
|
|
98
|
+
self.max_tokens = int(max_tokens * token_buffer)
|
|
99
|
+
self.token_counter = token_counter or self._estimate_tokens
|
|
100
|
+
self.components: List[ContextComponent] = []
|
|
101
|
+
|
|
102
|
+
def add_component(
|
|
103
|
+
self,
|
|
104
|
+
name: str,
|
|
105
|
+
content: str,
|
|
106
|
+
priority: int,
|
|
107
|
+
truncatable: bool = True
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Add a context component.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: Component identifier (e.g., "base_instructions", "retrieved_docs")
|
|
114
|
+
content: Component content
|
|
115
|
+
priority: Priority level (0-100, higher = more important)
|
|
116
|
+
truncatable: Whether this component can be truncated
|
|
117
|
+
"""
|
|
118
|
+
if not content or not content.strip():
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
token_count = self.token_counter(content)
|
|
122
|
+
component = ContextComponent(
|
|
123
|
+
name=name,
|
|
124
|
+
content=content.strip(),
|
|
125
|
+
priority=priority,
|
|
126
|
+
token_count=token_count,
|
|
127
|
+
truncatable=truncatable
|
|
128
|
+
)
|
|
129
|
+
self.components.append(component)
|
|
130
|
+
|
|
131
|
+
def assemble(self) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Assemble the final context from all components.
|
|
134
|
+
|
|
135
|
+
Strategy:
|
|
136
|
+
1. Sort components by priority (descending)
|
|
137
|
+
2. Add components until budget is reached
|
|
138
|
+
3. Truncate low-priority components if needed
|
|
139
|
+
4. Merge all components into final string
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Assembled context string
|
|
143
|
+
"""
|
|
144
|
+
if not self.components:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
# Sort by priority (highest first)
|
|
148
|
+
sorted_components = sorted(
|
|
149
|
+
self.components,
|
|
150
|
+
key=lambda c: c.priority,
|
|
151
|
+
reverse=True
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Calculate total tokens
|
|
155
|
+
total_tokens = sum(c.token_count for c in sorted_components)
|
|
156
|
+
|
|
157
|
+
# Truncate if over budget
|
|
158
|
+
if total_tokens > self.max_tokens:
|
|
159
|
+
sorted_components = self._truncate_components(sorted_components)
|
|
160
|
+
|
|
161
|
+
# Merge components
|
|
162
|
+
sections = []
|
|
163
|
+
for component in sorted_components:
|
|
164
|
+
# Add section header and content
|
|
165
|
+
header = f"# {component.name.replace('_', ' ').upper()}"
|
|
166
|
+
sections.append(f"{header}\n{component.content}")
|
|
167
|
+
|
|
168
|
+
return "\n\n".join(sections)
|
|
169
|
+
|
|
170
|
+
def _truncate_components(
|
|
171
|
+
self,
|
|
172
|
+
components: List[ContextComponent]
|
|
173
|
+
) -> List[ContextComponent]:
|
|
174
|
+
"""
|
|
175
|
+
Intelligently truncate components to fit token budget.
|
|
176
|
+
|
|
177
|
+
Strategy:
|
|
178
|
+
1. Always include non-truncatable components
|
|
179
|
+
2. Add truncatable components by priority
|
|
180
|
+
3. Truncate lower-priority components if needed
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
components: Sorted list of components (by priority, descending)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of components that fit within budget
|
|
187
|
+
"""
|
|
188
|
+
budget_remaining = self.max_tokens
|
|
189
|
+
result = []
|
|
190
|
+
|
|
191
|
+
# Phase 1: Add all non-truncatable components
|
|
192
|
+
for comp in components:
|
|
193
|
+
if not comp.truncatable:
|
|
194
|
+
if comp.token_count <= budget_remaining:
|
|
195
|
+
result.append(comp)
|
|
196
|
+
budget_remaining -= comp.token_count
|
|
197
|
+
else:
|
|
198
|
+
# Non-truncatable component is too large
|
|
199
|
+
print(
|
|
200
|
+
f"Warning: Non-truncatable component '{comp.name}' "
|
|
201
|
+
f"({comp.token_count} tokens) exceeds remaining budget "
|
|
202
|
+
f"({budget_remaining} tokens). Skipping."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Phase 2: Add truncatable components
|
|
206
|
+
truncatable = [c for c in components if c.truncatable]
|
|
207
|
+
|
|
208
|
+
for comp in truncatable:
|
|
209
|
+
if comp.token_count <= budget_remaining:
|
|
210
|
+
# Add complete component
|
|
211
|
+
result.append(comp)
|
|
212
|
+
budget_remaining -= comp.token_count
|
|
213
|
+
elif budget_remaining > 100: # Minimum 100 tokens to be useful
|
|
214
|
+
# Truncate and add
|
|
215
|
+
truncated_content = self._truncate_content(
|
|
216
|
+
comp.content,
|
|
217
|
+
budget_remaining - 20 # Reserve 20 tokens for "... (truncated)" marker
|
|
218
|
+
)
|
|
219
|
+
truncated_comp = ContextComponent(
|
|
220
|
+
name=comp.name,
|
|
221
|
+
content=truncated_content,
|
|
222
|
+
priority=comp.priority,
|
|
223
|
+
token_count=self.token_counter(truncated_content),
|
|
224
|
+
truncatable=comp.truncatable
|
|
225
|
+
)
|
|
226
|
+
result.append(truncated_comp)
|
|
227
|
+
budget_remaining = 0
|
|
228
|
+
break
|
|
229
|
+
else:
|
|
230
|
+
# Not enough budget left, skip remaining components
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
def _truncate_content(self, content: str, max_tokens: int) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Truncate content to fit within token limit.
|
|
238
|
+
|
|
239
|
+
Strategy: Proportional character truncation with conservative estimation
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
content: Content to truncate
|
|
243
|
+
max_tokens: Maximum tokens allowed
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Truncated content with marker
|
|
247
|
+
"""
|
|
248
|
+
current_tokens = self.token_counter(content)
|
|
249
|
+
|
|
250
|
+
if current_tokens <= max_tokens:
|
|
251
|
+
return content
|
|
252
|
+
|
|
253
|
+
# Calculate target character count (conservative)
|
|
254
|
+
ratio = max_tokens / current_tokens
|
|
255
|
+
target_chars = int(len(content) * ratio * 0.95) # 5% safety margin
|
|
256
|
+
|
|
257
|
+
if target_chars < 100:
|
|
258
|
+
# Too small to be useful
|
|
259
|
+
return ""
|
|
260
|
+
|
|
261
|
+
# Truncate and add marker
|
|
262
|
+
truncated = content[:target_chars].rsplit(' ', 1)[0] # Truncate at word boundary
|
|
263
|
+
return f"{truncated}\n\n... (truncated due to token limit)"
|
|
264
|
+
|
|
265
|
+
def _estimate_tokens(self, text: str) -> int:
|
|
266
|
+
"""
|
|
267
|
+
Simple token estimation.
|
|
268
|
+
|
|
269
|
+
Rule of thumb: 1 token ≈ 4 characters for English text
|
|
270
|
+
This is a conservative estimate that works reasonably well.
|
|
271
|
+
|
|
272
|
+
For precise counting, use a model-specific tokenizer.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
text: Text to estimate
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Estimated token count
|
|
279
|
+
"""
|
|
280
|
+
return len(text) // 4
|
|
281
|
+
|
|
282
|
+
def get_summary(self) -> Dict:
|
|
283
|
+
"""
|
|
284
|
+
Get assembly summary for debugging and monitoring.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Dictionary containing:
|
|
288
|
+
- components: List of component info (name, priority, tokens, truncatable)
|
|
289
|
+
- total_tokens: Sum of all component tokens
|
|
290
|
+
- budget: Maximum token budget
|
|
291
|
+
- overflow: Tokens over budget (0 if within budget)
|
|
292
|
+
- utilization: Budget utilization percentage
|
|
293
|
+
"""
|
|
294
|
+
total_tokens = sum(c.token_count for c in self.components)
|
|
295
|
+
overflow = max(0, total_tokens - self.max_tokens)
|
|
296
|
+
utilization = (total_tokens / self.max_tokens * 100) if self.max_tokens > 0 else 0
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"components": [
|
|
300
|
+
{
|
|
301
|
+
"name": c.name,
|
|
302
|
+
"priority": c.priority,
|
|
303
|
+
"tokens": c.token_count,
|
|
304
|
+
"truncatable": c.truncatable
|
|
305
|
+
}
|
|
306
|
+
for c in sorted(self.components, key=lambda x: x.priority, reverse=True)
|
|
307
|
+
],
|
|
308
|
+
"total_tokens": total_tokens,
|
|
309
|
+
"budget": self.max_tokens,
|
|
310
|
+
"overflow": overflow,
|
|
311
|
+
"utilization": round(utilization, 2)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def clear(self) -> None:
|
|
315
|
+
"""Clear all components."""
|
|
316
|
+
self.components.clear()
|
|
317
|
+
|
|
318
|
+
def __len__(self) -> int:
|
|
319
|
+
"""Return number of components."""
|
|
320
|
+
return len(self.components)
|
|
321
|
+
|
|
322
|
+
def __repr__(self) -> str:
|
|
323
|
+
"""String representation."""
|
|
324
|
+
summary = self.get_summary()
|
|
325
|
+
return (
|
|
326
|
+
f"ContextAssembler(components={len(self.components)}, "
|
|
327
|
+
f"tokens={summary['total_tokens']}/{summary['budget']}, "
|
|
328
|
+
f"utilization={summary['utilization']}%)"
|
|
329
|
+
)
|