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.
@@ -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
+ )
@@ -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("Delegating to sub-agent", extra={"agent": agent_key})
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("Delegation failed", extra={"agent": agent_key, "error": str(e)})
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={"agents": [a.get("agent") for a in agents_param]},
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
@@ -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
 
@@ -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
 
@@ -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.debug(
75
- "Agent thinking",
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"]