Skip to content

Comments

fix: chatui cannot persiste file segment#5384

Closed
Soulter wants to merge 4 commits intomasterfrom
fix/chatui-file
Closed

fix: chatui cannot persiste file segment#5384
Soulter wants to merge 4 commits intomasterfrom
fix/chatui-file

Conversation

@Soulter
Copy link
Member

@Soulter Soulter commented Feb 23, 2026

  • feat: add stop functionality for active agent sessions and improve handling of stop requests
  • feat: update stop button icon and tooltip in ChatInput component
  • fix: correct indentation in tool call handling within ChatRoute class
  • fix: chatui cannot persiste file segment

Modifications / 改动点

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

Screenshots or Test Results / 运行截图或测试结果


Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

为正在进行的 agent 运行添加用户发起的停止支持,同时确保在聊天会话中保留部分响应和文件附件。

New Features:

  • 在 agent 运行器和事件工具中引入停止处理,使用户可以中断活跃的 agent 会话而不会破坏处理流水线。
  • 添加聊天后端和 Web UI 端点、线路连接和控制,从仪表盘和独立聊天视图中允许停止当前聊天生成。
  • 暴露内置的会话停止命令,允许通过文本命令停止正在运行的 agents。

Bug Fixes:

  • 确保保存在新附件目录中的文件和其他附件在加载聊天附件时能够被正确解析,修复文件片段的持久化问题。
  • 修复工具调用流式处理中的缩进问题,以便仅在结果可用后才发出工具调用完成片段。
  • 使流式错误日志记录对用户有意发起的停止操作具备更好的鲁棒性,从而避免中止的流被错误地记录为错误。

Enhancements:

  • 追踪被中止的 agent 状态和最终的部分 LLM 响应,使在用户停止运行时部分输出也能保存到历史记录中。
  • 扩展历史记录保存逻辑,以更稳健地处理用户中止的运行以及没有 assistant 消息的响应。
  • 优化活跃事件注册表,以支持不会中断事件传播的软停止请求。
  • 调整 Web 聊天存储路径,将所有媒体类型统一使用同一个附件目录。

Tests:

  • 添加测试,覆盖工具循环 agent 运行器中的停止信号处理,并验证中止状态和部分消息的持久化。
Original summary in English

Summary by Sourcery

Add user-initiated stop support for ongoing agent runs while ensuring partial responses and file attachments are preserved in chat sessions.

New Features:

  • Introduce stop handling in the agent runner and event utilities so users can interrupt active agent sessions without breaking the pipeline.
  • Add chat backend and web UI endpoints, wiring, and controls to allow stopping the current chat generation from the dashboard and standalone chat views.
  • Expose a built-in conversation stop command to stop running agents via text commands.

Bug Fixes:

  • Ensure file and other attachments saved in the new attachments directory are correctly resolved when loading chat attachments, fixing persistence issues for file segments.
  • Fix tool-call streaming handling indentation so tool call completion segments are emitted only after results are available.
  • Make streaming error logging resilient to intentional user stop actions so aborted streams do not surface as errors.

Enhancements:

  • Track aborted agent state and final partial LLM responses so partial outputs are saved to history when users stop a run.
  • Extend history-saving logic to handle user-aborted runs and assistant-less responses more robustly.
  • Refine active event registry to support soft stop requests that do not interrupt event propagation.
  • Adjust webchat storage paths to use a unified attachments directory for all media types.

Tests:

  • Add tests covering stop-signal handling in the tool loop agent runner and verifying aborted status and partial message persistence.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Feb 23, 2026
@Soulter Soulter closed this Feb 23, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Soulter, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances user control over active agent sessions by implementing a robust stop mechanism across both the backend and frontend. It allows users to interrupt ongoing agent tasks via a new command and a dedicated UI button, with the system designed to gracefully handle these interruptions and preserve any partial outputs. Additionally, it addresses a critical bug related to file attachment persistence, ensuring a more reliable chat experience.

Highlights

  • Agent Session Stop Functionality: Introduced a new stop command and a corresponding API endpoint to allow users to terminate active agent sessions gracefully. This includes handling stop requests within the agent runner, preserving partial outputs.
  • Chat UI Enhancements: The chat interface now features a 'stop' button that appears when an agent is actively running, replacing the 'send' button. This provides an intuitive way for users to interrupt ongoing generation.
  • File Attachment Persistence Fix: Resolved an issue where file segments were not persisting correctly in the chat UI. This involved standardizing the attachment storage directory and updating the retrieval logic to support both new and legacy paths.
  • Improved Agent Runner Control: The agent runner now includes internal flags (_stop_requested, _aborted) and a stop_watcher mechanism to manage and respond to stop signals, ensuring that agent execution can be interrupted and partial results are handled appropriately.
