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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- 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"]
|