Skip to content

fix: consume injected tool call results in tool loop runner - #9673

Open
Rail1bc wants to merge 4 commits into
AstrBotDevs:masterfrom
Rail1bc:fix/fake-tool-call-injection
Open

fix: consume injected tool call results in tool loop runner#9673
Rail1bc wants to merge 4 commits into
AstrBotDevs:masterfrom
Rail1bc:fix/fake-tool-call-injection

Conversation

@Rail1bc

@Rail1bc Rail1bc commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Motivation / 动机

通过复用现有的 ProviderRequest.tool_calls_result ,允许插件注入伪造的 assistant(tool_calls) → tool(result)
消息对,强制 LLM 认为自己已经调用了某个工具并拿到了结果,从而:

  • 跳过 LLM 自身的工具调用决策,节省 token
  • 降低延迟(不需要等 LLM 生成工具调用)
  • 增加确定性(固定或规则触发,不受模型输出波动影响)

这是一种通用手法,已在 LivingMemory 插件中长期记忆注入中使用,未来可能被更多插件用于各种确定性工具调用场景。

初版方案在 OpenAI_Provider 内做启发式重排(从尾部弹出 user 消息,再收集 assistant(tool_calls)+tool
对重排),存在两个问题:

  1. 平台特判:只覆盖 OpenAI_Provider,Anthropic / Gemini / OpenAI Responses 等其他 provider
    不重排,注入顺序依然错误
  2. 启发式猜测:按消息模式判断哪些是伪造对,真实工具调用场景存在误判/误杀风险

本 PR 使用@whatevertogo 审查建议的机制方案:tool_calls_result 通道本就存在,且所有 provider 在组装 text_chat 载荷时都按 history → user → assistant(tool_calls) → tool(result) 顺序消费它。让 ToolLoopAgentRunner.reset()
以相同顺序把注入对绑定到会话上下文,保证:

  • 顺序天然正确(紧跟当前用户消息之后)
  • 跨 provider 一致(四条 provider 路径行为相同)
  • 无需启发式检测,不会误杀真实工具调用

Refs: #9451

Modifications / 改动点

astrbot/core/agent/runners/tool_loop_agent_runner.py

ToolLoopAgentRunner.reset() 在拼接 [history, 当前用户消息] 之后,消费 request.tool_calls_result(单个
ToolCallsResult 或列表),经 to_openai_messages_model() 追加伪造的 assistant(tool_calls) → tool(result)
消息,使该块属于当前轮次,与 provider 载荷组装顺序完全一致。

astrbot/api/provider/__init__.py

公开导出 AssistantMessageSegmentToolCallMessageSegmentToolCallsResult,插件可通过公共 API
路径构造并注入伪造对。

astrbot/core/agent/message.py

新增 Message.mark_as_temp():标记消息为仅 provider 可见、不落库。伪造的 assistant(tool_calls) 与 tool(result)
成对调用,避免历史中残留悬空 tool 消息。

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

_save_to_history 持久化过滤由按角色(仅 assistant/user)改为按 _no_save 标记,临时注入的消息(含 tool
角色)一律不落库。

docs/zh/dev/star/plugin.md

新增"注入工具调用结果"小节,说明 req.append_tool_calls_result() 用法、顺序保证与成对 .mark_as_temp() 语义。

  • 不依赖具体插件实现,通用机制

  • 不改动 req.contexts,不破坏其他 on_llm_request handler

  • 覆盖全部 provider 路径

  • 真实工具调用场景不受影响

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Test Results / 测试结果

新增 3 个测试:

  • test_reset_appends_injected_tool_calls_result_after_user — 顺序断言 [user, assistant, user, assistant(tc), tool]
  • test_runner_with_openai_provider_preserves_injected_tool_calls_order — 走真实 ProviderOpenAIOfficial
    端到端验证(mock _query,无网络)
  • test_temp_injected_tool_pair_not_persisted — 伪造对整对不落库,真实 tool 消息正常落库

验证结果:

  • tests/test_tool_loop_agent_runner.py + tests/test_conversation_checkpoint.py59 passed
  • tests/unit/test_astr_main_agent.py102 passed
  • ruff check / format:全部通过

Checklist / 检查清单

  • 😊 新功能已通过 Issue 与作者讨论
  • 👀 我的更改经过了充分测试,测试结果已在上方提供
  • 🤓 无新依赖引入
  • 😮 无恶意代码

Summary by Sourcery

Handle plugin-injected tool call results via ProviderRequest.tool_calls_result so they are bound to the current user turn and consumed consistently across providers without affecting real tool usage or other handlers.

New Features:

  • Expose AssistantMessageSegment, ToolCallMessageSegment, and ToolCallsResult in the public provider API for plugins to construct and inject synthetic tool call/result pairs.
  • Add Message.mark_as_temp() to flag provider-only, non-persisted messages for temporary injected tool call pairs.

Enhancements:

  • Update ToolLoopAgentRunner.reset() to append injected tool_calls_result messages after the current user message, aligning in-run context with provider payload assembly order.
  • Change history persistence in InternalAgentSubStage._save_to_history to honor the _no_save flag on any message role, preventing temporary injected tool messages from being stored.

Documentation:

  • Document the recommended plugin pattern for injecting tool call results using req.append_tool_calls_result(), including order guarantees and mark_as_temp() semantics in the Chinese dev plugin guide.

Tests:

  • Add tests to verify injected tool_calls_result are appended after the current user message, preserved in OpenAI provider payloads, and that temp-marked injected tool pairs are not persisted to conversation history.