Changelog
  • astrbot/builtin_stars/builtin_commands/commands/conversation.py
    • Added stop asynchronous method to handle agent session termination requests.
  • astrbot/builtin_stars/builtin_commands/main.py
    • Registered the new stop command, making it accessible to users.
  • astrbot/core/agent/runners/tool_loop_agent_runner.py
    • Implemented _stop_requested and _aborted flags to manage agent state during stop requests.
    • Added logic to handle stop requests during agent execution, ensuring partial LLM responses are preserved.
  • astrbot/core/astr_agent_run_util.py
    • Introduced _should_stop_agent utility function to check for stop signals.
    • Integrated a stop_watcher task to monitor and propagate stop signals to agent runners.
    • Added cancellation logic for the stop_watcher task upon agent completion or exceptions.
  • astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
    • Modified history saving logic to correctly account for user-aborted agent runs and ensure partial messages are saved.
  • astrbot/core/platform/sources/webchat/webchat_event.py
    • Renamed imgs_dir to attachments_dir to standardize the storage location for various media types.
    • Updated file paths for saving images, records, and other files to the new attachments_dir.
  • astrbot/core/utils/active_event_registry.py
    • Added request_agent_stop_all method to signal agent runners to stop without interrupting event propagation.
  • astrbot/dashboard/routes/chat.py
    • Added a new /chat/stop API endpoint to handle stopping chat sessions.
    • Updated attachment retrieval logic to search both the new attachments_dir and the legacy imgs_dir for files.
  • dashboard/src/components/chat/Chat.vue
    • Updated ChatInput component usage to pass the isRunning prop and handle the stop event.
  • dashboard/src/components/chat/ChatInput.vue
    • Implemented a conditional stop button that appears when an agent is running, replacing the send button.
    • Added isRunning prop and stop emit to manage the visibility and functionality of the stop button.
    • Updated tooltip text for the stop button.
  • dashboard/src/components/chat/StandaloneChat.vue
    • Updated ChatInput component usage to pass the isRunning prop and handle the stop event in standalone mode.
  • dashboard/src/composables/useMessages.ts
    • Introduced state variables (currentRequestController, currentReader, currentRunningSessionId, userStopRequested) for managing stop requests.
    • Implemented the stopMessage function to abort ongoing requests and clean up resources.
    • Modified error handling during SSE reading to account for user-initiated stops.
  • dashboard/src/i18n/locales/en-US/features/chat.json
    • Added 'Stop generating' translation for the English locale.
  • dashboard/src/i18n/locales/zh-CN/features/chat.json
    • Added '停止生成' translation for the Chinese (Simplified) locale.
  • tests/test_tool_loop_agent_runner.py
    • Added a MockAbortableStreamProvider to simulate an LLM provider that can be aborted during streaming.
    • Added a new test case (test_stop_signal_returns_aborted_and_persists_partial_message) to verify that stop signals correctly abort agent execution and persist partial messages.
Activity
  • New stop command and API endpoint were introduced to allow users to terminate active agent sessions.
  • Agent runners were updated to gracefully handle stop requests, preserving partial outputs and marking the run as aborted.
  • The chat UI was enhanced with a dynamic 'stop' button that appears when an agent is running, improving user interaction.
  • A bug related to file attachment persistence was fixed by standardizing the storage directory and improving file retrieval logic.
  • New test cases were added to validate the agent's ability to handle stop signals and correctly persist partial messages.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the feature:chatui The bug / feature is about astrbot's chatui, webchat label Feb 23, 2026
@dosubot
Copy link

dosubot bot commented Feb 23, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a valuable feature to stop active agent sessions, along with several related bug fixes and enhancements. The implementation for both backend and frontend is well-structured, allowing for user-initiated interruption of chat generation while preserving partial results. The fix for file segment persistence by unifying attachment paths and handling legacy paths is a good improvement. I've identified one area for refactoring in astrbot/core/astr_agent_run_util.py where duplicated code for task cancellation can be simplified using a try...finally block, which would improve maintainability. Overall, this is a solid contribution.

Comment on lines +63 to 72
if resp.type == "aborted":
if not stop_watcher.done():
stop_watcher.cancel()
try:
await stop_watcher
except asyncio.CancelledError:
pass
astr_event.set_extra("agent_user_aborted", True)
astr_event.set_extra("agent_stop_requested", False)
return
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic to cancel the stop_watcher task is duplicated in three places (here, after the loop on lines 145-150, and in the except block on lines 164-169). This increases code complexity and the risk of bugs if one of the locations is missed during future changes.

Consider refactoring this using a try...finally block to centralize the cleanup logic. This ensures stop_watcher is always cancelled, regardless of how the try block is exited (e.g., via return, break, or an exception).

Example:

