loom-agent 0.0.1__py3-none-any.whl → 0.0.3__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.

Files changed (39) hide show
  1. loom/builtin/tools/calculator.py +4 -0
  2. loom/builtin/tools/document_search.py +5 -0
  3. loom/builtin/tools/glob.py +4 -0
  4. loom/builtin/tools/grep.py +4 -0
  5. loom/builtin/tools/http_request.py +5 -0
  6. loom/builtin/tools/python_repl.py +5 -0
  7. loom/builtin/tools/read_file.py +4 -0
  8. loom/builtin/tools/task.py +105 -0
  9. loom/builtin/tools/web_search.py +4 -0
  10. loom/builtin/tools/write_file.py +4 -0
  11. loom/components/agent.py +121 -5
  12. loom/core/agent_executor.py +777 -321
  13. loom/core/compression_manager.py +17 -10
  14. loom/core/context_assembly.py +437 -0
  15. loom/core/events.py +660 -0
  16. loom/core/execution_context.py +119 -0
  17. loom/core/tool_orchestrator.py +383 -0
  18. loom/core/turn_state.py +188 -0
  19. loom/core/types.py +15 -4
  20. loom/core/unified_coordination.py +389 -0
  21. loom/interfaces/event_producer.py +172 -0
  22. loom/interfaces/tool.py +22 -1
  23. loom/security/__init__.py +13 -0
  24. loom/security/models.py +85 -0
  25. loom/security/path_validator.py +128 -0
  26. loom/security/validator.py +346 -0
  27. loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
  28. loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
  29. loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
  30. loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
  31. loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
  32. loom/tasks/README.md +109 -0
  33. loom/tasks/__init__.py +11 -0
  34. loom/tasks/sql_placeholder.py +100 -0
  35. loom_agent-0.0.3.dist-info/METADATA +292 -0
  36. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/RECORD +38 -19
  37. loom_agent-0.0.1.dist-info/METADATA +0 -457
  38. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/WHEEL +0 -0
  39. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
