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
@@ -0,0 +1,521 @@
1
+ # Task 1.2: 重构 Agent.execute() 为流式接口
2
+
3
+ **状态**: ⏳ 待开始
4
+ **优先级**: P0
5
+ **预计时间**: 1-2 天
6
+ **依赖**: Task 1.1 (AgentEvent 模型) ✅
7
+
8
+ ---
9
+
10
+ ## 📋 任务概述
11
+
12
+ ### 目标
13
+
14
+ 将 `Agent.execute()` 方法改为返回 `AsyncGenerator[AgentEvent, None]`,实现全链路流式架构。
15
+
16
+ ### 为什么需要这个任务?
17
+
18
+ **当前问题**:
19
+ ```python
20
+ # Loom 1.0 - 非流式
21
+ result = await agent.run(prompt) # 等待完成,无实时进度
22
+ print(result)
23
+ ```
24
+
25
+ **期望结果**:
26
+ ```python
27
+ # Loom 2.0 - 流式
28
+ async for event in agent.execute(prompt):
29
+ if event.type == AgentEventType.LLM_DELTA:
30
+ print(event.content, end="", flush=True)
31
+ elif event.type == AgentEventType.TOOL_PROGRESS:
32
+ print(f"\n[Tool] {event.metadata['status']}")
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 📐 详细步骤
38
+
39
+ ### Step 1: 修改 Agent 接口
40
+
41
+ **文件**: `loom/components/agent.py`
42
+
43
+ **当前代码** (简化):
44
+ ```python
45
+ class Agent:
46
+ async def run(self, input: str) -> str:
47
+ """Execute agent and return final response"""
48
+ return await self.executor.execute(input)
49
+ ```
50
+
51
+ **修改为**:
52
+ ```python
53
+ class Agent:
54
+ async def execute(self, input: str) -> AsyncGenerator[AgentEvent, None]:
55
+ """
56
+ Execute agent with streaming events.
57
+
58
+ Yields:
59
+ AgentEvent: Events representing execution progress
60
+
61
+ Example:
62
+ ```python
63
+ async for event in agent.execute("Your prompt"):
64
+ if event.type == AgentEventType.LLM_DELTA:
65
+ print(event.content, end="")
66
+ ```
67
+ """
68
+ # 初始化 turn state
69
+ turn_state = TurnState(turn_counter=0, turn_id=str(uuid.uuid4()))
70
+
71
+ # 创建初始消息
72
+ messages = [Message(role="user", content=input)]
73
+
74
+ # 调用 executor 的流式接口
75
+ async for event in self.executor.execute_stream(messages, turn_state):
76
+ yield event
77
+
78
+ async def run(self, input: str) -> str:
79
+ """
80
+ Execute agent and return final response (backward compatible).
81
+
82
+ This is a convenience method that wraps execute() and extracts
83
+ the final response.
84
+
85
+ Args:
86
+ input: User input
87
+
88
+ Returns:
89
+ Final response text
90
+ """
91
+ final_content = ""
92
+
93
+ async for event in self.execute(input):
94
+ # Accumulate LLM deltas
95
+ if event.type == AgentEventType.LLM_DELTA:
96
+ final_content += event.content or ""
97
+
98
+ # Return on finish
99
+ elif event.type == AgentEventType.AGENT_FINISH:
100
+ return event.content or final_content
101
+
102
+ # Raise on error
103
+ elif event.type == AgentEventType.ERROR:
104
+ raise event.error
105
+
106
+ return final_content
107
+ ```
108
+
109
+ **新增数据类**:
110
+ ```python
111
+ from dataclasses import dataclass
112
+
113
+ @dataclass
114
+ class TurnState:
115
+ """State for recursive agent execution"""
116
+ turn_counter: int
117
+ turn_id: str
118
+ compacted: bool = False
119
+ max_iterations: int = 10
120
+ ```
121
+
122
+ ---
123
+
124
+ ### Step 2: 修改 AgentExecutor 接口
125
+
126
+ **文件**: `loom/core/agent_executor.py`
127
+
128
+ **当前代码** (简化):
129
+ ```python
130
+ class AgentExecutor:
131
+ async def execute(self, user_input: str) -> str:
132
+ """Execute agent and return final response"""
133
+ # ... 复杂的逻辑
134
+ return final_response
135
+ ```
136
+
137
+ **修改为**:
138
+ ```python
139
+ class AgentExecutor:
140
+ async def execute_stream(
141
+ self,
142
+ messages: List[Message],
143
+ turn_state: TurnState
144
+ ) -> AsyncGenerator[AgentEvent, None]:
145
+ """
146
+ Execute agent with streaming events.
147
+
148
+ Args:
149
+ messages: Conversation history
150
+ turn_state: Current turn state
151
+
152
+ Yields:
153
+ AgentEvent: Events representing execution progress
154
+ """
155
+
156
+ # Phase 0: Iteration check
157
+ yield AgentEvent(
158
+ type=AgentEventType.ITERATION_START,
159
+ iteration=turn_state.turn_counter,
160
+ turn_id=turn_state.turn_id
161
+ )
162
+
163
+ if turn_state.turn_counter >= turn_state.max_iterations:
164
+ yield AgentEvent(type=AgentEventType.MAX_ITERATIONS_REACHED)
165
+ return
166
+
167
+ # Phase 1: Context assembly
168
+ yield AgentEvent.phase_start("context_assembly")
169
+
170
+ system_prompt = self.system_prompt_builder.build()
171
+ # TODO: 后续任务会替换为 ContextAssembler
172
+
173
+ yield AgentEvent.phase_end(
174
+ "context_assembly",
175
+ tokens_used=self._count_tokens(system_prompt)
176
+ )
177
+
178
+ # Phase 2: RAG retrieval (if enabled)
179
+ if self.context_retriever:
180
+ yield AgentEvent(type=AgentEventType.RETRIEVAL_START)
181
+
182
+ retrieved_docs = await self.context_retriever.retrieve(
183
+ messages[-1].content
184
+ )
185
+
186
+ for doc in retrieved_docs:
187
+ yield AgentEvent(
188
+ type=AgentEventType.RETRIEVAL_PROGRESS,
189
+ metadata={
190
+ "doc_title": doc.metadata.get("title", "Unknown"),
191
+ "relevance_score": doc.metadata.get("score", 0.0)
192
+ }
193
+ )
194
+
195
+ yield AgentEvent(type=AgentEventType.RETRIEVAL_COMPLETE)
196
+
197
+ # TODO: 正确注入 RAG 上下文(Task 1.3)
198
+
199
+ # Phase 3: LLM call
200
+ yield AgentEvent(type=AgentEventType.LLM_START)
201
+
202
+ # 构建完整消息
203
+ full_messages = [
204
+ Message(role="system", content=system_prompt),
205
+ *messages
206
+ ]
207
+
208
+ # 流式调用 LLM
209
+ llm_response = ""
210
+ tool_calls = []
211
+
212
+ async for chunk in self.llm.stream(full_messages, tools=self.tools):
213
+ if chunk.get("type") == "text_delta":
214
+ text = chunk["content"]
215
+ llm_response += text
216
+ yield AgentEvent.llm_delta(text)
217
+
218
+ elif chunk.get("type") == "tool_calls":
219
+ tool_calls = chunk["tool_calls"]
220
+ yield AgentEvent(
221
+ type=AgentEventType.LLM_TOOL_CALLS,
222
+ metadata={"tool_count": len(tool_calls)}
223
+ )
224
+
225
+ yield AgentEvent(type=AgentEventType.LLM_COMPLETE)
226
+
227
+ # Phase 4: Tool execution (if needed)
228
+ if tool_calls:
229
+ yield AgentEvent(type=AgentEventType.TOOL_CALLS_START)
230
+
231
+ tool_results = []
232
+
233
+ for tool_call in tool_calls:
234
+ # TODO: 后续会使用 ToolOrchestrator(Task 2.1)
235
+ # 现在简单顺序执行
236
+
237
+ tool = self.tools[tool_call.name]
238
+
239
+ yield AgentEvent(
240
+ type=AgentEventType.TOOL_EXECUTION_START,
241
+ tool_call=ToolCall(
242
+ id=tool_call.id,
243
+ name=tool_call.name,
244
+ arguments=tool_call.arguments
245
+ )
246
+ )
247
+
248
+ try:
249
+ result_content = await tool.execute(tool_call.arguments)
250
+
251
+ result = ToolResult(
252
+ tool_call_id=tool_call.id,
253
+ tool_name=tool_call.name,
254
+ content=result_content,
255
+ is_error=False
256
+ )
257
+
258
+ yield AgentEvent.tool_result(result)
259
+ tool_results.append(result)
260
+
261
+ except Exception as e:
262
+ result = ToolResult(
263
+ tool_call_id=tool_call.id,
264
+ tool_name=tool_call.name,
265
+ content=str(e),
266
+ is_error=True
267
+ )
268
+
269
+ yield AgentEvent(
270
+ type=AgentEventType.TOOL_ERROR,
271
+ tool_result=result,
272
+ error=e
273
+ )
274
+ tool_results.append(result)
275
+
276
+ # Phase 5: Recursion (if tools were executed)
277
+ # 创建新消息包含工具结果
278
+ new_messages = [
279
+ *messages,
280
+ Message(role="assistant", content=llm_response, tool_calls=tool_calls),
281
+ *[Message(role="tool", content=r.content, tool_call_id=r.tool_call_id)
282
+ for r in tool_results]
283
+ ]
284
+
285
+ # 递归调用
286
+ new_turn_state = TurnState(
287
+ turn_counter=turn_state.turn_counter + 1,
288
+ turn_id=turn_state.turn_id
289
+ )
290
+
291
+ async for event in self.execute_stream(new_messages, new_turn_state):
292
+ yield event
293
+
294
+ else:
295
+ # Phase 5: Finish (no tools)
296
+ yield AgentEvent.agent_finish(llm_response)
297
+
298
+ # 保留向后兼容方法
299
+ async def execute(self, user_input: str) -> str:
300
+ """Legacy method - wraps execute_stream"""
301
+ messages = [Message(role="user", content=user_input)]
302
+ turn_state = TurnState(turn_counter=0, turn_id=str(uuid.uuid4()))
303
+
304
+ final_response = ""
305
+ async for event in self.execute_stream(messages, turn_state):
306
+ if event.type == AgentEventType.AGENT_FINISH:
307
+ return event.content or final_response
308
+ elif event.type == AgentEventType.LLM_DELTA:
309
+ final_response += event.content or ""
310
+
311
+ return final_response
312
+ ```
313
+
314
+ ---
315
+
316
+ ### Step 3: 修改 LLM 接口(可选)
317
+
318
+ **文件**: `loom/interfaces/llm.py`
319
+
320
+ 如果 LLM 接口不支持流式工具调用,可以添加 fallback:
321
+
322
+ ```python
323
+ class BaseLLM(ABC):
324
+ # 现有方法...
325
+
326
+ async def stream(
327
+ self,
328
+ messages: List[Dict],
329
+ tools: List[Dict] = None
330
+ ) -> AsyncGenerator[Dict, None]:
331
+ """
332
+ Stream LLM response.
333
+
334
+ Yields:
335
+ Dict with one of:
336
+ - {"type": "text_delta", "content": "..."}
337
+ - {"type": "tool_calls", "tool_calls": [...]}
338
+ """
339
+ # Default implementation: fallback to non-streaming
340
+ response = await self.generate_with_tools(messages, tools)
341
+
342
+ # Yield as single chunk
343
+ if "tool_calls" in response:
344
+ yield {"type": "tool_calls", "tool_calls": response["tool_calls"]}
345
+ else:
346
+ yield {"type": "text_delta", "content": response["content"]}
347
+ ```
348
+
349
+ ---
350
+
351
+ ## 🧪 测试要求
352
+
353
+ ### 单元测试
354
+
355
+ **文件**: `tests/unit/test_streaming_api.py`
356
+
357
+ 测试用例:
358
+
359
+ 1. ✅ `test_agent_execute_returns_generator` - 验证返回类型
360
+ 2. ✅ `test_agent_run_backward_compatible` - 验证向后兼容
361
+ 3. ✅ `test_llm_delta_events` - 验证 LLM 流式事件
362
+ 4. ✅ `test_tool_execution_events` - 验证工具执行事件
363
+ 5. ✅ `test_iteration_events` - 验证迭代事件
364
+ 6. ✅ `test_error_propagation` - 验证错误处理
365
+
366
+ ```python
367
+ import pytest
368
+ from loom import Agent
369
+ from loom.core.events import AgentEventType, EventCollector
370
+
371
+ @pytest.mark.asyncio
372
+ async def test_agent_execute_returns_generator():
373
+ """Test that execute() returns AsyncGenerator"""
374
+ agent = Agent(llm=mock_llm, tools=[])
375
+
376
+ result = agent.execute("test")
377
+
378
+ # Should be async generator
379
+ assert hasattr(result, '__aiter__')
380
+
381
+ @pytest.mark.asyncio
382
+ async def test_agent_run_backward_compatible():
383
+ """Test that run() still works"""
384
+ agent = Agent(llm=mock_llm, tools=[])
385
+
386
+ result = await agent.run("test")
387
+
388
+ # Should return string
389
+ assert isinstance(result, str)
390
+
391
+ @pytest.mark.asyncio
392
+ async def test_llm_delta_events():
393
+ """Test LLM streaming produces delta events"""
394
+ agent = Agent(llm=streaming_mock_llm, tools=[])
395
+ collector = EventCollector()
396
+
397
+ async for event in agent.execute("test"):
398
+ collector.add(event)
399
+
400
+ # Should have LLM_START, LLM_DELTA, LLM_COMPLETE
401
+ assert any(e.type == AgentEventType.LLM_START for e in collector.events)
402
+ assert any(e.type == AgentEventType.LLM_DELTA for e in collector.events)
403
+ assert any(e.type == AgentEventType.LLM_COMPLETE for e in collector.events)
404
+
405
+ # Reconstructed content should match
406
+ llm_content = collector.get_llm_content()
407
+ assert len(llm_content) > 0
408
+ ```
409
+
410
+ ### 集成测试
411
+
412
+ **文件**: `tests/integration/test_agent_streaming.py`
413
+
414
+ 测试真实场景:
415
+
416
+ 1. ✅ End-to-end 流式执行
417
+ 2. ✅ 工具调用 + 递归
418
+ 3. ✅ RAG 集成(如果启用)
419
+ 4. ✅ 错误恢复
420
+
421
+ ---
422
+
423
+ ## ✅ 验收标准
424
+
425
+ | 标准 | 要求 | 检查 |
426
+ |------|------|------|
427
+ | API 修改 | `execute()` 返回 `AsyncGenerator[AgentEvent]` | [ ] |
428
+ | 向后兼容 | `run()` 方法仍工作 | [ ] |
429
+ | 事件产生 | 产生所有必需事件类型 | [ ] |
430
+ | 测试覆盖 | ≥ 80% 覆盖率 | [ ] |
431
+ | 所有测试通过 | 单元 + 集成测试 | [ ] |
432
+ | 文档更新 | 更新 API 文档和示例 | [ ] |
433
+ | 性能 | 无明显性能下降 | [ ] |
434
+
435
+ ---
436
+
437
+ ## 📋 实施检查清单
438
+
439
+ ### 代码修改
440
+
441
+ - [ ] 修改 `loom/components/agent.py`
442
+ - [ ] 新增 `execute()` 方法(返回 AsyncGenerator)
443
+ - [ ] 修改 `run()` 为包装方法(向后兼容)
444
+ - [ ] 添加 `TurnState` 数据类
445
+
446
+ - [ ] 修改 `loom/core/agent_executor.py`
447
+ - [ ] 新增 `execute_stream()` 方法
448
+ - [ ] 产生所有必需事件
449
+ - [ ] 保留 `execute()` 向后兼容
450
+
451
+ - [ ] 修改 `loom/interfaces/llm.py` (可选)
452
+ - [ ] 添加 `stream()` 方法
453
+
454
+ ### 测试
455
+
456
+ - [ ] 创建 `tests/unit/test_streaming_api.py`
457
+ - [ ] 6+ 个单元测试
458
+ - [ ] Mock LLM 和 Tools
459
+
460
+ - [ ] 创建 `tests/integration/test_agent_streaming.py`
461
+ - [ ] End-to-end 测试
462
+ - [ ] 真实 LLM 集成(可选)
463
+
464
+ - [ ] 运行所有测试
465
+ ```bash
466
+ pytest tests/unit/test_streaming_api.py -v
467
+ pytest tests/integration/test_agent_streaming.py -v
468
+ pytest tests/ -v # 确保没有破坏现有测试
469
+ ```
470
+
471
+ ### 文档
472
+
473
+ - [ ] 更新 `docs/api_reference.md`
474
+ - [ ] 记录新的 `execute()` API
475
+ - [ ] 记录事件流模式
476
+
477
+ - [ ] 更新 `README.md`
478
+ - [ ] 添加流式 API 示例
479
+
480
+ - [ ] 创建 `examples/streaming_example.py`
481
+ - [ ] 基础流式示例
482
+ - [ ] 工具执行示例
483
+ - [ ] 错误处理示例
484
+
485
+ ### 完成
486
+
487
+ - [ ] 创建 `docs/TASK_1.2_COMPLETION_SUMMARY.md`
488
+ - [ ] 更新 `LOOM_2.0_DEVELOPMENT_PLAN.md`
489
+ - [ ] 更新 `loom/tasks/README.md`
490
+ - [ ] 代码审查
491
+ - [ ] 合并到主分支
492
+
493
+ ---
494
+
495
+ ## 🔗 参考资源
496
+
497
+ - [Task 1.1: AgentEvent 模型](task_1.1_agent_events.md)
498
+ - [AgentEvent 使用指南](../../../docs/agent_events_guide.md)
499
+ - [Claude Code 控制流程](../../../cc分析/Control%20Flow.md)
500
+
501
+ ---
502
+
503
+ ## 📝 注意事项
504
+
505
+ ### 关键决策
506
+
507
+ 1. **递归 vs 循环**: 当前使用递归实现多轮对话,后续可能改为循环(Task 3.3)
508
+ 2. **工具执行**: 暂时顺序执行,Task 2.1 会改为智能并行
509
+ 3. **RAG 注入**: 暂时保留原有逻辑,Task 1.3 会修复
510
+
511
+ ### 潜在问题
512
+
513
+ 1. **性能**: 流式可能引入轻微延迟,需要性能测试
514
+ 2. **错误处理**: 确保错误正确传播到事件流
515
+ 3. **内存**: 大量事件可能占用内存,需要考虑事件限流
516
+
517
+ ---
518
+
519
+ **创建日期**: 2025-10-25
520
+ **预计开始**: 2025-10-26
521
+ **预计完成**: 2025-10-27