stop_watcher = asyncio.create_task(...)
try:
    # Main agent loop logic
    async for resp in agent_runner.step():
        # ...
        if resp.type == "aborted":
            # ... set extras
            return
        # ...
    # ...
except Exception as e:
    # ... error handling
    return
finally:
    if not stop_watcher.done():
        stop_watcher.cancel()
        try:
            await stop_watcher
        except asyncio.CancelledError:
            pass

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并留下了一些高层次的反馈:

  • 围绕 run_agentToolLoopAgentRunner 的停止逻辑有点变得复杂了(行内的 _should_stop_agent 检查,加上 _watch_agent_stop_signal 任务,再加上对 resp.type == "aborted" 的处理);建议把这些合并到一条单一且文档清晰的停止路径中,以减少竞态条件或重复取消行为的可能性。
  • _save_to_history 中,user_aborted 分支有多个特殊处理,还有一段被注释掉的 message 追加代码;最好要么删除这些注释掉的代码并简化 role/completion 的处理,要么清晰定义一次中止 run 时的历史记录表示形式,以便更容易理解这段逻辑。
给 AI Agents 的提示
Please address the comments from this code review:

## Overall Comments
- The stop logic around `run_agent` and `ToolLoopAgentRunner` has become a bit convoluted (inline `_should_stop_agent` checks plus the `_watch_agent_stop_signal` task plus `resp.type == "aborted"` handling); consider consolidating this into a single, clearly documented stopping path to reduce the chance of races or duplicated cancellation behavior.
- In `_save_to_history`, the `user_aborted` path has several special cases and a commented‑out message append block; it would be good to either remove the commented code and simplify the role/completion handling, or explicitly define the intended history representation for aborted runs so that this logic is easier to reason about.

## Individual Comments

### Comment 1
<location> `tests/test_tool_loop_agent_runner.py:419-428` </location>
<code_context>
+@pytest.mark.asyncio
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen assertions to verify the persisted partial message content, not only the role.

Right now the test only validates `final_resp.completion_text == "partial "` and that the last `run_context` message has role `"assistant"`. To better validate persistence, also assert that the last message’s content (e.g., its `TextPart`) includes the expected text chunk `"partial "`. This ensures we catch regressions where `final_llm_resp` is correct but the `run_context` messages are not updated.

Suggested implementation:

```python
    assert final_resp.completion_text == "partial "
    messages = runner.run_context.messages
    last_msg = messages[-1]

    # Ensure the last message is from the assistant
    assert last_msg.role == "assistant"

    # Ensure the partial text was actually persisted on the last message content
    # Adjust the content access if your message/content model differs.
    assert any(
        getattr(part, "text", "") and "partial " in getattr(part, "text", "")
        for part in getattr(last_msg, "content", [])  # e.g. a list of TextPart / content parts
    )

```

If your codebase uses a specific content type (e.g. `TextPart`) instead of generic objects with a `.text` attribute, it is better to tighten the assertion:

- Update the `any(...)` check to:

```python
assert any(
    isinstance(part, TextPart) and "partial " in part.text
    for part in last_msg.content
)
```

- Ensure `TextPart` (or the appropriate class) is imported at the top of `tests/test_tool_loop_agent_runner.py`, e.g.:

```python
from your_package.messages import TextPart
```

Adjust the import path and attribute names (`content`, `text`) to match the actual message model used in the test suite.
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得我们的代码评审有帮助,请考虑分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The stop logic around run_agent and ToolLoopAgentRunner has become a bit convoluted (inline _should_stop_agent checks plus the _watch_agent_stop_signal task plus resp.type == "aborted" handling); consider consolidating this into a single, clearly documented stopping path to reduce the chance of races or duplicated cancellation behavior.
  • In _save_to_history, the user_aborted path has several special cases and a commented‑out message append block; it would be good to either remove the commented code and simplify the role/completion handling, or explicitly define the intended history representation for aborted runs so that this logic is easier to reason about.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The stop logic around `run_agent` and `ToolLoopAgentRunner` has become a bit convoluted (inline `_should_stop_agent` checks plus the `_watch_agent_stop_signal` task plus `resp.type == "aborted"` handling); consider consolidating this into a single, clearly documented stopping path to reduce the chance of races or duplicated cancellation behavior.
- In `_save_to_history`, the `user_aborted` path has several special cases and a commented‑out message append block; it would be good to either remove the commented code and simplify the role/completion handling, or explicitly define the intended history representation for aborted runs so that this logic is easier to reason about.

## Individual Comments

### Comment 1
<location> `tests/test_tool_loop_agent_runner.py:419-428` </location>
<code_context>
+@pytest.mark.asyncio
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen assertions to verify the persisted partial message content, not only the role.