- original_tokens=0,
155
- compressed_tokens=0,
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
- original_tokens=count_messages_tokens(messages),
170
- compressed_tokens=count_messages_tokens(messages),
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
- original_tokens=original_tokens,
209
- compressed_tokens=compressed_tokens,
210
- compression_ratio=compressed_tokens / original_tokens if original_tokens > 0 else 0.0,
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
- original_tokens=original_tokens,
231
- compressed_tokens=compressed_tokens,
232
- compression_ratio=compressed_tokens / original_tokens if original_tokens > 0 else 0.0,
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,437 @@
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
+ import hashlib
14
+ from dataclasses import dataclass
15
+ from typing import Dict, List, Optional, Callable, Any
16
+ from enum import IntEnum
17
+
18
+
19
+ class ComponentPriority(IntEnum):
20
+ """
21
+ Component priority levels for context assembly.
22
+
23
+ Higher values = higher priority = less likely to be truncated.
24
+ """
25
+ CRITICAL = 100 # Base instructions (must be included)
26
+ HIGH = 90 # RAG context, important configurations
27
+ MEDIUM = 70 # Tool definitions
28
+ LOW = 50 # Examples, additional hints
29
+ OPTIONAL = 30 # Optional content
30
+
31
+
32
+ @dataclass
33
+ class ContextComponent:
34
+ """
35
+ A single component of the context.
36
+
37
+ Attributes:
38
+ name: Component identifier (e.g., "base_instructions", "retrieved_docs")
39
+ content: The actual content text
40
+ priority: Priority level (0-100)
41
+ token_count: Estimated number of tokens
42
+ truncatable: Whether this component can be truncated
43
+ """
44
+ name: str
45
+ content: str
46
+ priority: int
47
+ token_count: int
48
+ truncatable: bool = True
49
+
50
+
51
+ class ContextAssembler:
52
+ """
53
+ Intelligent context assembler with priority-based management.
54
+
55
+ Features:
56
+ - Priority-based component ordering
57
+ - Token budget management
58
+ - Smart truncation of low-priority components
59
+ - Guarantee high-priority component integrity
60
+ - Component caching for performance
61
+ - Dynamic priority adjustment
62
+ - Context reuse optimization
63
+
64
+ Example:
65
+ ```python
66
+ assembler = ContextAssembler(max_tokens=4000)
67
+
68
+ # Add components with priorities
69
+ assembler.add_component(
70
+ "base_instructions",
71
+ "You are a helpful assistant.",
72
+ priority=ComponentPriority.CRITICAL,
73
+ truncatable=False
74
+ )
75
+
76
+ assembler.add_component(
77
+ "retrieved_docs",
78
+ doc_context,
79
+ priority=ComponentPriority.HIGH,
80
+ truncatable=True
81
+ )
82
+
83
+ # Assemble final context
84
+ final_prompt = assembler.assemble()
85
+ ```
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ max_tokens: int = 16000,
91
+ token_counter: Optional[Callable[[str], int]] = None,
92
+ token_buffer: float = 0.9, # Use 90% of budget for safety
93
+ enable_caching: bool = True,
94
+ cache_size: int = 100
95
+ ):
96
+ """
97
+ Initialize the context assembler.
98
+
99
+ Args:
100
+ max_tokens: Maximum token budget
101
+ token_counter: Custom token counting function (defaults to simple estimation)
102
+ token_buffer: Safety buffer ratio (0.9 = use 90% of max_tokens)
103
+ enable_caching: Enable component caching for performance
104
+ cache_size: Maximum number of cached components
105
+ """
106
+ self.max_tokens = int(max_tokens * token_buffer)
107
+ self.token_counter = token_counter or self._estimate_tokens
108
+ self.components: List[ContextComponent] = []
109
+
110
+ # Performance optimizations
111
+ self.enable_caching = enable_caching
112
+ self._component_cache: Dict[str, ContextComponent] = {}
113
+ self._cache_size = cache_size
114
+ self._assembly_cache: Optional[str] = None
115
+ self._last_components_hash: Optional[str] = None
116
+
117
+ def add_component(
118
+ self,
119
+ name: str,
120
+ content: str,
121
+ priority: int,
122
+ truncatable: bool = True
123
+ ) -> None:
124
+ """
125
+ Add a context component.
126
+
127
+ Args:
128
+ name: Component identifier (e.g., "base_instructions", "retrieved_docs")
129
+ content: Component content
130
+ priority: Priority level (0-100, higher = more important)
131
+ truncatable: Whether this component can be truncated
132
+ """
133
+ if not content or not content.strip():
134
+ return
135
+
136
+ token_count = self.token_counter(content)
137
+ component = ContextComponent(
138
+ name=name,
139
+ content=content.strip(),
140
+ priority=priority,
141
+ token_count=token_count,
142
+ truncatable=truncatable
143
+ )
144
+ self.components.append(component)
145
+
146
+ # Update cache if enabled
147
+ if self.enable_caching:
148
+ self._component_cache[name] = component
149
+ # Maintain cache size limit
150
+ if len(self._component_cache) > self._cache_size:
151
+ # Remove oldest entries (simple LRU)
152
+ oldest_key = next(iter(self._component_cache))
153
+ del self._component_cache[oldest_key]
154
+
155
+ def assemble(self) -> str:
156
+ """
157
+ Assemble the final context from all components.
158
+
159
+ Strategy:
160
+ 1. Check cache for identical component configuration
161
+ 2. Sort components by priority (descending)
162
+ 3. Add components until budget is reached
163
+ 4. Truncate low-priority components if needed
164
+ 5. Merge all components into final string
165
+
166
+ Returns:
167
+ Assembled context string
168
+ """
169
+ if not self.components:
170
+ return ""
171
+
172
+ # Check cache if enabled
173
+ if self.enable_caching:
174
+ current_hash = self._get_components_hash()
175
+ if (self._assembly_cache is not None and
176
+ self._last_components_hash == current_hash):
177
+ return self._assembly_cache
178
+
179
+ # Sort by priority (highest first)
180
+ sorted_components = sorted(
181
+ self.components,
182
+ key=lambda c: c.priority,
183
+ reverse=True
184
+ )
185
+
186
+ # Calculate total tokens
187
+ total_tokens = sum(c.token_count for c in sorted_components)
188
+
189
+ # Truncate if over budget
190
+ if total_tokens > self.max_tokens:
191
+ sorted_components = self._truncate_components(sorted_components)
192
+
193
+ # Merge components
194
+ sections = []
195
+ for component in sorted_components:
196
+ # Add section header and content
197
+ header = f"# {component.name.replace('_', ' ').upper()}"
198
+ sections.append(f"{header}\n{component.content}")
199
+
200
+ result = "\n\n".join(sections)
201
+
202
+ # Update cache if enabled
203
+ if self.enable_caching:
204
+ self._assembly_cache = result
205
+ self._last_components_hash = self._get_components_hash()
206
+
207
+ return result
208
+
209
+ def _get_components_hash(self) -> str:
210
+ """
211
+ Generate hash for current component configuration
212
+
213
+ 优化版本:
214
+ - 使用 blake2b 替代 MD5(更快)
215
+ - 直接update字节而非拼接字符串
216
+ - 移除不必要的排序
217
+ """
218
+ # 使用 blake2b,比 MD5 更快且安全
219
+ hasher = hashlib.blake2b(digest_size=16)
220
+
221
+ # 直接更新hasher,避免字符串拼接
222
+ for comp in self.components:
223
+ hasher.update(comp.name.encode())
224
+ hasher.update(str(comp.priority).encode())
225
+ hasher.update(str(comp.token_count).encode())
226
+ hasher.update(b'1' if comp.truncatable else b'0')
227
+
228
+ return hasher.hexdigest()
229
+
230
+ def adjust_priority(self, component_name: str, new_priority: int) -> bool:
231
+ """
232
+ Dynamically adjust component priority.
233
+
234
+ Args:
235
+ component_name: Name of the component to adjust
236
+ new_priority: New priority value
237
+
238
+ Returns:
239
+ True if component was found and adjusted, False otherwise
240
+ """
241
+ for component in self.components:
242
+ if component.name == component_name:
243
+ component.priority = new_priority
244
+ # Clear cache since configuration changed
245
+ if self.enable_caching:
246
+ self._assembly_cache = None
247
+ self._last_components_hash = None
248
+ return True
249
+ return False
250
+
251
+ def get_component_stats(self) -> Dict[str, Any]:
252
+ """Get statistics about current components"""
253
+ if not self.components:
254
+ return {"total_components": 0, "total_tokens": 0}
255
+
256
+ total_tokens = sum(c.token_count for c in self.components)
257
+ priority_distribution = {}
258
+
259
+ for comp in self.components:
260
+ priority_distribution[comp.priority] = priority_distribution.get(comp.priority, 0) + 1
261
+
262
+ return {
263
+ "total_components": len(self.components),
264
+ "total_tokens": total_tokens,
265
+ "budget_utilization": total_tokens / self.max_tokens if self.max_tokens > 0 else 0,
266
+ "priority_distribution": priority_distribution,
267
+ "cache_enabled": self.enable_caching,
268
+ "cache_size": len(self._component_cache) if self.enable_caching else 0
269
+ }
270
+
271
+ def clear_cache(self) -> None:
272
+ """Clear all caches"""
273
+ if self.enable_caching:
274
+ self._component_cache.clear()
275
+ self._assembly_cache = None
276
+ self._last_components_hash = None
277
+
278
+ def _truncate_components(
279
+ self,
280
+ components: List[ContextComponent]
281
+ ) -> List[ContextComponent]:
282
+ """
283
+ Intelligently truncate components to fit token budget.
284
+
285
+ Strategy:
286
+ 1. Always include non-truncatable components
287
+ 2. Add truncatable components by priority
288
+ 3. Truncate lower-priority components if needed
289
+
290
+ Args:
291
+ components: Sorted list of components (by priority, descending)
292
+
293
+ Returns:
294
+ List of components that fit within budget
295
+ """
296
+ budget_remaining = self.max_tokens
297
+ result = []
298
+
299
+ # Phase 1: Add all non-truncatable components
300
+ for comp in components:
301
+ if not comp.truncatable:
302
+ if comp.token_count <= budget_remaining:
303
+ result.append(comp)
304
+ budget_remaining -= comp.token_count
305
+ else:
306
+ # Non-truncatable component is too large
307
+ print(
308
+ f"Warning: Non-truncatable component '{comp.name}' "
309
+ f"({comp.token_count} tokens) exceeds remaining budget "
310
+ f"({budget_remaining} tokens). Skipping."
311
+ )
312
+
313
+ # Phase 2: Add truncatable components
314
+ truncatable = [c for c in components if c.truncatable]
315
+
316
+ for comp in truncatable:
317
+ if comp.token_count <= budget_remaining:
318
+ # Add complete component
319
+ result.append(comp)
320
+ budget_remaining -= comp.token_count
321
+ elif budget_remaining > 100: # Minimum 100 tokens to be useful
322
+ # Truncate and add
323
+ truncated_content = self._truncate_content(
324
+ comp.content,
325
+ budget_remaining - 20 # Reserve 20 tokens for "... (truncated)" marker
326
+ )
327
+ truncated_comp = ContextComponent(
328
+ name=comp.name,
329
+ content=truncated_content,
330
+ priority=comp.priority,
331
+ token_count=self.token_counter(truncated_content),
332
+ truncatable=comp.truncatable
333
+ )
334
+ result.append(truncated_comp)
335
+ budget_remaining = 0
336
+ break
337
+ else:
338
+ # Not enough budget left, skip remaining components
339
+ break
340
+
341
+ return result
342
+
343
+ def _truncate_content(self, content: str, max_tokens: int) -> str:
344
+ """
345
+ Truncate content to fit within token limit.
346
+
347
+ Strategy: Proportional character truncation with conservative estimation
348
+
349
+ Args:
350
+ content: Content to truncate
351
+ max_tokens: Maximum tokens allowed
352
+
353
+ Returns:
354
+ Truncated content with marker
355
+ """
356
+ current_tokens = self.token_counter(content)
357
+
358
+ if current_tokens <= max_tokens:
359
+ return content
360
+
361
+ # Calculate target character count (conservative)
362
+ ratio = max_tokens / current_tokens
363
+ target_chars = int(len(content) * ratio * 0.95) # 5% safety margin
364
+
365
+ if target_chars < 100:
366
+ # Too small to be useful
367
+ return ""
368
+
369
+ # Truncate and add marker
370
+ truncated = content[:target_chars].rsplit(' ', 1)[0] # Truncate at word boundary
371
+ return f"{truncated}\n\n... (truncated due to token limit)"
372
+
373
+ def _estimate_tokens(self, text: str) -> int:
374
+ """
375
+ Simple token estimation.
376
+
377
+ Rule of thumb: 1 token ≈ 4 characters for English text
378
+ This is a conservative estimate that works reasonably well.
379
+
380
+ For precise counting, use a model-specific tokenizer.
381
+
382
+ Args:
383
+ text: Text to estimate
384
+
385
+ Returns:
386
+ Estimated token count
387
+ """
388
+ return len(text) // 4
389
+
390
+ def get_summary(self) -> Dict:
391
+ """
392
+ Get assembly summary for debugging and monitoring.
393
+
394
+ Returns:
395
+ Dictionary containing:
396
+ - components: List of component info (name, priority, tokens, truncatable)
397
+ - total_tokens: Sum of all component tokens
398
+ - budget: Maximum token budget
399
+ - overflow: Tokens over budget (0 if within budget)
400
+ - utilization: Budget utilization percentage
401
+ """
402
+ total_tokens = sum(c.token_count for c in self.components)
403
+ overflow = max(0, total_tokens - self.max_tokens)
404
+ utilization = (total_tokens / self.max_tokens * 100) if self.max_tokens > 0 else 0
405
+
406
+ return {
407
+ "components": [
408
+ {
409
+ "name": c.name,
410
+ "priority": c.priority,
411
+ "tokens": c.token_count,
412
+ "truncatable": c.truncatable
413
+ }
414
+ for c in sorted(self.components, key=lambda x: x.priority, reverse=True)
415
+ ],
416
+ "total_tokens": total_tokens,
417
+ "budget": self.max_tokens,
418
+ "overflow": overflow,
419
+ "utilization": round(utilization, 2)
420
+ }
421
+
422
+ def clear(self) -> None:
423
+ """Clear all components."""
424
+ self.components.clear()
425
+
426
+ def __len__(self) -> int:
427
+ """Return number of components."""
428
+ return len(self.components)
429
+
430
+ def __repr__(self) -> str:
431
+ """String representation."""
432
+ summary = self.get_summary()
433
+ return (
434
+ f"ContextAssembler(components={len(self.components)}, "
435
+ f"tokens={summary['total_tokens']}/{summary['budget']}, "
436
+ f"utilization={summary['utilization']}%)"
437
+ )