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
@@ -1,23 +1,35 @@
1
+ """
2
+ Agent Executor with tt (Tail-Recursive) Control Loop
3
+
4
+ Core execution engine implementing recursive conversation management,
5
+ inspired by Claude Code's tt function design.
6
+ """
7
+
1
8
  from __future__ import annotations
2
9
 
3
10
  import asyncio
4
- from typing import AsyncGenerator, Dict, List, Optional
11
+ import json
12
+ import time
13
+ from pathlib import Path
14
+ from typing import AsyncGenerator, Dict, List, Optional, Any
5
15
  from uuid import uuid4
6
16
 
17
+ from loom.callbacks.base import BaseCallback
18
+ from loom.callbacks.metrics import MetricsCollector
19
+ from loom.core.context_assembly import ComponentPriority, ContextAssembler
20
+ from loom.core.events import AgentEvent, AgentEventType, ToolResult
21
+ from loom.core.execution_context import ExecutionContext
22
+ from loom.core.permissions import PermissionManager
7
23
  from loom.core.steering_control import SteeringControl
24
+ from loom.core.tool_orchestrator import ToolOrchestrator
8
25
  from loom.core.tool_pipeline import ToolExecutionPipeline
9
- from loom.core.types import Message, StreamEvent, ToolCall
10
- from loom.core.system_prompt import build_system_prompt
26
+ from loom.core.turn_state import TurnState
27
+ from loom.core.types import Message, ToolCall
11
28
  from loom.interfaces.compressor import BaseCompressor
12
29
  from loom.interfaces.llm import BaseLLM
13
30
  from loom.interfaces.memory import BaseMemory
14
31
  from loom.interfaces.tool import BaseTool
15
- from loom.core.permissions import PermissionManager
16
- from loom.callbacks.metrics import MetricsCollector
17
- import time
18
32
  from loom.utils.token_counter import count_messages_tokens
19
- from loom.callbacks.base import BaseCallback
20
- from loom.core.errors import ExecutionAbortedError
21
33
 
22
34
  # RAG support
23
35
  try:
@@ -25,9 +37,77 @@ try:
25
37
  except ImportError:
26
38
  ContextRetriever = None # type: ignore
27
39
 
40
+ # Unified coordination support
41
+ try:
42
+ from loom.core.unified_coordination import UnifiedExecutionContext, IntelligentCoordinator
43
+ except ImportError:
44
+ UnifiedExecutionContext = None # type: ignore
45
+ IntelligentCoordinator = None # type: ignore
46
+
47
+
48
+ class TaskHandler:
49
+ """
50
+ 任务处理器基类
51
+
52
+ 开发者可以继承此类来实现自定义的任务处理逻辑
53
+ """
54
+
55
+ def can_handle(self, task: str) -> bool:
56
+ """
57
+ 判断是否能处理给定的任务
58
+
59
+ Args:
60
+ task: 任务描述
61
+
62
+ Returns:
63
+ bool: 是否能处理此任务
64
+ """
65
+ raise NotImplementedError
66
+
67
+ def generate_guidance(
68
+ self,
69
+ original_task: str,
70
+ result_analysis: Dict[str, Any],
71
+ recursion_depth: int
72
+ ) -> str:
73
+ """
74
+ 生成递归指导消息
75
+
76
+ Args:
77
+ original_task: 原始任务
78
+ result_analysis: 工具结果分析
79
+ recursion_depth: 递归深度
80
+
81
+ Returns:
82
+ str: 生成的指导消息
83
+ """
84
+ raise NotImplementedError
85
+
28
86
 
29
87
  class AgentExecutor:
30
- """Agent 执行器:封装主循环,连接 LLM、内存、工具流水线与事件流。"""
88
+ """
89
+ Agent Executor with tt Recursive Control Loop.
90
+
91
+ Core Design:
92
+ - tt() is the only execution method (tail-recursive)
93
+ - All other methods are thin wrappers around tt()
94
+ - No iteration loops - only recursion
95
+ - Immutable state (TurnState)
96
+
97
+ Example:
98
+ ```python
99
+ executor = AgentExecutor(llm=llm, tools=tools)
100
+
101
+ # Initialize state
102
+ turn_state = TurnState.initial(max_iterations=10)
103
+ context = ExecutionContext.create()
104
+ messages = [Message(role="user", content="Hello")]
105
+
106
+ # Execute with tt recursion
107
+ async for event in executor.tt(messages, turn_state, context):
108
+ print(event)
109
+ ```
110
+ """
31
111
 
32
112
  def __init__(
33
113
  self,
@@ -35,7 +115,7 @@ class AgentExecutor:
35
115
  tools: Dict[str, BaseTool] | None = None,
36
116
  memory: BaseMemory | None = None,
37
117
  compressor: BaseCompressor | None = None,
38
- context_retriever: Optional["ContextRetriever"] = None, # 🆕 RAG support
118
+ context_retriever: Optional["ContextRetriever"] = None,
39
119
  steering_control: SteeringControl | None = None,
40
120
  max_iterations: int = 50,
41
121
  max_context_tokens: int = 16000,
@@ -43,325 +123,632 @@ class AgentExecutor:
43
123
  metrics: MetricsCollector | None = None,
44
124
  system_instructions: Optional[str] = None,
45
125
  callbacks: Optional[List[BaseCallback]] = None,
46
- enable_steering: bool = False, # 🆕 US1: Real-time steering
126
+ enable_steering: bool = False,
127
+ task_handlers: Optional[List[TaskHandler]] = None,
128
+ unified_context: Optional["UnifiedExecutionContext"] = None,
129
+ enable_unified_coordination: bool = True,
47
130
  ) -> None:
48
131
  self.llm = llm
49
132
  self.tools = tools or {}
50
133
  self.memory = memory
51
134
  self.compressor = compressor
52
- self.context_retriever = context_retriever # 🆕 RAG support
135
+ self.context_retriever = context_retriever
53
136
  self.steering_control = steering_control or SteeringControl()
54
137
  self.max_iterations = max_iterations
