Skip to content

fix(cron): deliver proactive agent final text to the bound session - #10008

Closed
xiaoyuyu6420 wants to merge 4 commits into
AstrBotDevs:masterfrom
xiaoyuyu6420:fix/9980-proactive-agent-final-delivery
Closed

fix(cron): deliver proactive agent final text to the bound session#10008
xiaoyuyu6420 wants to merge 4 commits into
AstrBotDevs:masterfrom
xiaoyuyu6420:fix/9980-proactive-agent-final-delivery

Conversation

@xiaoyuyu6420

@xiaoyuyu6420 xiaoyuyu6420 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Proactive agent runs (cron active_agent jobs and background-task result wakeups) discard every AgentResponse: both call sites consume runner.step_until_done() with async for _ ... pass. The final assistant text is only folded into the persisted history summary — it is never sent to the delivery session.

Two details make this worse and turn it into the exact symptom reported in #9980 ("only intermediate messages, no final result"):

  1. At forced wrap-up (ToolLoopAgentRunner.step_until_done, max steps reached) all tools are removed before the final step, so the model cannot use send_message_to_user to deliver its forced summary.
  2. SendMessageToUserTool's description says "For other normal text replies, you can output directly and no need to use this tool" — models that follow that hint output plain text, which is exactly the text being discarded.

This is the remaining gap from the #9980 breakdown; #9987 fixed the ERROR-state propagation and #9992 fixed compression-config sharing. This PR fixes final-response delivery for both wake paths.

Fix

  • CronJobManager._woke_main_agent() and AstrAgentToolExec._wake_main_agent_for_background_result() now send the final assistant text through the cron event after persist_agent_history(), when:
    • the runner finished and llm_resp.role == "assistant" with non-empty text (error responses are never pushed to chats);
    • the job has a real delivery session (cron jobs without one keep their current behavior);
    • the model did not already deliver this exact text to the current session via send_message_to_user earlier in the same run.
  • The double-delivery guard reads the per-run record that SendMessageToUserTool already writes to the event; the key is now a shared constant (SENT_TO_CURRENT_SESSION_PLAIN_TEXTS_EXTRA_KEY).
  • A failed delivery is logged and does not fail the job — the agent work and history persistence already succeeded.

Testing

  • New tests in tests/unit/test_cron_manager.py (TestWokeMainAgentFinalDelivery):
    • final assistant text is delivered to the delivery session;
    • no duplicate send when the tool already delivered the same text;
    • no send when there is no delivery session / empty text / non-assistant role;
    • a delivery failure is swallowed and does not mark the job failed.
  • Full suite: 2420 passed.

Fixes part of #9980 (final-response delivery half).

Summary by Sourcery

Deliver final assistant text from forced proactive-agent wrap-ups to their bound sessions without duplicating messages or affecting job success.

New Features:

  • Deliver forced-wrap-up assistant responses from cron jobs and background-task wakeups to their bound sessions.

Bug Fixes:

  • Prevent proactive agent runs from discarding final assistant text when tool execution reaches the maximum step limit.
  • Avoid duplicate final-message delivery when the same text was already sent through send_message_to_user.
  • Ensure delivery failures are logged without failing the completed agent job.

Enhancements:

  • Expose whether an agent run ended through forced max-step completion and centralize tracking of texts sent to the current session.

Tests:

  • Add coverage for forced-wrap-up delivery, duplicate suppression, invalid delivery conditions, session targeting, normal-completion silence, delivery failures, and max-step state tracking.

