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
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
# Task 2.1: Implement ToolOrchestrator
|
|
2
|
+
|
|
3
|
+
**Status**: 🔄 In Progress
|
|
4
|
+
**Priority**: P0 (Critical)
|
|
5
|
+
**Estimated Time**: 2-3 days
|
|
6
|
+
**Started**: 2025-10-25
|
|
7
|
+
**Dependencies**: Task 1.1, 1.2, 1.3
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 📋 Overview
|
|
12
|
+
|
|
13
|
+
### Objective
|
|
14
|
+
|
|
15
|
+
Implement intelligent tool orchestration that distinguishes between read-only and write tools, executing them in parallel or sequentially to prevent race conditions and improve safety.
|
|
16
|
+
|
|
17
|
+
### Current Problem
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# Loom 1.0 - All tools execute in parallel
|
|
21
|
+
await asyncio.gather(*[tool.execute() for tool in tool_calls])
|
|
22
|
+
|
|
23
|
+
# Problem scenarios:
|
|
24
|
+
# 1. ReadTool and EditTool on same file → race condition
|
|
25
|
+
# 2. Multiple EditTools on same file → data corruption
|
|
26
|
+
# 3. No consideration for tool side effects
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Real Bug Example**:
|
|
30
|
+
```python
|
|
31
|
+
# User asks: "Read config.json and update the version field"
|
|
32
|
+
# LLM generates:
|
|
33
|
+
tool_calls = [
|
|
34
|
+
ToolCall(name="Read", arguments={"path": "config.json"}),
|
|
35
|
+
ToolCall(name="Edit", arguments={"path": "config.json", ...})
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Current behavior: Both execute in parallel
|
|
39
|
+
# Result: Edit might start before Read completes → race condition!
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Solution
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Loom 2.0 - Intelligent orchestration
|
|
46
|
+
class ToolOrchestrator:
|
|
47
|
+
async def execute_batch(self, tool_calls: List[ToolCall]):
|
|
48
|
+
# 1. Categorize tools
|
|
49
|
+
read_only = [tc for tc in tool_calls if tools[tc.name].is_read_only]
|
|
50
|
+
write_tools = [tc for tc in tool_calls if not tools[tc.name].is_read_only]
|
|
51
|
+
|
|
52
|
+
# 2. Execute read-only tools in parallel (safe)
|
|
53
|
+
if read_only:
|
|
54
|
+
async for result in self.execute_parallel(read_only):
|
|
55
|
+
yield result
|
|
56
|
+
|
|
57
|
+
# 3. Execute write tools sequentially (safe)
|
|
58
|
+
for tc in write_tools:
|
|
59
|
+
result = await self.execute_one(tc)
|
|
60
|
+
yield result
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🎯 Goals
|
|
66
|
+
|
|
67
|
+
1. **Safety**: Prevent race conditions between tools
|
|
68
|
+
2. **Performance**: Parallel execution for safe operations
|
|
69
|
+
3. **Flexibility**: Easy to classify new tools
|
|
70
|
+
4. **Observability**: Yield AgentEvent for each execution phase
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 🏗️ Architecture
|
|
75
|
+
|
|
76
|
+
### Component Diagram
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
┌─────────────────────────────────────────────────────┐
|
|
80
|
+
│ AgentExecutor │
|
|
81
|
+
│ │
|
|
82
|
+
│ ┌───────────────────────────────────────────────┐ │
|
|
83
|
+
│ │ ToolOrchestrator │ │
|
|
84
|
+
│ │ │ │
|
|
85
|
+
│ │ 1. Categorize tools (read-only vs write) │ │
|
|
86
|
+
│ │ 2. Execute parallel/sequential │ │
|
|
87
|
+
│ │ 3. Yield AgentEvent for each phase │ │
|
|
88
|
+
│ │ │ │
|
|
89
|
+
│ │ ┌─────────────┐ ┌──────────────┐ │ │
|
|
90
|
+
│ │ │ Read-Only │ │ Write Tools │ │ │
|
|
91
|
+
│ │ │ (Parallel) │ │ (Sequential) │ │ │
|
|
92
|
+
│ │ │ │ │ │ │ │
|
|
93
|
+
│ │ │ • ReadTool │ │ • EditTool │ │ │
|
|
94
|
+
│ │ │ • GrepTool │ │ • WriteTool │ │ │
|
|
95
|
+
│ │ │ • GlobTool │ │ • BashTool │ │ │
|
|
96
|
+
│ │ └─────────────┘ └──────────────┘ │ │
|
|
97
|
+
│ └───────────────────────────────────────────────┘ │
|
|
98
|
+
└─────────────────────────────────────────────────────┘
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Class Design
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# loom/core/tool_orchestrator.py
|
|
105
|
+
|
|
106
|
+
from enum import Enum
|
|
107
|
+
from typing import AsyncGenerator, Dict, List
|
|
108
|
+
from loom.core.types import ToolCall, ToolResult
|
|
109
|
+
from loom.core.events import AgentEvent, AgentEventType
|
|
110
|
+
from loom.interfaces.tool import BaseTool
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ToolCategory(str, Enum):
|
|
114
|
+
"""Tool execution categories for safety classification."""
|
|
115
|
+
READ_ONLY = "read_only" # Safe to parallelize
|
|
116
|
+
WRITE = "write" # Must execute sequentially
|
|
117
|
+
NETWORK = "network" # May need rate limiting (future)
|
|
118
|
+
DESTRUCTIVE = "destructive" # Requires extra validation (future)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ToolOrchestrator:
|
|
122
|
+
"""
|
|
123
|
+
Intelligent tool execution orchestrator.
|
|
124
|
+
|
|
125
|
+
Features:
|
|
126
|
+
- Categorize tools by safety (read-only vs write)
|
|
127
|
+
- Execute read-only tools in parallel
|
|
128
|
+
- Execute write tools sequentially
|
|
129
|
+
- Yield AgentEvent for observability
|
|
130
|
+
- Integration with permission system
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
```python
|
|
134
|
+
orchestrator = ToolOrchestrator(
|
|
135
|
+
tools={"Read": ReadTool(), "Edit": EditTool()},
|
|
136
|
+
permission_manager=pm
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
tool_calls = [
|
|
140
|
+
ToolCall(name="Read", arguments={"path": "a.txt"}),
|
|
141
|
+
ToolCall(name="Read", arguments={"path": "b.txt"}),
|
|
142
|
+
ToolCall(name="Edit", arguments={"path": "c.txt"})
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
async for event in orchestrator.execute_batch(tool_calls):
|
|
146
|
+
if event.type == AgentEventType.TOOL_RESULT:
|
|
147
|
+
print(event.tool_result.content)
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
tools: Dict[str, BaseTool],
|
|
154
|
+
permission_manager: Optional[PermissionManager] = None,
|
|
155
|
+
max_parallel: int = 5
|
|
156
|
+
):
|
|
157
|
+
self.tools = tools
|
|
158
|
+
self.permission_manager = permission_manager
|
|
159
|
+
self.max_parallel = max_parallel
|
|
160
|
+
|
|
161
|
+
async def execute_batch(
|
|
162
|
+
self,
|
|
163
|
+
tool_calls: List[ToolCall]
|
|
164
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
165
|
+
"""
|
|
166
|
+
Execute a batch of tool calls with intelligent orchestration.
|
|
167
|
+
|
|
168
|
+
Strategy:
|
|
169
|
+
1. Categorize tools (read-only vs write)
|
|
170
|
+
2. Execute read-only in parallel (up to max_parallel)
|
|
171
|
+
3. Execute write tools sequentially
|
|
172
|
+
4. Yield AgentEvent for each execution phase
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
tool_calls: List of tool calls to execute
|
|
176
|
+
|
|
177
|
+
Yields:
|
|
178
|
+
AgentEvent: Execution progress events
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
def categorize_tools(
|
|
183
|
+
self,
|
|
184
|
+
tool_calls: List[ToolCall]
|
|
185
|
+
) -> tuple[List[ToolCall], List[ToolCall]]:
|
|
186
|
+
"""
|
|
187
|
+
Categorize tool calls into read-only and write.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
(read_only_calls, write_calls)
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
async def execute_parallel(
|
|
195
|
+
self,
|
|
196
|
+
tool_calls: List[ToolCall]
|
|
197
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
198
|
+
"""Execute read-only tools in parallel."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
async def execute_sequential(
|
|
202
|
+
self,
|
|
203
|
+
tool_calls: List[ToolCall]
|
|
204
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
205
|
+
"""Execute write tools sequentially."""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
async def execute_one(
|
|
209
|
+
self,
|
|
210
|
+
tool_call: ToolCall
|
|
211
|
+
) -> AgentEvent:
|
|
212
|
+
"""Execute a single tool call."""
|
|
213
|
+
...
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 📝 Implementation Steps
|
|
219
|
+
|
|
220
|
+
### Step 1: Modify BaseTool Interface
|
|
221
|
+
|
|
222
|
+
**File**: `loom/interfaces/tool.py`
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# Add to BaseTool
|
|
226
|
+
class BaseTool(ABC):
|
|
227
|
+
name: str
|
|
228
|
+
description: str
|
|
229
|
+
args_schema: Type[BaseModel]
|
|
230
|
+
|
|
231
|
+
# 🆕 New attributes for orchestration
|
|
232
|
+
is_read_only: bool = False
|
|
233
|
+
"""Whether this tool only reads data (safe to parallelize)."""
|
|
234
|
+
|
|
235
|
+
category: str = "general"
|
|
236
|
+
"""Tool category: general, destructive, network."""
|
|
237
|
+
|
|
238
|
+
requires_confirmation: bool = False
|
|
239
|
+
"""Whether this tool requires user confirmation (future)."""
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Changes**:
|
|
243
|
+
- Add `is_read_only` boolean attribute
|
|
244
|
+
- Add `category` string attribute
|
|
245
|
+
- Add `requires_confirmation` for future use
|
|
246
|
+
- Update docstrings
|
|
247
|
+
|
|
248
|
+
### Step 2: Classify Built-in Tools
|
|
249
|
+
|
|
250
|
+
**Files to modify**:
|
|
251
|
+
- `loom/builtin/tools/read.py`
|
|
252
|
+
- `loom/builtin/tools/edit.py`
|
|
253
|
+
- `loom/builtin/tools/write.py`
|
|
254
|
+
- `loom/builtin/tools/grep.py`
|
|
255
|
+
- `loom/builtin/tools/glob.py`
|
|
256
|
+
- `loom/builtin/tools/bash.py`
|
|
257
|
+
- Any other tools in `loom/builtin/tools/`
|
|
258
|
+
|
|
259
|
+
**Classification**:
|
|
260
|
+
|
|
261
|
+
| Tool | is_read_only | category | Rationale |
|
|
262
|
+
|------|--------------|----------|-----------|
|
|
263
|
+
| ReadTool | ✅ True | general | Only reads files |
|
|
264
|
+
| GrepTool | ✅ True | general | Only searches content |
|
|
265
|
+
| GlobTool | ✅ True | general | Only lists files |
|
|
266
|
+
| EditTool | ❌ False | destructive | Modifies files |
|
|
267
|
+
| WriteTool | ❌ False | destructive | Creates/overwrites files |
|
|
268
|
+
| BashTool | ❌ False | general | May have side effects |
|
|
269
|
+
|
|
270
|
+
**Example**:
|
|
271
|
+
```python
|
|
272
|
+
# loom/builtin/tools/read.py
|
|
273
|
+
class ReadTool(BaseTool):
|
|
274
|
+
name = "Read"
|
|
275
|
+
description = "Read file contents"
|
|
276
|
+
is_read_only = True # 🆕
|
|
277
|
+
category = "general" # 🆕
|
|
278
|
+
|
|
279
|
+
async def run(self, path: str) -> str:
|
|
280
|
+
...
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Step 3: Implement ToolOrchestrator
|
|
284
|
+
|
|
285
|
+
**File**: `loom/core/tool_orchestrator.py` (new file)
|
|
286
|
+
|
|
287
|
+
**Implementation outline**:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
class ToolOrchestrator:
|
|
291
|
+
def __init__(self, tools, permission_manager, max_parallel=5):
|
|
292
|
+
self.tools = tools
|
|
293
|
+
self.permission_manager = permission_manager
|
|
294
|
+
self.max_parallel = max_parallel
|
|
295
|
+
|
|
296
|
+
async def execute_batch(self, tool_calls):
|
|
297
|
+
"""Main orchestration logic."""
|
|
298
|
+
# Emit start event
|
|
299
|
+
yield AgentEvent(
|
|
300
|
+
type=AgentEventType.TOOL_CALLS_START,
|
|
301
|
+
metadata={"total_tools": len(tool_calls)}
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Categorize
|
|
305
|
+
read_only, write_tools = self.categorize_tools(tool_calls)
|
|
306
|
+
|
|
307
|
+
# Execute read-only in parallel
|
|
308
|
+
if read_only:
|
|
309
|
+
yield AgentEvent(
|
|
310
|
+
type=AgentEventType.TOOL_ORCHESTRATION,
|
|
311
|
+
metadata={
|
|
312
|
+
"phase": "parallel",
|
|
313
|
+
"count": len(read_only)
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
async for event in self.execute_parallel(read_only):
|
|
317
|
+
yield event
|
|
318
|
+
|
|
319
|
+
# Execute write sequentially
|
|
320
|
+
if write_tools:
|
|
321
|
+
yield AgentEvent(
|
|
322
|
+
type=AgentEventType.TOOL_ORCHESTRATION,
|
|
323
|
+
metadata={
|
|
324
|
+
"phase": "sequential",
|
|
325
|
+
"count": len(write_tools)
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
async for event in self.execute_sequential(write_tools):
|
|
329
|
+
yield event
|
|
330
|
+
|
|
331
|
+
def categorize_tools(self, tool_calls):
|
|
332
|
+
"""Split into read-only and write."""
|
|
333
|
+
read_only = []
|
|
334
|
+
write_tools = []
|
|
335
|
+
|
|
336
|
+
for tc in tool_calls:
|
|
337
|
+
tool = self.tools.get(tc.name)
|
|
338
|
+
if tool and tool.is_read_only:
|
|
339
|
+
read_only.append(tc)
|
|
340
|
+
else:
|
|
341
|
+
write_tools.append(tc)
|
|
342
|
+
|
|
343
|
+
return read_only, write_tools
|
|
344
|
+
|
|
345
|
+
async def execute_parallel(self, tool_calls):
|
|
346
|
+
"""Execute read-only tools in parallel."""
|
|
347
|
+
import asyncio
|
|
348
|
+
|
|
349
|
+
# Use semaphore to limit concurrency
|
|
350
|
+
semaphore = asyncio.Semaphore(self.max_parallel)
|
|
351
|
+
|
|
352
|
+
async def execute_with_semaphore(tc):
|
|
353
|
+
async with semaphore:
|
|
354
|
+
return await self.execute_one(tc)
|
|
355
|
+
|
|
356
|
+
# Execute all in parallel
|
|
357
|
+
tasks = [execute_with_semaphore(tc) for tc in tool_calls]
|
|
358
|
+
|
|
359
|
+
for coro in asyncio.as_completed(tasks):
|
|
360
|
+
event = await coro
|
|
361
|
+
yield event
|
|
362
|
+
|
|
363
|
+
async def execute_sequential(self, tool_calls):
|
|
364
|
+
"""Execute write tools sequentially."""
|
|
365
|
+
for tc in tool_calls:
|
|
366
|
+
event = await self.execute_one(tc)
|
|
367
|
+
yield event
|
|
368
|
+
|
|
369
|
+
async def execute_one(self, tool_call):
|
|
370
|
+
"""Execute a single tool."""
|
|
371
|
+
# Emit start event
|
|
372
|
+
yield AgentEvent(
|
|
373
|
+
type=AgentEventType.TOOL_EXECUTION_START,
|
|
374
|
+
tool_call=EventToolCall(
|
|
375
|
+
id=tool_call.id,
|
|
376
|
+
name=tool_call.name,
|
|
377
|
+
arguments=tool_call.arguments
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
# Check permissions
|
|
383
|
+
if self.permission_manager:
|
|
384
|
+
allowed = await self.permission_manager.check_tool(
|
|
385
|
+
tool_call.name,
|
|
386
|
+
tool_call.arguments
|
|
387
|
+
)
|
|
388
|
+
if not allowed:
|
|
389
|
+
raise PermissionError(f"Tool {tool_call.name} not allowed")
|
|
390
|
+
|
|
391
|
+
# Execute tool
|
|
392
|
+
tool = self.tools[tool_call.name]
|
|
393
|
+
result_content = await tool.run(**tool_call.arguments)
|
|
394
|
+
|
|
395
|
+
# Create result
|
|
396
|
+
result = ToolResult(
|
|
397
|
+
tool_call_id=tool_call.id,
|
|
398
|
+
tool_name=tool_call.name,
|
|
399
|
+
content=result_content,
|
|
400
|
+
is_error=False
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Emit result event
|
|
404
|
+
yield AgentEvent.tool_result(result)
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
# Handle error
|
|
408
|
+
result = ToolResult(
|
|
409
|
+
tool_call_id=tool_call.id,
|
|
410
|
+
tool_name=tool_call.name,
|
|
411
|
+
content=str(e),
|
|
412
|
+
is_error=True
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
yield AgentEvent(
|
|
416
|
+
type=AgentEventType.TOOL_ERROR,
|
|
417
|
+
tool_result=result,
|
|
418
|
+
error=e
|
|
419
|
+
)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Step 4: Integrate into AgentExecutor
|
|
423
|
+
|
|
424
|
+
**File**: `loom/core/agent_executor.py`
|
|
425
|
+
|
|
426
|
+
**Changes**:
|
|
427
|
+
1. Add ToolOrchestrator import
|
|
428
|
+
2. Initialize in `__init__`
|
|
429
|
+
3. Replace existing tool execution with orchestrator
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
# In __init__
|
|
433
|
+
self.tool_orchestrator = ToolOrchestrator(
|
|
434
|
+
tools=self.tools,
|
|
435
|
+
permission_manager=self.permission_manager,
|
|
436
|
+
max_parallel=5
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# In execute_stream (replace tool execution section)
|
|
440
|
+
if tool_calls:
|
|
441
|
+
# Use orchestrator instead of direct execution
|
|
442
|
+
async for event in self.tool_orchestrator.execute_batch(tc_models):
|
|
443
|
+
yield event
|
|
444
|
+
|
|
445
|
+
# Update history with results
|
|
446
|
+
if event.type == AgentEventType.TOOL_RESULT:
|
|
447
|
+
tool_msg = Message(
|
|
448
|
+
role="tool",
|
|
449
|
+
content=event.tool_result.content,
|
|
450
|
+
tool_call_id=event.tool_result.tool_call_id
|
|
451
|
+
)
|
|
452
|
+
history.append(tool_msg)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Step 5: Add New Event Types (if needed)
|
|
456
|
+
|
|
457
|
+
**File**: `loom/core/events.py`
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
# Add to AgentEventType
|
|
461
|
+
class AgentEventType(str, Enum):
|
|
462
|
+
# ... existing events ...
|
|
463
|
+
|
|
464
|
+
# 🆕 Orchestration events
|
|
465
|
+
TOOL_ORCHESTRATION = "tool_orchestration"
|
|
466
|
+
"""Tool orchestration phase (parallel/sequential)."""
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## 🧪 Testing Requirements
|
|
472
|
+
|
|
473
|
+
### Unit Tests
|
|
474
|
+
|
|
475
|
+
**File**: `tests/unit/test_tool_orchestrator.py`
|
|
476
|
+
|
|
477
|
+
**Test cases** (target 25-30 tests):
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
class TestToolOrchestrator:
|
|
481
|
+
"""Test ToolOrchestrator class."""
|
|
482
|
+
|
|
483
|
+
def test_init(self):
|
|
484
|
+
"""Test initialization."""
|
|
485
|
+
...
|
|
486
|
+
|
|
487
|
+
def test_categorize_read_only_tools(self):
|
|
488
|
+
"""Test categorization of read-only tools."""
|
|
489
|
+
...
|
|
490
|
+
|
|
491
|
+
def test_categorize_write_tools(self):
|
|
492
|
+
"""Test categorization of write tools."""
|
|
493
|
+
...
|
|
494
|
+
|
|
495
|
+
def test_categorize_mixed_tools(self):
|
|
496
|
+
"""Test categorization of mixed tool calls."""
|
|
497
|
+
...
|
|
498
|
+
|
|
499
|
+
async def test_execute_parallel_read_only(self):
|
|
500
|
+
"""Test parallel execution of read-only tools."""
|
|
501
|
+
...
|
|
502
|
+
|
|
503
|
+
async def test_execute_sequential_write(self):
|
|
504
|
+
"""Test sequential execution of write tools."""
|
|
505
|
+
...
|
|
506
|
+
|
|
507
|
+
async def test_execute_batch_mixed(self):
|
|
508
|
+
"""Test batch execution with mixed tools."""
|
|
509
|
+
...
|
|
510
|
+
|
|
511
|
+
async def test_parallel_respects_max_parallel(self):
|
|
512
|
+
"""Test max_parallel limit is respected."""
|
|
513
|
+
...
|
|
514
|
+
|
|
515
|
+
async def test_execute_one_success(self):
|
|
516
|
+
"""Test single tool execution success."""
|
|
517
|
+
...
|
|
518
|
+
|
|
519
|
+
async def test_execute_one_error(self):
|
|
520
|
+
"""Test single tool execution error handling."""
|
|
521
|
+
...
|
|
522
|
+
|
|
523
|
+
async def test_permission_check_integration(self):
|
|
524
|
+
"""Test integration with permission manager."""
|
|
525
|
+
...
|
|
526
|
+
|
|
527
|
+
async def test_event_emission(self):
|
|
528
|
+
"""Test AgentEvent emission during execution."""
|
|
529
|
+
...
|
|
530
|
+
|
|
531
|
+
async def test_parallel_execution_order_independent(self):
|
|
532
|
+
"""Test parallel tools can complete in any order."""
|
|
533
|
+
...
|
|
534
|
+
|
|
535
|
+
async def test_sequential_execution_order(self):
|
|
536
|
+
"""Test sequential tools execute in order."""
|
|
537
|
+
...
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class TestToolCategorization:
|
|
541
|
+
"""Test tool categorization logic."""
|
|
542
|
+
|
|
543
|
+
def test_read_tool_is_read_only(self):
|
|
544
|
+
"""Verify ReadTool is classified as read-only."""
|
|
545
|
+
...
|
|
546
|
+
|
|
547
|
+
def test_grep_tool_is_read_only(self):
|
|
548
|
+
"""Verify GrepTool is classified as read-only."""
|
|
549
|
+
...
|
|
550
|
+
|
|
551
|
+
def test_edit_tool_is_write(self):
|
|
552
|
+
"""Verify EditTool is classified as write."""
|
|
553
|
+
...
|
|
554
|
+
|
|
555
|
+
def test_bash_tool_is_write(self):
|
|
556
|
+
"""Verify BashTool is classified as write."""
|
|
557
|
+
...
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class TestRaceConditionPrevention:
|
|
561
|
+
"""Test race condition prevention."""
|
|
562
|
+
|
|
563
|
+
async def test_read_and_edit_same_file(self):
|
|
564
|
+
"""Test Read and Edit on same file execute safely."""
|
|
565
|
+
# Read should complete before Edit starts
|
|
566
|
+
...
|
|
567
|
+
|
|
568
|
+
async def test_multiple_edits_same_file(self):
|
|
569
|
+
"""Test multiple Edits on same file are sequential."""
|
|
570
|
+
...
|
|
571
|
+
|
|
572
|
+
async def test_multiple_reads_same_file(self):
|
|
573
|
+
"""Test multiple Reads on same file can be parallel."""
|
|
574
|
+
...
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class TestPerformance:
|
|
578
|
+
"""Test performance improvements."""
|
|
579
|
+
|
|
580
|
+
async def test_parallel_faster_than_sequential(self):
|
|
581
|
+
"""Test parallel execution is faster for read-only tools."""
|
|
582
|
+
...
|
|
583
|
+
|
|
584
|
+
async def test_max_parallel_limiting(self):
|
|
585
|
+
"""Test max_parallel limits concurrent execution."""
|
|
586
|
+
...
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Integration Tests
|
|
590
|
+
|
|
591
|
+
**File**: `tests/integration/test_tool_orchestration.py`
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
class TestOrchestrationIntegration:
|
|
595
|
+
"""Test orchestration in full agent context."""
|
|
596
|
+
|
|
597
|
+
async def test_agent_with_mixed_tool_calls(self):
|
|
598
|
+
"""Test agent executing mixed read/write tools."""
|
|
599
|
+
...
|
|
600
|
+
|
|
601
|
+
async def test_orchestration_with_rag(self):
|
|
602
|
+
"""Test orchestration with RAG context."""
|
|
603
|
+
...
|
|
604
|
+
|
|
605
|
+
async def test_orchestration_events(self):
|
|
606
|
+
"""Test AgentEvent emission during orchestration."""
|
|
607
|
+
...
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## ✅ Acceptance Criteria
|
|
613
|
+
|
|
614
|
+
- [ ] `BaseTool` interface has `is_read_only` and `category` attributes
|
|
615
|
+
- [ ] All built-in tools are classified (read-only vs write)
|
|
616
|
+
- [ ] `ToolOrchestrator` class implemented
|
|
617
|
+
- [ ] `categorize_tools()` correctly splits tools
|
|
618
|
+
- [ ] `execute_parallel()` executes read-only tools concurrently
|
|
619
|
+
- [ ] `execute_sequential()` executes write tools in order
|
|
620
|
+
- [ ] `max_parallel` limit is respected
|
|
621
|
+
- [ ] Integration with `AgentExecutor`
|
|
622
|
+
- [ ] `execute_stream()` uses orchestrator
|
|
623
|
+
- [ ] `execute()` uses orchestrator
|
|
624
|
+
- [ ] `stream()` uses orchestrator
|
|
625
|
+
- [ ] Test coverage ≥ 80%
|
|
626
|
+
- [ ] 25-30 unit tests
|
|
627
|
+
- [ ] All tests pass
|
|
628
|
+
- [ ] No race conditions in concurrent execution
|
|
629
|
+
- [ ] Performance: Parallel execution faster than sequential for read-only
|
|
630
|
+
- [ ] Events emitted correctly
|
|
631
|
+
- [ ] Backward compatible (existing tests still pass)
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## 📦 Deliverables
|
|
636
|
+
|
|
637
|
+
1. **Core Implementation**
|
|
638
|
+
- [ ] `loom/core/tool_orchestrator.py` (~300-400 lines)
|
|
639
|
+
- [ ] Modified `loom/interfaces/tool.py`
|
|
640
|
+
- [ ] Modified `loom/core/agent_executor.py`
|
|
641
|
+
|
|
642
|
+
2. **Tool Classification**
|
|
643
|
+
- [ ] All tools in `loom/builtin/tools/` classified
|
|
644
|
+
- [ ] Documentation for each classification
|
|
645
|
+
|
|
646
|
+
3. **Tests**
|
|
647
|
+
- [ ] `tests/unit/test_tool_orchestrator.py` (25-30 tests)
|
|
648
|
+
- [ ] `tests/integration/test_tool_orchestration.py` (3-5 tests)
|
|
649
|
+
- [ ] All tests passing
|
|
650
|
+
|
|
651
|
+
4. **Documentation**
|
|
652
|
+
- [ ] Code docstrings complete
|
|
653
|
+
- [ ] Example usage in docstrings
|
|
654
|
+
- [ ] `examples/tool_orchestration_demo.py`
|
|
655
|
+
- [ ] `docs/TASK_2.1_COMPLETION_SUMMARY.md`
|
|
656
|
+
|
|
657
|
+
5. **Validation**
|
|
658
|
+
- [ ] Run full test suite: `pytest tests/ -v`
|
|
659
|
+
- [ ] Coverage report: `pytest --cov=loom --cov-report=html`
|
|
660
|
+
- [ ] Performance benchmark (parallel vs sequential)
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## 📚 References
|
|
665
|
+
|
|
666
|
+
### Claude Code Inspiration
|
|
667
|
+
|
|
668
|
+
From `cc分析/Tools.md`:
|
|
669
|
+
- Intelligent tool parallelization
|
|
670
|
+
- Safety-first execution model
|
|
671
|
+
- Read-only vs write distinction
|
|
672
|
+
|
|
673
|
+
### Design Principles
|
|
674
|
+
|
|
675
|
+
1. **Safety First**: Prevent race conditions by default
|
|
676
|
+
2. **Performance**: Parallelize when safe
|
|
677
|
+
3. **Observability**: Emit events for all phases
|
|
678
|
+
4. **Flexibility**: Easy to add new tools
|
|
679
|
+
5. **Backward Compatibility**: Don't break existing code
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## 🔍 Testing Checklist
|
|
684
|
+
|
|
685
|
+
Before marking as complete:
|
|
686
|
+
|
|
687
|
+
- [ ] All unit tests pass
|
|
688
|
+
- [ ] All integration tests pass
|
|
689
|
+
- [ ] All existing tests still pass (no regressions)
|
|
690
|
+
- [ ] Code coverage ≥ 80%
|
|
691
|
+
- [ ] Type checking passes: `mypy loom/`
|
|
692
|
+
- [ ] Linting passes: `ruff check loom/`
|
|
693
|
+
- [ ] Performance test shows parallel is faster
|
|
694
|
+
- [ ] Manual testing with real agent
|
|
695
|
+
- [ ] Example code runs successfully
|
|
696
|
+
- [ ] Documentation reviewed
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 🎓 Learning Notes
|
|
701
|
+
|
|
702
|
+
### Key Concepts
|
|
703
|
+
|
|
704
|
+
1. **Read-only Tools**: Can execute in parallel safely
|
|
705
|
+
- ReadTool, GrepTool, GlobTool
|
|
706
|
+
- No side effects
|
|
707
|
+
- No file modifications
|
|
708
|
+
|
|
709
|
+
2. **Write Tools**: Must execute sequentially
|
|
710
|
+
- EditTool, WriteTool, BashTool
|
|
711
|
+
- Have side effects
|
|
712
|
+
- May modify files/system state
|
|
713
|
+
|
|
714
|
+
3. **Race Condition Example**:
|
|
715
|
+
```python
|
|
716
|
+
# Dangerous: Read and Write same file in parallel
|
|
717
|
+
await asyncio.gather(
|
|
718
|
+
read_tool.run("config.json"),
|
|
719
|
+
edit_tool.run("config.json", ...)
|
|
720
|
+
)
|
|
721
|
+
# Result: undefined behavior!
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
4. **Safe Orchestration**:
|
|
725
|
+
```python
|
|
726
|
+
# Safe: Execute sequentially
|
|
727
|
+
content = await read_tool.run("config.json")
|
|
728
|
+
await edit_tool.run("config.json", ...)
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Implementation Tips
|
|
732
|
+
|
|
733
|
+
1. Use `asyncio.Semaphore` for parallel execution limiting
|
|
734
|
+
2. Use `asyncio.as_completed` for result streaming
|
|
735
|
+
3. Always emit events for observability
|
|
736
|
+
4. Handle tool errors gracefully
|
|
737
|
+
5. Test with actual file I/O for race conditions
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
**Created**: 2025-10-25
|
|
742
|
+
**Last Updated**: 2025-10-25
|
|
743
|
+
**Status**: 🔄 In Progress
|