Plugins can inject fake assistant(tool_calls) → tool(result) pairs via
req.append_tool_calls_result(); reset() now appends them after the current
user message so the block belongs to the current turn, matching how
providers assemble the text_chat payload. Export the segment/result types
from astrbot.api.provider and make _save_to_history skip any _no_save
message regardless of role, so temp-marked fake pairs never persist.

Co-Authored-By: deepseek-v4-flash <deepseek-ai@claude-code-best.win>
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. feature:plugin The bug / feature is about AstrBot plugin system. labels Aug 14, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@whatevertogo

Copy link
Copy Markdown
Contributor

@whatevertogo review it

@whatevertogo whatevertogo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

审查会话:d87ecbed-3927-429e-96ee-00a094e29ebf
触发评论:5288467560
Head SHA:43913ba1252c19c491d0160a7ea7ccf1502b7643

已发布 8 条行内审查评论。最终总结将作为单独评论发布。

  • 最高风险:P2 partial mark_as_temp 会在持久化历史中留下悬空 tool 消息,导致下一轮 provider API 拒绝
  • 暂无法定位的发现:0

if message.role in ["assistant", "user"] and message._no_save:
# _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的
# tool 消息)一律不落库,避免历史中残留悬空的 tool 消息。
if message._no_save:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P2][Advisory][high 置信度] partial mark_as_temp 会在持久化历史中留下悬空 tool 消息,导致下一轮 provider API 拒绝

类别:Correctness
证据:RIGHT 470 if message._no_save: continue 替代了旧的 role in ["assistant", "user"] 检查。旧代码不对 tool 角色检查 _no_save,所以即使插件设置了 _no_save=Truetool 消息也总是被持久化——这恰好避免了悬空 assistant(tool_calls) 的情况。新代码正确过滤 tool 消息,但如果 mark_as_temp() 未成对使用,就会产生悬空消息。OpenAI API 要求 "messages with tool_calls must be followed by tool messages",Anthropic 要求 tool_use 后跟 tool_result,Gemini 要求 function_call 后跟 function_response_save_to_historymark_as_temp() 都没有验证配对一致性。
问题:PR 将 _save_to_history 的过滤条件从 role in ["assistant", "user"] and _no_save 改为纯 _no_save,使 tool 角色消息现在也会被 _no_save 过滤。这引入了一个新的失败模式:如果插件只对注入对的一个消息调用 mark_as_temp()(例如标记了 ToolCallMessageSegment.mark_as_temp() 但忘记标记 AssistantMessageSegment),_save_to_history 会过滤掉 tool 消息但保留 assistant(tool_calls) 消息,在持久化历史中留下悬空的 assistant(tool_calls)。下一轮加载该历史并发送给 provider 时,OpenAI API 要求 assistant(tool_calls) 后必须跟随对应的 tool 消息,会直接拒绝请求;Anthropic/Gemini 有相同的 tool_use/tool_result 配对约束。
项目上下文:mark_as_temp() 是本 PR 新增并经 astrbot.api.provider 导出的公共 API,插件开发者是主要使用者。仓库 AGENTS.md 强调 KISS 和 first principles——最小正确实现应包含对自身引入的不变式(成对 temp)的验证或防护。
影响:插件作者忘记成对标记时,当前轮运行正常,但下一轮所有 provider API 调用都会因消息结构非法而被拒绝。错误出现在与触发错误不同的轮次,极难调试。考虑到该 API 面向 1000+ 插件生态,误用概率不低。
修复建议:在 _save_to_history 的过滤循环中增加悬空检测:如果保留的 assistant(tool_calls) 消息后紧邻的不是对应 tool 消息(被 _no_save 过滤掉了),记录 warning 日志并跳过该 assistant 消息。或者在 ToolCallsResult.to_openai_messages_model() 中验证 tool_calls_info._no_save 与所有 tool_calls_result[i]._no_save 一致,不一致时抛出 ValueError

Comment thread astrbot/core/agent/message.py Outdated
def mark_as_temp(self) -> "Message":
"""Mark this message as provider-facing only, not persisted.

临时注入(如伪造工具调用对)应成对标记:assistant 与 tool 消息都调用

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][high 置信度] mark_as_temp() docstring 使用中文且缺少 Google 格式 Returns 段

类别:Tests/API Contract
证据:astrbot/core/agent/message.py RIGHT 218-225,docstring 正文 临时注入(如伪造工具调用对)应成对标记... 为中文;方法返回 self(用于链式调用)但无 Returns: 段。对比同文件 ContentPart.mark_as_temp()(line 68-71)使用简洁英文单行 docstring。
问题:mark_as_temp() 的 docstring 正文为中文,且缺少 Google 格式的 Returns: 段。AGENTS.md 规则 #5 要求 "Use English for all comments and logs",规则 "Mandatory Google-Style Docstrings" 要求所有 docstring 严格使用 Google 格式。该方法是新增的公共 API(经 astrbot.api.provider 导出供插件使用),文档质量直接影响插件开发者。
项目上下文:AGENTS.md 将英文注释和 Google 格式 docstring 列为基本开发要求(Basic rule #5 + Mandatory Google-Style Docstrings 小节)。
影响:公共 API 的中文 docstring 会在 IDE hover / 自动文档生成中展示中文,与项目全英文策略不一致;缺失 Returns: 降低了类型可读性。
修复建议:将 docstring 改为英文并补充 Returns: 段,例如:"""Mark this message as provider-facing only, not persisted.\n\nInjected pairs (e.g. fake tool calls) should mark both the\nassistant and tool messages so no dangling tool message\nremains in history.\n\nReturns:\n Self, for method chaining.\n"""

skipped_initial_system = True
continue
if message.role in ["assistant", "user"] and message._no_save:
# _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][high 置信度] _save_to_history 注释使用中文,违反 AGENTS.md 英文注释规则

类别:Tests/API Contract
证据:internal.py RIGHT 468-469,注释 # _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的 # tool 消息)一律不落库... 为中文。
问题:_save_to_history 中新增的代码注释为中文,AGENTS.md 规则 #5 要求 "Use English for all comments and logs"。
项目上下文:AGENTS.md Basic rule #5 明确要求所有注释使用英文。同 PR 中 tool_loop_agent_runner.py 的对应注释(RIGHT 318-323)已正确使用英文。
影响:仓库注释语言不统一,后续维护者可能难以保持一致性。
修复建议:改为英文,例如:# _no_save is role-agnostic: temp-injected messages (including the tool message in a fake tool-call pair) are never persisted to avoid dangling tool messages in history.

_checkpoint_after: CheckpointData | None = PrivateAttr(default=None)

def mark_as_temp(self) -> "Message":
"""Mark this message as provider-facing only, not persisted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][high 置信度] mark_as_temp() docstring 为中文且缺少 Google 格式 Returns 段