Cron and background-task wakeups consumed runner.step_until_done() with
a discarded iterator, so the final assistant text was only folded into
the persisted history summary and never sent to the delivery session.
At forced wrap-up (max steps) all tools are removed, so the model cannot
use send_message_to_user either — the user only ever saw intermediate
messages (AstrBotDevs#9980).

Deliver the final assistant text through the cron event after history
persistence on both wake paths, skipping when the model already sent
this exact text to the current session via send_message_to_user during
the same run (recorded by the tool as
SENT_TO_CURRENT_SESSION_PLAIN_TEXTS_EXTRA_KEY). A failed send is logged
but does not fail the job.

Signed-off-by: xiaoyuyu6420

@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 found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/cron/manager.py" line_range="546-558" />
<code_context>
             logger.warning("background task agent got no response")
             return

+        final_text = (llm_resp.completion_text or "").strip()
+        if llm_resp.role == "assistant" and final_text:
+            # Same delivery gap as the cron path (#9980): the final text only
+            # lands in persisted history unless it is explicitly sent. Skip
+            # when the model already delivered this exact text via
+            # send_message_to_user earlier in the same run.
+            already_sent = cron_event.get_extra(
+                SENT_TO_CURRENT_SESSION_PLAIN_TEXTS_EXTRA_KEY, []
+            )
+            if final_text not in already_sent:
+                try:
+                    await cron_event.send(MessageChain().message(final_text))
+                except Exception as e:  # noqa: BLE001
+                    logger.warning(
</code_context>
<issue_to_address>
**issue (bug_risk):** The final response is sent through `cron_event`, whose session was constructed from `session_str`, while the new condition only checks that `delivery_session_str` is non-empty. When those values differ, the response is delivered to the cron/history session instead of the configured delivery session, so the bound recipient still receives no final result.

**Triggers:** When a cron job's `session_str` and `delivery_session_str` are different.

**Suggested fix:** Construct the delivery event/session from `delivery_session_str`, or send the final message through an API that explicitly targets that session; use the same target when recording duplicate-delivery metadata.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and the change explicitly sends the agent's final response to the bound user session, so an incorrect response or session binding can produce an unintended external message that cannot be retracted. Reverting prevents future messages but does not undo messages already delivered.

Blocking findings: astrbot/core/cron/manager.py:558


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/cron/manager.py Outdated
Address Sourcery bug_risk: delivery went through cron_event (built from
session_str) while the gate checked delivery_session_str. When a caller
passes differing values the text would land on the wrong session. Send
via Context.send_message(delivery_session_str, ...) so the gate and the
target are the same variable; no-platform and failure outcomes are
logged without failing the job.

Signed-off-by: xiaoyuyu6420
@xiaoyuyu6420

Copy link
Copy Markdown
Contributor Author

Addressed the Sourcery bug_risk in a11ce12: the cron path now delivers via Context.send_message(delivery_session_str, ...) instead of cron_event.send(...), so the gate condition and the delivery target are the same variable — a caller passing mismatched session_str/delivery_session_str can no longer route the text to the wrong session (no-platform and exceptions are logged without failing the job). The background-result path still sends through its cron event, whose session is built from the original event's own unified_msg_origin, so target and gate are identical there by construction.

Added test_delivery_targets_delivery_session_not_event_session covering the mismatch case.

Review feedback on AstrBotDevs#9980: blanket delivery of the final assistant text
also fired on normally completed runs, where the model still has
send_message_to_user available and may intentionally stay silent
(e.g. conditional notify-only cron jobs). DONE alone does not imply the
user should be notified.

ToolLoopAgentRunner now exposes reached_max_steps, set only in the
forced final-response branch of step_until_done where all tools were
removed and the model had no channel to deliver its summary. Cron and
background-task wakeups deliver the final text only in that case;
normal completion keeps the current silent behavior.

Signed-off-by: xiaoyuyu6420
reached_max_steps was only initialized in reset(), so reading the
property on a fresh runner raised AttributeError instead of returning
the documented default. Add a class-level False default and tests
covering both the forced wrap-up and normal-completion paths against a
real runner.

Signed-off-by: xiaoyuyu6420

@kilisamemarisaaa kilisamemarisaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the current head and the forced-wrap-up delivery path. The
eached_max_steps guard is set only where tools are removed, while normal completion remains silent; the cron path now targets delivery_session_str directly and the duplicate-text check is scoped to the current event. The added tests cover target mismatch, duplicate suppression, invalid responses, delivery failure, and the runner flag, and the full CI matrix is green. I found no blocking issue in the current diff.

@xiaoyuyu6420

Copy link
Copy Markdown
Contributor Author

Closing this to avoid a dangling PR: Soulter closed #9980 as completed on Sep 12.

I checked current master to confirm whether this is still needed before closing:

Since the issue is closed and there has been no maintainer review, the honest state is that this half is currently not in scope. The branch fix/9980-proactive-agent-final-delivery and its commits remain; happy to reopen or rebase if the maintainers decide the delivery half should be picked up.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants