aury-agent 0.0.4__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.
Files changed (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,726 @@
1
+ """Delegate tool - delegate tasks to sub-agents.
2
+
3
+ Uses SubAgentBackend to retrieve available agents.
4
+ LLM specifies agent key and task data.
5
+ Mode is determined by agent config, not LLM.
6
+
7
+ Supports parallel execution of multiple sub-agents.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from datetime import datetime
13
+ from typing import Any, TYPE_CHECKING
14
+
15
+ from ...core.logging import tool_logger as logger
16
+ from ...core.types.tool import BaseTool, ToolContext, ToolResult
17
+ from ...core.types.session import generate_id, ControlFrame
18
+ from ...core.types.subagent import SubAgentMode, SubAgentResult, SubAgentMetadata
19
+ from ...core.types.block import BlockEvent, BlockKind, BlockOp
20
+ from ...core.parallel import ParallelSubAgentContext
21
+
22
+ if TYPE_CHECKING:
23
+ from ...backends.subagent import SubAgentBackend, AgentConfig
24
+ from ...middleware import MiddlewareChain
25
+
26
+
27
+ class DelegateTool(BaseTool):
28
+ """Delegate tasks to sub-agents.
29
+
30
+ Uses SubAgentBackend to retrieve available agents.
31
+ The execution mode (embedded/delegated) is determined by agent config.
32
+ """
33
+
34
+ _name = "delegate"
35
+
36
+ def __init__(
37
+ self,
38
+ subagent_backend: "SubAgentBackend",
39
+ middleware: "MiddlewareChain | None" = None,
40
+ ):
41
+ """Initialize with SubAgentBackend.
42
+
43
+ Args:
44
+ subagent_backend: Backend for retrieving sub-agents
45
+ middleware: Optional middleware chain for progressive disclosure
46
+ """
47
+ self.backend = subagent_backend
48
+ self.middleware = middleware
49
+ self._description_cache: str | None = None
50
+ self._parameters_cache: dict | None = None
51
+ self._active_block_ids: dict[str, str] = {} # session_id:agent -> block_id
52
+ self._dynamic_agents: dict[str, "AgentConfig"] = {} # Dynamic agents from middleware
53
+
54
+ @property
55
+ def name(self) -> str:
56
+ return self._name
57
+
58
+ def _get_agents_sync(self) -> list["AgentConfig"]:
59
+ """Synchronously get agent list for property getters."""
60
+ if hasattr(self.backend, 'list_sync'):
61
+ return self.backend.list_sync()
62
+ return []
63
+
64
+ @property
65
+ def description(self) -> str:
66
+ """Build description with available agents."""
67
+ agents = self._get_agents_sync()
68
+ if not agents:
69
+ return "Delegate a task to a specialized sub-agent. No agents currently available."
70
+
71
+ agent_list = "\n".join([
72
+ f" - {a.key}: {a.description or 'No description'}"
73
+ for a in agents
74
+ ])
75
+ return f"""Delegate a task to a specialized sub-agent.
76
+
77
+ Available agents:
78
+ {agent_list}
79
+
80
+ Provide task_context (user intent, background, requirements) and artifact_refs (related materials) for the sub-agent."""
81
+
82
+ @property
83
+ def parameters(self) -> dict[str, Any]:
84
+ """Build parameters with agent enum."""
85
+ agents = self._get_agents_sync()
86
+ agent_keys = [a.key for a in agents] if agents else []
87
+
88
+ agent_schema: dict[str, Any] = {
89
+ "type": "string",
90
+ "description": "Key of the agent to delegate to",
91
+ }
92
+ if agent_keys:
93
+ agent_schema["enum"] = agent_keys
94
+
95
+ return {
96
+ "type": "object",
97
+ "properties": {
98
+ "agent": agent_schema,
99
+ "task_context": {
100
+ "type": "string",
101
+ "description": "任务上下文:尽可能描述用户意图、背景信息、具体要求。包括用户最初的问题、对话中提到的偏好、强调的重点等。",
102
+ },
103
+ "artifact_refs": {
104
+ "type": "array",
105
+ "description": "相关资料引用列表,每项包含 id 和 summary",
106
+ "items": {
107
+ "type": "object",
108
+ "properties": {
109
+ "id": {"type": "string", "description": "Artifact ID"},
110
+ "summary": {"type": "string", "description": "摘要"},
111
+ },
112
+ "required": ["id"],
113
+ },
114
+ },
115
+ },
116
+ "required": ["agent"],
117
+ }
118
+
119
+ async def get_dynamic_description(self, ctx: ToolContext | None = None) -> str:
120
+ """Build description with available agents.
121
+
122
+ Includes both static agents from backend and dynamic agents from middleware.
123
+ """
124
+ agents = await self._get_all_agents(ctx)
125
+ if not agents:
126
+ return "Delegate a task to a sub-agent. No agents currently available."
127
+
128
+ agent_list = "\n".join([
129
+ f"- {a.key}: {a.description or 'No description'} (mode: {a.mode})"
130
+ for a in agents
131
+ ])
132
+ return f"""Delegate a task to a specialized sub-agent.
133
+
134
+ Available agents:
135
+ {agent_list}
136
+
137
+ Specify the agent key and task data."""
138
+
139
+ async def _get_all_agents(
140
+ self,
141
+ ctx: ToolContext | None = None,
142
+ ) -> list["AgentConfig"]:
143
+ """Get all available agents (static + dynamic from middleware).
144
+
145
+ Args:
146
+ ctx: Optional tool context for middleware disclosure
147
+
148
+ Returns:
149
+ Combined list of AgentConfig
150
+ """
151
+ # Get static agents from backend
152
+ agents = list(await self.backend.list())
153
+
154
+ # Get dynamic agents from middleware (progressive disclosure)
155
+ if self.middleware and ctx:
156
+ mw_context = {
157
+ "session_id": ctx.session_id,
158
+ "invocation_id": ctx.invocation_id,
159
+ "agent_id": ctx.agent,
160
+ }
161
+ dynamic_agents = await self.middleware.get_dynamic_subagents(mw_context)
162
+ if dynamic_agents:
163
+ # Store dynamic agents for later lookup
164
+ for config in dynamic_agents:
165
+ self._dynamic_agents[config.key] = config
166
+ agents.extend(dynamic_agents)
167
+
168
+ return agents
169
+
170
+ async def _get_agent_config(
171
+ self,
172
+ key: str,
173
+ ctx: ToolContext | None = None,
174
+ ) -> "AgentConfig | None":
175
+ """Get agent config by key (static or dynamic).
176
+
177
+ Args:
178
+ key: Agent key
179
+ ctx: Optional tool context
180
+
181
+ Returns:
182
+ AgentConfig or None if not found
183
+ """
184
+ # First check static backend
185
+ config = await self.backend.get(key)
186
+ if config:
187
+ return config
188
+
189
+ # Then check dynamic agents from middleware
190
+ if key in self._dynamic_agents:
191
+ return self._dynamic_agents[key]
192
+
193
+ # If not found and we have middleware, try to refresh dynamic agents
194
+ if self.middleware and ctx:
195
+ await self._get_all_agents(ctx)
196
+ return self._dynamic_agents.get(key)
197
+
198
+ return None
199
+
200
+ async def execute(
201
+ self,
202
+ params: dict[str, Any],
203
+ ctx: ToolContext,
204
+ ) -> ToolResult:
205
+ """Execute delegation.
206
+
207
+ Supports two modes:
208
+ 1. Single agent: {"agent": "name", "data": {...}}
209
+ 2. Parallel agents: {"agents": [{"agent": "name1", "data": {...}}, ...]}
210
+ """
211
+ # Check if parallel execution
212
+ agents_param = params.get("agents")
213
+ if agents_param and isinstance(agents_param, list) and len(agents_param) > 1:
214
+ return await self._execute_parallel(agents_param, ctx)
215
+
216
+ # Single agent execution
217
+ agent_key = params.get("agent", "")
218
+ task_context = params.get("task_context")
219
+ artifact_refs = params.get("artifact_refs")
220
+
221
+ # Handle single item in agents array
222
+ if agents_param and len(agents_param) == 1:
223
+ agent_key = agents_param[0].get("agent", "")
224
+ task_context = task_context or agents_param[0].get("task_context")
225
+ artifact_refs = artifact_refs or agents_param[0].get("artifact_refs")
226
+
227
+ if not agent_key:
228
+ return ToolResult.error("Missing 'agent' parameter")
229
+
230
+ logger.info("Delegating to sub-agent", extra={"agent": agent_key})
231
+
232
+ # Get agent config (static or dynamic)
233
+ config = await self._get_agent_config(agent_key, ctx)
234
+ if config is None:
235
+ agents = await self._get_all_agents(ctx)
236
+ available = ", ".join(a.key for a in agents) or "none"
237
+ return ToolResult.error(f"Unknown agent: {agent_key}. Available: {available}")
238
+
239
+ # Create block_id for this delegation
240
+ block_key = f"{ctx.session_id}:{config.key}"
241
+ block_id = generate_id("blk")
242
+ self._active_block_ids[block_key] = block_id
243
+
244
+ # Emit SUB_AGENT block (start)
245
+ await self._emit_subagent_block(ctx, config, "start", block_id)
246
+
247
+ try:
248
+ # 根据 create_invocation 决定执行模式
249
+ if config.create_invocation:
250
+ result = await self._execute_delegated(
251
+ config, ctx, task_context, artifact_refs
252
+ )
253
+ else:
254
+ result = await self._execute_embedded(
255
+ config, ctx, task_context, artifact_refs
256
+ )
257
+
258
+ # Emit SUB_AGENT block (end)
259
+ await self._emit_subagent_block(ctx, config, "end", block_id)
260
+
261
+ # Cleanup
262
+ self._active_block_ids.pop(block_key, None)
263
+
264
+ return result
265
+ except Exception as e:
266
+ logger.error("Delegation failed", extra={"agent": agent_key, "error": str(e)})
267
+ # Emit error state
268
+ await self._emit_subagent_block(ctx, config, "error", block_id)
269
+ self._active_block_ids.pop(block_key, None)
270
+ return ToolResult.error(f"Delegation failed: {str(e)}")
271
+
272
+ async def _execute_parallel(
273
+ self,
274
+ agents_param: list[dict[str, Any]],
275
+ ctx: ToolContext,
276
+ ) -> ToolResult:
277
+ """Execute multiple sub-agents in parallel.
278
+
279
+ Args:
280
+ agents_param: List of {"agent": "name", ...}
281
+ ctx: Tool context
282
+
283
+ Returns:
284
+ Combined results from all agents
285
+ """
286
+ logger.info(
287
+ "Parallel delegation",
288
+ extra={"agents": [a.get("agent") for a in agents_param]},
289
+ )
290
+
291
+ # Validate all agents exist
292
+ configs: list["AgentConfig"] = []
293
+ for item in agents_param:
294
+ agent_key = item.get("agent", "")
295
+
296
+ config = await self.backend.get(agent_key)
297
+ if config is None:
298
+ agents = await self.backend.list()
299
+ available = ", ".join(a.key for a in agents) or "none"
300
+ return ToolResult.error(f"Unknown agent: {agent_key}. Available: {available}")
301
+ configs.append(config)
302
+
303
+ # Create parallel context for tracking
304
+ parallel_ctx = ParallelSubAgentContext(
305
+ parent_invocation_id=ctx.invocation_id,
306
+ session_id=ctx.session_id,
307
+ )
308
+
309
+ # Emit parallel start block
310
+ parallel_block_id = generate_id("blk")
311
+ await self._emit_parallel_block(
312
+ ctx, [c.key for c in configs], "start", parallel_block_id
313
+ )
314
+
315
+ # Execute all agents in parallel
316
+ async def run_one(
317
+ config: "AgentConfig",
318
+ ) -> tuple[str, ToolResult]:
319
+ """Run single agent and return (name, result)."""
320
+ branch_id = parallel_ctx.create_branch(config.key)
321
+ block_id = generate_id("blk")
322
+
323
+ try:
324
+ # Emit individual agent start
325
+ await self._emit_subagent_block(
326
+ ctx, config, "start", block_id, branch=branch_id
327
+ )
328
+
329
+ # 根据 create_invocation 决定执行模式
330
+ if config.create_invocation:
331
+ result = await self._execute_delegated(config, ctx)
332
+ else:
333
+ result = await self._execute_embedded(config, ctx)
334
+
335
+ # Emit individual agent end
336
+ await self._emit_subagent_block(
337
+ ctx, config, "end", block_id, branch=branch_id
338
+ )
339
+
340
+ parallel_ctx.mark_completed(config.key, result)
341
+ return (config.key, result)
342
+
343
+ except Exception as e:
344
+ error_msg = str(e)
345
+ parallel_ctx.mark_failed(config.key, error_msg)
346
+ await self._emit_subagent_block(
347
+ ctx, config, "error", block_id, branch=branch_id
348
+ )
349
+ return (
350
+ config.key,
351
+ ToolResult.error(f"Delegation failed: {error_msg}"),
352
+ )
353
+
354
+ # Run all agents in parallel using asyncio.gather
355
+ tasks = [run_one(config) for config in configs]
356
+ results = await asyncio.gather(*tasks, return_exceptions=True)
357
+
358
+ # Emit parallel end block
359
+ await self._emit_parallel_block(
360
+ ctx, [c.key for c in configs], "end", parallel_block_id
361
+ )
362
+
363
+ # Combine results
364
+ combined_output = []
365
+ combined_data = {}
366
+ all_success = True
367
+
368
+ for item in results:
369
+ if isinstance(item, Exception):
370
+ all_success = False
371
+ combined_output.append(f"Error: {str(item)}")
372
+ elif isinstance(item, tuple):
373
+ name, result = item
374
+ combined_data[name] = {"output": result.output}
375
+ if result.is_error:
376
+ all_success = False
377
+ combined_output.append(f"[{name}] {result.output}")
378
+
379
+ return ToolResult(output="\n\n".join(combined_output))
380
+
381
+ def _get_merged_middleware(
382
+ self,
383
+ config: "AgentConfig",
384
+ ctx: ToolContext,
385
+ ) -> "MiddlewareChain | None":
386
+ """Get merged middleware for sub-agent execution.
387
+
388
+ Merges caller's inheritable middlewares with sub-agent's own middlewares.
389
+
390
+ Args:
391
+ config: Sub-agent config
392
+ ctx: Tool context containing caller's middleware
393
+
394
+ Returns:
395
+ Merged MiddlewareChain or None
396
+ """
397
+ caller_middleware = ctx.middleware
398
+
399
+ # Get sub-agent's middleware (if agent is an instance with middleware)
400
+ sub_agent = config.agent
401
+ sub_middleware = getattr(sub_agent, 'middleware', None)
402
+
403
+ # Merge: caller's inheritable + sub-agent's own
404
+ if caller_middleware:
405
+ return caller_middleware.merge(sub_middleware)
406
+ elif sub_middleware:
407
+ return sub_middleware
408
+
409
+ return None
410
+
411
+ async def _execute_embedded(
412
+ self,
413
+ config: "AgentConfig",
414
+ ctx: ToolContext,
415
+ task_context: str | None = None,
416
+ artifact_refs: list[dict[str, str]] | None = None,
417
+ ) -> ToolResult:
418
+ """Execute in embedded mode (inline, same invocation).
419
+
420
+ Embedded 模式特点:
421
+ - 不创建新 invocation
422
+ - BlockEvent 透传到父 agent 的 queue
423
+ - ActionEvent 被收集,用于获取 sub-agent 结果
424
+ - 客户端可以实时看到 sub-agent 的执行过程
425
+
426
+ 消息记录配置:
427
+ - config.record_messages=False → SubAgent 不记录消息
428
+ - config.message_namespace → 消息写入独立命名空间(隔离)
429
+ """
430
+ from ...core.types.action import ActionEvent, ActionType
431
+ from ...core.context import _emit_queue_var
432
+
433
+ start_time = datetime.now()
434
+
435
+ # Get agent instance
436
+ agent = config.agent
437
+ if agent is None:
438
+ return ToolResult.error(f"Agent '{config.key}' not found")
439
+
440
+ # Configure message recording for sub-agent
441
+ self._configure_subagent_messages(agent, config, ctx)
442
+
443
+ # Build input message
444
+ input_message = self._build_input_message(task_context, artifact_refs)
445
+
446
+ logger.info(f"Starting sub-agent '{config.key}' in embedded mode")
447
+ logger.info(f"Input: {input_message[:300]}...")
448
+
449
+ # Capture parent queue BEFORE sub-agent sets its own
450
+ # This is critical - sub-agent's run() will set ContextVar to its own queue
451
+ try:
452
+ parent_queue = _emit_queue_var.get()
453
+ except LookupError:
454
+ parent_queue = None
455
+
456
+ async def forward_to_parent(event):
457
+ """Forward event directly to parent queue, bypassing ContextVar."""
458
+ if parent_queue is not None:
459
+ await parent_queue.put(event)
460
+ # Yield control so event can be processed
461
+ import asyncio
462
+ await asyncio.sleep(0)
463
+
464
+ # Get timeout from config (default 5 min)
465
+ timeout = getattr(config, 'timeout', 300.0)
466
+
467
+ try:
468
+ # 消费事件流:BlockEvent 转发,ActionEvent 收集
469
+ # 使用 _force_own_queue=True 让 sub-agent 创建自己的 queue
470
+ # 实现活跃刷新超时:收到事件时重置超时
471
+ import asyncio
472
+
473
+ async def iter_with_activity_timeout():
474
+ """Iterate with activity-based timeout refresh."""
475
+ # Use list as mutable container for last_activity
476
+ state = {"last_activity": asyncio.get_event_loop().time()}
477
+
478
+ async def check_timeout():
479
+ while True:
480
+ await asyncio.sleep(1.0) # Check every second
481
+ if asyncio.get_event_loop().time() - state["last_activity"] > timeout:
482
+ raise asyncio.TimeoutError(f"Sub-agent timed out after {timeout}s of inactivity")
483
+
484
+ timeout_task = asyncio.create_task(check_timeout())
485
+
486
+ try:
487
+ async for event in agent.run(input_message, _force_own_queue=True):
488
+ state["last_activity"] = asyncio.get_event_loop().time()
489
+ yield event
490
+ finally:
491
+ timeout_task.cancel()
492
+ try:
493
+ await timeout_task
494
+ except asyncio.CancelledError:
495
+ pass
496
+
497
+ async for event in iter_with_activity_timeout():
498
+ if isinstance(event, ActionEvent):
499
+ # 非 internal 的 ActionEvent 转发
500
+ if not event.internal:
501
+ await forward_to_parent(event)
502
+ else:
503
+ # 转发 BlockEvent 给 parent(直接往 parent queue 发,不用 ctx.emit)
504
+ await forward_to_parent(event)
505
+
506
+ logger.info(f"Sub-agent '{config.key}' completed")
507
+
508
+ except Exception as e:
509
+ logger.error(f"Sub-agent execution failed: {e}")
510
+ return ToolResult.error(f"Sub-agent '{config.key}' failed: {str(e)}")
511
+
512
+ duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
513
+
514
+ # 从 agent.action_collector 获取结果(比从事件流收集更可靠)
515
+ result_data: dict[str, Any] | None = None
516
+ if agent.action_collector:
517
+ result_data = agent.action_collector.get_result()
518
+
519
+ # 构建结果 - 直接返回 result_data,让调用方自己处理格式
520
+ if result_data:
521
+ import json
522
+ output_text = json.dumps(result_data, ensure_ascii=False, indent=2)
523
+ else:
524
+ output_text = f"Sub-agent '{config.key}' completed (no result data)"
525
+
526
+ return ToolResult(output=output_text)
527
+
528
+ def _configure_subagent_messages(
529
+ self,
530
+ agent: Any,
531
+ config: "AgentConfig",
532
+ ctx: ToolContext,
533
+ ) -> None:
534
+ """Configure sub-agent's message recording based on config.
535
+
536
+ 消息记录策略(由 return_to_parent 派生):
537
+ - record_messages=False → 禁用消息保存
538
+ - record_messages=True → 保存到独立 namespace(agent_key:call_id)
539
+
540
+ 每次委派调用使用 call_id 保证对话独立。
541
+ """
542
+ if not config.record_messages:
543
+ # 禁用消息保存
544
+ agent._disable_message_save = True
545
+ if hasattr(agent, '_message_namespace'):
546
+ delattr(agent, '_message_namespace')
547
+ else:
548
+ # 保存到独立 namespace
549
+ agent._message_namespace = f"{config.key}:{ctx.call_id}"
550
+ agent._disable_message_save = False
551
+
552
+ def _build_input_message(
553
+ self,
554
+ task_context: str | None = None,
555
+ artifact_refs: list[dict[str, str]] | None = None,
556
+ ) -> str:
557
+ """构建输入消息.
558
+
559
+ Args:
560
+ task_context: 任务上下文
561
+ artifact_refs: 相关资料引用
562
+ """
563
+ parts = []
564
+
565
+ # 任务上下文
566
+ if task_context:
567
+ parts.append(f"任务背景:\n{task_context}")
568
+
569
+ # 相关资料
570
+ if artifact_refs:
571
+ refs_text = "\n".join([
572
+ f"- [{r.get('id', '')}] {r.get('summary', '')[:100]}"
573
+ for r in artifact_refs
574
+ ])
575
+ parts.append(f"可用资料(使用 read_artifact 工具获取完整内容):\n{refs_text}")
576
+
577
+ return "\n\n".join(parts) if parts else "请执行任务"
578
+
579
+ def _extract_text_from_event(self, event: Any) -> str | None:
580
+ """从 agent event 中提取文本."""
581
+ # BlockEvent with text content
582
+ if hasattr(event, 'kind'):
583
+ kind = event.kind.value if hasattr(event.kind, 'value') else event.kind
584
+ if kind == "text" and hasattr(event, 'data') and event.data:
585
+ return event.data.get('content', '')
586
+
587
+ # Direct text delta
588
+ if hasattr(event, 'text_delta'):
589
+ return event.text_delta
590
+
591
+ return None
592
+
593
+ def _extract_data_from_event(self, event: Any) -> dict[str, Any] | None:
594
+ """从 agent event 中提取结构化数据."""
595
+ # ARTIFACT block
596
+ if hasattr(event, 'kind'):
597
+ kind = event.kind.value if hasattr(event.kind, 'value') else event.kind
598
+ if kind == "artifact" and hasattr(event, 'data') and event.data:
599
+ return {
600
+ "artifact_id": event.data.get('artifact_id'),
601
+ "url": event.data.get('url'),
602
+ }
603
+
604
+ # Tool result with metadata
605
+ if hasattr(event, 'metadata') and event.metadata:
606
+ if 'artifact_id' in event.metadata:
607
+ return {
608
+ "artifact_id": event.metadata.get('artifact_id'),
609
+ "url": event.metadata.get('url'),
610
+ }
611
+
612
+ return None
613
+
614
+ async def _emit_subagent_progress(
615
+ self,
616
+ ctx: ToolContext,
617
+ config: "AgentConfig",
618
+ event: Any,
619
+ ) -> None:
620
+ """发送 sub-agent 进度事件(transient,不持久化)."""
621
+ emit = getattr(ctx, 'emit', None)
622
+ if emit is None:
623
+ return
624
+
625
+ # 直接转发 event,但标记为 sub-agent 的
626
+ if hasattr(event, 'to_dict'):
627
+ # 已经是 BlockEvent,直接转发
628
+ await emit(event)
629
+
630
+ async def _execute_delegated(
631
+ self,
632
+ config: "AgentConfig",
633
+ ctx: ToolContext,
634
+ task_context: str | None = None,
635
+ artifact_refs: list[dict[str, str]] | None = None,
636
+ ) -> ToolResult:
637
+ """Execute in delegated mode (new invocation, user can interact)."""
638
+ child_inv_id = generate_id("inv")
639
+
640
+ # Get merged middleware for sub-agent
641
+ merged_middleware = self._get_merged_middleware(config, ctx)
642
+
643
+ # Create control frame
644
+ frame = ControlFrame(
645
+ agent_id=config.key,
646
+ invocation_id=child_inv_id,
647
+ parent_invocation_id=ctx.invocation_id,
648
+ )
649
+
650
+ # Note: Real implementation would:
651
+ # 1. Push frame to session.control_stack
652
+ # 2. Create child InvocationContext with merged_middleware
653
+ # 3. Inject yield_result tool
654
+ # 4. Start agent execution
655
+ # 5. Return - user continues with sub-agent
656
+
657
+ result = SubAgentResult(
658
+ output=f"[DELEGATED] Control transferred to {config.key}. User can interact directly.",
659
+ status="completed",
660
+ metadata=SubAgentMetadata(
661
+ child_invocation_id=child_inv_id,
662
+ agent_name=config.key,
663
+ agent_type="react",
664
+ ),
665
+ )
666
+
667
+ return ToolResult(output=result.output)
668
+
669
+ async def _emit_subagent_block(
670
+ self,
671
+ ctx: ToolContext,
672
+ config: "AgentConfig",
673
+ stage: str,
674
+ block_id: str,
675
+ branch: str | None = None,
676
+ ) -> None:
677
+ """Emit SUB_AGENT block."""
678
+ emit = getattr(ctx, 'emit', None)
679
+ if emit is None:
680
+ return
681
+
682
+ block = BlockEvent(
683
+ block_id=block_id,
684
+ kind=BlockKind.SUB_AGENT,
685
+ op=BlockOp.APPLY if stage == "start" else BlockOp.PATCH,
686
+ data={
687
+ "agent": config.key,
688
+ "mode": config.mode,
689
+ "stage": stage,
690
+ "branch": branch,
691
+ },
692
+ session_id=ctx.session_id,
693
+ invocation_id=ctx.invocation_id,
694
+ )
695
+
696
+ await emit(block)
697
+
698
+ async def _emit_parallel_block(
699
+ self,
700
+ ctx: ToolContext,
701
+ agents: list[str],
702
+ stage: str,
703
+ block_id: str,
704
+ ) -> None:
705
+ """Emit PARALLEL block for parallel execution."""
706
+ emit = getattr(ctx, 'emit', None)
707
+ if emit is None:
708
+ return
709
+
710
+ block = BlockEvent(
711
+ block_id=block_id,
712
+ kind=BlockKind.SUB_AGENT, # Use SUB_AGENT with parallel flag
713
+ op=BlockOp.APPLY if stage == "start" else BlockOp.PATCH,
714
+ data={
715
+ "parallel": True,
716
+ "agents": agents,
717
+ "stage": stage,
718
+ },
719
+ session_id=ctx.session_id,
720
+ invocation_id=ctx.invocation_id,
721
+ )
722
+
723
+ await emit(block)
724
+
725
+
726
+ __all__ = ["DelegateTool"]