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.
@@ -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 execute_tool(agent: "ReactAgent", invocation: ToolInvocation) -> ToolResult:
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(error_msg, is_error=True)
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
- if agent.middleware:
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=tool_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
- # Execute tool (with optional timeout from tool.config)
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
- if timeout is not None:
130
- result = await asyncio.wait_for(
131
- tool.execute(invocation.args, tool_ctx),
132
- timeout=timeout,
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
- else:
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
- invocation.mark_result(result.output, is_error=result.is_error)
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(error_msg, is_error=True)
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(error_msg, is_error=True)
186
- logger.error(
187
- f"Tool execution failed: {invocation.tool_name} - {error_type}: {e}\n{stack_trace}",
188
- extra={
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 add results to history.
361
+ """Execute tool calls and emit results in real-time.
199
362
 
200
- Executes tools in parallel or sequentially based on config.
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 using asyncio.gather with create_task
232
- # create_task ensures each task gets its own ContextVar copy
233
- tasks = [asyncio.create_task(execute_tool(agent, inv)) for inv in agent._tool_invocations]
234
- results = await asyncio.gather(*tasks, return_exceptions=True)
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
- # Check abort after parallel execution - cancel remaining if aborted
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(f"Aborted before execution")
254
- results.append(error_result)
255
- inv.mark_result(error_result.output, is_error=True)
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
- results.append(result)
261
- except Exception as e:
262
- results.append(e)
263
-
264
- # Check for SuspendSignal - record tool_results first, then propagate
265
- suspend_signal = None
266
- for result in results:
267
- if isinstance(result, SuspendSignal):
268
- suspend_signal = result
269
- break
270
-
271
- if suspend_signal:
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
- # Add to in-memory history (use truncated for context window)
289
- agent._message_history.append(
290
- LLMMessage(
291
- role="tool",
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
- # Persist via _save_tool_messages
300
- await agent._save_tool_messages()
301
-
302
- logger.info(
303
- "Tool execution suspended (HITL), tool_results recorded",
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"]
@@ -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