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/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
|
+
)
|