fix(agent): on_progress tool_events only when callback accepts; align progress tests with main

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-04-23 20:06:11 +08:00
committed by Xubin Ren
parent c23d719780
commit 469fc90fe6
2 changed files with 50 additions and 6 deletions
+42 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import dataclasses import dataclasses
import inspect
import json import json
import os import os
import time import time
@@ -104,17 +105,32 @@ class _LoopHook(AgentHook):
await self._on_progress(thought) await self._on_progress(thought)
tool_hint = self._loop._strip_think(self._loop._tool_hint(context.tool_calls)) tool_hint = self._loop._strip_think(self._loop._tool_hint(context.tool_calls))
tool_events = [self._loop._tool_event_start_payload(tc) for tc in context.tool_calls] tool_events = [self._loop._tool_event_start_payload(tc) for tc in context.tool_calls]
await self._on_progress(tool_hint, tool_hint=True, tool_events=tool_events) await self._loop._invoke_on_progress(
self._on_progress,
tool_hint,
tool_hint=True,
tool_events=tool_events,
)
for tc in context.tool_calls: for tc in context.tool_calls:
args_str = json.dumps(tc.arguments, ensure_ascii=False) args_str = json.dumps(tc.arguments, ensure_ascii=False)
logger.info("Tool call: {}({})", tc.name, args_str[:200]) logger.info("Tool call: {}({})", tc.name, args_str[:200])
self._loop._set_tool_context(self._channel, self._chat_id, self._message_id) self._loop._set_tool_context(self._channel, self._chat_id, self._message_id)
async def after_iteration(self, context: AgentHookContext) -> None: async def after_iteration(self, context: AgentHookContext) -> None:
if self._on_progress and context.tool_calls and context.tool_events: if (
self._on_progress
and context.tool_calls
and context.tool_events
and self._loop._on_progress_accepts_tool_events(self._on_progress)
):
tool_events = self._loop._tool_event_finish_payloads(context) tool_events = self._loop._tool_event_finish_payloads(context)
if tool_events: if tool_events:
await self._on_progress("", tool_events=tool_events) await self._loop._invoke_on_progress(
self._on_progress,
"",
tool_hint=False,
tool_events=tool_events,
)
u = context.usage or {} u = context.usage or {}
logger.debug( logger.debug(
"LLM usage: prompt={} completion={} cached={}", "LLM usage: prompt={} completion={} cached={}",
@@ -380,6 +396,29 @@ class AgentLoop:
sub_cancelled = await self.subagents.cancel_by_session(key) sub_cancelled = await self.subagents.cancel_by_session(key)
return cancelled + sub_cancelled return cancelled + sub_cancelled
@staticmethod
def _on_progress_accepts_tool_events(cb: Callable[..., Any]) -> bool:
try:
sig = inspect.signature(cb)
except (TypeError, ValueError):
return False
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
return True
return "tool_events" in sig.parameters
@staticmethod
async def _invoke_on_progress(
on_progress: Callable[..., Awaitable[None]],
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
) -> None:
if tool_events and AgentLoop._on_progress_accepts_tool_events(on_progress):
await on_progress(content, tool_hint=tool_hint, tool_events=tool_events)
else:
await on_progress(content, tool_hint=tool_hint)
@staticmethod @staticmethod
def _tool_event_start_payload(tool_call: Any) -> dict[str, Any]: def _tool_event_start_payload(tool_call: Any) -> dict[str, Any]:
return { return {
+8 -3
View File
@@ -99,12 +99,17 @@ class TestToolEventProgress:
loop.tools.prepare_call = MagicMock(return_value=(None, {"command": "ls"}, None)) loop.tools.prepare_call = MagicMock(return_value=(None, {"command": "ls"}, None))
loop.tools.execute = AsyncMock(return_value="file.txt") loop.tools.execute = AsyncMock(return_value="file.txt")
msg = InboundMessage(channel="telegram", chat_id="chat1", content="run ls") msg = InboundMessage(
await loop.run(msg) channel="telegram",
sender_id="u1",
chat_id="chat1",
content="run ls",
)
await loop._dispatch(msg)
# Drain all outbound messages and find the one carrying _tool_events # Drain all outbound messages and find the one carrying _tool_events
outbound = [] outbound = []
while bus.outbound_size() > 0: while bus.outbound_size > 0:
outbound.append(await bus.consume_outbound()) outbound.append(await bus.consume_outbound())
tool_event_msgs = [m for m in outbound if m.metadata and m.metadata.get("_tool_events")] tool_event_msgs = [m for m in outbound if m.metadata and m.metadata.get("_tool_events")]