aury-agent 0.0.12__py3-none-any.whl → 0.0.14__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/agents/backends/__init__.py +8 -0
- aury/agents/backends/hitl/__init__.py +8 -0
- aury/agents/backends/hitl/memory.py +100 -0
- aury/agents/backends/hitl/types.py +132 -0
- aury/agents/core/base.py +5 -0
- aury/agents/core/context.py +1 -0
- aury/agents/core/signals.py +37 -17
- aury/agents/core/types/__init__.py +0 -2
- aury/agents/core/types/block.py +6 -23
- aury/agents/core/types/session.py +10 -3
- aury/agents/core/types/tool.py +194 -18
- aury/agents/hitl/__init__.py +2 -0
- aury/agents/hitl/ask_user.py +59 -47
- aury/agents/hitl/exceptions.py +214 -13
- aury/agents/react/agent.py +47 -0
- aury/agents/react/context.py +51 -25
- aury/agents/react/factory.py +2 -0
- aury/agents/react/pause.py +13 -2
- aury/agents/react/step.py +39 -12
- aury/agents/react/tools.py +277 -147
- aury/agents/tool/builtin/ask_user.py +1 -5
- aury/agents/tool/builtin/delegate.py +3 -15
- aury/agents/tool/builtin/plan.py +1 -5
- aury/agents/tool/builtin/thinking.py +1 -6
- aury/agents/tool/builtin/yield_result.py +1 -6
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/METADATA +1 -1
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/RECORD +29 -26
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/WHEEL +0 -0
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/entry_points.txt +0 -0
aury/agents/react/tools.py
CHANGED
|
@@ -12,6 +12,7 @@ from ..core.types import (
|
|
|
12
12
|
ToolContext,
|
|
13
13
|
ToolResult,
|
|
14
14
|
ToolInvocation,
|
|
15
|
+
ToolInvocationState,
|
|
15
16
|
)
|
|
16
17
|
from ..core.signals import SuspendSignal
|
|
17
18
|
from ..llm import LLMMessage
|
|
@@ -39,12 +40,104 @@ def get_tool(agent: "ReactAgent", tool_name: str) -> "BaseTool | None":
|
|
|
39
40
|
return None
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
async def
|
|
43
|
+
async def _complete_tool_invocation(
|
|
44
|
+
agent: "ReactAgent",
|
|
45
|
+
invocation: ToolInvocation,
|
|
46
|
+
result: ToolResult,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Complete a tool invocation.
|
|
49
|
+
|
|
50
|
+
This function:
|
|
51
|
+
1. Updates TOOL_USE block status (success/failed/aborted)
|
|
52
|
+
2. Publishes TOOL_END event
|
|
53
|
+
3. Adds result to LLM message history
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
agent: ReactAgent instance
|
|
57
|
+
invocation: Tool invocation
|
|
58
|
+
result: Tool execution result
|
|
59
|
+
"""
|
|
60
|
+
# 1. Update invocation state if not already marked
|
|
61
|
+
if not invocation.result:
|
|
62
|
+
invocation.mark_result(
|
|
63
|
+
result.output,
|
|
64
|
+
is_error=result.is_error,
|
|
65
|
+
truncated_result=result.truncated_output,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 2. Map invocation state to string status for frontend
|
|
69
|
+
status_map = {
|
|
70
|
+
ToolInvocationState.SUCCESS: "success",
|
|
71
|
+
ToolInvocationState.FAILED: "failed",
|
|
72
|
+
ToolInvocationState.ABORTED: "aborted",
|
|
73
|
+
ToolInvocationState.RESULT: "success", # Backward compatibility
|
|
74
|
+
}
|
|
75
|
+
status = status_map.get(invocation.state, "failed")
|
|
76
|
+
|
|
77
|
+
# 3. Update TOOL_USE block with final status
|
|
78
|
+
# Tools can PATCH their own block during execution for UI updates.
|
|
79
|
+
# Here we only update the final status.
|
|
80
|
+
if invocation.block_id:
|
|
81
|
+
patch_data: dict[str, Any] = {"status": status}
|
|
82
|
+
if invocation.is_error:
|
|
83
|
+
patch_data["error"] = result.output
|
|
84
|
+
|
|
85
|
+
await agent.ctx.emit(BlockEvent(
|
|
86
|
+
block_id=invocation.block_id,
|
|
87
|
+
kind=BlockKind.TOOL_USE,
|
|
88
|
+
op=BlockOp.PATCH,
|
|
89
|
+
data=patch_data,
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
# 5. Publish TOOL_END event (for internal subscribers: monitoring, billing, etc.)
|
|
93
|
+
await agent.bus.publish(
|
|
94
|
+
Events.TOOL_END,
|
|
95
|
+
{
|
|
96
|
+
"call_id": invocation.tool_call_id,
|
|
97
|
+
"tool": invocation.tool_name,
|
|
98
|
+
"result": result.output[:500],
|
|
99
|
+
"is_error": invocation.is_error,
|
|
100
|
+
"status": status,
|
|
101
|
+
"duration_ms": invocation.duration_ms,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 6. Add to message history (for LLM, use truncated for context window)
|
|
106
|
+
agent._message_history.append(
|
|
107
|
+
LLMMessage(
|
|
108
|
+
role="tool",
|
|
109
|
+
content=invocation.result,
|
|
110
|
+
tool_call_id=invocation.tool_call_id,
|
|
111
|
+
name=invocation.tool_name,
|
|
112
|
+
truncated_content=invocation.truncated_result,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
logger.debug(
|
|
117
|
+
f"Tool completed: {invocation.tool_name} ({status})",
|
|
118
|
+
extra={
|
|
119
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
120
|
+
"call_id": invocation.tool_call_id,
|
|
121
|
+
"status": status,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def execute_tool(
|
|
127
|
+
agent: "ReactAgent",
|
|
128
|
+
invocation: ToolInvocation,
|
|
129
|
+
checkpoint: Any | None = None,
|
|
130
|
+
) -> ToolResult:
|
|
43
131
|
"""Execute a single tool call.
|
|
44
132
|
|
|
133
|
+
Supports two execution modes:
|
|
134
|
+
1. Standard mode: Tool runs to completion
|
|
135
|
+
2. Continuation mode: Tool can be resumed from checkpoint
|
|
136
|
+
|
|
45
137
|
Args:
|
|
46
138
|
agent: ReactAgent instance
|
|
47
139
|
invocation: Tool invocation to execute
|
|
140
|
+
checkpoint: ToolCheckpoint for resuming (continuation mode only)
|
|
48
141
|
|
|
49
142
|
Returns:
|
|
50
143
|
ToolResult from tool execution
|
|
@@ -52,7 +145,11 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
52
145
|
# Check abort before execution
|
|
53
146
|
if await agent._check_abort():
|
|
54
147
|
error_msg = f"Tool {invocation.tool_name} aborted before execution"
|
|
55
|
-
invocation.mark_result(
|
|
148
|
+
invocation.mark_result(
|
|
149
|
+
error_msg,
|
|
150
|
+
is_error=True,
|
|
151
|
+
status=ToolInvocationState.ABORTED, # Explicit ABORTED status
|
|
152
|
+
)
|
|
56
153
|
logger.info(
|
|
57
154
|
f"Tool aborted before execution: {invocation.tool_name}",
|
|
58
155
|
extra={
|
|
@@ -63,13 +160,17 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
63
160
|
return ToolResult.error(error_msg)
|
|
64
161
|
|
|
65
162
|
invocation.mark_call_complete()
|
|
163
|
+
|
|
164
|
+
is_resuming = checkpoint is not None
|
|
66
165
|
|
|
67
166
|
logger.info(
|
|
68
|
-
f"Executing tool: {invocation.tool_name}",
|
|
167
|
+
f"Executing tool: {invocation.tool_name}" + (" (resuming from checkpoint)" if is_resuming else ""),
|
|
69
168
|
extra={
|
|
70
169
|
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
71
170
|
"call_id": invocation.tool_call_id,
|
|
72
171
|
"arguments": invocation.args,
|
|
172
|
+
"is_resuming": is_resuming,
|
|
173
|
+
"checkpoint_id": checkpoint.checkpoint_id if checkpoint else None,
|
|
73
174
|
},
|
|
74
175
|
)
|
|
75
176
|
|
|
@@ -85,8 +186,13 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
85
186
|
)
|
|
86
187
|
return ToolResult.error(error_msg)
|
|
87
188
|
|
|
189
|
+
# Set tool context on ctx for middleware/tool access
|
|
190
|
+
agent.ctx.tool_call_id = invocation.tool_call_id
|
|
191
|
+
agent.ctx.tool_block_id = invocation.block_id
|
|
192
|
+
|
|
88
193
|
# === Middleware: on_tool_call ===
|
|
89
|
-
|
|
194
|
+
# Skip middleware on resume - already processed on first call
|
|
195
|
+
if agent.middleware and not is_resuming:
|
|
90
196
|
logger.debug(
|
|
91
197
|
f"Calling middleware: on_tool_call ({invocation.tool_name})",
|
|
92
198
|
extra={"invocation_id": agent._current_invocation.id, "call_id": invocation.tool_call_id},
|
|
@@ -111,29 +217,65 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
111
217
|
invocation.args = hook_result.modified_data
|
|
112
218
|
|
|
113
219
|
# Create ToolContext
|
|
114
|
-
# Get block_id for this tool call
|
|
115
|
-
tool_block_id = agent._tool_call_blocks.get(invocation.tool_call_id, "")
|
|
116
220
|
tool_ctx = ToolContext(
|
|
117
221
|
session_id=agent.session.id,
|
|
118
222
|
invocation_id=agent._current_invocation.id if agent._current_invocation else "",
|
|
119
|
-
block_id=
|
|
223
|
+
block_id=invocation.block_id,
|
|
120
224
|
call_id=invocation.tool_call_id,
|
|
121
225
|
agent=agent.config.name,
|
|
122
226
|
abort_signal=agent._abort,
|
|
123
227
|
update_metadata=agent._noop_update_metadata,
|
|
124
228
|
middleware=agent.middleware,
|
|
125
229
|
)
|
|
230
|
+
|
|
231
|
+
# Emit running status before execution
|
|
232
|
+
if invocation.block_id:
|
|
233
|
+
await agent.ctx.emit(BlockEvent(
|
|
234
|
+
block_id=invocation.block_id,
|
|
235
|
+
kind=BlockKind.TOOL_USE,
|
|
236
|
+
op=BlockOp.PATCH,
|
|
237
|
+
data={"status": "running"},
|
|
238
|
+
))
|
|
126
239
|
|
|
127
|
-
#
|
|
240
|
+
# Determine execution mode
|
|
241
|
+
# Use continuation mode if: tool supports it AND (resuming OR tool always uses continuation)
|
|
242
|
+
use_continuation = tool.supports_continuation
|
|
128
243
|
timeout = tool.config.timeout
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
244
|
+
|
|
245
|
+
# Set ctx on tool for self.emit() support
|
|
246
|
+
tool._set_ctx(tool_ctx)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
if use_continuation:
|
|
250
|
+
# Continuation mode: use execute_resumable
|
|
251
|
+
if timeout is not None:
|
|
252
|
+
result = await asyncio.wait_for(
|
|
253
|
+
tool.execute_resumable(invocation.args, tool_ctx, checkpoint),
|
|
254
|
+
timeout=timeout,
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
result = await tool.execute_resumable(invocation.args, tool_ctx, checkpoint)
|
|
258
|
+
else:
|
|
259
|
+
# Standard mode: use execute
|
|
260
|
+
if timeout is not None:
|
|
261
|
+
result = await asyncio.wait_for(
|
|
262
|
+
tool.execute(invocation.args, tool_ctx),
|
|
263
|
+
timeout=timeout,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
result = await tool.execute(invocation.args, tool_ctx)
|
|
267
|
+
finally:
|
|
268
|
+
# Clear ctx after execution
|
|
269
|
+
tool._set_ctx(None)
|
|
270
|
+
|
|
271
|
+
# Check abort after execution
|
|
272
|
+
if await agent._check_abort():
|
|
273
|
+
invocation.mark_result(
|
|
274
|
+
"Tool execution interrupted by user",
|
|
275
|
+
is_error=True,
|
|
276
|
+
status=ToolInvocationState.ABORTED,
|
|
133
277
|
)
|
|
134
|
-
|
|
135
|
-
# No timeout - tool runs until completion
|
|
136
|
-
result = await tool.execute(invocation.args, tool_ctx)
|
|
278
|
+
return ToolResult.error("Tool execution interrupted by user")
|
|
137
279
|
|
|
138
280
|
# === Middleware: on_tool_end ===
|
|
139
281
|
if agent.middleware:
|
|
@@ -148,7 +290,12 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
148
290
|
extra={"invocation_id": agent._current_invocation.id},
|
|
149
291
|
)
|
|
150
292
|
|
|
151
|
-
|
|
293
|
+
# Mark result - state will be auto-inferred (SUCCESS if not error, FAILED if error)
|
|
294
|
+
invocation.mark_result(
|
|
295
|
+
result.output,
|
|
296
|
+
is_error=result.is_error,
|
|
297
|
+
truncated_result=result.truncated_output,
|
|
298
|
+
)
|
|
152
299
|
logger.info(
|
|
153
300
|
f"Tool executed: {invocation.tool_name}",
|
|
154
301
|
extra={
|
|
@@ -163,7 +310,11 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
163
310
|
except asyncio.TimeoutError:
|
|
164
311
|
timeout = tool.config.timeout if tool else None
|
|
165
312
|
error_msg = f"Tool {invocation.tool_name} timed out after {timeout}s"
|
|
166
|
-
invocation.mark_result(
|
|
313
|
+
invocation.mark_result(
|
|
314
|
+
error_msg,
|
|
315
|
+
is_error=True,
|
|
316
|
+
status=ToolInvocationState.FAILED, # Timeout is treated as FAILED
|
|
317
|
+
)
|
|
167
318
|
logger.error(
|
|
168
319
|
f"Tool timeout: {invocation.tool_name}",
|
|
169
320
|
extra={
|
|
@@ -179,25 +330,38 @@ async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolR
|
|
|
179
330
|
|
|
180
331
|
except Exception as e:
|
|
181
332
|
import traceback
|
|
333
|
+
import sys
|
|
182
334
|
error_type = type(e).__name__
|
|
183
335
|
error_msg = f"Tool execution error ({error_type}): {e}"
|
|
184
336
|
stack_trace = traceback.format_exc()
|
|
185
|
-
invocation.mark_result(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
190
|
-
"error_type": error_type,
|
|
191
|
-
"error": str(e),
|
|
192
|
-
},
|
|
337
|
+
invocation.mark_result(
|
|
338
|
+
error_msg,
|
|
339
|
+
is_error=True,
|
|
340
|
+
status=ToolInvocationState.FAILED,
|
|
193
341
|
)
|
|
342
|
+
# Log error - wrap in try to ensure we always return
|
|
343
|
+
try:
|
|
344
|
+
logger.error(
|
|
345
|
+
f"Tool execution failed: {invocation.tool_name} - {error_type}: {e}\n{stack_trace}",
|
|
346
|
+
extra={
|
|
347
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
348
|
+
"error_type": error_type,
|
|
349
|
+
"error": str(e),
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
except Exception as log_err:
|
|
353
|
+
# If standard logging fails, print directly to stderr
|
|
354
|
+
print(f"[TOOL_ERROR] Logging failed: {log_err}", file=sys.stderr)
|
|
355
|
+
print(f"[TOOL_ERROR] Original error: {invocation.tool_name} - {error_type}: {e}", file=sys.stderr)
|
|
356
|
+
print(stack_trace, file=sys.stderr)
|
|
194
357
|
return ToolResult.error(error_msg)
|
|
195
358
|
|
|
196
359
|
|
|
197
360
|
async def process_tool_results(agent: "ReactAgent") -> None:
|
|
198
|
-
"""Execute tool calls and
|
|
361
|
+
"""Execute tool calls and emit results in real-time.
|
|
199
362
|
|
|
200
|
-
|
|
363
|
+
For parallel execution, uses asyncio.as_completed to emit each tool's
|
|
364
|
+
result immediately upon completion (not waiting for all tools).
|
|
201
365
|
|
|
202
366
|
This function directly modifies agent's internal state:
|
|
203
367
|
- agent._message_history
|
|
@@ -223,25 +387,72 @@ async def process_tool_results(agent: "ReactAgent") -> None:
|
|
|
223
387
|
"Tool execution aborted before starting",
|
|
224
388
|
extra={"invocation_id": agent._current_invocation.id},
|
|
225
389
|
)
|
|
226
|
-
# Return empty results - agent loop will handle abort
|
|
227
390
|
return
|
|
228
391
|
|
|
229
392
|
# Execute tools based on configuration
|
|
230
393
|
if agent.config.parallel_tool_execution:
|
|
231
|
-
# Parallel execution
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
394
|
+
# Parallel execution with real-time result emission
|
|
395
|
+
# Use as_completed to process and emit results as they finish
|
|
396
|
+
|
|
397
|
+
# Wrapper to return both invocation and result
|
|
398
|
+
async def execute_with_invocation(inv: ToolInvocation) -> tuple[ToolInvocation, ToolResult]:
|
|
399
|
+
try:
|
|
400
|
+
result = await execute_tool(agent, inv)
|
|
401
|
+
return inv, result
|
|
402
|
+
except SuspendSignal:
|
|
403
|
+
# HITL signal - record placeholder before propagating
|
|
404
|
+
inv.mark_result("[等待用户输入]", is_error=False)
|
|
405
|
+
agent._message_history.append(
|
|
406
|
+
LLMMessage(
|
|
407
|
+
role="tool",
|
|
408
|
+
content=inv.result,
|
|
409
|
+
tool_call_id=inv.tool_call_id,
|
|
410
|
+
name=inv.tool_name,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
await agent._save_tool_messages()
|
|
414
|
+
raise # Re-raise to abort all tools
|
|
235
415
|
|
|
236
|
-
|
|
416
|
+
tasks = [
|
|
417
|
+
asyncio.create_task(execute_with_invocation(inv))
|
|
418
|
+
for inv in agent._tool_invocations
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
# Process each tool as it completes
|
|
422
|
+
for coro in asyncio.as_completed(tasks):
|
|
423
|
+
try:
|
|
424
|
+
invocation, result = await coro
|
|
425
|
+
|
|
426
|
+
# Complete invocation immediately (don't wait for other tools)
|
|
427
|
+
await _complete_tool_invocation(agent, invocation, result)
|
|
428
|
+
|
|
429
|
+
except SuspendSignal:
|
|
430
|
+
# HITL signal - placeholder already recorded in execute_with_invocation
|
|
431
|
+
logger.info(
|
|
432
|
+
"Tool execution suspended (HITL) during parallel execution",
|
|
433
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
434
|
+
)
|
|
435
|
+
raise # Propagate to abort all other tools
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
# Unexpected error in the wrapper itself (not from execute_tool)
|
|
439
|
+
# This should be very rare as execute_tool catches all exceptions
|
|
440
|
+
logger.error(
|
|
441
|
+
f"Unexpected error in parallel tool wrapper: {e}",
|
|
442
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
443
|
+
exc_info=True,
|
|
444
|
+
)
|
|
445
|
+
# Continue with other tools
|
|
446
|
+
|
|
447
|
+
# Check abort after all parallel tools complete
|
|
237
448
|
if await agent._check_abort():
|
|
238
449
|
logger.info(
|
|
239
450
|
"Tool execution aborted after parallel execution",
|
|
240
451
|
extra={"invocation_id": agent._current_invocation.id},
|
|
241
452
|
)
|
|
453
|
+
|
|
242
454
|
else:
|
|
243
455
|
# Sequential execution with abort check between tools
|
|
244
|
-
results = []
|
|
245
456
|
for inv in agent._tool_invocations:
|
|
246
457
|
# Check abort before each tool
|
|
247
458
|
if await agent._check_abort():
|
|
@@ -250,123 +461,42 @@ async def process_tool_results(agent: "ReactAgent") -> None:
|
|
|
250
461
|
extra={"invocation_id": agent._current_invocation.id},
|
|
251
462
|
)
|
|
252
463
|
# Mark remaining as aborted
|
|
253
|
-
error_result = ToolResult.error(
|
|
254
|
-
|
|
255
|
-
|
|
464
|
+
error_result = ToolResult.error("Aborted before execution")
|
|
465
|
+
inv.mark_result(
|
|
466
|
+
error_result.output,
|
|
467
|
+
is_error=True,
|
|
468
|
+
status=ToolInvocationState.ABORTED,
|
|
469
|
+
)
|
|
470
|
+
await _complete_tool_invocation(agent, inv, error_result)
|
|
256
471
|
continue
|
|
257
472
|
|
|
258
473
|
try:
|
|
259
474
|
result = await execute_tool(agent, inv)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# Must record tool_results to history BEFORE suspending
|
|
273
|
-
# Otherwise Claude API will reject next request (tool_use without tool_result)
|
|
274
|
-
for invocation, result in zip(agent._tool_invocations, results):
|
|
275
|
-
if isinstance(result, SuspendSignal):
|
|
276
|
-
# HITL tool - record placeholder result
|
|
277
|
-
invocation.mark_result(f"[等待用户输入: {suspend_signal.request_type}]", is_error=False)
|
|
278
|
-
else:
|
|
279
|
-
# Normal tool result - pass truncated_output
|
|
280
|
-
output = result.output if hasattr(result, 'output') else str(result)
|
|
281
|
-
truncated = getattr(result, 'truncated_output', None)
|
|
282
|
-
invocation.mark_result(
|
|
283
|
-
output,
|
|
284
|
-
is_error=getattr(result, 'is_error', False),
|
|
285
|
-
truncated_result=truncated,
|
|
475
|
+
await _complete_tool_invocation(agent, inv, result)
|
|
476
|
+
|
|
477
|
+
except SuspendSignal:
|
|
478
|
+
# HITL signal - record placeholder and propagate
|
|
479
|
+
inv.mark_result("[等待用户输入]", is_error=False)
|
|
480
|
+
agent._message_history.append(
|
|
481
|
+
LLMMessage(
|
|
482
|
+
role="tool",
|
|
483
|
+
content=inv.result,
|
|
484
|
+
tool_call_id=inv.tool_call_id,
|
|
485
|
+
name=inv.tool_name,
|
|
486
|
+
)
|
|
286
487
|
)
|
|
488
|
+
await agent._save_tool_messages()
|
|
489
|
+
raise
|
|
287
490
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
content=invocation.result,
|
|
293
|
-
tool_call_id=invocation.tool_call_id,
|
|
294
|
-
name=invocation.tool_name, # Required for Gemini
|
|
295
|
-
truncated_content=invocation.truncated_result,
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(
|
|
493
|
+
f"Unexpected error in sequential tool execution: {e}",
|
|
494
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
296
495
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
extra={"invocation_id": agent._current_invocation.id},
|
|
305
|
-
)
|
|
306
|
-
raise suspend_signal
|
|
307
|
-
|
|
308
|
-
# Process results
|
|
309
|
-
tool_results = []
|
|
310
|
-
|
|
311
|
-
for invocation, result in zip(agent._tool_invocations, results):
|
|
312
|
-
# Handle exceptions from gather
|
|
313
|
-
if isinstance(result, Exception):
|
|
314
|
-
error_msg = f"Tool execution error: {str(result)}"
|
|
315
|
-
invocation.mark_result(error_msg, is_error=True)
|
|
316
|
-
result = ToolResult.error(error_msg)
|
|
317
|
-
else:
|
|
318
|
-
# Mark result with truncated_output
|
|
319
|
-
invocation.mark_result(
|
|
320
|
-
result.output,
|
|
321
|
-
is_error=result.is_error,
|
|
322
|
-
truncated_result=result.truncated_output,
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
# Get parent block_id from tool_call mapping
|
|
326
|
-
parent_block_id = agent._tool_call_blocks.get(invocation.tool_call_id)
|
|
327
|
-
|
|
328
|
-
await agent.ctx.emit(BlockEvent(
|
|
329
|
-
kind=BlockKind.TOOL_RESULT,
|
|
330
|
-
op=BlockOp.APPLY,
|
|
331
|
-
parent_id=parent_block_id,
|
|
332
|
-
data={
|
|
333
|
-
"call_id": invocation.tool_call_id,
|
|
334
|
-
"content": result.output,
|
|
335
|
-
"is_error": invocation.is_error,
|
|
336
|
-
},
|
|
337
|
-
))
|
|
338
|
-
|
|
339
|
-
await agent.bus.publish(
|
|
340
|
-
Events.TOOL_END,
|
|
341
|
-
{
|
|
342
|
-
"call_id": invocation.tool_call_id,
|
|
343
|
-
"tool": invocation.tool_name,
|
|
344
|
-
"result": result.output[:500], # Truncate for event
|
|
345
|
-
"is_error": invocation.is_error,
|
|
346
|
-
"duration_ms": invocation.duration_ms,
|
|
347
|
-
},
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
tool_results.append(
|
|
351
|
-
{
|
|
352
|
-
"type": "tool_result",
|
|
353
|
-
"tool_use_id": invocation.tool_call_id,
|
|
354
|
-
"tool_name": invocation.tool_name,
|
|
355
|
-
"content": result.output,
|
|
356
|
-
"truncated_content": result.truncated_output,
|
|
357
|
-
"is_error": invocation.is_error,
|
|
358
|
-
}
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# Add tool results as tool messages (OpenAI format)
|
|
362
|
-
for tr in tool_results:
|
|
363
|
-
print(f"[DEBUG _process_tool_results] Adding tool_result to history: {tr}")
|
|
364
|
-
agent._message_history.append(
|
|
365
|
-
LLMMessage(
|
|
366
|
-
role="tool",
|
|
367
|
-
content=tr["content"],
|
|
368
|
-
tool_call_id=tr["tool_use_id"],
|
|
369
|
-
name=tr["tool_name"], # Required for Gemini
|
|
370
|
-
truncated_content=tr.get("truncated_content"),
|
|
371
|
-
)
|
|
372
|
-
)
|
|
496
|
+
error_result = ToolResult.error(f"Unexpected error: {str(e)}")
|
|
497
|
+
inv.mark_result(
|
|
498
|
+
error_result.output,
|
|
499
|
+
is_error=True,
|
|
500
|
+
status=ToolInvocationState.FAILED,
|
|
501
|
+
)
|
|
502
|
+
await _complete_tool_invocation(agent, inv, error_result)
|
|
@@ -131,10 +131,6 @@ The conversation will pause until the user responds."""
|
|
|
131
131
|
context: str | None,
|
|
132
132
|
) -> None:
|
|
133
133
|
"""Emit ASK block."""
|
|
134
|
-
emit = getattr(ctx, 'emit', None)
|
|
135
|
-
if emit is None:
|
|
136
|
-
return
|
|
137
|
-
|
|
138
134
|
block = BlockEvent(
|
|
139
135
|
kind=BlockKind.ASK,
|
|
140
136
|
op=BlockOp.APPLY,
|
|
@@ -149,7 +145,7 @@ The conversation will pause until the user responds."""
|
|
|
149
145
|
invocation_id=ctx.invocation_id,
|
|
150
146
|
)
|
|
151
147
|
|
|
152
|
-
await emit(block)
|
|
148
|
+
await self.emit(block)
|
|
153
149
|
|
|
154
150
|
|
|
155
151
|
__all__ = ["AskUserTool"]
|
|
@@ -648,14 +648,10 @@ Specify the agent key and task data."""
|
|
|
648
648
|
event: Any,
|
|
649
649
|
) -> None:
|
|
650
650
|
"""发送 sub-agent 进度事件(transient,不持久化)."""
|
|
651
|
-
emit = getattr(ctx, 'emit', None)
|
|
652
|
-
if emit is None:
|
|
653
|
-
return
|
|
654
|
-
|
|
655
651
|
# 直接转发 event,但标记为 sub-agent 的
|
|
656
652
|
if hasattr(event, 'to_dict'):
|
|
657
653
|
# 已经是 BlockEvent,直接转发
|
|
658
|
-
await emit(event)
|
|
654
|
+
await self.emit(event)
|
|
659
655
|
|
|
660
656
|
async def _execute_delegated(
|
|
661
657
|
self,
|
|
@@ -705,10 +701,6 @@ Specify the agent key and task data."""
|
|
|
705
701
|
branch: str | None = None,
|
|
706
702
|
) -> None:
|
|
707
703
|
"""Emit SUB_AGENT block."""
|
|
708
|
-
emit = getattr(ctx, 'emit', None)
|
|
709
|
-
if emit is None:
|
|
710
|
-
return
|
|
711
|
-
|
|
712
704
|
block = BlockEvent(
|
|
713
705
|
block_id=block_id,
|
|
714
706
|
kind=BlockKind.SUB_AGENT,
|
|
@@ -723,7 +715,7 @@ Specify the agent key and task data."""
|
|
|
723
715
|
invocation_id=ctx.invocation_id,
|
|
724
716
|
)
|
|
725
717
|
|
|
726
|
-
await emit(block)
|
|
718
|
+
await self.emit(block)
|
|
727
719
|
|
|
728
720
|
async def _emit_parallel_block(
|
|
729
721
|
self,
|
|
@@ -733,10 +725,6 @@ Specify the agent key and task data."""
|
|
|
733
725
|
block_id: str,
|
|
734
726
|
) -> None:
|
|
735
727
|
"""Emit PARALLEL block for parallel execution."""
|
|
736
|
-
emit = getattr(ctx, 'emit', None)
|
|
737
|
-
if emit is None:
|
|
738
|
-
return
|
|
739
|
-
|
|
740
728
|
block = BlockEvent(
|
|
741
729
|
block_id=block_id,
|
|
742
730
|
kind=BlockKind.SUB_AGENT, # Use SUB_AGENT with parallel flag
|
|
@@ -750,7 +738,7 @@ Specify the agent key and task data."""
|
|
|
750
738
|
invocation_id=ctx.invocation_id,
|
|
751
739
|
)
|
|
752
740
|
|
|
753
|
-
await emit(block)
|
|
741
|
+
await self.emit(block)
|
|
754
742
|
|
|
755
743
|
|
|
756
744
|
__all__ = ["DelegateTool"]
|
aury/agents/tool/builtin/plan.py
CHANGED
|
@@ -238,10 +238,6 @@ Use this to track your progress on complex tasks."""
|
|
|
238
238
|
|
|
239
239
|
async def _emit_plan_block(self, ctx: ToolContext, plan: dict, action: str) -> None:
|
|
240
240
|
"""Emit PLAN block event."""
|
|
241
|
-
emit = getattr(ctx, 'emit', None)
|
|
242
|
-
if emit is None:
|
|
243
|
-
return
|
|
244
|
-
|
|
245
241
|
# Get or generate block_id
|
|
246
242
|
block_id = plan.get("block_id")
|
|
247
243
|
if not block_id:
|
|
@@ -262,7 +258,7 @@ Use this to track your progress on complex tasks."""
|
|
|
262
258
|
invocation_id=ctx.invocation_id,
|
|
263
259
|
)
|
|
264
260
|
|
|
265
|
-
await emit(block)
|
|
261
|
+
await self.emit(block)
|
|
266
262
|
|
|
267
263
|
def _format_plan(self, plan: dict) -> str:
|
|
268
264
|
"""Format plan as text."""
|
|
@@ -92,11 +92,6 @@ This makes your reasoning visible and traceable."""
|
|
|
92
92
|
invocation_id: int | None = None,
|
|
93
93
|
) -> None:
|
|
94
94
|
"""Emit THINKING block."""
|
|
95
|
-
emit = getattr(ctx, 'emit', None)
|
|
96
|
-
if emit is None:
|
|
97
|
-
logger.debug(f"ThinkingTool: emit not available, invocation_id={invocation_id}")
|
|
98
|
-
return
|
|
99
|
-
|
|
100
95
|
logger.debug(f"ThinkingTool: emitting block, category={category}, invocation_id={invocation_id}")
|
|
101
96
|
|
|
102
97
|
block = BlockEvent(
|
|
@@ -110,7 +105,7 @@ This makes your reasoning visible and traceable."""
|
|
|
110
105
|
invocation_id=ctx.invocation_id,
|
|
111
106
|
)
|
|
112
107
|
|
|
113
|
-
await emit(block)
|
|
108
|
+
await self.emit(block)
|
|
114
109
|
logger.debug(f"ThinkingTool: block emitted successfully, invocation_id={invocation_id}")
|
|
115
110
|
|
|
116
111
|
|
|
@@ -106,11 +106,6 @@ This will end your current session and resume the parent."""
|
|
|
106
106
|
invocation_id: int | None = None,
|
|
107
107
|
) -> None:
|
|
108
108
|
"""Emit YIELD block."""
|
|
109
|
-
emit = getattr(ctx, 'emit', None)
|
|
110
|
-
if emit is None:
|
|
111
|
-
logger.debug(f"YieldResultTool: emit not available, invocation_id={invocation_id}")
|
|
112
|
-
return
|
|
113
|
-
|
|
114
109
|
logger.debug(f"YieldResultTool: emitting yield block, status={status}, invocation_id={invocation_id}")
|
|
115
110
|
|
|
116
111
|
block = BlockEvent(
|
|
@@ -126,7 +121,7 @@ This will end your current session and resume the parent."""
|
|
|
126
121
|
invocation_id=ctx.invocation_id,
|
|
127
122
|
)
|
|
128
123
|
|
|
129
|
-
await emit(block)
|
|
124
|
+
await self.emit(block)
|
|
130
125
|
logger.debug(f"YieldResultTool: yield block emitted successfully, invocation_id={invocation_id}")
|
|
131
126
|
|
|
132
127
|
|