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
loom/core/events.py ADDED
@@ -0,0 +1,414 @@
1
+ """
2
+ Agent Event System for Loom 2.0
3
+
4
+ This module defines the unified event model for streaming agent execution.
5
+ Inspired by Claude Code's event-driven architecture.
6
+
7
+ Example:
8
+ ```python
9
+ agent = Agent(llm=llm, tools=tools)
10
+
11
+ async for event in agent.execute("Search for TODO comments"):
12
+ if event.type == AgentEventType.LLM_DELTA:
13
+ print(event.content, end="", flush=True)
14
+ elif event.type == AgentEventType.TOOL_PROGRESS:
15
+ print(f"\\nTool: {event.metadata['tool_name']}")
16
+ elif event.type == AgentEventType.AGENT_FINISH:
17
+ print(f"\\n✓ {event.content}")
18
+ ```
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+ from enum import Enum
23
+ from typing import Optional, Dict, Any, List
24
+ import time
25
+ import uuid
26
+
27
+
28
+ class AgentEventType(Enum):
29
+ """
30
+ Agent event types for different execution phases.
31
+
32
+ Event Categories:
33
+ - Phase Events: Lifecycle events for execution phases
34
+ - Context Events: Context assembly and management
35
+ - RAG Events: Retrieval-augmented generation events
36
+ - LLM Events: Language model interaction events
37
+ - Tool Events: Tool execution and progress
38
+ - Agent Events: High-level agent state changes
39
+ - Error Events: Error handling and recovery
40
+ """
41
+
42
+ # ===== Phase Events =====
43
+ PHASE_START = "phase_start"
44
+ """A new execution phase has started"""
45
+
46
+ PHASE_END = "phase_end"
47
+ """An execution phase has completed"""
48
+
49
+ # ===== Context Events =====
50
+ CONTEXT_ASSEMBLY_START = "context_assembly_start"
51
+ """Starting to assemble system context"""
52
+
53
+ CONTEXT_ASSEMBLY_COMPLETE = "context_assembly_complete"
54
+ """System context assembly completed"""
55
+
56
+ COMPRESSION_APPLIED = "compression_applied"
57
+ """Conversation history was compressed"""
58
+
59
+ # ===== RAG Events =====
60
+ RETRIEVAL_START = "retrieval_start"
61
+ """Starting document retrieval"""
62
+
63
+ RETRIEVAL_PROGRESS = "retrieval_progress"
64
+ """Progress update during retrieval (documents found)"""
65
+
66
+ RETRIEVAL_COMPLETE = "retrieval_complete"
67
+ """Document retrieval completed"""
68
+
69
+ # ===== LLM Events =====
70
+ LLM_START = "llm_start"
71
+ """LLM call initiated"""
72
+
73
+ LLM_DELTA = "llm_delta"
74
+ """Streaming text chunk from LLM"""
75
+
76
+ LLM_COMPLETE = "llm_complete"
77
+ """LLM call completed"""
78
+
79
+ LLM_TOOL_CALLS = "llm_tool_calls"
80
+ """LLM requested tool calls"""
81
+
82
+ # ===== Tool Events =====
83
+ TOOL_CALLS_START = "tool_calls_start"
84
+ """Starting to execute tool calls"""
85
+
86
+ TOOL_EXECUTION_START = "tool_execution_start"
87
+ """Individual tool execution started"""
88
+
89
+ TOOL_PROGRESS = "tool_progress"
90
+ """Progress update from tool execution"""
91
+
92
+ TOOL_RESULT = "tool_result"
93
+ """Tool execution completed with result"""
94
+
95
+ TOOL_ERROR = "tool_error"
96
+ """Tool execution failed"""
97
+
98
+ TOOL_CALLS_COMPLETE = "tool_calls_complete"
99
+ """All tool calls completed (batch execution finished)"""
100
+
101
+ # ===== Agent Events =====
102
+ ITERATION_START = "iteration_start"
103
+ """New agent iteration started (for recursive loops)"""
104
+
105
+ ITERATION_END = "iteration_end"
106
+ """Agent iteration completed"""
107
+
108
+ RECURSION = "recursion"
109
+ """Recursive call initiated (tt mode)"""
110
+
111
+ AGENT_FINISH = "agent_finish"
112
+ """Agent execution finished successfully"""
113
+
114
+ MAX_ITERATIONS_REACHED = "max_iterations_reached"
115
+ """Maximum iteration limit reached"""
116
+
117
+ EXECUTION_CANCELLED = "execution_cancelled"
118
+ """Execution was cancelled via cancel_token"""
119
+
120
+ # ===== Error Events =====
121
+ ERROR = "error"
122
+ """Error occurred during execution"""
123
+
124
+ RECOVERY_ATTEMPT = "recovery_attempt"
125
+ """Attempting to recover from error"""
126
+
127
+ RECOVERY_SUCCESS = "recovery_success"
128
+ """Error recovery succeeded"""
129
+
130
+ RECOVERY_FAILED = "recovery_failed"
131
+ """Error recovery failed"""
132
+
133
+
134
+ @dataclass
135
+ class ToolCall:
136
+ """Represents a tool invocation request from the LLM"""
137
+
138
+ id: str
139
+ """Unique identifier for this tool call"""
140
+
141
+ name: str
142
+ """Name of the tool to execute"""
143
+
144
+ arguments: Dict[str, Any]
145
+ """Arguments to pass to the tool"""
146
+
147
+ def __post_init__(self):
148
+ if not self.id:
149
+ self.id = f"call_{uuid.uuid4().hex[:8]}"
150
+
151
+
152
+ @dataclass
153
+ class ToolResult:
154
+ """Represents the result of a tool execution"""
155
+
156
+ tool_call_id: str
157
+ """ID of the tool call this result corresponds to"""
158
+
159
+ tool_name: str
160
+ """Name of the tool that was executed"""
161
+
162
+ content: str
163
+ """Result content (or error message)"""
164
+
165
+ is_error: bool = False
166
+ """Whether this result represents an error"""
167
+
168
+ execution_time_ms: Optional[float] = None
169
+ """Time taken to execute the tool in milliseconds"""
170
+
171
+ metadata: Dict[str, Any] = field(default_factory=dict)
172
+ """Additional metadata about the execution"""
173
+
174
+
175
+ @dataclass
176
+ class AgentEvent:
177
+ """
178
+ Unified event model for agent execution streaming.
179
+
180
+ All components in Loom 2.0 produce AgentEvent instances to communicate
181
+ their state and progress. This enables:
182
+ - Real-time progress updates to users
183
+ - Fine-grained control over execution flow
184
+ - Debugging and observability
185
+ - Flexible consumption patterns
186
+
187
+ Attributes:
188
+ type: The type of event (see AgentEventType)
189
+ timestamp: Unix timestamp when event was created
190
+ phase: Optional execution phase name (e.g., "context", "retrieval", "llm")
191
+ content: Optional text content (for LLM deltas, final responses)
192
+ tool_call: Optional tool call request
193
+ tool_result: Optional tool execution result
194
+ error: Optional exception that occurred
195
+ metadata: Additional event-specific data
196
+ iteration: Current iteration number (for recursive loops)
197
+ turn_id: Unique ID for this conversation turn
198
+ """
199
+
200
+ type: AgentEventType
201
+ """The type of this event"""
202
+
203
+ timestamp: float = field(default_factory=time.time)
204
+ """Unix timestamp when this event was created"""
205
+
206
+ # ===== Optional Fields (based on event type) =====
207
+
208
+ phase: Optional[str] = None
209
+ """Execution phase name (e.g., 'context_assembly', 'tool_execution')"""
210
+
211
+ content: Optional[str] = None
212
+ """Text content (for LLM_DELTA, AGENT_FINISH, etc.)"""
213
+
214
+ tool_call: Optional[ToolCall] = None
215
+ """Tool call request (for LLM_TOOL_CALLS, TOOL_EXECUTION_START)"""
216
+
217
+ tool_result: Optional[ToolResult] = None
218
+ """Tool execution result (for TOOL_RESULT, TOOL_ERROR)"""
219
+
220
+ error: Optional[Exception] = None
221
+ """Exception that occurred (for ERROR events)"""
222
+
223
+ metadata: Dict[str, Any] = field(default_factory=dict)
224
+ """Additional event-specific data"""
225
+
226
+ # ===== Tracking Fields =====
227
+
228
+ iteration: Optional[int] = None
229
+ """Current iteration number (for recursive agent loops)"""
230
+
231
+ turn_id: Optional[str] = None
232
+ """Unique identifier for this conversation turn"""
233
+
234
+ def __post_init__(self):
235
+ """Generate turn_id if not provided"""
236
+ if self.turn_id is None:
237
+ self.turn_id = f"turn_{uuid.uuid4().hex[:12]}"
238
+
239
+ # ===== Convenience Constructors =====
240
+
241
+ @classmethod
242
+ def phase_start(cls, phase: str, **metadata) -> "AgentEvent":
243
+ """Create a PHASE_START event"""
244
+ return cls(
245
+ type=AgentEventType.PHASE_START,
246
+ phase=phase,
247
+ metadata=metadata
248
+ )
249
+
250
+ @classmethod
251
+ def phase_end(cls, phase: str, **metadata) -> "AgentEvent":
252
+ """Create a PHASE_END event"""
253
+ return cls(
254
+ type=AgentEventType.PHASE_END,
255
+ phase=phase,
256
+ metadata=metadata
257
+ )
258
+
259
+ @classmethod
260
+ def llm_delta(cls, content: str, **metadata) -> "AgentEvent":
261
+ """Create an LLM_DELTA event for streaming text"""
262
+ return cls(
263
+ type=AgentEventType.LLM_DELTA,
264
+ content=content,
265
+ metadata=metadata
266
+ )
267
+
268
+ @classmethod
269
+ def tool_progress(
270
+ cls,
271
+ tool_name: str,
272
+ status: str,
273
+ **metadata
274
+ ) -> "AgentEvent":
275
+ """Create a TOOL_PROGRESS event"""
276
+ return cls(
277
+ type=AgentEventType.TOOL_PROGRESS,
278
+ metadata={"tool_name": tool_name, "status": status, **metadata}
279
+ )
280
+
281
+ @classmethod
282
+ def tool_result(
283
+ cls,
284
+ tool_result: ToolResult,
285
+ **metadata
286
+ ) -> "AgentEvent":
287
+ """Create a TOOL_RESULT event"""
288
+ return cls(
289
+ type=AgentEventType.TOOL_RESULT,
290
+ tool_result=tool_result,
291
+ metadata=metadata
292
+ )
293
+
294
+ @classmethod
295
+ def agent_finish(cls, content: str, **metadata) -> "AgentEvent":
296
+ """Create an AGENT_FINISH event"""
297
+ return cls(
298
+ type=AgentEventType.AGENT_FINISH,
299
+ content=content,
300
+ metadata=metadata
301
+ )
302
+
303
+ @classmethod
304
+ def error(cls, error: Exception, **metadata) -> "AgentEvent":
305
+ """Create an ERROR event"""
306
+ return cls(
307
+ type=AgentEventType.ERROR,
308
+ error=error,
309
+ metadata=metadata
310
+ )
311
+
312
+ # ===== Utility Methods =====
313
+
314
+ def is_terminal(self) -> bool:
315
+ """Check if this event signals execution completion"""
316
+ return self.type in {
317
+ AgentEventType.AGENT_FINISH,
318
+ AgentEventType.MAX_ITERATIONS_REACHED,
319
+ AgentEventType.ERROR
320
+ }
321
+
322
+ def is_llm_content(self) -> bool:
323
+ """Check if this event contains LLM-generated content"""
324
+ return self.type in {
325
+ AgentEventType.LLM_DELTA,
326
+ AgentEventType.LLM_COMPLETE,
327
+ AgentEventType.AGENT_FINISH
328
+ }
329
+
330
+ def is_tool_event(self) -> bool:
331
+ """Check if this is a tool-related event"""
332
+ return self.type.value.startswith("tool_")
333
+
334
+ def __repr__(self) -> str:
335
+ """Human-readable representation"""
336
+ parts = [f"AgentEvent({self.type.value}"]
337
+
338
+ if self.phase:
339
+ parts.append(f"phase={self.phase}")
340
+
341
+ if self.content:
342
+ preview = self.content[:50] + "..." if len(self.content) > 50 else self.content
343
+ parts.append(f"content='{preview}'")
344
+
345
+ if self.tool_call:
346
+ parts.append(f"tool={self.tool_call.name}")
347
+
348
+ # Access instance variable directly to avoid class method with same name
349
+ tool_result_instance = self.__dict__.get('tool_result')
350
+ if tool_result_instance and isinstance(tool_result_instance, ToolResult):
351
+ parts.append(f"tool={tool_result_instance.tool_name}")
352
+
353
+ if self.error:
354
+ parts.append(f"error={type(self.error).__name__}")
355
+
356
+ if self.iteration is not None:
357
+ parts.append(f"iter={self.iteration}")
358
+
359
+ return ", ".join(parts) + ")"
360
+
361
+
362
+ # ===== Event Consumer Helpers =====
363
+
364
+ class EventCollector:
365
+ """
366
+ Helper class to collect and filter events during agent execution.
367
+
368
+ Example:
369
+ ```python
370
+ collector = EventCollector()
371
+
372
+ async for event in agent.execute(prompt):
373
+ collector.add(event)
374
+
375
+ # Get all LLM content
376
+ llm_text = collector.get_llm_content()
377
+
378
+ # Get all tool results
379
+ tool_results = collector.get_tool_results()
380
+ ```
381
+ """
382
+
383
+ def __init__(self):
384
+ self.events: List[AgentEvent] = []
385
+
386
+ def add(self, event: AgentEvent):
387
+ """Add an event to the collection"""
388
+ self.events.append(event)
389
+
390
+ def filter(self, event_type: AgentEventType) -> List[AgentEvent]:
391
+ """Get all events of a specific type"""
392
+ return [e for e in self.events if e.type == event_type]
393
+
394
+ def get_llm_content(self) -> str:
395
+ """Reconstruct full LLM output from LLM_DELTA events"""
396
+ deltas = self.filter(AgentEventType.LLM_DELTA)
397
+ return "".join(e.content or "" for e in deltas)
398
+
399
+ def get_tool_results(self) -> List[ToolResult]:
400
+ """Get all tool results"""
401
+ result_events = self.filter(AgentEventType.TOOL_RESULT)
402
+ return [e.tool_result for e in result_events if e.tool_result]
403
+
404
+ def get_errors(self) -> List[Exception]:
405
+ """Get all errors that occurred"""
406
+ error_events = self.filter(AgentEventType.ERROR)
407
+ return [e.error for e in error_events if e.error]
408
+
409
+ def get_final_response(self) -> Optional[str]:
410
+ """Get the final agent response"""
411
+ finish_events = self.filter(AgentEventType.AGENT_FINISH)
412
+ if finish_events:
413
+ return finish_events[-1].content
414
+ return None
@@ -0,0 +1,119 @@
1
+ """
2
+ Execution Context for tt Recursive Control Loop
3
+
4
+ Provides shared runtime configuration that persists across recursive calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Optional, Dict, Any
13
+ from uuid import uuid4
14
+
15
+
16
+ @dataclass
17
+ class ExecutionContext:
18
+ """
19
+ Shared execution context for tt recursion.
20
+
21
+ Contains runtime configuration and state that doesn't change
22
+ between recursive calls. This is passed down the recursion chain
23
+ alongside messages and TurnState.
24
+
25
+ Design Principles:
26
+ - Immutable configuration: working_dir, correlation_id don't change
27
+ - Shared cancellation: cancel_token is shared across all recursive calls
28
+ - Extensible: metadata dict for custom data
29
+
30
+ Attributes:
31
+ working_dir: Working directory for file operations
32
+ correlation_id: Unique ID for request tracing
33
+ cancel_token: Optional cancellation event (shared)
34
+ git_context: Git repository context (future feature)
35
+ project_context: Project-specific context (future feature)
36
+ metadata: Additional runtime data
37
+
38
+ Example:
39
+ ```python
40
+ context = ExecutionContext(
41
+ working_dir=Path.cwd(),
42
+ correlation_id="req-12345"
43
+ )
44
+
45
+ # All recursive tt calls share this context
46
+ async for event in executor.tt(messages, turn_state, context):
47
+ ...
48
+ ```
49
+ """
50
+
51
+ working_dir: Path
52
+ correlation_id: str
53
+ cancel_token: Optional[asyncio.Event] = None
54
+ git_context: Optional[Dict[str, Any]] = None
55
+ project_context: Optional[Dict[str, Any]] = None
56
+ metadata: Dict[str, Any] = field(default_factory=dict)
57
+
58
+ @staticmethod
59
+ def create(
60
+ working_dir: Optional[Path] = None,
61
+ correlation_id: Optional[str] = None,
62
+ cancel_token: Optional[asyncio.Event] = None,
63
+ **metadata
64
+ ) -> ExecutionContext:
65
+ """
66
+ Create execution context with defaults.
67
+
68
+ Args:
69
+ working_dir: Working directory (defaults to cwd)
70
+ correlation_id: Request ID (defaults to new UUID)
71
+ cancel_token: Cancellation event
72
+ **metadata: Additional metadata
73
+
74
+ Returns:
75
+ ExecutionContext: New context
76
+ """
77
+ return ExecutionContext(
78
+ working_dir=working_dir or Path.cwd(),
79
+ correlation_id=correlation_id or str(uuid4()),
80
+ cancel_token=cancel_token,
81
+ metadata=metadata
82
+ )
83
+
84
+ def is_cancelled(self) -> bool:
85
+ """
86
+ Check if execution is cancelled.
87
+
88
+ Returns:
89
+ bool: True if cancel_token is set
90
+ """
91
+ return self.cancel_token is not None and self.cancel_token.is_set()
92
+
93
+ def with_metadata(self, **kwargs) -> ExecutionContext:
94
+ """
95
+ Create new context with updated metadata.
96
+
97
+ Args:
98
+ **kwargs: Metadata updates
99
+
100
+ Returns:
101
+ ExecutionContext: New context with merged metadata
102
+ """
103
+ new_metadata = {**self.metadata, **kwargs}
104
+
105
+ return ExecutionContext(
106
+ working_dir=self.working_dir,
107
+ correlation_id=self.correlation_id,
108
+ cancel_token=self.cancel_token,
109
+ git_context=self.git_context,
110
+ project_context=self.project_context,
111
+ metadata=new_metadata
112
+ )
113
+
114
+ def __repr__(self) -> str:
115
+ """Human-readable representation."""
116
+ return (
117
+ f"ExecutionContext(cwd={self.working_dir}, "
118
+ f"correlation_id={self.correlation_id[:8]}...)"
119
+ )