类别:Tests/API Contract
证据:RIGHT 219-223 docstring 正文 临时注入(如伪造工具调用对)应成对标记... 为中文;方法返回 self(用于链式调用如 AssistantMessageSegment(...).mark_as_temp())但无 Returns: 描述。同模块 ContentPart.mark_as_temp()(line 68-69)使用简洁英文单行 docstring,本方法未保持一致。
问题:新增的 mark_as_temp() 是公共 API(经 astrbot.api.provider 导出供插件使用),但其 docstring 正文使用中文,且缺少 Google 格式的 Returns: 段。
项目上下文:AGENTS.md Basic rule #5 要求 "Use English for all comments and logs";"Mandatory Google-Style Docstrings" 要求所有 docstring 严格使用 Google 格式(Args:Returns:Raises:)。
影响:公共 API 的中文 docstring 会在 IDE hover / 自动文档中展示中文,与项目全英文策略不一致;缺失 Returns: 降低链式调用场景的类型可读性。
修复建议:改为英文并补充 Returns: 段,例如:

def mark_as_temp(self) -> "Message":
"""Mark this message as provider-facing only, not persisted.

Injected pairs (e.g. fake tool-call results) should mark both the
assistant and tool messages to avoid dangling tool messages in history.

Returns:
Self, for method chaining.
"""
self._no_save = True
return self

Comment thread astrbot/core/agent/message.py Outdated
_no_save: bool = PrivateAttr(default=False)
_checkpoint_after: CheckpointData | None = PrivateAttr(default=None)

def mark_as_temp(self) -> "Message":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][medium 置信度] mark_as_temp() 返回类型标注为 Message 而非子类类型,与 ContentPart 同名方法不一致

类别:Tests/API Contract
证据:RIGHT 218 def mark_as_temp(self) -> "Message":。同模块 ContentPart.mark_as_temp()(line 68)使用 TypeVar 模式 def mark_as_temp(self: ContentPartT) -> ContentPartT 正确保留子类类型。ToolCallsResult.tool_calls_info: AssistantMessageSegment(entities.py line 69),文档示例传入 AssistantMessageSegment(...).mark_as_temp(),类型推断为 Message 而非 AssistantMessageSegment
问题:Message.mark_as_temp() 返回类型标注为 "Message",但 AssistantMessageSegment/ToolCallMessageSegment 继承该方法后,调用 .mark_as_temp() 在静态类型检查下返回 Message 而非子类类型。文档示例 AssistantMessageSegment(...).mark_as_temp() 传给 ToolCallsResult.tool_calls_info(类型 AssistantMessageSegment),strict type checker 会报不兼容。
项目上下文:ContentPart 已建立 TypeVar 链式返回的先例(line 16 定义 ContentPartT,line 68 使用),Message 应保持一致。本方法是新导出的公共 API,类型正确性影响插件开发者。
影响:使用 mypy/pyright 严格模式的插件会收到类型错误;IDE 自动补全会失去子类信息。运行时无影响。
修复建议:定义 MessageT = TypeVar("MessageT", bound="Message") 并改为 def mark_as_temp(self: MessageT) -> MessageT:

skipped_initial_system = True
continue
if message.role in ["assistant", "user"] and message._no_save:
# _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][high 置信度] _save_to_history 新增注释使用中文,违反 AGENTS.md 英文注释规则

类别:Tests/API Contract
证据:RIGHT 468-469 # _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的 # tool 消息)一律不落库... 为中文。对比 tool_loop_agent_runner.py RIGHT 318-323 的英文注释 # Plugin-injected tool call results (on_llm_request → ...)
问题:_save_to_history 中新增的代码注释为中文,AGENTS.md rule #5 要求所有注释使用英文。同 PR 的 tool_loop_agent_runner.py 对应注释(RIGHT 318-323)已正确使用英文,两处风格不一致。
项目上下文:AGENTS.md Basic rule #5 明要求 "Use English for all comments and logs"。
影响:仓库注释语言不统一,后续维护者可能难以保持一致性。
修复建议:改为英文,例如:# _no_save is role-agnostic: temp-injected messages (including the tool message in a fake tool-call pair) are never persisted to avoid dangling tool messages in history.

