aury-agent 0.0.4__py3-none-any.whl → 0.0.5__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/context_providers/message.py +8 -5
- aury/agents/core/base.py +11 -0
- aury/agents/core/factory.py +8 -0
- aury/agents/core/parallel.py +26 -4
- aury/agents/core/state.py +25 -0
- aury/agents/core/types/tool.py +1 -0
- aury/agents/hitl/ask_user.py +44 -0
- aury/agents/llm/adapter.py +55 -26
- aury/agents/llm/openai.py +5 -1
- aury/agents/memory/manager.py +33 -2
- aury/agents/messages/store.py +27 -1
- aury/agents/middleware/base.py +57 -0
- aury/agents/middleware/chain.py +81 -18
- aury/agents/react/agent.py +161 -1484
- aury/agents/react/context.py +309 -0
- aury/agents/react/factory.py +301 -0
- aury/agents/react/pause.py +241 -0
- aury/agents/react/persistence.py +182 -0
- aury/agents/react/step.py +680 -0
- aury/agents/react/tools.py +318 -0
- aury/agents/tool/builtin/bash.py +11 -0
- aury/agents/tool/builtin/delegate.py +38 -3
- aury/agents/tool/builtin/edit.py +16 -0
- aury/agents/tool/builtin/plan.py +19 -0
- aury/agents/tool/builtin/read.py +13 -0
- aury/agents/tool/builtin/thinking.py +10 -4
- aury/agents/tool/builtin/yield_result.py +9 -6
- aury/agents/tool/set.py +23 -0
- aury/agents/workflow/adapter.py +22 -3
- aury/agents/workflow/executor.py +51 -7
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/METADATA +1 -1
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/RECORD +34 -28
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/WHEEL +0 -0
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Tool execution helpers for ReactAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from ..core.logging import react_logger as logger
|
|
9
|
+
from ..core.event_bus import Events
|
|
10
|
+
from ..core.types.block import BlockEvent, BlockKind, BlockOp
|
|
11
|
+
from ..core.types import (
|
|
12
|
+
ToolContext,
|
|
13
|
+
ToolResult,
|
|
14
|
+
ToolInvocation,
|
|
15
|
+
)
|
|
16
|
+
from ..core.signals import SuspendSignal
|
|
17
|
+
from ..llm import LLMMessage
|
|
18
|
+
from ..middleware import HookAction
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .agent import ReactAgent
|
|
22
|
+
from ..core.types.tool import BaseTool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_tool(agent: "ReactAgent", tool_name: str) -> "BaseTool | None":
|
|
26
|
+
"""Get tool by name from agent context.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
agent: ReactAgent instance
|
|
30
|
+
tool_name: Name of the tool to find
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tool instance or None if not found
|
|
34
|
+
"""
|
|
35
|
+
if agent._agent_context:
|
|
36
|
+
for tool in agent._agent_context.tools:
|
|
37
|
+
if tool.name == tool_name:
|
|
38
|
+
return tool
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolResult:
|
|
43
|
+
"""Execute a single tool call.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
agent: ReactAgent instance
|
|
47
|
+
invocation: Tool invocation to execute
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ToolResult from tool execution
|
|
51
|
+
"""
|
|
52
|
+
invocation.mark_call_complete()
|
|
53
|
+
|
|
54
|
+
logger.info(
|
|
55
|
+
f"Executing tool: {invocation.tool_name}",
|
|
56
|
+
extra={
|
|
57
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
58
|
+
"call_id": invocation.tool_call_id,
|
|
59
|
+
"arguments": invocation.args,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Build middleware context
|
|
64
|
+
mw_context = {
|
|
65
|
+
"session_id": agent.session.id,
|
|
66
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
67
|
+
"tool_call_id": invocation.tool_call_id,
|
|
68
|
+
"agent_id": agent.name,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Get tool from agent context
|
|
73
|
+
tool = get_tool(agent, invocation.tool_name)
|
|
74
|
+
if tool is None:
|
|
75
|
+
error_msg = f"Unknown tool: {invocation.tool_name}"
|
|
76
|
+
invocation.mark_result(error_msg, is_error=True)
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"Tool not found: {invocation.tool_name}",
|
|
79
|
+
extra={"invocation_id": agent._current_invocation.id if agent._current_invocation else ""},
|
|
80
|
+
)
|
|
81
|
+
return ToolResult.error(error_msg)
|
|
82
|
+
|
|
83
|
+
# === Middleware: on_tool_call ===
|
|
84
|
+
if agent.middleware:
|
|
85
|
+
logger.debug(
|
|
86
|
+
f"Calling middleware: on_tool_call ({invocation.tool_name})",
|
|
87
|
+
extra={"invocation_id": agent._current_invocation.id, "call_id": invocation.tool_call_id},
|
|
88
|
+
)
|
|
89
|
+
hook_result = await agent.middleware.process_tool_call(
|
|
90
|
+
tool, invocation.args, mw_context
|
|
91
|
+
)
|
|
92
|
+
if hook_result.action == HookAction.SKIP:
|
|
93
|
+
logger.warning(
|
|
94
|
+
f"Tool {invocation.tool_name} skipped by middleware",
|
|
95
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
96
|
+
)
|
|
97
|
+
return ToolResult(
|
|
98
|
+
output=hook_result.message or "Skipped by middleware",
|
|
99
|
+
is_error=False,
|
|
100
|
+
)
|
|
101
|
+
elif hook_result.action == HookAction.RETRY and hook_result.modified_data:
|
|
102
|
+
logger.debug(
|
|
103
|
+
f"Tool args modified by middleware",
|
|
104
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
105
|
+
)
|
|
106
|
+
invocation.args = hook_result.modified_data
|
|
107
|
+
|
|
108
|
+
# Create ToolContext
|
|
109
|
+
tool_ctx = ToolContext(
|
|
110
|
+
session_id=agent.session.id,
|
|
111
|
+
invocation_id=agent._current_invocation.id if agent._current_invocation else "",
|
|
112
|
+
block_id="",
|
|
113
|
+
call_id=invocation.tool_call_id,
|
|
114
|
+
agent=agent.config.name,
|
|
115
|
+
abort_signal=agent._abort,
|
|
116
|
+
update_metadata=agent._noop_update_metadata,
|
|
117
|
+
middleware=agent.middleware,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Execute tool (with optional timeout from tool.config)
|
|
121
|
+
timeout = tool.config.timeout
|
|
122
|
+
if timeout is not None:
|
|
123
|
+
result = await asyncio.wait_for(
|
|
124
|
+
tool.execute(invocation.args, tool_ctx),
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
# No timeout - tool runs until completion
|
|
129
|
+
result = await tool.execute(invocation.args, tool_ctx)
|
|
130
|
+
|
|
131
|
+
# === Middleware: on_tool_end ===
|
|
132
|
+
if agent.middleware:
|
|
133
|
+
logger.debug(
|
|
134
|
+
f"Calling middleware: on_tool_end ({invocation.tool_name})",
|
|
135
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
136
|
+
)
|
|
137
|
+
hook_result = await agent.middleware.process_tool_end(tool, result, mw_context)
|
|
138
|
+
if hook_result.action == HookAction.RETRY:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"Tool {invocation.tool_name} retry requested by middleware",
|
|
141
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
invocation.mark_result(result.output, is_error=result.is_error)
|
|
145
|
+
logger.info(
|
|
146
|
+
f"Tool executed: {invocation.tool_name}",
|
|
147
|
+
extra={
|
|
148
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
149
|
+
"call_id": invocation.tool_call_id,
|
|
150
|
+
"is_error": result.is_error,
|
|
151
|
+
"output_length": len(result.output) if result.output else 0,
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
except asyncio.TimeoutError:
|
|
157
|
+
timeout = tool.config.timeout if tool else None
|
|
158
|
+
error_msg = f"Tool {invocation.tool_name} timed out after {timeout}s"
|
|
159
|
+
invocation.mark_result(error_msg, is_error=True)
|
|
160
|
+
logger.error(
|
|
161
|
+
f"Tool timeout: {invocation.tool_name}",
|
|
162
|
+
extra={
|
|
163
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
164
|
+
"timeout": timeout,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
return ToolResult.error(error_msg)
|
|
168
|
+
|
|
169
|
+
except SuspendSignal:
|
|
170
|
+
# HITL/Suspend signal must propagate up
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
error_msg = f"Tool execution error: {str(e)}"
|
|
175
|
+
invocation.mark_result(error_msg, is_error=True)
|
|
176
|
+
logger.error(
|
|
177
|
+
f"Tool execution failed: {invocation.tool_name}",
|
|
178
|
+
extra={
|
|
179
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
180
|
+
"error": str(e),
|
|
181
|
+
},
|
|
182
|
+
exc_info=True,
|
|
183
|
+
)
|
|
184
|
+
return ToolResult.error(error_msg)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def process_tool_results(agent: "ReactAgent") -> None:
|
|
188
|
+
"""Execute tool calls and add results to history.
|
|
189
|
+
|
|
190
|
+
Executes tools in parallel or sequentially based on config.
|
|
191
|
+
|
|
192
|
+
This function directly modifies agent's internal state:
|
|
193
|
+
- agent._message_history
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
agent: ReactAgent instance
|
|
197
|
+
"""
|
|
198
|
+
if not agent._tool_invocations:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"Executing {len(agent._tool_invocations)} tools",
|
|
203
|
+
extra={
|
|
204
|
+
"invocation_id": agent._current_invocation.id,
|
|
205
|
+
"mode": "parallel" if agent.config.parallel_tool_execution else "sequential",
|
|
206
|
+
"tools": [inv.tool_name for inv in agent._tool_invocations],
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Execute tools based on configuration
|
|
211
|
+
if agent.config.parallel_tool_execution:
|
|
212
|
+
# Parallel execution using asyncio.gather with create_task
|
|
213
|
+
# create_task ensures each task gets its own ContextVar copy
|
|
214
|
+
tasks = [asyncio.create_task(execute_tool(agent, inv)) for inv in agent._tool_invocations]
|
|
215
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
216
|
+
else:
|
|
217
|
+
# Sequential execution
|
|
218
|
+
results = []
|
|
219
|
+
for inv in agent._tool_invocations:
|
|
220
|
+
try:
|
|
221
|
+
result = await execute_tool(agent, inv)
|
|
222
|
+
results.append(result)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
results.append(e)
|
|
225
|
+
|
|
226
|
+
# Check for SuspendSignal - record tool_results first, then propagate
|
|
227
|
+
suspend_signal = None
|
|
228
|
+
for result in results:
|
|
229
|
+
if isinstance(result, SuspendSignal):
|
|
230
|
+
suspend_signal = result
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
if suspend_signal:
|
|
234
|
+
# Must record tool_results to history BEFORE suspending
|
|
235
|
+
# Otherwise Claude API will reject next request (tool_use without tool_result)
|
|
236
|
+
for invocation, result in zip(agent._tool_invocations, results):
|
|
237
|
+
if isinstance(result, SuspendSignal):
|
|
238
|
+
# HITL tool - record placeholder result
|
|
239
|
+
invocation.mark_result(f"[等待用户输入: {suspend_signal.request_type}]", is_error=False)
|
|
240
|
+
else:
|
|
241
|
+
# Normal tool result
|
|
242
|
+
invocation.mark_result(
|
|
243
|
+
result.output if hasattr(result, 'output') else str(result),
|
|
244
|
+
is_error=getattr(result, 'is_error', False)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Add to in-memory history
|
|
248
|
+
agent._message_history.append(
|
|
249
|
+
LLMMessage(
|
|
250
|
+
role="tool",
|
|
251
|
+
content=invocation.result,
|
|
252
|
+
tool_call_id=invocation.tool_call_id,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Persist via _save_tool_messages
|
|
257
|
+
await agent._save_tool_messages()
|
|
258
|
+
|
|
259
|
+
logger.info(
|
|
260
|
+
"Tool execution suspended (HITL), tool_results recorded",
|
|
261
|
+
extra={"invocation_id": agent._current_invocation.id},
|
|
262
|
+
)
|
|
263
|
+
raise suspend_signal
|
|
264
|
+
|
|
265
|
+
# Process results
|
|
266
|
+
tool_results = []
|
|
267
|
+
|
|
268
|
+
for invocation, result in zip(agent._tool_invocations, results):
|
|
269
|
+
# Handle exceptions from gather
|
|
270
|
+
if isinstance(result, Exception):
|
|
271
|
+
error_msg = f"Tool execution error: {str(result)}"
|
|
272
|
+
invocation.mark_result(error_msg, is_error=True)
|
|
273
|
+
result = ToolResult.error(error_msg)
|
|
274
|
+
|
|
275
|
+
# Get parent block_id from tool_call mapping
|
|
276
|
+
parent_block_id = agent._tool_call_blocks.get(invocation.tool_call_id)
|
|
277
|
+
|
|
278
|
+
await agent.ctx.emit(BlockEvent(
|
|
279
|
+
kind=BlockKind.TOOL_RESULT,
|
|
280
|
+
op=BlockOp.APPLY,
|
|
281
|
+
parent_id=parent_block_id,
|
|
282
|
+
data={
|
|
283
|
+
"call_id": invocation.tool_call_id,
|
|
284
|
+
"content": result.output,
|
|
285
|
+
"is_error": invocation.is_error,
|
|
286
|
+
},
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
await agent.bus.publish(
|
|
290
|
+
Events.TOOL_END,
|
|
291
|
+
{
|
|
292
|
+
"call_id": invocation.tool_call_id,
|
|
293
|
+
"tool": invocation.tool_name,
|
|
294
|
+
"result": result.output[:500], # Truncate for event
|
|
295
|
+
"is_error": invocation.is_error,
|
|
296
|
+
"duration_ms": invocation.duration_ms,
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
tool_results.append(
|
|
301
|
+
{
|
|
302
|
+
"type": "tool_result",
|
|
303
|
+
"tool_use_id": invocation.tool_call_id,
|
|
304
|
+
"content": result.output,
|
|
305
|
+
"is_error": invocation.is_error,
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Add tool results as tool messages (OpenAI format)
|
|
310
|
+
for tr in tool_results:
|
|
311
|
+
print(f"[DEBUG _process_tool_results] Adding tool_result to history: {tr}")
|
|
312
|
+
agent._message_history.append(
|
|
313
|
+
LLMMessage(
|
|
314
|
+
role="tool",
|
|
315
|
+
content=tr["content"],
|
|
316
|
+
tool_call_id=tr["tool_use_id"],
|
|
317
|
+
)
|
|
318
|
+
)
|
aury/agents/tool/builtin/bash.py
CHANGED
|
@@ -6,6 +6,7 @@ import subprocess
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from ...core.types.tool import BaseTool, ToolContext, ToolResult, ToolInfo
|
|
9
|
+
from ...core.logging import tool_logger as logger
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class BashTool(BaseTool):
|
|
@@ -57,17 +58,23 @@ class BashTool(BaseTool):
|
|
|
57
58
|
command = params.get("command", "")
|
|
58
59
|
timeout = params.get("timeout", 30)
|
|
59
60
|
working_dir = params.get("working_dir")
|
|
61
|
+
invocation_id = ctx.invocation_id if ctx else None
|
|
62
|
+
|
|
63
|
+
logger.info(f"BashTool executing command, invocation_id={invocation_id}, timeout={timeout}")
|
|
60
64
|
|
|
61
65
|
if not command:
|
|
66
|
+
logger.warning(f"BashTool: command is empty, invocation_id={invocation_id}")
|
|
62
67
|
return ToolResult.error("Command is required")
|
|
63
68
|
|
|
64
69
|
# Security check
|
|
65
70
|
if self._allowed_commands:
|
|
66
71
|
cmd_prefix = command.split()[0] if command.split() else ""
|
|
67
72
|
if cmd_prefix not in self._allowed_commands:
|
|
73
|
+
logger.warning(f"BashTool: command not allowed, cmd={cmd_prefix}, invocation_id={invocation_id}")
|
|
68
74
|
return ToolResult.error(f"Command '{cmd_prefix}' is not allowed")
|
|
69
75
|
|
|
70
76
|
try:
|
|
77
|
+
logger.debug(f"BashTool: starting subprocess, working_dir={working_dir}, invocation_id={invocation_id}")
|
|
71
78
|
# Run command
|
|
72
79
|
process = await asyncio.create_subprocess_shell(
|
|
73
80
|
command,
|
|
@@ -83,12 +90,14 @@ class BashTool(BaseTool):
|
|
|
83
90
|
)
|
|
84
91
|
except asyncio.TimeoutError:
|
|
85
92
|
process.kill()
|
|
93
|
+
logger.warning(f"BashTool: command timeout, timeout={timeout}, invocation_id={invocation_id}")
|
|
86
94
|
return ToolResult.error(f"Command timed out after {timeout} seconds")
|
|
87
95
|
|
|
88
96
|
output = stdout.decode("utf-8", errors="replace")
|
|
89
97
|
error_output = stderr.decode("utf-8", errors="replace")
|
|
90
98
|
|
|
91
99
|
if process.returncode != 0:
|
|
100
|
+
logger.info(f"BashTool: command failed, exit_code={process.returncode}, invocation_id={invocation_id}")
|
|
92
101
|
return ToolResult(
|
|
93
102
|
output=f"Exit code: {process.returncode}\n\nSTDOUT:\n{output}\n\nSTDERR:\n{error_output}",
|
|
94
103
|
is_error=True,
|
|
@@ -98,9 +107,11 @@ class BashTool(BaseTool):
|
|
|
98
107
|
if error_output:
|
|
99
108
|
result_output += f"\n\nSTDERR:\n{error_output}"
|
|
100
109
|
|
|
110
|
+
logger.info(f"BashTool: command completed successfully, output_len={len(result_output)}, invocation_id={invocation_id}")
|
|
101
111
|
return ToolResult(output=result_output.strip() or "(no output)")
|
|
102
112
|
|
|
103
113
|
except Exception as e:
|
|
114
|
+
logger.error(f"BashTool: execution error, error={type(e).__name__}, invocation_id={invocation_id}", exc_info=True)
|
|
104
115
|
return ToolResult.error(str(e))
|
|
105
116
|
|
|
106
117
|
|
|
@@ -227,11 +227,22 @@ Specify the agent key and task data."""
|
|
|
227
227
|
if not agent_key:
|
|
228
228
|
return ToolResult.error("Missing 'agent' parameter")
|
|
229
229
|
|
|
230
|
-
logger.info(
|
|
230
|
+
logger.info(
|
|
231
|
+
"Delegating to sub-agent",
|
|
232
|
+
extra={
|
|
233
|
+
"agent": agent_key,
|
|
234
|
+
"invocation_id": ctx.invocation_id,
|
|
235
|
+
"session_id": ctx.session_id,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
231
238
|
|
|
232
239
|
# Get agent config (static or dynamic)
|
|
233
240
|
config = await self._get_agent_config(agent_key, ctx)
|
|
234
241
|
if config is None:
|
|
242
|
+
logger.error(
|
|
243
|
+
f"Unknown sub-agent: {agent_key}",
|
|
244
|
+
extra={"invocation_id": ctx.invocation_id},
|
|
245
|
+
)
|
|
235
246
|
agents = await self._get_all_agents(ctx)
|
|
236
247
|
available = ", ".join(a.key for a in agents) or "none"
|
|
237
248
|
return ToolResult.error(f"Unknown agent: {agent_key}. Available: {available}")
|
|
@@ -246,6 +257,12 @@ Specify the agent key and task data."""
|
|
|
246
257
|
|
|
247
258
|
try:
|
|
248
259
|
# 根据 create_invocation 决定执行模式
|
|
260
|
+
mode = "delegated" if config.create_invocation else "embedded"
|
|
261
|
+
logger.debug(
|
|
262
|
+
f"Executing sub-agent in {mode} mode",
|
|
263
|
+
extra={"agent": agent_key, "invocation_id": ctx.invocation_id},
|
|
264
|
+
)
|
|
265
|
+
|
|
249
266
|
if config.create_invocation:
|
|
250
267
|
result = await self._execute_delegated(
|
|
251
268
|
config, ctx, task_context, artifact_refs
|
|
@@ -255,6 +272,15 @@ Specify the agent key and task data."""
|
|
|
255
272
|
config, ctx, task_context, artifact_refs
|
|
256
273
|
)
|
|
257
274
|
|
|
275
|
+
logger.info(
|
|
276
|
+
f"Sub-agent execution completed",
|
|
277
|
+
extra={
|
|
278
|
+
"agent": agent_key,
|
|
279
|
+
"invocation_id": ctx.invocation_id,
|
|
280
|
+
"is_error": result.is_error,
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
258
284
|
# Emit SUB_AGENT block (end)
|
|
259
285
|
await self._emit_subagent_block(ctx, config, "end", block_id)
|
|
260
286
|
|
|
@@ -263,7 +289,11 @@ Specify the agent key and task data."""
|
|
|
263
289
|
|
|
264
290
|
return result
|
|
265
291
|
except Exception as e:
|
|
266
|
-
logger.error(
|
|
292
|
+
logger.error(
|
|
293
|
+
"Sub-agent delegation failed",
|
|
294
|
+
extra={"agent": agent_key, "invocation_id": ctx.invocation_id, "error": str(e)},
|
|
295
|
+
exc_info=True,
|
|
296
|
+
)
|
|
267
297
|
# Emit error state
|
|
268
298
|
await self._emit_subagent_block(ctx, config, "error", block_id)
|
|
269
299
|
self._active_block_ids.pop(block_key, None)
|
|
@@ -285,7 +315,12 @@ Specify the agent key and task data."""
|
|
|
285
315
|
"""
|
|
286
316
|
logger.info(
|
|
287
317
|
"Parallel delegation",
|
|
288
|
-
extra={
|
|
318
|
+
extra={
|
|
319
|
+
"agents": [a.get("agent") for a in agents_param],
|
|
320
|
+
"invocation_id": ctx.invocation_id,
|
|
321
|
+
"session_id": ctx.session_id,
|
|
322
|
+
"count": len(agents_param),
|
|
323
|
+
},
|
|
289
324
|
)
|
|
290
325
|
|
|
291
326
|
# Validate all agents exist
|
aury/agents/tool/builtin/edit.py
CHANGED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Literal
|
|
6
6
|
|
|
7
7
|
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
8
|
+
from ...core.logging import tool_logger as logger
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class EditTool(BaseTool):
|
|
@@ -62,33 +63,43 @@ class EditTool(BaseTool):
|
|
|
62
63
|
line = params.get("line")
|
|
63
64
|
encoding = params.get("encoding", "utf-8")
|
|
64
65
|
create_dirs = params.get("create_dirs", True)
|
|
66
|
+
invocation_id = ctx.invocation_id if ctx else None
|
|
67
|
+
|
|
68
|
+
logger.info(f"EditTool editing file, path={file_path}, mode={mode}, content_len={len(content)}, invocation_id={invocation_id}")
|
|
65
69
|
|
|
66
70
|
if not file_path:
|
|
71
|
+
logger.warning(f"EditTool: path is empty, invocation_id={invocation_id}")
|
|
67
72
|
return ToolResult.error("Path is required")
|
|
68
73
|
|
|
69
74
|
path = Path(file_path).expanduser().resolve()
|
|
75
|
+
logger.debug(f"EditTool: resolved path, path={path}, invocation_id={invocation_id}")
|
|
70
76
|
|
|
71
77
|
# Security check
|
|
72
78
|
if self._allowed_paths:
|
|
73
79
|
if not any(str(path).startswith(p) for p in self._allowed_paths):
|
|
80
|
+
logger.warning(f"EditTool: path not allowed, path={path}, invocation_id={invocation_id}")
|
|
74
81
|
return ToolResult.error(f"Path not allowed: {path}")
|
|
75
82
|
|
|
76
83
|
try:
|
|
77
84
|
# Create parent directories
|
|
78
85
|
if create_dirs and not path.parent.exists():
|
|
86
|
+
logger.debug(f"EditTool: creating parent directories, path={path.parent}, invocation_id={invocation_id}")
|
|
79
87
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
88
|
|
|
81
89
|
if mode == "overwrite":
|
|
82
90
|
path.write_text(content, encoding=encoding)
|
|
91
|
+
logger.info(f"EditTool: file overwritten, path={path}, content_len={len(content)}, invocation_id={invocation_id}")
|
|
83
92
|
return ToolResult(output=f"File written ({len(content)} chars)")
|
|
84
93
|
|
|
85
94
|
elif mode == "append":
|
|
86
95
|
existing = path.read_text(encoding=encoding) if path.exists() else ""
|
|
87
96
|
path.write_text(existing + content, encoding=encoding)
|
|
97
|
+
logger.info(f"EditTool: content appended, path={path}, appended_len={len(content)}, invocation_id={invocation_id}")
|
|
88
98
|
return ToolResult(output=f"Content appended ({len(content)} chars)")
|
|
89
99
|
|
|
90
100
|
elif mode == "insert":
|
|
91
101
|
if line is None:
|
|
102
|
+
logger.warning(f"EditTool: insert mode requires line number, invocation_id={invocation_id}")
|
|
92
103
|
return ToolResult.error("Line number required for insert mode")
|
|
93
104
|
|
|
94
105
|
if path.exists():
|
|
@@ -96,6 +107,8 @@ class EditTool(BaseTool):
|
|
|
96
107
|
else:
|
|
97
108
|
lines = []
|
|
98
109
|
|
|
110
|
+
logger.debug(f"EditTool: read existing content, total_lines={len(lines)}, invocation_id={invocation_id}")
|
|
111
|
+
|
|
99
112
|
# Pad with empty lines if needed
|
|
100
113
|
while len(lines) < line - 1:
|
|
101
114
|
lines.append("\n")
|
|
@@ -109,12 +122,15 @@ class EditTool(BaseTool):
|
|
|
109
122
|
new_lines = lines[:insert_idx] + content_lines + lines[insert_idx:]
|
|
110
123
|
path.write_text("".join(new_lines), encoding=encoding)
|
|
111
124
|
|
|
125
|
+
logger.info(f"EditTool: content inserted, path={path}, line={line}, inserted_lines={len(content_lines)}, invocation_id={invocation_id}")
|
|
112
126
|
return ToolResult(output=f"Content inserted at line {line} ({len(content_lines)} lines)")
|
|
113
127
|
|
|
114
128
|
else:
|
|
129
|
+
logger.warning(f"EditTool: unknown mode, mode={mode}, invocation_id={invocation_id}")
|
|
115
130
|
return ToolResult.error(f"Unknown mode: {mode}")
|
|
116
131
|
|
|
117
132
|
except Exception as e:
|
|
133
|
+
logger.error(f"EditTool: edit error, error={type(e).__name__}, path={path}, mode={mode}, invocation_id={invocation_id}", exc_info=True)
|
|
118
134
|
return ToolResult.error(str(e))
|
|
119
135
|
|
|
120
136
|
|
aury/agents/tool/builtin/plan.py
CHANGED
|
@@ -100,7 +100,16 @@ Use this to track your progress on complex tasks."""
|
|
|
100
100
|
ctx: ToolContext,
|
|
101
101
|
) -> ToolResult:
|
|
102
102
|
"""Execute plan action."""
|
|
103
|
+
from ...core.logging import tool_logger as logger
|
|
104
|
+
|
|
103
105
|
action = params.get("action", "view")
|
|
106
|
+
logger.info(
|
|
107
|
+
f"Plan tool action: {action}",
|
|
108
|
+
extra={
|
|
109
|
+
"invocation_id": ctx.invocation_id,
|
|
110
|
+
"session_id": ctx.session_id,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
104
113
|
|
|
105
114
|
# Storage key
|
|
106
115
|
key = f"plan:{ctx.session_id}"
|
|
@@ -132,6 +141,16 @@ Use this to track your progress on complex tasks."""
|
|
|
132
141
|
# Save plan
|
|
133
142
|
await storage.set("plan", key, plan)
|
|
134
143
|
|
|
144
|
+
from ...core.logging import tool_logger as logger
|
|
145
|
+
logger.debug(
|
|
146
|
+
f"Plan updated",
|
|
147
|
+
extra={
|
|
148
|
+
"action": action,
|
|
149
|
+
"status": plan.get("status"),
|
|
150
|
+
"item_count": len(plan.get("items", [])),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
135
154
|
# Emit PLAN block
|
|
136
155
|
await self._emit_plan_block(ctx, plan, action)
|
|
137
156
|
|
aury/agents/tool/builtin/read.py
CHANGED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
8
|
+
from ...core.logging import tool_logger as logger
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ReadTool(BaseTool):
|
|
@@ -49,32 +50,42 @@ class ReadTool(BaseTool):
|
|
|
49
50
|
start_line = params.get("start_line")
|
|
50
51
|
end_line = params.get("end_line")
|
|
51
52
|
encoding = params.get("encoding", "utf-8")
|
|
53
|
+
invocation_id = ctx.invocation_id if ctx else None
|
|
54
|
+
|
|
55
|
+
logger.info(f"ReadTool reading file, path={file_path}, start_line={start_line}, end_line={end_line}, invocation_id={invocation_id}")
|
|
52
56
|
|
|
53
57
|
if not file_path:
|
|
58
|
+
logger.warning(f"ReadTool: path is empty, invocation_id={invocation_id}")
|
|
54
59
|
return ToolResult.error("Path is required")
|
|
55
60
|
|
|
56
61
|
path = Path(file_path).expanduser().resolve()
|
|
62
|
+
logger.debug(f"ReadTool: resolved path, path={path}, invocation_id={invocation_id}")
|
|
57
63
|
|
|
58
64
|
# Security check
|
|
59
65
|
if self._allowed_paths:
|
|
60
66
|
if not any(str(path).startswith(p) for p in self._allowed_paths):
|
|
67
|
+
logger.warning(f"ReadTool: path not allowed, path={path}, invocation_id={invocation_id}")
|
|
61
68
|
return ToolResult.error(f"Path not allowed: {path}")
|
|
62
69
|
|
|
63
70
|
if not path.exists():
|
|
71
|
+
logger.warning(f"ReadTool: file not found, path={path}, invocation_id={invocation_id}")
|
|
64
72
|
return ToolResult.error(f"File not found: {path}")
|
|
65
73
|
|
|
66
74
|
if not path.is_file():
|
|
75
|
+
logger.warning(f"ReadTool: not a file, path={path}, invocation_id={invocation_id}")
|
|
67
76
|
return ToolResult.error(f"Not a file: {path}")
|
|
68
77
|
|
|
69
78
|
try:
|
|
70
79
|
content = path.read_text(encoding=encoding)
|
|
71
80
|
lines = content.splitlines(keepends=True)
|
|
81
|
+
logger.debug(f"ReadTool: read file, total_lines={len(lines)}, invocation_id={invocation_id}")
|
|
72
82
|
|
|
73
83
|
# Apply line range
|
|
74
84
|
if start_line is not None or end_line is not None:
|
|
75
85
|
start_idx = (start_line - 1) if start_line else 0
|
|
76
86
|
end_idx = end_line if end_line else len(lines)
|
|
77
87
|
lines = lines[start_idx:end_idx]
|
|
88
|
+
logger.debug(f"ReadTool: applied line range, start_idx={start_idx}, end_idx={end_idx}, selected_lines={len(lines)}, invocation_id={invocation_id}")
|
|
78
89
|
|
|
79
90
|
# Add line numbers
|
|
80
91
|
output_lines = []
|
|
@@ -82,9 +93,11 @@ class ReadTool(BaseTool):
|
|
|
82
93
|
output_lines.append(f"{i:4d}| {line.rstrip()}")
|
|
83
94
|
content = "\n".join(output_lines)
|
|
84
95
|
|
|
96
|
+
logger.info(f"ReadTool: read completed, content_len={len(content)}, invocation_id={invocation_id}")
|
|
85
97
|
return ToolResult(output=content or "(empty file)")
|
|
86
98
|
|
|
87
99
|
except Exception as e:
|
|
100
|
+
logger.error(f"ReadTool: read error, error={type(e).__name__}, path={path}, invocation_id={invocation_id}", exc_info=True)
|
|
88
101
|
return ToolResult.error(str(e))
|
|
89
102
|
|
|
90
103
|
|
|
@@ -70,17 +70,18 @@ This makes your reasoning visible and traceable."""
|
|
|
70
70
|
"""Execute thinking - emit reasoning block."""
|
|
71
71
|
thought = params.get("thought", "")
|
|
72
72
|
category = params.get("category", "analysis")
|
|
73
|
+
invocation_id = ctx.invocation_id if ctx else None
|
|
73
74
|
|
|
74
|
-
logger.
|
|
75
|
-
"
|
|
76
|
-
extra={"category": category, "length": len(thought)},
|
|
75
|
+
logger.info(
|
|
76
|
+
f"ThinkingTool processing, category={category}, thought_len={len(thought)}, invocation_id={invocation_id}"
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
# Emit THINKING block
|
|
80
|
-
await self._emit_thinking_block(ctx, thought, category)
|
|
80
|
+
await self._emit_thinking_block(ctx, thought, category, invocation_id)
|
|
81
81
|
|
|
82
82
|
# Thinking doesn't produce actionable output
|
|
83
83
|
# It's purely for transparency/logging
|
|
84
|
+
logger.debug(f"ThinkingTool completed, invocation_id={invocation_id}")
|
|
84
85
|
return ToolResult(output=thought)
|
|
85
86
|
|
|
86
87
|
async def _emit_thinking_block(
|
|
@@ -88,12 +89,16 @@ This makes your reasoning visible and traceable."""
|
|
|
88
89
|
ctx: ToolContext,
|
|
89
90
|
thought: str,
|
|
90
91
|
category: str,
|
|
92
|
+
invocation_id: int | None = None,
|
|
91
93
|
) -> None:
|
|
92
94
|
"""Emit THINKING block."""
|
|
93
95
|
emit = getattr(ctx, 'emit', None)
|
|
94
96
|
if emit is None:
|
|
97
|
+
logger.debug(f"ThinkingTool: emit not available, invocation_id={invocation_id}")
|
|
95
98
|
return
|
|
96
99
|
|
|
100
|
+
logger.debug(f"ThinkingTool: emitting block, category={category}, invocation_id={invocation_id}")
|
|
101
|
+
|
|
97
102
|
block = BlockEvent(
|
|
98
103
|
kind=BlockKind.THINKING,
|
|
99
104
|
op=BlockOp.APPLY,
|
|
@@ -106,6 +111,7 @@ This makes your reasoning visible and traceable."""
|
|
|
106
111
|
)
|
|
107
112
|
|
|
108
113
|
await emit(block)
|
|
114
|
+
logger.debug(f"ThinkingTool: block emitted successfully, invocation_id={invocation_id}")
|
|
109
115
|
|
|
110
116
|
|
|
111
117
|
__all__ = ["ThinkingTool"]
|