55
138
  self.max_context_tokens = max_context_tokens
56
139
  self.metrics = metrics or MetricsCollector()
57
- self.permission_manager = permission_manager or PermissionManager(policy={"default": "allow"})
140
+ self.permission_manager = permission_manager or PermissionManager(
141
+ policy={"default": "allow"}
142
+ )
58
143
  self.system_instructions = system_instructions
59
144
  self.callbacks = callbacks or []
60
- self.enable_steering = enable_steering # 🆕 US1
145
+ self.enable_steering = enable_steering
146
+ self.task_handlers = task_handlers or []
147
+
148
+ # Unified coordination
149
+ self.unified_context = unified_context
150
+ self.enable_unified_coordination = enable_unified_coordination
151
+
152
+ # Initialize unified coordination if enabled
153
+ if self.enable_unified_coordination and UnifiedExecutionContext and IntelligentCoordinator:
154
+ self._setup_unified_coordination()
155
+
156
+ # Tool execution (legacy pipeline for backward compatibility)
61
157
  self.tool_pipeline = ToolExecutionPipeline(
62
- self.tools, permission_manager=self.permission_manager, metrics=self.metrics
158
+ self.tools,
159
+ permission_manager=self.permission_manager,
160
+ metrics=self.metrics,
63
161
  )
64
162
 
65
- async def _emit(self, event_type: str, payload: Dict) -> None:
66
- if not self.callbacks:
67
- return
68
- enriched = dict(payload)
69
- enriched.setdefault("ts", time.time())
70
- enriched.setdefault("type", event_type)
71
- for cb in self.callbacks:
72
- try:
73
- await cb.on_event(event_type, enriched)
74
- except Exception:
75
- # best-effort; don't fail agent execution on callback errors
76
- pass
163
+ def _setup_unified_coordination(self):
164
+ """设置统一协调机制"""
165
+ if not self.unified_context:
166
+ # 创建默认的统一执行上下文
167
+ from loom.core.unified_coordination import CoordinationConfig
168
+ self.unified_context = UnifiedExecutionContext(
169
+ execution_id=f"exec_{int(time.time())}",
170
+ config=CoordinationConfig() # 使用默认配置
171
+ )
172
+
173
+ # 集成四大核心能力
174
+ self._integrate_core_capabilities()
175
+
176
+ # 创建智能协调器
177
+ self.coordinator = IntelligentCoordinator(self.unified_context)
178
+
179
+ # 设置跨组件引用
180
+ self._setup_cross_component_references()
181
+
182
+ def _integrate_core_capabilities(self):
183
+ """集成四大核心能力到统一上下文"""
184
+
185
+ config = self.unified_context.config
186
+
187
+ # 1. 集成 ContextAssembler
188
+ if not self.unified_context.context_assembler:
189
+ from loom.core.context_assembly import ContextAssembler
190
+ self.unified_context.context_assembler = ContextAssembler(
191
+ max_tokens=self.max_context_tokens,
192
+ enable_caching=True,
193
+ cache_size=config.context_cache_size
194
+ )
77
195
 