Right now the test only validates `final_resp.completion_text == "partial "` and that the last `run_context` message has role `"assistant"`. To better validate persistence, also assert that the last message’s content (e.g., its `TextPart`) includes the expected text chunk `"partial "`. This ensures we catch regressions where `final_llm_resp` is correct but the `run_context` messages are not updated.

Suggested implementation:

```python
    assert final_resp.completion_text == "partial "
    messages = runner.run_context.messages
    last_msg = messages[-1]

    # Ensure the last message is from the assistant
    assert last_msg.role == "assistant"

    # Ensure the partial text was actually persisted on the last message content
    # Adjust the content access if your message/content model differs.
    assert any(
        getattr(part, "text", "") and "partial " in getattr(part, "text", "")
        for part in getattr(last_msg, "content", [])  # e.g. a list of TextPart / content parts
    )

```

If your codebase uses a specific content type (e.g. `TextPart`) instead of generic objects with a `.text` attribute, it is better to tighten the assertion:

- Update the `any(...)` check to:

```python
assert any(
    isinstance(part, TextPart) and "partial " in part.text
    for part in last_msg.content
)
```

- Ensure `TextPart` (or the appropriate class) is imported at the top of `tests/test_tool_loop_agent_runner.py`, e.g.:

```python
from your_package.messages import TextPart
```

Adjust the import path and attribute names (`content`, `text`) to match the actual message model used in the test suite.
</issue_to_address>

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.

Comment on lines +419 to +428
@pytest.mark.asyncio
async def test_stop_signal_returns_aborted_and_persists_partial_message(
runner, provider_request, mock_tool_executor, mock_hooks
):
provider = MockAbortableStreamProvider()

await runner.reset(
provider=provider,
request=provider_request,
run_context=ContextWrapper(context=None),
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): 加强断言,以验证持久化的部分消息内容,而不仅仅是角色。

目前测试只验证了 final_resp.completion_text == "partial ",以及最后一条 run_context 消息的角色是 "assistant"。为了更好地验证持久化结果,建议同时断言最后一条消息的内容(例如其 TextPart)包含预期的文本片段 "partial "。这样可以确保在 final_llm_resp 是正确的但 run_context 消息未更新的情况下,我们也能捕获回归问题。

建议的实现:

    assert final_resp.completion_text == "partial "
    messages = runner.run_context.messages
    last_msg = messages[-1]

    # Ensure the last message is from the assistant
    assert last_msg.role == "assistant"

    # Ensure the partial text was actually persisted on the last message content
    # Adjust the content access if your message/content model differs.
    assert any(
        getattr(part, "text", "") and "partial " in getattr(part, "text", "")
        for part in getattr(last_msg, "content", [])  # e.g. a list of TextPart / content parts
    )

如果你的代码库使用的是特定的内容类型(例如 TextPart),而不是带有 .text 属性的通用对象,那么最好进一步收紧断言:

  • any(...) 检查更新为:
assert any(
    isinstance(part, TextPart) and "partial " in part.text
    for part in last_msg.content
)
  • 确保在 tests/test_tool_loop_agent_runner.py 文件顶部导入 TextPart(或相应的类),例如:
from your_package.messages import TextPart

根据测试套件中实际使用的消息模型,调整导入路径以及属性名(contenttext)。

Original comment in English

suggestion (testing): Strengthen assertions to verify the persisted partial message content, not only the role.

Right now the test only validates final_resp.completion_text == "partial " and that the last run_context message has role "assistant". To better validate persistence, also assert that the last message’s content (e.g., its TextPart) includes the expected text chunk "partial ". This ensures we catch regressions where final_llm_resp is correct but the run_context messages are not updated.

Suggested implementation:

    assert final_resp.completion_text == "partial "
    messages = runner.run_context.messages
    last_msg = messages[-1]

    # Ensure the last message is from the assistant
    assert last_msg.role == "assistant"

    # Ensure the partial text was actually persisted on the last message content
    # Adjust the content access if your message/content model differs.
    assert any(
        getattr(part, "text", "") and "partial " in getattr(part, "text", "")
        for part in getattr(last_msg, "content", [])  # e.g. a list of TextPart / content parts
    )

If your codebase uses a specific content type (e.g. TextPart) instead of generic objects with a .text attribute, it is better to tighten the assertion:

  • Update the any(...) check to:
assert any(
    isinstance(part, TextPart) and "partial " in part.text
    for part in last_msg.content
)
  • Ensure TextPart (or the appropriate class) is imported at the top of tests/test_tool_loop_agent_runner.py, e.g.:
from your_package.messages import TextPart

Adjust the import path and attribute names (content, text) to match the actual message model used in the test suite.

@Soulter Soulter deleted the fix/chatui-file branch February 23, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:chatui The bug / feature is about astrbot's chatui, webchat size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant