fix(discord): send tool-using responses (api_calls>=2)#33842
fix(discord): send tool-using responses (api_calls>=2)#33842sweetcornna wants to merge 1 commit into
Conversation
When the agent returns a non-empty reply that the extract pipeline (extract_media / extract_images / extract_local_files plus the inline directive strips) happens to reduce to an empty string, the ``if text_content:`` guard in ``_process_message_background`` bypassed the send entirely. The symptom on Discord was a ``response ready`` log followed by silence — no ``Sending response`` line, no error in ``errors.log``, and the final answer never reached the channel. The failure is most visible on tool-using turns (api_calls>=2) where the larger, more varied response content is more likely to trip an over-eager strip path. Preserve the pre-extract response and, when no native attachment was produced to deliver in its place, sanitize the directive tags and send it as a fallback so the user actually receives the answer. Log a ``WARNING`` so future occurrences of the silent-drop pattern are observable. Scoped to Discord because that is the confirmed reproducer; other adapters keep current behavior to avoid unintended regressions in their delivery pipelines. Closes NousResearch#29346
40f2c28 to
34e2a6e
Compare
|
CI note — the red checks are inherited from This branch was just rebased onto the latest
This PR only changes Happy to rebase again once |
NousResearch#29346) The extract pipeline (extract_media/extract_images/extract_local_files + directive strips) can reduce a non-empty tool-using response to empty text_content with no deliverable attachment. The 'if text_content' send guard then silently skips delivery: a 'response ready' log with no 'Sending response', no error, and the answer never reaches the user. - A2: snapshot the pre-extract response; when extraction yields empty text and no image/local/media attachment, deliver the recovered original from the post-extract_media body (so a spaced MEDIA path can't leak). Applies on ALL platforms (supersedes the Discord-only NousResearch#33842 and the unsafe raw-fallback NousResearch#29499). - A3: loud delivery invariant - a non-empty response that produces nothing deliverable logs response_delivery_dropped at ERROR; every recovery logs response_delivery_recovered. No silent drop survives. - Factor a _strip_media_directives helper for the [[...]] strips; MEDIA stripping stays owned by extract_media, whose grammar handles spaced and quoted paths. - Salvaged + de-scoped the NousResearch#33842 test harness to all platforms; added unrecoverable-drop and no-leak regression tests.
#29346) The extract pipeline (extract_media/extract_images/extract_local_files + directive strips) can reduce a non-empty tool-using response to empty text_content with no deliverable attachment. The 'if text_content' send guard then silently skips delivery: a 'response ready' log with no 'Sending response', no error, and the answer never reaches the user. - A2: snapshot the pre-extract response; when extraction yields empty text and no image/local/media attachment, deliver the recovered original from the post-extract_media body (so a spaced MEDIA path can't leak). Applies on ALL platforms (supersedes the Discord-only #33842 and the unsafe raw-fallback #29499). - A3: loud delivery invariant - a non-empty response that produces nothing deliverable logs response_delivery_dropped at ERROR; every recovery logs response_delivery_recovered. No silent drop survives. - Factor a _strip_media_directives helper for the [[...]] strips; MEDIA stripping stays owned by extract_media, whose grammar handles spaced and quoted paths. - Salvaged + de-scoped the #33842 test harness to all platforms; added unrecoverable-drop and no-leak regression tests.
|
Closing as redundant. After rebasing onto current |
NousResearch#29346) The extract pipeline (extract_media/extract_images/extract_local_files + directive strips) can reduce a non-empty tool-using response to empty text_content with no deliverable attachment. The 'if text_content' send guard then silently skips delivery: a 'response ready' log with no 'Sending response', no error, and the answer never reaches the user. - A2: snapshot the pre-extract response; when extraction yields empty text and no image/local/media attachment, deliver the recovered original from the post-extract_media body (so a spaced MEDIA path can't leak). Applies on ALL platforms (supersedes the Discord-only NousResearch#33842 and the unsafe raw-fallback NousResearch#29499). - A3: loud delivery invariant - a non-empty response that produces nothing deliverable logs response_delivery_dropped at ERROR; every recovery logs response_delivery_recovered. No silent drop survives. - Factor a _strip_media_directives helper for the [[...]] strips; MEDIA stripping stays owned by extract_media, whose grammar handles spaced and quoted paths. - Salvaged + de-scoped the NousResearch#33842 test harness to all platforms; added unrecoverable-drop and no-leak regression tests.
#29346) The extract pipeline (extract_media/extract_images/extract_local_files + directive strips) can reduce a non-empty tool-using response to empty text_content with no deliverable attachment. The 'if text_content' send guard then silently skips delivery: a 'response ready' log with no 'Sending response', no error, and the answer never reaches the user. - A2: snapshot the pre-extract response; when extraction yields empty text and no image/local/media attachment, deliver the recovered original from the post-extract_media body (so a spaced MEDIA path can't leak). Applies on ALL platforms (supersedes the Discord-only #33842 and the unsafe raw-fallback #29499). - A3: loud delivery invariant - a non-empty response that produces nothing deliverable logs response_delivery_dropped at ERROR; every recovery logs response_delivery_recovered. No silent drop survives. - Factor a _strip_media_directives helper for the [[...]] strips; MEDIA stripping stays owned by extract_media, whose grammar handles spaced and quoted paths. - Salvaged + de-scoped the #33842 test harness to all platforms; added unrecoverable-drop and no-leak regression tests.
Problem
On Discord, tool-using replies (
api_calls >= 2) were silently dropped. The log showedresponse ready: ... api_calls=2 response=NNNN charsand then nothing — noSending responseline, no error — and the answer never reached the channel. Single-call replies (api_calls == 1) were largely unaffected because their typically smaller content rarely tripped the strip path.Root cause
In
BasePlatformAdapter._process_message_background(gateway/platforms/base.py), the response passes through an extract pipeline:extract_media->extract_images-> inline-directive strips ->extract_local_files. When that pipeline reduced a non-empty reply to""while producing no native attachment (no image / media / local file), the downstreamif text_content:send guard was skipped, so the message was dropped with nothing logged.Fix
Recover the stripped reply on Discord and make the drop observable.
_response_pre_extractbefore the pipeline runs.self.platform == Platform.DISCORD and not text_content and not images and not local_files and not media_files and _response_pre_extract.strip().[[audio_as_voice]],[[as_document]], andMEDIA:<path>directives (re.sub(r"MEDIA:\s*\S+", ...)) — and, if anything remains, assign it totext_contentso the existing send path delivers it.WARNING(Response stripped to empty after extract pipeline (orig=%d chars). Sending raw response as fallback.) including the original char count, so the silent-drop pattern is visible next time.Testing
New regression suite
tests/gateway/test_discord_tool_response_drop.py(4 cases) added in the diff. The extract helpers are monkeypatched to deterministically reduce text to empty, isolating the send-path bug from the strip heuristics:test_response_reduced_to_empty_is_recovered_and_sent: a multi-hundred-char tool reply (the issue's repeated sample string,assert len > 500) stripped to empty is still sent (exactly one send) and astripped to emptyWARNING is logged.test_directives_stripped_from_fallback_text:[[audio_as_voice]],[[as_document]], andMEDIA:directives are removed from the fallback text while the real answer survives.test_no_fallback_when_attachment_produced: when an image attachment is extracted (empty text is intentional), no text fallback is sent, so the attachment content is not duplicated.test_fallback_is_discord_scoped: a Telegram adapter with the same empty-after-strip condition sends nothing, confirming the fallback is Discord-only.Per the PR description, the author also ran
pytest tests/gateway/test_platform_base.py tests/gateway/test_base_topic_sessions.py tests/gateway/test_delivery.py tests/gateway/test_discord_tool_response_drop.py(138 passed, 2 skipped) and confirmed the recovery tests fail (0 sends) when thebase.pychange is reverted. These run results are reported by the author and not reproduced here.Risk & impact
WARNINGlog line when recovery fires, improving observability of this class of failure.Closes #29346