78
- async def execute(
196
+ # 2. 集成 TaskTool
197
+ if "task" in self.tools and not self.unified_context.task_tool:
198
+ task_tool = self.tools["task"]
199
+ # 使用配置更新 TaskTool
200
+ task_tool.pool_size = config.subagent_pool_size
201
+ task_tool.enable_pooling = True
202
+ self.unified_context.task_tool = task_tool
203
+
204
+ # 3. 集成 EventProcessor
205
+ if not self.unified_context.event_processor:
206
+ from loom.core.events import EventFilter, EventProcessor, AgentEventType
207
+
208
+ # 创建智能事件过滤器,使用配置值
209
+ llm_filter = EventFilter(
210
+ allowed_types=[
211
+ AgentEventType.LLM_DELTA,
212
+ AgentEventType.TOOL_RESULT,
213
+ AgentEventType.AGENT_FINISH
214
+ ],
215
+ enable_batching=True,
216
+ batch_size=config.event_batch_size,
217
+ batch_timeout=config.event_batch_timeout
218
+ )
219
+
220
+ self.unified_context.event_processor = EventProcessor(
221
+ filters=[llm_filter],
222
+ enable_stats=True
223
+ )
224
+
225
+ # 4. 集成 TaskHandlers
226
+ if not self.unified_context.task_handlers:
227
+ self.unified_context.task_handlers = self.task_handlers or []
228
+
229
+ def _setup_cross_component_references(self):
230
+ """
231
+ 设置跨组件引用(已简化)
232
+
233
+ 移除了魔法属性注入,改为通过协调器处理所有跨组件通信
234
+ """
235
+ pass # 跨组件通信现在通过 IntelligentCoordinator 处理
236
+
237
+ # Tool orchestration (Loom 2.0 - intelligent parallel/sequential execution)
238
+ self.tool_orchestrator = ToolOrchestrator(
239
+ tools=self.tools,
240
+ permission_manager=self.permission_manager,
241
+ max_parallel=5,
242
+ )
243
+
244
+ # ==========================================
245
+ # CORE METHOD: tt (Tail-Recursive Control Loop)
246
+ # ==========================================
247
+
248
+ async def tt(
79
249
  self,
80
- user_input: str,
81
- cancel_token: Optional[asyncio.Event] = None, # 🆕 US1: Cancellation support
82
- correlation_id: Optional[str] = None, # 🆕 US1: Request tracing
83
- ) -> str:
84
- """非流式执行,包含工具调用的 ReAct 循环(最小实现)。
250
+ messages: List[Message],
251
+ turn_state: TurnState,
252
+ context: ExecutionContext,
253
+ ) -> AsyncGenerator[AgentEvent, None]:
254
+ """
255
+ Tail-recursive control loop (inspired by Claude Code).
85
256
 
86
- Args:
87
- user_input: User query/instruction
88
- cancel_token: Optional Event to signal cancellation (US1)
89
- correlation_id: Optional correlation ID for request tracing (US1)
257
+ This is the ONLY core execution method. It processes one turn of the
258
+ conversation, then recursively calls itself if tools were used.
90
259
 
91
- Returns:
92
- Final agent response (or partial results if cancelled)
260
+ Recursion Flow:
261
+ tt(messages, state_0, ctx)
262
+ → LLM generates tool calls
263
+ → Execute tools
264
+ → tt(messages + tool_results, state_1, ctx) # Recursive call
265
+ → LLM generates final answer
266
+ → return (base case)
267
+
268
+ Base Cases (recursion terminates):
269
+ 1. LLM returns final answer (no tools)
270
+ 2. Maximum recursion depth reached
271
+ 3. Execution cancelled
272
+ 4. Error occurred
273
+
274
+ Args:
275
+ messages: New messages for this turn (not full history)
276
+ turn_state: Immutable turn state
277
+ context: Shared execution context
278
+
279
+ Yields:
280
+ AgentEvent: Events representing execution progress
281
+
282
+ Example:
283
+ ```python
284
+ # Initial turn
285
+ state = TurnState.initial(max_iterations=10)
286
+ context = ExecutionContext.create()
287
+ messages = [Message(role="user", content="Search files")]
288
+
289
+ async for event in executor.tt(messages, state, context):
290
+ if event.type == AgentEventType.AGENT_FINISH:
291
+ print(f"Done: {event.content}")
292
+ ```
93
293
  """
94
- # Generate correlation_id if not provided
95
- if correlation_id is None:
96
- correlation_id = str(uuid4())
97
-
98
- await self._emit("request_start", {
99
- "input": user_input,
100
- "source": "execute",
101
- "iteration": 0,
102
- "correlation_id": correlation_id, # 🆕 US1
103
- })
104
-
105
- # Check cancellation before starting
106
- if cancel_token and cancel_token.is_set():
107
- await self._emit("agent_finish", {
108
- "content": "Request cancelled before execution",
109
- "source": "execute",
110
- "correlation_id": correlation_id,
111
- })
112
- return "Request cancelled before execution"
294
+ # ==========================================
295
+ # Phase 0: Recursion Control
296
+ # ==========================================
297
+ yield AgentEvent(
298
+ type=AgentEventType.ITERATION_START,
299
+ iteration=turn_state.turn_counter,
300
+ turn_id=turn_state.turn_id,
301
+ metadata={"parent_turn_id": turn_state.parent_turn_id},
302
+ )
303
+
304
+ # Base case 1: Maximum recursion depth reached
305
+ if turn_state.is_final:
306
+ yield AgentEvent(
307
+ type=AgentEventType.MAX_ITERATIONS_REACHED,
308
+ metadata={
309
+ "turn_counter": turn_state.turn_counter,
310
+ "max_iterations": turn_state.max_iterations,
311
+ },
312
+ )
313
+ await self._emit(
314
+ "max_iterations_reached",
315
+ {
316
+ "turn_counter": turn_state.turn_counter,
317
+ "max_iterations": turn_state.max_iterations,
318
+ },
319
+ )
320
+ return
321
+
322
+ # Base case 2: Execution cancelled
323
+ if context.is_cancelled():
324
+ yield AgentEvent(
325
+ type=AgentEventType.EXECUTION_CANCELLED,
326
+ metadata={"correlation_id": context.correlation_id},
327
+ )
328
+ await self._emit(
329
+ "execution_cancelled",
330
+ {"correlation_id": context.correlation_id},
331
+ )
332
+ return
333
+
334
+ # ==========================================
335
+ # Phase 1: Context Assembly
336
+ # ==========================================
337
+ yield AgentEvent.phase_start("context_assembly")
113
338
 
339
+ # Load conversation history from memory
114
340
  history = await self._load_history()
115
341
 
116
- # 🆕 Step 1: RAG - 自动检索相关文档(如果配置了 context_retriever)
117
- retrieved_docs = []
342
+ # RAG retrieval (if configured)
343
+ rag_context = None
118
344
  if self.context_retriever:
119
- retrieved_docs = await self.context_retriever.retrieve_for_query(user_input)
120
- if retrieved_docs:
121
- # 注入检索到的文档上下文
122
- if self.context_retriever.inject_as == "system":
123
- doc_context = self.context_retriever.format_documents(retrieved_docs)
124
- history.append(Message(
125
- role="system",
126
- content=doc_context,
127
- metadata={"type": "retrieved_context", "doc_count": len(retrieved_docs)}
128
- ))
129
- # 记录检索指标
130
- self.metrics.metrics.retrievals = getattr(self.metrics.metrics, "retrievals", 0) + 1
131
- await self._emit("retrieval_complete", {"doc_count": len(retrieved_docs), "source": "execute"})
132
-
133
- # Step 2: 添加用户消息
134
- history.append(Message(role="user", content=user_input))
135
-
136
- # Step 3: 压缩检查
137
- history = await self._maybe_compress(history)
138
-
139
- # Step 4: 动态生成系统提示
140
- context = {"retrieved_docs_count": len(retrieved_docs)} if retrieved_docs else None
141
- system_prompt = build_system_prompt(self.tools, self.system_instructions, context)
142
- history = self._inject_system_prompt(history, system_prompt)
143
-
144
- if not self.llm.supports_tools or not self.tools:
145
- try:
146
- # Create LLM task that can be cancelled
147
- llm_task = asyncio.create_task(self.llm.generate([m.__dict__ for m in history]))
148
-
149
- # Poll for cancellation while waiting for LLM
150
- while not llm_task.done():
151
- if cancel_token and cancel_token.is_set():
152
- llm_task.cancel()
153
- try:
154
- await llm_task
155
- except asyncio.CancelledError:
156
- pass
157
- partial_result = "Execution interrupted during LLM call"
158
- await self._emit("agent_finish", {
159
- "content": partial_result,
160
- "source": "execute",
161
- "correlation_id": correlation_id,
162
- "interrupted": True,
163
- })
164
- return partial_result
165
- await asyncio.sleep(0.1) # Check every 100ms
166
-
167
- text = await llm_task
168
- except asyncio.CancelledError:
169
- partial_result = "Execution interrupted during LLM call"
170
- await self._emit("agent_finish", {
171
- "content": partial_result,
172
- "source": "execute",
173
- "correlation_id": correlation_id,
174
- "interrupted": True,
175
- })
176
- return partial_result
177
- except Exception as e:
178
- self.metrics.metrics.total_errors += 1
179
- await self._emit("error", {"stage": "llm_generate", "message": str(e)})
180
- raise
181
- self.metrics.metrics.llm_calls += 1
182
- if self.memory:
183
- await self.memory.add_message(Message(role="assistant", content=text))
184
- await self._emit("agent_finish", {"content": text, "source": "execute"})
185
- return text
186
-
187
- tools_spec = self._serialize_tools()
188
- iterations = 0
189
- final_text = ""
190
- while iterations < self.max_iterations:
191
- # 🆕 US1: Check cancellation before each iteration
192
- if cancel_token and cancel_token.is_set():
193
- partial_result = f"Execution interrupted after {iterations} iterations. Partial progress: {final_text or '(in progress)'}"
194
- await self._emit("agent_finish", {
195
- "content": partial_result,
196
- "source": "execute",
197
- "correlation_id": correlation_id,
198
- "interrupted": True,
199
- "iterations_completed": iterations,
200
- })
201
- return partial_result
345
+ yield AgentEvent(type=AgentEventType.RETRIEVAL_START)
202
346
 
203
347
  try:
204
- resp = await self.llm.generate_with_tools([m.__dict__ for m in history], tools_spec)
348
+ # Extract user query from last message
349
+ user_query = ""
350
+ for msg in reversed(messages):
351
+ if msg.role == "user":
352
+ user_query = msg.content
353
+ break
354
+
355
+ if user_query:
356
+ retrieved_docs = await self.context_retriever.retrieve_for_query(
357
+ user_query
358
+ )
359
+
360
+ if retrieved_docs:
361
+ rag_context = self.context_retriever.format_documents(
362
+ retrieved_docs
363
+ )
364
+
365
+ # Emit retrieval progress
366
+ for doc in retrieved_docs:
367
+ yield AgentEvent(
368
+ type=AgentEventType.RETRIEVAL_PROGRESS,
369
+ metadata={
370
+ "doc_title": doc.metadata.get("title", "Unknown"),
371
+ "relevance_score": doc.metadata.get("score", 0.0),
372
+ },
373
+ )
374
+
375
+ yield AgentEvent(
376
+ type=AgentEventType.RETRIEVAL_COMPLETE,
377
+ metadata={"doc_count": len(retrieved_docs)},
378
+ )
379
+ self.metrics.metrics.retrievals = (
380
+ getattr(self.metrics.metrics, "retrievals", 0) + 1
381
+ )
382
+
205
383
  except Exception as e:
206
- self.metrics.metrics.total_errors += 1
207
- await self._emit("error", {
208
- "stage": "llm_generate_with_tools",
209
- "message": str(e),
210
- "source": "execute",
211
- "iteration": iterations,
212
- "correlation_id": correlation_id, # 🆕 US1
213
- })
214
- raise
215
- self.metrics.metrics.llm_calls += 1
216
- tool_calls = resp.get("tool_calls") or []
217
- content = resp.get("content") or ""
218
-
219
- if tool_calls:
220
- # 广播工具调用开始(非流式路径)
221
- try:
222
- meta = [
223
- {"id": str(tc.get("id", "")), "name": str(tc.get("name", ""))}
224
- for tc in tool_calls
225
- ]
226
- await self._emit("tool_calls_start", {"tool_calls": meta, "source": "execute", "iteration": iterations})
227
- except Exception:
228
- pass
229
- # 执行工具并把结果写回消息
230
- try:
231
- for tr in await self._execute_tool_batch(tool_calls):
232
- tool_msg = Message(role="tool", content=tr.content, tool_call_id=tr.tool_call_id)
233
- history.append(tool_msg)
234
- if self.memory:
235
- await self.memory.add_message(tool_msg)
236
- await self._emit("tool_result", {"tool_call_id": tr.tool_call_id, "content": tr.content, "source": "execute", "iteration": iterations})
237
- except Exception as e:
238
- self.metrics.metrics.total_errors += 1
239
- await self._emit("error", {"stage": "tool_execute", "message": str(e), "source": "execute", "iteration": iterations})
240
- raise
241
- iterations += 1
242
- self.metrics.metrics.total_iterations += 1
243
- history = await self._maybe_compress(history)
244
- continue
245
-
246
- # 无工具调用:认为生成最终答案
247
- final_text = content
248
- if self.memory:
249
- await self.memory.add_message(Message(role="assistant", content=final_text))
250
- await self._emit("agent_finish", {
251
- "content": final_text,
252
- "source": "execute",
253
- "correlation_id": correlation_id, # 🆕 US1
254
- })
255
- break
256
-
257
- return final_text
258
-
259
- async def stream(self, user_input: str) -> AsyncGenerator[StreamEvent, None]:
260
- """流式执行:输出 text_delta/agent_finish 事件。后续可接入 tool_calls。"""
261
- yield StreamEvent(type="request_start")
262
- await self._emit("request_start", {"input": user_input, "source": "stream", "iteration": 0})
263
- history = await self._load_history()
384
+ yield AgentEvent.error(e, retrieval_failed=True)
385
+
386
+ # Add new messages to history
387
+ history.extend(messages)
388
+
389
+ # Compression check
390
+ old_len = len(history)
391
+ history_compacted = await self._maybe_compress(history)
392
+ compacted_this_turn = len(history_compacted) < old_len
393
+
394
+ if compacted_this_turn:
395
+ history = history_compacted
396
+ yield AgentEvent(
397
+ type=AgentEventType.COMPRESSION_APPLIED,
398
+ metadata={
399
+ "messages_before": old_len,
400
+ "messages_after": len(history),
401
+ },
402
+ )
264
403
 
265
- # 🆕 RAG - 自动检索文档
266
- retrieved_docs = []
267
- if self.context_retriever:
268
- retrieved_docs = await self.context_retriever.retrieve_for_query(user_input)
269
- if retrieved_docs:
270
- if self.context_retriever.inject_as == "system":
271
- doc_context = self.context_retriever.format_documents(retrieved_docs)
272
- history.append(Message(
273
- role="system",
274
- content=doc_context,
275
- metadata={"type": "retrieved_context", "doc_count": len(retrieved_docs)}
276
- ))
277
- self.metrics.metrics.retrievals = getattr(self.metrics.metrics, "retrievals", 0) + 1
278
- # 🆕 广播检索事件
279
- yield StreamEvent(type="retrieval_complete", metadata={"doc_count": len(retrieved_docs)})
280
- await self._emit("retrieval_complete", {"doc_count": len(retrieved_docs), "source": "stream"})
281
-
282
- history.append(Message(role="user", content=user_input))
283
-
284
- # 压缩检查
285
- compressed = await self._maybe_compress(history)
286
- if compressed is not history:
287
- history = compressed
288
- yield StreamEvent(type="compression_applied")
289
-
290
- # 动态生成系统提示
291
- context = {"retrieved_docs_count": len(retrieved_docs)} if retrieved_docs else None
292
- system_prompt = build_system_prompt(self.tools, self.system_instructions, context)
293
- history = self._inject_system_prompt(history, system_prompt)
294
-
295
- if not self.llm.supports_tools or not self.tools:
296
- try:
404
+ # 使用统一协调的智能上下文组装
405
+ if self.enable_unified_coordination and hasattr(self, 'coordinator'):
406
+ # 使用智能协调器进行上下文组装
407
+ execution_plan = self.coordinator.coordinate_tt_recursion(
408
+ messages, turn_state, context
409
+ )
410
+ final_system_prompt = execution_plan.get("context", "")
411
+ # 使用统一协调器的 assembler
412
+ assembler = self.unified_context.context_assembler
413
+ else:
414
+ # 传统方式组装系统提示
415
+ assembler = ContextAssembler(max_tokens=self.max_context_tokens)
416
+
417
+ # Add base instructions (critical priority)
418
+ if self.system_instructions:
419
+ assembler.add_component(
420
+ name="base_instructions",
421
+ content=self.system_instructions,
422
+ priority=ComponentPriority.CRITICAL,
423
+ truncatable=False,
424
+ )
425
+
426
+ # Add RAG context (high priority)
427
+ if rag_context:
428
+ assembler.add_component(
429
+ name="retrieved_context",
430
+ content=rag_context,
431
+ priority=ComponentPriority.HIGH,
432
+ truncatable=True,
433
+ )
434
+
435
+ # Add tool definitions (medium priority)
436
+ if self.tools:
437
+ tools_spec = self._serialize_tools()
438
+ tools_prompt = f"Available tools:\n{json.dumps(tools_spec, indent=2)}"
439
+ assembler.add_component(
440
+ name="tool_definitions",
441
+ content=tools_prompt,
442
+ priority=ComponentPriority.MEDIUM,
443
+ truncatable=False,
444
+ )
445
+
446
+ # Assemble final system prompt
447
+ final_system_prompt = assembler.assemble()
448
+
449
+ # Inject system prompt into history
450
+ if history and history[0].role == "system":
451
+ history[0] = Message(role="system", content=final_system_prompt)
452
+ else:
453
+ history.insert(0, Message(role="system", content=final_system_prompt))
454
+
455
+ # Emit context assembly summary
456
+ summary = assembler.get_summary()
457
+ yield AgentEvent.phase_end(
458
+ "context_assembly",
459
+ tokens_used=summary["total_tokens"],
460
+ metadata={
461
+ "components": len(summary["components"]),
462
+ "utilization": summary["utilization"],
463
+ },
464
+ )
465
+
466
+ # ==========================================
467
+ # Phase 2: LLM Call
468
+ # ==========================================
469
+ yield AgentEvent(type=AgentEventType.LLM_START)
470
+
471
+ try:
472
+ if self.llm.supports_tools and self.tools:
473
+ # LLM with tool support
474
+ tools_spec = self._serialize_tools()
475
+ response = await self.llm.generate_with_tools(
476
+ [m.__dict__ for m in history], tools_spec
477
+ )
478
+
479
+ content = response.get("content", "")
480
+ tool_calls = response.get("tool_calls", [])
481
+
482
+ # Emit LLM content if available
483
+ if content:
484
+ yield AgentEvent(type=AgentEventType.LLM_DELTA, content=content)
485
+
486
+ else:
487
+ # Simple LLM generation (streaming)
488
+ content_parts = []
297
489
  async for delta in self.llm.stream([m.__dict__ for m in history]):
298
- yield StreamEvent(type="text_delta", content=delta)
299
- except Exception as e:
300
- self.metrics.metrics.total_errors += 1
301
- await self._emit("error", {"stage": "llm_stream", "message": str(e), "source": "stream"})
302
- raise
303
- yield StreamEvent(type="agent_finish")
490
+ content_parts.append(delta)
491
+ yield AgentEvent(type=AgentEventType.LLM_DELTA, content=delta)
492
+
493
+ content = "".join(content_parts)
494
+ tool_calls = []
495
+
496
+ yield AgentEvent(type=AgentEventType.LLM_COMPLETE)
497
+
498
+ except Exception as e:
499
+ self.metrics.metrics.total_errors += 1
500
+ yield AgentEvent.error(e, llm_failed=True)
501
+ await self._emit("error", {"stage": "llm_call", "message": str(e)})
304
502
  return
305
503
 
306
- tools_spec = self._serialize_tools()
307
- iterations = 0
308
- while iterations < self.max_iterations:
309
- try:
310
- resp = await self.llm.generate_with_tools([m.__dict__ for m in history], tools_spec)
311
- except Exception as e:
312
- self.metrics.metrics.total_errors += 1
313
- await self._emit("error", {"stage": "llm_generate_with_tools", "message": str(e), "source": "stream", "iteration": iterations})
314
- raise
315
- self.metrics.metrics.llm_calls += 1
316
- tool_calls = resp.get("tool_calls") or []
317
- content = resp.get("content") or ""
318
-
319
- if tool_calls:
320
- # 广播工具调用开始
321
- tc_models = [self._to_tool_call(tc) for tc in tool_calls]
322
- yield StreamEvent(type="tool_calls_start", tool_calls=tc_models)
323
- await self._emit(
324
- "tool_calls_start",
325
- {"tool_calls": [{"id": t.id, "name": t.name} for t in tc_models], "source": "stream", "iteration": iterations},
504
+ self.metrics.metrics.llm_calls += 1
505
+
506
+ # ==========================================
507
+ # Phase 3: Decision Point (Base Case or Recurse)
508
+ # ==========================================
509
+
510
+ if not tool_calls:
511
+ # Base case: No tools Conversation complete
512
+ yield AgentEvent(
513
+ type=AgentEventType.AGENT_FINISH,
514
+ content=content,
515
+ metadata={
516
+ "turn_counter": turn_state.turn_counter,
517
+ "total_llm_calls": self.metrics.metrics.llm_calls,
518
+ },
519
+ )
520
+
521
+ # Save to memory
522
+ if self.memory and content:
523
+ await self.memory.add_message(
524
+ Message(role="assistant", content=content)
326
525
  )
327
- # 执行工具
328
- try:
329
- async for tr in self._execute_tool_calls_async(tc_models):
330
- yield StreamEvent(type="tool_result", result=tr)
331
- await self._emit("tool_result", {"tool_call_id": tr.tool_call_id, "content": tr.content, "source": "stream", "iteration": iterations})
332
- tool_msg = Message(role="tool", content=tr.content, tool_call_id=tr.tool_call_id)
333
- history.append(tool_msg)
334
- if self.memory:
335
- await self.memory.add_message(tool_msg)
336
- except Exception as e:
337
- self.metrics.metrics.total_errors += 1
338
- await self._emit("error", {"stage": "tool_execute", "message": str(e), "source": "stream", "iteration": iterations})
339
- raise
340
- iterations += 1
341
- self.metrics.metrics.total_iterations += 1
342
- # 每轮结束后做压缩检查
343
- history = await self._maybe_compress(history)
344
- continue
345
-
346
- # 无工具调用:输出最终文本并结束
347
- if content:
348
- yield StreamEvent(type="text_delta", content=content)
349
- yield StreamEvent(type="agent_finish")
526
+
350
527
  await self._emit("agent_finish", {"content": content})
351
- if self.memory and content:
352
- await self.memory.add_message(Message(role="assistant", content=content))
353
- break
528
+ return
529
+
530
+ # ==========================================
531
+ # Phase 4: Tool Execution
532
+ # ==========================================
533
+ yield AgentEvent(
534
+ type=AgentEventType.LLM_TOOL_CALLS,
535
+ metadata={
536
+ "tool_count": len(tool_calls),
537
+ "tool_names": [tc.get("name") for tc in tool_calls],
538
+ },
539
+ )
540
+
541
+ # Convert to ToolCall models
542
+ tc_models = [self._to_tool_call(tc) for tc in tool_calls]
543
+
544
+ # Execute tools using ToolOrchestrator
545
+ tool_results: List[ToolResult] = []
546
+ try:
547
+ async for event in self.tool_orchestrator.execute_batch(tc_models):
548
+ yield event # Forward all tool events
549
+
550
+ if event.type == AgentEventType.TOOL_RESULT:
551
+ tool_results.append(event.tool_result)
552
+
553
+ # Add to memory
554
+ tool_msg = Message(
555
+ role="tool",
556
+ content=event.tool_result.content,
557
+ tool_call_id=event.tool_result.tool_call_id,
558
+ )
559
+ if self.memory:
560
+ await self.memory.add_message(tool_msg)
561
+
562
+ elif event.type == AgentEventType.TOOL_ERROR:
563
+ # Collect error results too
564
+ if event.tool_result:
565
+ tool_results.append(event.tool_result)
566
+
567
+ except Exception as e:
568
+ self.metrics.metrics.total_errors += 1
569
+ yield AgentEvent.error(e, tool_execution_failed=True)
570
+ await self._emit("error", {"stage": "tool_execution", "message": str(e)})
571
+ return
572
+
573
+ yield AgentEvent(
574
+ type=AgentEventType.TOOL_CALLS_COMPLETE,
575
+ metadata={"results_count": len(tool_results)},
576
+ )
577
+
578
+ self.metrics.metrics.total_iterations += 1
579
+
580
+ # ==========================================
581
+ # Phase 5: Recursive Call (Tail Recursion)
582
+ # ==========================================
583
+
584
+ # Prepare next turn state
585
+ next_state = turn_state.next_turn(compacted=compacted_this_turn)
586
+
587
+ # Prepare next turn messages with intelligent context guidance
588
+ next_messages = self._prepare_recursive_messages(
589
+ messages, tool_results, turn_state, context
590
+ )
591
+
592
+ # Add tool results
593
+ for r in tool_results:
594
+ next_messages.append(
595
+ Message(
596
+ role="tool",
597
+ content=r.content,
598
+ tool_call_id=r.tool_call_id,
599
+ )
600
+ )
601
+
602
+ # Emit recursion event
603
+ yield AgentEvent(
604
+ type=AgentEventType.RECURSION,
605
+ metadata={
606
+ "from_turn": turn_state.turn_id,
607
+ "to_turn": next_state.turn_id,
608
+ "depth": next_state.turn_counter,
609
+ },
610
+ )
611
+
612
+ # 🔥 Tail-recursive call
613
+ async for event in self.tt(next_messages, next_state, context):
614
+ yield event
615
+
616
+ # ==========================================
617
+ # Intelligent Recursion Methods
618
+ # ==========================================
619
+
620
+ def _prepare_recursive_messages(
621
+ self,
622
+ messages: List[Message],
623
+ tool_results: List[ToolResult],
624
+ turn_state: TurnState,
625
+ context: ExecutionContext,
626
+ ) -> List[Message]:
627
+ """
628
+ 智能准备递归调用的消息
629
+
630
+ 基于工具结果类型、任务上下文和递归深度,生成合适的用户指导消息
631
+ """
632
+ # 分析工具结果
633
+ result_analysis = self._analyze_tool_results(tool_results)
634
+
635
+ # 获取原始任务
636
+ original_task = self._extract_original_task(messages)
637
+
638
+ # 生成智能指导消息
639
+ guidance_message = self._generate_recursion_guidance(
640
+ original_task, result_analysis, turn_state.turn_counter
641
+ )
642
+
643
+ return [Message(role="user", content=guidance_message)]
644
+
645
+ def _analyze_tool_results(self, tool_results: List[ToolResult]) -> Dict[str, Any]:
646
+ """分析工具结果类型和质量"""
647
+ analysis = {
648
+ "has_data": False,
649
+ "has_errors": False,
650
+ "suggests_completion": False,
651
+ "result_types": [],
652
+ "completeness_score": 0.0
653
+ }
654
+
655
+ for result in tool_results:
656
+ content = result.content.lower()
657
+
658
+ # 检查数据类型
659
+ if any(keyword in content for keyword in ["data", "found", "retrieved", "table", "schema", "获取到", "表结构", "结构"]):
660
+ analysis["has_data"] = True
661
+ analysis["result_types"].append("data")
662
+ analysis["completeness_score"] += 0.3
663
+
664
+ # 检查错误
665
+ if any(keyword in content for keyword in ["error", "failed", "exception", "not found"]):
666
+ analysis["has_errors"] = True
667
+ analysis["result_types"].append("error")
668
+
669
+ # 检查完成建议
670
+ if any(keyword in content for keyword in ["complete", "finished", "done", "ready"]):
671
+ analysis["suggests_completion"] = True
672
+ analysis["result_types"].append("completion")
673
+ analysis["completeness_score"] += 0.5
674
+
675
+ # 检查分析结果
676
+ if any(keyword in content for keyword in ["analysis", "summary", "conclusion", "insights"]):
677
+ analysis["result_types"].append("analysis")
678
+ analysis["completeness_score"] += 0.4
679
+
680
+ analysis["completeness_score"] = min(analysis["completeness_score"], 1.0)
681
+ return analysis
682
+
683
+ def _extract_original_task(self, messages: List[Message]) -> str:
684
+ """从消息历史中提取原始任务"""
685
+ # 查找第一个用户消息作为原始任务
686
+ for message in messages:
687
+ if message.role == "user" and message.content:
688
+ # 过滤掉系统生成的递归消息
689
+ if not any(keyword in message.content.lower() for keyword in [
690
+ "工具调用已完成", "请基于工具返回的结果", "不要继续调用工具"
691
+ ]):
692
+ return message.content
693
+ return "处理用户请求"
694
+
695
+ def _generate_recursion_guidance(
696
+ self,
697
+ original_task: str,
698
+ result_analysis: Dict[str, Any],
699
+ recursion_depth: int
700
+ ) -> str:
701
+ """生成递归指导消息"""
702
+
703
+ # 使用可扩展的任务处理器
704
+ if hasattr(self, 'task_handlers') and self.task_handlers:
705
+ for handler in self.task_handlers:
706
+ if handler.can_handle(original_task):
707
+ return handler.generate_guidance(original_task, result_analysis, recursion_depth)
708
+
709
+ # 默认处理
710
+ return self._generate_default_guidance(original_task, result_analysis, recursion_depth)
711
+
712
+
713
+ def _generate_default_guidance(
714
+ self,
715
+ original_task: str,
716
+ result_analysis: Dict[str, Any],
717
+ recursion_depth: int
718
+ ) -> str:
719
+ """生成默认的递归指导"""
720
+
721
+ if result_analysis["suggests_completion"] or recursion_depth >= 6:
722
+ return f"""工具调用已完成。请基于返回的结果完成任务:{original_task}
723
+
724
+ 请提供完整、准确的最终答案。"""
725
+
726
+ elif result_analysis["has_errors"]:
727
+ return f"""工具执行遇到问题。请重新尝试完成任务:{original_task}
728
+
729
+ 建议:
730
+ - 检查工具参数是否正确
731
+ - 尝试使用不同的工具或方法
732
+ - 如果问题持续,请说明具体错误"""
733
+
734
+ else:
735
+ return f"""继续处理任务:{original_task}
736
+
737
+ 当前进度:{result_analysis['completeness_score']:.0%}
738
+ 建议:使用更多工具收集信息或分析已获得的结果"""
739
+
740
+ # ==========================================
741
+ # Helper Methods
742
+ # ==========================================
354
743
 
355
744
  async def _load_history(self) -> List[Message]:
745
+ """Load conversation history from memory."""
356
746
  if not self.memory:
357
747
  return []
358
748
  return await self.memory.get_messages()
359
749
 
360
750
  async def _maybe_compress(self, history: List[Message]) -> List[Message]:
361
- """Check if compression needed and apply if threshold reached.
362
-
363
- US2: Automatic compression at 92% threshold with 8-segment summarization.
364
- """
751
+ """Check if compression needed and apply if threshold reached."""
365
752
  if not self.compressor:
366
753
  return history
367
754
 
@@ -369,16 +756,19 @@ class AgentExecutor:
369
756
 
370
757
  # Check if compression should be triggered (92% threshold)
371
758
  if self.compressor.should_compress(tokens_before, self.max_context_tokens):
372
- # Attempt compression
373
759
  try:
374
760
  compressed_messages, metadata = await self.compressor.compress(history)
375
761
 
376
762
  # Update metrics
377
- self.metrics.metrics.compressions = getattr(self.metrics.metrics, "compressions", 0) + 1
763
+ self.metrics.metrics.compressions = (
764
+ getattr(self.metrics.metrics, "compressions", 0) + 1
765
+ )
378
766
  if metadata.key_topics == ["fallback"]:
379
- self.metrics.metrics.compression_fallbacks = getattr(self.metrics.metrics, "compression_fallbacks", 0) + 1
767
+ self.metrics.metrics.compression_fallbacks = (
768
+ getattr(self.metrics.metrics, "compression_fallbacks", 0) + 1
769
+ )
380
770
 
381
- # Emit compression event with metadata
771
+ # Emit compression event
382
772
  await self._emit(
383
773
  "compression_applied",
384
774
  {
@@ -395,17 +785,17 @@ class AgentExecutor:
395
785
  return compressed_messages
396
786
 
397
787
  except Exception as e:
398
- # Compression failed - continue without compression
399
788
  self.metrics.metrics.total_errors += 1
400
- await self._emit("error", {
401
- "stage": "compression",
402
- "message": str(e),
403
- })
789
+ await self._emit(
790
+ "error",
791
+ {"stage": "compression", "message": str(e)},
792
+ )
404
793
  return history
405
794
 
406
795
  return history
407
796
 
408
797
  def _serialize_tools(self) -> List[Dict]:
798
+ """Serialize tools to LLM-compatible format."""
409
799
  tools_spec: List[Dict] = []
410
800
  for t in self.tools.values():
411
801
  schema = {}
@@ -413,6 +803,7 @@ class AgentExecutor:
413
803
  schema = t.args_schema.model_json_schema() # type: ignore[attr-defined]
414
804
  except Exception:
415
805
  schema = {"type": "object", "properties": {}}
806
+
416
807
  tools_spec.append(
417
808
  {
418
809
  "type": "function",
@@ -426,25 +817,90 @@ class AgentExecutor:
426
817
  return tools_spec
427
818
 
428
819
  def _to_tool_call(self, raw: Dict) -> ToolCall:
429
- # 允许 Rule/Mock LLM 输出简单 dict
430
- return ToolCall(id=str(raw.get("id", "call_0")), name=raw["name"], arguments=raw.get("arguments", {}))
431
-
432
- async def _execute_tool_batch(self, tool_calls_raw: List[Dict]) -> List[ToolResult]:
433
- tc_models = [self._to_tool_call(tc) for tc in tool_calls_raw]
434
- results: List[ToolResult] = []
435
- async for tr in self._execute_tool_calls_async(tc_models):
436
- results.append(tr)
437
- return results
438
-
439
- async def _execute_tool_calls_async(self, tool_calls: List[ToolCall]):
440
- async for tr in self.tool_pipeline.execute_calls(tool_calls):
441
- yield tr
442
-
443
- def _inject_system_prompt(self, history: List[Message], system_prompt: str) -> List[Message]:
444
- """注入或更新系统提示消息"""
445
- # 如果第一条是系统消息,则替换;否则在开头插入
446
- if history and history[0].role == "system":
447
- history[0] = Message(role="system", content=system_prompt)
448
- else:
449
- history.insert(0, Message(role="system", content=system_prompt))
450
- return history
820
+ """Convert raw dict to ToolCall model."""
821
+ return ToolCall(
822
+ id=str(raw.get("id", "call_0")),
823
+ name=raw["name"],
824
+ arguments=raw.get("arguments", {}),
825
+ )
826
+
827
+ async def _emit(self, event_type: str, payload: Dict) -> None:
828
+ """Emit event to callbacks."""
829
+ if not self.callbacks:
830
+ return
831
+
832
+ enriched = dict(payload)
833
+ enriched.setdefault("ts", time.time())
834
+ enriched.setdefault("type", event_type)
835
+
836
+ for cb in self.callbacks:
837
+ try:
838
+ await cb.on_event(event_type, enriched)
839
+ except Exception:
840
+ # Best-effort; don't fail execution on callback errors
841
+ pass
842
+
843
+ # ==========================================
844
+ # Backward Compatibility Wrappers
845
+ # ==========================================
846
+
847
+ async def execute(
848
+ self,
849
+ user_input: str,
850
+ cancel_token: Optional[asyncio.Event] = None,
851
+ correlation_id: Optional[str] = None,
852
+ ) -> str:
853
+ """
854
+ Execute agent and return final response (backward compatible wrapper).
855
+
856
+ This method wraps the new tt() recursive API and extracts the final
857
+ response for backward compatibility with existing code.
858
+
859
+ Args:
860
+ user_input: User input text
861
+ cancel_token: Optional cancellation event
862
+ correlation_id: Optional correlation ID for tracing
863
+
864
+ Returns:
865
+ str: Final response text
866
+
867
+ Example:
868
+ ```python
869
+ executor = AgentExecutor(llm=llm, tools=tools)
870
+ response = await executor.execute("Hello")
871
+ print(response)
872
+ ```
873
+ """
874
+ # Initialize state and context
875
+ turn_state = TurnState.initial(max_iterations=self.max_iterations)
876
+ context = ExecutionContext.create(
877
+ correlation_id=correlation_id,
878
+ cancel_token=cancel_token,
879
+ )
880
+ messages = [Message(role="user", content=user_input)]
881
+
882
+ # Execute with tt and collect result
883
+ final_content = ""
884
+ async for event in self.tt(messages, turn_state, context):
885
+ # Accumulate LLM deltas
886
+ if event.type == AgentEventType.LLM_DELTA:
887
+ final_content += event.content or ""
888
+
889
+ # Return on finish
890
+ elif event.type == AgentEventType.AGENT_FINISH:
891
+ return event.content or final_content
892
+
893
+ # Handle cancellation
894
+ elif event.type == AgentEventType.EXECUTION_CANCELLED:
895
+ return "cancelled"
896
+
897
+ # Handle max iterations
898
+ elif event.type == AgentEventType.MAX_ITERATIONS_REACHED:
899
+ return final_content or "Max iterations reached"
900
+
901
+ # Raise on error
902
+ elif event.type == AgentEventType.ERROR:
903
+ if event.error:
904
+ raise event.error
905
+
906
+ return final_content