Repository navigation
fix: dispatch response hooks for tool-call steps #10772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d6ba95c
a5e139e
df64734
72e4f13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -838,33 +838,48 @@ async def step(self): | |
| return | ||
| self.run_context.messages = processed_messages | ||
| self._simple_print_message_role("[AftCompact]", self.run_context.messages) | ||
| buffer_streaming_response = self.streaming and bool( | ||
| self.agent_hooks.should_buffer_streaming_response(self.run_context) | ||
| ) | ||
| buffered_streaming_responses: list[AgentResponse] = [] | ||
|
|
||
| async for llm_response in self._iter_llm_responses_with_fallback(): | ||
| if llm_response.is_chunk: | ||
| if self.stats.time_to_first_token == 0: | ||
| self.stats.time_to_first_token = time.time() - self.stats.start_time | ||
|
|
||
| if llm_response.reasoning_content: | ||
| yield AgentResponse( | ||
| response = AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData( | ||
| chain=MessageChain(type="reasoning").message( | ||
| llm_response.reasoning_content, | ||
| ), | ||
| ), | ||
| ) | ||
| if buffer_streaming_response: | ||
| buffered_streaming_responses.append(response) | ||
| else: | ||
| yield response | ||
| if llm_response.result_chain: | ||
| yield AgentResponse( | ||
| response = AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData(chain=llm_response.result_chain), | ||
| ) | ||
| if buffer_streaming_response: | ||
| buffered_streaming_responses.append(response) | ||
| else: | ||
| yield response | ||
| elif llm_response.completion_text: | ||
| yield AgentResponse( | ||
| response = AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData( | ||
| chain=MessageChain().message(llm_response.completion_text), | ||
| ), | ||
| ) | ||
| if buffer_streaming_response: | ||
| buffered_streaming_responses.append(response) | ||
| else: | ||
| yield response | ||
| if self._is_stop_requested(): | ||
| break | ||
| continue | ||
|
|
@@ -894,16 +909,22 @@ async def step(self): | |
| break # got final response | ||
|
|
||
| if self._is_stop_requested(): | ||
| for response in buffered_streaming_responses: | ||
| yield response | ||
| yield await self._finalize_aborted_step() | ||
| return | ||
|
|
||
| if not llm_resp_result: | ||
| for response in buffered_streaming_responses: | ||
| yield response | ||
| return | ||
|
|
||
| # 处理 LLM 响应 | ||
| llm_resp = llm_resp_result | ||
|
|
||
| if llm_resp.role == "err": | ||
| for response in buffered_streaming_responses: | ||
| yield response | ||
| # 如果 LLM 响应错误,转换到错误状态 | ||
| self.final_llm_resp = llm_resp | ||
| self.stats.end_time = time.time() | ||
|
|
@@ -923,6 +944,38 @@ async def step(self): | |
|
|
||
| if not llm_resp.tools_call_name: | ||
| await self._complete_with_assistant_response(llm_resp) | ||
| else: | ||
| try: | ||
| await self.agent_hooks.on_agent_response(self.run_context, llm_resp) | ||
|
sourcery-ai[bot] marked this conversation as resolved.
sourcery-ai[bot] marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High · Canceled requests still execute tools When the runner task is canceled while an Re-raise Prompt for AI agentsIn `astrbot/core/agent/runners/tool_loop_agent_runner.py` at line 949:
**Canceled requests still execute tools**
When the runner task is canceled while an `OnLLMResponseEvent` handler is running, `call_event_hook` catches `BaseException`, including `asyncio.CancelledError`, from a plugin handler and suppresses it. The runner then continues past the response hook into tool execution, so a canceled request can still perform tool side effects.
Re-raise `asyncio.CancelledError` from `call_event_hook` so task cancellation propagates before tool execution. |
||
| except Exception as e: | ||
| logger.error(f"Error in on_agent_response hook: {e}", exc_info=True) | ||
| if not llm_resp.tools_call_name: | ||
| await self._complete_with_assistant_response(llm_resp) | ||
|
|
||
| # Response hooks can modify the complete response. Emit streaming deltas | ||
| # only after they run so clients never receive content the hook changed. | ||
| if buffer_streaming_response: | ||
| if llm_resp.reasoning_content: | ||
| yield AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData( | ||
| chain=MessageChain(type="reasoning").message( | ||
| llm_resp.reasoning_content, | ||
| ), | ||
| ), | ||
| ) | ||
| if llm_resp.result_chain: | ||
| yield AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData(chain=llm_resp.result_chain), | ||
| ) | ||
| elif llm_resp.completion_text: | ||
| yield AgentResponse( | ||
| type="streaming_delta", | ||
| data=AgentResponseData( | ||
| chain=MessageChain().message(llm_resp.completion_text), | ||
| ), | ||
| ) | ||
|
|
||
| # 返回 LLM 结果 | ||
| if llm_resp.reasoning_content: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Long streams retain every chunk
When a hooked streaming response contains a very large number of chunks, with an external response handler enabled, the runner retains a separate
AgentResponsefor every streamed chunk until the provider finishes. Very long outputs therefore accumulate unbounded per-chunk objects in memory, increasing memory pressure or exhausting the agent process.Bound or compact buffered partial output while preserving the ability to flush it on failure or stop.
Prompt for AI agents