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.

Files changed (38) 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 +5 -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 +505 -320
  13. loom/core/compression_manager.py +17 -10
  14. loom/core/context_assembly.py +329 -0
  15. loom/core/events.py +414 -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/interfaces/event_producer.py +172 -0
  21. loom/interfaces/tool.py +22 -1
  22. loom/security/__init__.py +13 -0
  23. loom/security/models.py +85 -0
  24. loom/security/path_validator.py +128 -0
  25. loom/security/validator.py +346 -0
  26. loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
  27. loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
  28. loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
  29. loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
  30. loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
  31. loom/tasks/README.md +109 -0
  32. loom/tasks/__init__.py +11 -0
  33. loom/tasks/sql_placeholder.py +100 -0
  34. loom_agent-0.0.2.dist-info/METADATA +295 -0
  35. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/RECORD +37 -19
  36. loom_agent-0.0.1.dist-info/METADATA +0 -457
  37. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/WHEEL +0 -0
  38. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.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,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
+ )