Comment thread tests/test_tool_loop_agent_runner.py Outdated
payload 组装路径(真实 provider,仅对 SDK create 的入口 _query 打桩捕获)。
顺序:history → 当前 user → assistant(tool_calls) → tool(result)。
"""
from astrbot.core.agent.tool import FunctionTool, ToolSet

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][high 置信度] 测试函数内存在与模块级重复的 import

类别:Tests/API Contract
证据:RIGHT 1634 from astrbot.core.agent.tool import FunctionTool, ToolSet 与 RIGHT 1635 from astrbot.core.provider.entities import ToolCallsResult 已分别在模块级导入(本文件 RIGHT 25 from astrbot.core.agent.tool import FunctionTool, ToolSet 和 RIGHT 29-34 from astrbot.core.provider.entities import ... ToolCallsResult)。仅 ProviderOpenAIOfficial(RIGHT 1636)是合理的 lazy import。
问题:test_runner_with_openai_provider_preserves_injected_tool_calls_order 在函数内重新导入了已在模块级导入的符号。
项目上下文:项目无明确的 lazy import 约定;既有测试(如 test_reset_appends_injected_tool_calls_result_after_user)直接使用模块级导入的 ToolCallsResult/AssistantMessageSegment
影响:代码冗余,后续维护者可能困惑为何同一符号在不同测试中导入方式不同。
修复建议:删除 RIGHT 1634-1635 两行冗余 import,仅保留 from astrbot.core.provider.sources.openai_source import ProviderOpenAIOfficial



@pytest.mark.asyncio
async def test_reset_appends_injected_tool_calls_result_after_user(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 whatevertogo 的替身。

[P3][Advisory][medium 置信度] list 形式的多个 ToolCallsResult 注入路径未被测试覆盖

类别:Tests/API Contract
证据:RIGHT 1587 传入 tool_calls_result=ToolCallsResult(...)(单对象)。文档推荐使用的 req.append_tool_calls_result()(entities.py append_tool_calls_result)在第二次调用时会把单对象转换为 list,所以 list 路径在实际使用中会被触及——例如插件注入两次记忆召回。
问题:runner 的 reset()(shard 1 RIGHT 324-331)有一个 isinstance(request.tool_calls_result, list) 分支用于处理多个 ToolCallsResult,但三个新增测试都只传入单个 ToolCallsResult 对象,list 分支未被覆盖。
项目上下文:PR 声明的核心卖点之一是支持插件通用注入,多次 append_tool_calls_result() 是预期用法。
影响:如果 list 分支有 off-by-one 或顺序错误(如多个注入对的追加顺序),现有测试无法捕获。
修复建议:增加一个测试,构造 request.tool_calls_result = [ToolCallsResult(...), ToolCallsResult(...)](两个不同的 tool_call_id),断言 run_context.messages 中两组 assistant→tool 对按列表顺序追加且 ID 各自正确。

@whatevertogo

Copy link
Copy Markdown
Contributor

我是 whatevertogo 的替身。

审查会话:d87ecbed-3927-429e-96ee-00a094e29ebf
Trigger comment: 5288467560
Head SHA:43913ba1252c19c491d0160a7ea7ccf1502b7643

行内评论已发布(ID 4933200049,查看审查)。以下是我的总结。


代码审查

范围

本次审查覆盖了当前 PR diff,并按 Correctness、Security、Reliability/Performance、Tests/API Contract 四个角度检查。

验证

  • 1 项确定性检查通过

发现

[P2][Advisory][high] partial mark_as_temp 会在持久化历史中留下悬空 tool 消息,导致下一轮 provider API 拒绝

  • 文件: astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py:470
  • 视角: Correctness
  • 问题: PR 将 _save_to_history 的过滤条件从 role in ["assistant", "user"] and _no_save 改为纯 _no_save,使 tool 角色消息现在也会被 _no_save 过滤。这引入了一个新的失败模式:如果插件只对注入对的一个消息调用 mark_as_temp()(例如标记了 ToolCallMessageSegment.mark_as_temp() 但忘记标记 AssistantMessageSegment),_save_to_history 会过滤掉 tool 消息但保留 assistant(tool_calls) 消息,在持久化历史中留下悬空的 assistant(tool_calls)。下一轮加载该历史并发送给 provider 时,OpenAI API 要求 assistant(tool_calls) 后必须跟随对应的 tool 消息,会直接拒绝请求;Anthropic/Gemini 有相同的 tool_use/tool_result 配对约束。
  • 证据: RIGHT 470 if message._no_save: continue 替代了旧的 role in ["assistant", "user"] 检查。旧代码不对 tool 角色检查 _no_save,所以即使插件设置了 _no_save=Truetool 消息也总是被持久化——这恰好避免了悬空 assistant(tool_calls) 的情况。新代码正确过滤 tool 消息,但如果 mark_as_temp() 未成对使用,就会产生悬空消息。OpenAI API 要求 "messages with tool_calls must be followed by tool messages",Anthropic 要求 tool_use 后跟 tool_result,Gemini 要求 function_call 后跟 function_response_save_to_historymark_as_temp() 都没有验证配对一致性。
  • 项目上下文: mark_as_temp() 是本 PR 新增并经 astrbot.api.provider 导出的公共 API,插件开发者是主要使用者。仓库 AGENTS.md 强调 KISS 和 first principles——最小正确实现应包含对自身引入的不变式(成对 temp)的验证或防护。
  • 影响: 插件作者忘记成对标记时,当前轮运行正常,但下一轮所有 provider API 调用都会因消息结构非法而被拒绝。错误出现在与触发错误不同的轮次,极难调试。考虑到该 API 面向 1000+ 插件生态,误用概率不低。
  • 修复:_save_to_history 的过滤循环中增加悬空检测:如果保留的 assistant(tool_calls) 消息后紧邻的不是对应 tool 消息(被 _no_save 过滤掉了),记录 warning 日志并跳过该 assistant 消息。或者在 ToolCallsResult.to_openai_messages_model() 中验证 tool_calls_info._no_save 与所有 tool_calls_result[i]._no_save 一致,不一致时抛出 ValueError

[P3][Advisory][high] mark_as_temp() docstring 使用中文且缺少 Google 格式 Returns 段

  • 文件: astrbot/core/agent/message.py:221
  • 视角: Tests/API Contract
  • 问题: mark_as_temp() 的 docstring 正文为中文,且缺少 Google 格式的 Returns: 段。AGENTS.md 规则 可选指令界面缺少/count #5 要求 "Use English for all comments and logs",规则 "Mandatory Google-Style Docstrings" 要求所有 docstring 严格使用 Google 格式。该方法是新增的公共 API(经 astrbot.api.provider 导出供插件使用),文档质量直接影响插件开发者。
  • 证据: astrbot/core/agent/message.py RIGHT 218-225,docstring 正文 临时注入(如伪造工具调用对)应成对标记... 为中文;方法返回 self(用于链式调用)但无 Returns: 段。对比同文件 ContentPart.mark_as_temp()(line 68-71)使用简洁英文单行 docstring。
  • 项目上下文: AGENTS.md 将英文注释和 Google 格式 docstring 列为基本开发要求(Basic rule 可选指令界面缺少/count #5 + Mandatory Google-Style Docstrings 小节)。
  • 影响: 公共 API 的中文 docstring 会在 IDE hover / 自动文档生成中展示中文,与项目全英文策略不一致;缺失 Returns: 降低了类型可读性。
  • 修复: 将 docstring 改为英文并补充 Returns: 段,例如:"""Mark this message as provider-facing only, not persisted.\n\nInjected pairs (e.g. fake tool calls) should mark both the\nassistant and tool messages so no dangling tool message\nremains in history.\n\nReturns:\n Self, for method chaining.\n"""

[P3][Advisory][high] _save_to_history 注释使用中文,违反 AGENTS.md 英文注释规则

  • 文件: astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py:468
  • 视角: Tests/API Contract
  • 问题: _save_to_history 中新增的代码注释为中文,AGENTS.md 规则 可选指令界面缺少/count #5 要求 "Use English for all comments and logs"。
  • 证据: internal.py RIGHT 468-469,注释 # _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的 # tool 消息)一律不落库... 为中文。
  • 项目上下文: AGENTS.md Basic rule 可选指令界面缺少/count #5 明确要求所有注释使用英文。同 PR 中 tool_loop_agent_runner.py 的对应注释(RIGHT 318-323)已正确使用英文。
  • 影响: 仓库注释语言不统一,后续维护者可能难以保持一致性。
  • 修复: 改为英文,例如:# _no_save is role-agnostic: temp-injected messages (including the tool message in a fake tool-call pair) are never persisted to avoid dangling tool messages in history.

[P3][Advisory][high] mark_as_temp() docstring 为中文且缺少 Google 格式 Returns 段

  • 文件: astrbot/core/agent/message.py:219
  • 视角: Tests/API Contract
  • 问题: 新增的 mark_as_temp() 是公共 API(经 astrbot.api.provider 导出供插件使用),但其 docstring 正文使用中文,且缺少 Google 格式的 Returns: 段。
  • 证据: RIGHT 219-223 docstring 正文 临时注入(如伪造工具调用对)应成对标记... 为中文;方法返回 self(用于链式调用如 AssistantMessageSegment(...).mark_as_temp())但无 Returns: 描述。同模块 ContentPart.mark_as_temp()(line 68-69)使用简洁英文单行 docstring,本方法未保持一致。
  • 项目上下文: AGENTS.md Basic rule 可选指令界面缺少/count #5 要求 "Use English for all comments and logs";"Mandatory Google-Style Docstrings" 要求所有 docstring 严格使用 Google 格式(Args:Returns:Raises:)。
  • 影响: 公共 API 的中文 docstring 会在 IDE hover / 自动文档中展示中文,与项目全英文策略不一致;缺失 Returns: 降低链式调用场景的类型可读性。
  • 修复: 改为英文并补充 Returns: 段,例如: python def mark_as_temp(self) -> "Message": """Mark this message as provider-facing only, not persisted. Injected pairs (e.g. fake tool-call results) should mark both the assistant and tool messages to avoid dangling tool messages in history. Returns: Self, for method chaining. """ self._no_save = True return self

[P3][Advisory][medium] mark_as_temp() 返回类型标注为 Message 而非子类类型,与 ContentPart 同名方法不一致

  • 文件: astrbot/core/agent/message.py:218
  • 视角: Tests/API Contract
  • 问题: Message.mark_as_temp() 返回类型标注为 "Message",但 AssistantMessageSegment/ToolCallMessageSegment 继承该方法后,调用 .mark_as_temp() 在静态类型检查下返回 Message 而非子类类型。文档示例 AssistantMessageSegment(...).mark_as_temp() 传给 ToolCallsResult.tool_calls_info(类型 AssistantMessageSegment),strict type checker 会报不兼容。
  • 证据: RIGHT 218 def mark_as_temp(self) -> "Message":。同模块 ContentPart.mark_as_temp()(line 68)使用 TypeVar 模式 def mark_as_temp(self: ContentPartT) -> ContentPartT 正确保留子类类型。ToolCallsResult.tool_calls_info: AssistantMessageSegment(entities.py line 69),文档示例传入 AssistantMessageSegment(...).mark_as_temp(),类型推断为 Message 而非 AssistantMessageSegment
  • 项目上下文: ContentPart 已建立 TypeVar 链式返回的先例(line 16 定义 ContentPartT,line 68 使用),Message 应保持一致。本方法是新导出的公共 API,类型正确性影响插件开发者。
  • 影响: 使用 mypy/pyright 严格模式的插件会收到类型错误;IDE 自动补全会失去子类信息。运行时无影响。
  • 修复: 定义 MessageT = TypeVar("MessageT", bound="Message") 并改为 def mark_as_temp(self: MessageT) -> MessageT:

[P3][Advisory][high] _save_to_history 新增注释使用中文,违反 AGENTS.md 英文注释规则

  • 文件: astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py:468
  • 视角: Tests/API Contract
  • 问题: _save_to_history 中新增的代码注释为中文,AGENTS.md rule 可选指令界面缺少/count #5 要求所有注释使用英文。同 PR 的 tool_loop_agent_runner.py 对应注释(RIGHT 318-323)已正确使用英文,两处风格不一致。
  • 证据: RIGHT 468-469 # _no_save 语义与角色无关:临时注入的消息(含伪造工具调用对的 # tool 消息)一律不落库... 为中文。对比 tool_loop_agent_runner.py RIGHT 318-323 的英文注释 # Plugin-injected tool call results (on_llm_request → ...)
  • 项目上下文: AGENTS.md Basic rule 可选指令界面缺少/count #5 明要求 "Use English for all comments and logs"。
  • 影响: 仓库注释语言不统一,后续维护者可能难以保持一致性。
  • 修复: 改为英文,例如:# _no_save is role-agnostic: temp-injected messages (including the tool message in a fake tool-call pair) are never persisted to avoid dangling tool messages in history.

[P3][Advisory][high] 测试函数内存在与模块级重复的 import

  • 文件: tests/test_tool_loop_agent_runner.py:1634
  • 视角: Tests/API Contract
  • 问题: test_runner_with_openai_provider_preserves_injected_tool_calls_order 在函数内重新导入了已在模块级导入的符号。
  • 证据: RIGHT 1634 from astrbot.core.agent.tool import FunctionTool, ToolSet 与 RIGHT 1635 from astrbot.core.provider.entities import ToolCallsResult 已分别在模块级导入(本文件 RIGHT 25 from astrbot.core.agent.tool import FunctionTool, ToolSet 和 RIGHT 29-34 from astrbot.core.provider.entities import ... ToolCallsResult)。仅 ProviderOpenAIOfficial(RIGHT 1636)是合理的 lazy import。
  • 项目上下文: 项目无明确的 lazy import 约定;既有测试(如 test_reset_appends_injected_tool_calls_result_after_user)直接使用模块级导入的 ToolCallsResult/AssistantMessageSegment
  • 影响: 代码冗余,后续维护者可能困惑为何同一符号在不同测试中导入方式不同。
  • 修复: 删除 RIGHT 1634-1635 两行冗余 import,仅保留 from astrbot.core.provider.sources.openai_source import ProviderOpenAIOfficial

[P3][Advisory][medium] list 形式的多个 ToolCallsResult 注入路径未被测试覆盖

  • 文件: tests/test_tool_loop_agent_runner.py:1574
  • 视角: Tests/API Contract
  • 问题: runner 的 reset()(shard 1 RIGHT 324-331)有一个 isinstance(request.tool_calls_result, list) 分支用于处理多个 ToolCallsResult,但三个新增测试都只传入单个 ToolCallsResult 对象,list 分支未被覆盖。
  • 证据: RIGHT 1587 传入 tool_calls_result=ToolCallsResult(...)(单对象)。文档推荐使用的 req.append_tool_calls_result()(entities.py append_tool_calls_result)在第二次调用时会把单对象转换为 list,所以 list 路径在实际使用中会被触及——例如插件注入两次记忆召回。
  • 项目上下文: PR 声明的核心卖点之一是支持插件通用注入,多次 append_tool_calls_result() 是预期用法。
  • 影响: 如果 list 分支有 off-by-one 或顺序错误(如多个注入对的追加顺序),现有测试无法捕获。
  • 修复: 增加一个测试,构造 request.tool_calls_result = [ToolCallsResult(...), ToolCallsResult(...)](两个不同的 tool_call_id),断言 run_context.messages 中两组 assistant→tool 对按列表顺序追加且 ID 各自正确。

设计提醒

  • 无 summary-only 或无法定位的发现。

低置信度观察

  • [medium][Tests/API Contract] 跨 provider 端到端测试仅覆盖 OpenAI,Anthropic/Gemini/Responses 路径未验证 tests/test_tool_loop_agent_runner.py:低概率的跨 provider 顺序/格式回归,仅影响使用注入功能的插件。 下一步:后续 pass 检查 Anthropic/Gemini provider 是否对来自 contextstool 角色 Message 有额外的转换逻辑,如果有则需补充测试。
  • [low][Correctness] mark_as_temp 的 _no_save 在 LLM 压缩触发时的存活路径未直接测试 astrbot/core/agent/context/compressor.py:如果未来 compressor 实现改为重建 recent messages(如 model_validate),temp 标记会丢失,注入对会被错误持久化。 下一步:后续 pass 确认 compressor 的 recent rounds 路径不会重建 Message 对象;或在 PR 中补充一条"注入 temp 对 + 长历史触发压缩 + 断言不落库"的测试。
  • [medium][Tests/API Contract] mark_as_temp 的 _no_save 标记经 reset() 后的保留未被直接断言 tests/test_conversation_checkpoint.py:335:低概率回归——mark_as_temp() 的消息在压缩或对象重建后可能被错误持久化。 下一步:后续 pass 确认 to_openai_messages_model() 始终返回原始引用;或在 test_reset_appends_injected_tool_calls_result_after_user 中追加断言 assert runner.run_context.messages[-1]._no_save is False(无 temp)和一个带 mark_as_temp() 的变体断言 ._no_save is True
  • [low][Tests/API Contract] 端到端 provider 测试仅覆盖 OpenAI,Anthropic/Gemini/Responses 路径未验证 tests/test_tool_loop_agent_runner.py:1625:低概率的 provider 特定 payload 格式回归(如 Anthropic 对 assistant(tool_calls) 消息的 content 转换),仅影响使用注入功能的插件在非 OpenAI provider 上的行为。 下一步:后续 pass 检查 Anthropic/Gemini provider 是否对来自 contextsassistant + tool 角色 Message 有额外转换逻辑;如有差异则补充对应测试。

合并评估

总体:需要处理已发布的 inline findings

做了什么

完成覆盖优先审查并发布 8 条行内评论;未能定位到 diff 行的发现数为 0。

下一步建议

  1. 优先处理行内评论中的 P0/P1/P2 问题。
  2. 如果需要风格或细节审查,可以评论 @whatevertogo nitpick review 重新触发。

剩余风险

  • 无额外剩余风险。

覆盖范围

  • 已审查文件:7/7

最终报告生成降级:parse final report JSON: expected , or } at line 1 column 4340

Partial mark_as_temp() on an injected pair would persist a dangling
assistant(tool_calls) message that providers reject on the next round.
Validate _no_save consistency in ToolCallsResult.to_openai_messages_model()
so mismatched pairs fail fast at injection time. Also switch Message.
mark_as_temp() to a TypeVar return type (matching ContentPart) and move
comments to English per AGENTS.md.

Co-Authored-By: deepseek-v4-flash <deepseek-ai@claude-code-best.win>
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Aug 14, 2026
@whatevertogo

Copy link
Copy Markdown
Contributor

@whatevertogo 讲解一下pr内容,每个文件添加了那些东西?

@whatevertogo

whatevertogo commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

多个插件之间的顺序可以复用现有 filter.on_llm_request(priority=...):priority 越高,handler 越早执行,其 append 的结果也越靠前。建议在文档中明确这一点,并增加一个覆盖高低 priority 注入顺序的测试。只是建议哈

Lock in that an injected assistant(tool_calls) + tool(result) pair converts
correctly through the three non-OpenAI-format providers: Anthropic
(tool_use/tool_result), Gemini (functionCall/functionResponse), and OpenAI
Responses (function_call/function_call_output).

Co-Authored-By: deepseek-v4-flash <deepseek-ai@claude-code-best.win>
@whatevertogo

Copy link
Copy Markdown
Contributor

我是 whatevertogo 的替身。

审查会话:d87ecbed-3927-429e-96ee-00a094e29ebf
触发评论:5288757794
Head SHA:1306655b1bea8570e75e728a5643eccde033eb8d

已经看完全部 diff。以下是这个 PR 的完整讲解。


PR 总体目标

这个 PR 让插件能够向 LLM 注入伪造的工具调用结果——即插件可以"欺骗" LLM,让它认为自己调用了某个工具并拿到了结果,而不需要真正等待 LLM 自己发起工具调用。典型用途是 LivingMemory 插件把检索到的长期记忆包装成一次"记忆召回"工具调用。

核心做法是复用已有的 ProviderRequest.tool_calls_result 通道,让 ToolLoopAgentRunner.reset() 把注入的 assistant(tool_calls) → tool(result) 消息对追加到当前用户消息之后,天然保证跨所有 provider 的消息顺序一致。这替代了之前 issue #9451 中只在 OpenAI provider 内做启发式重排的方案。


逐文件讲解

1. astrbot/api/provider/__init__.py(+8)

公开导出插件需要的类型,让插件开发者可以直接从 astrbot.api.provider 导入:

  • AssistantMessageSegment — 伪造的 assistant 工具调用消息
  • ToolCallMessageSegment — 伪造的 tool 结果消息
  • ToolCallsResult — 把上述两者打包成一对的容器

这三个类型加进了 __all__,成为正式的公共 API。

2. astrbot/core/agent/message.py(+14)

新增 Message.mark_as_temp() 方法:设置 _no_save = True,标记消息为"仅 provider 可见、不落库"。伪造的工具调用对如果只想参与本轮请求、不想持久化到会话历史,就需要成对调用这个方法。

关键实现细节:

  • 引入 MessageT = TypeVar("MessageT", bound="Message"),返回类型用 MessageT 而非硬编码 "Message",这样 AssistantMessageSegment(...).mark_as_temp() 返回的仍是 AssistantMessageSegment 类型(链式调用类型不丢),与同模块 ContentPart.mark_as_temp() 的模式一致。
  • docstring 使用英文 + Google 格式(含 Returns: 段)。

3. astrbot/core/agent/runners/tool_loop_agent_runner.py(+14)

reset() 中消费注入的工具调用结果,这是整个 PR 的核心。在 [history, 当前用户消息] 拼接之后,检查 request.tool_calls_result

  • 如果是单个 ToolCallsResult 对象,包装成列表
  • 如果已经是列表,直接遍历
  • 对每个 ToolCallsResult 调用 to_openai_messages_model(),把返回的 [assistant(tool_calls), tool(result)] 消息追加到 messages 末尾

这样注入对就紧跟在当前用户消息之后,属于当前轮次。由于 runner 之后调用 provider 时只传 contexts(从 run_context.messages 转换),不传 tool_calls_result 参数,所以不会与 provider 内部的消费路径重复。

4. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py(+4 −1)

修改 _save_to_history 的过滤逻辑

# 旧:只有 assistant/user 角色才检查 _no_save
if message.role in ["assistant", "user"] and message._no_save:
# 新:任何角色的 _no_save 消息都不落库
if message._no_save:

这个变更让 tool 角色的 temp 消息也会被正确过滤。对既有的 _no_save 用法(persona begin dialogs 的 user/assistant 角色)没有行为变化,只是新增了对 tool 角色的覆盖。

注释改为英文。

5. astrbot/core/provider/entities.py(+10)

ToolCallsResult.to_openai_messages_model() 中添加成对一致性校验:如果 tool_calls_info 标记了 mark_as_temp() 但某个 tool_calls_result 没标记(或反过来),直接抛出 ValueError

这是一个防御性校验——防止插件作者只标记了注入对的一半,导致持久化历史中残留悬空的 assistant(tool_calls)tool 消息,下一轮 provider API 会拒绝这种不完整的消息结构。

6. docs/zh/dev/star/plugin.md(+59)

新增"注入工具调用结果"文档小节,包含:

  • 适用版本标注(> v4.27.3)
  • 消息顺序保证图示:history → 当前 user → assistant(tool_calls) → tool(result)
  • 完整的 on_llm_request 钩子代码示例
  • mark_as_temp() 成对使用说明和注意事项(含 ValueError 提示)

7. tests/test_conversation_checkpoint.py(+59)

新增 test_temp_injected_tool_pair_not_persisted 测试:验证成对 mark_as_temp() 的伪造工具调用对不落库,同时真实的 tool 消息正常持久化。构造了混合消息列表(真实 user + 伪造 temp 对 + 真实 tool),断言持久化的历史只包含合法消息。

8. tests/test_tool_loop_agent_runner.py(+277 −2)

新增 5 个测试,覆盖了多个维度:

测试 覆盖内容
test_reset_appends_injected_tool_calls_result_after_user 单个注入对的追加顺序 + _no_save 标记经 reset() 后保留
test_reset_appends_multiple_injected_tool_calls_results_in_order list 形式多个注入对按列表顺序追加(两个不同 tool_call_id)
test_injected_tool_calls_result_partial_temp_mark_raises 只标记一半时抛 ValueError(成对一致性校验)
test_runner_with_openai_provider_preserves_injected_tool_calls_order 端到端验证:真实 ProviderOpenAIOfficial,mock _query 捕获 payload,断言消息顺序
test_temp_injected_tool_pair_not_persisted(checkpoint 文件) temp 对不落库

与上一版的关键变化

这个 head SHA(1306655b)相比之前审查的版本(43913ba),作者修复了之前审查提出的多个问题:

  • P2(悬空消息):在 entities.pyto_openai_messages_model() 增加了成对一致性校验 + 文档增加 ValueError 提示
  • P3(中文 docstring)mark_as_temp() 已改为英文 Google 格式
  • P3(中文注释)_save_to_history 注释改为英文
  • P3(返回类型):引入 MessageT TypeVar 替代硬编码 "Message"
  • P3(list 测试缺口):新增 test_reset_appends_multiple_injected_tool_calls_results_in_order
  • P3(冗余 import):测试函数内的重复 import 已清理

测试从 3 个增加到 5 个,覆盖度和断言强度都有提升。

Higher on_llm_request priority handlers run earlier, so their appended
tool_calls_result appears earlier in the final context. Add a dispatch-level
test through call_event_hook asserting high-priority injection precedes
low-priority, and document the ordering guarantee.

Co-Authored-By: deepseek-v4-flash <deepseek-ai@claude-code-best.win>
@whatevertogo

Copy link
Copy Markdown
Contributor

look good 偷 me, @Soulter 粉毛大人看看呢

@whatevertogo

Copy link
Copy Markdown
Contributor

我是 whatevertogo 的替身。

PR 审查失败,未继续重试。

触发评论:5288467560
Head SHA:474fe3350b8eeca7da7538ce631653f9f70a24ac
错误:gh repo clone AstrBotDevs/AstrBot /tmp/pra-test/home/.astrcode/pr-review-agent/worktrees/AstrBotDevs__AstrBot/pr-9673 timed out after 60s

可以在本 PR 评论 @whatevertogo review it 手动重新触发。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. feature:plugin The bug / feature is about AstrBot plugin system. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants