feat: native Discord embeds via [EMBED] marker - #75945
Conversation
Agents can now emit real Discord embeds by including an [EMBED]{json}[/EMBED]
block in a message. The gateway parses the JSON and sends it as a native
discord.Embed (title, description, color, inline fields, footer), keeping
any surrounding text as the plain message content.
- _extract_embed(): JSON block -> discord.Embed (strict=False so literal
newlines work; color accepts int or #hex; footer accepts str or dict;
fields capped at Discord's 25)
- send(): attaches the embed to the first chunk, marker stripped
- edit_message(): streaming finalize swaps the marker for the embed;
mid-stream edits show a placeholder instead of raw JSON
- Normal messages are completely unaffected (marker-gated)
- 11 unit tests covering parse, fallback, colors, footer, caps, send wiring
teknium1
left a comment
There was a problem hiding this comment.
Thanks for adding a small, platform-scoped native Discord capability. Static review found two delivery gaps.
Problems
- Forum sends return through
_send_to_forum(channel, content)before the new normal-send extraction path (plugins/platforms/discord/adapter.py:2938-2948)._send_to_forum()creates its starter with rawcontent(plugins/platforms/discord/adapter.py:3050-3061), so forum posts will show[EMBED]JSON rather than receive an embed. - Finalized streaming content over 2,000 characters returns to
_edit_overflow_split()before the new extraction code (plugins/platforms/discord/adapter.py:3218-3222). That helper chunks the raw content (plugins/platforms/discord/adapter.py:3317-3347), so a large marker is exposed instead of rendered.
Suggested changes
- Parse before forum routing and pass the embed through the forum-thread creation path, with a forum regression test.
- Parse finalized complete markers before overflow handling, then size-check only the remaining text; add a >2,000-character finalized-stream test.
Automated hermes-sweeper review.
| # Format and split message if needed | ||
| formatted = self.format_message(content) | ||
| embed = None | ||
| if isinstance(formatted, str) and "[EMBED]" in formatted: |
There was a problem hiding this comment.
Forum parents return at the earlier _send_to_forum(channel, content) branch, before this normal-send extraction runs. Parse before that branch and carry the embed into _send_to_forum()/create_thread(); otherwise forum posts expose the marker as text.
| try: | ||
| await msg.edit(content=formatted) | ||
| embed = None | ||
| edit_content = formatted |
There was a problem hiding this comment.
This runs after the finalized >2,000-character branch has already returned to _edit_overflow_split(), which chunks the raw marker content. Extract a complete finalized marker before overflow handling, then apply the text limit to the remaining content.
Addresses review feedback (delivery gaps): - send(): embed extraction now runs BEFORE forum routing, and _send_to_forum() passes the parsed embed through to create_thread() so forum starters render the embed instead of raw JSON - edit_message(): finalized oversized streams parse the marker before the overflow pre-flight; _edit_overflow_split() carries the embed on the first chunk and only size-checks the remaining text - 4 new regression tests: forum send, oversized finalized stream, normal finalize edit, mid-stream placeholder
|
Thanks for the review @teknium1 — both gaps are fixed. 1. Forum routing: embed extraction now runs before the forum branch in 2. Overflow splitting: Also added tests for the normal finalize edit path and the mid-stream placeholder. 15 tests in the file, 45 Discord tests total — all green. |
Models generating the [EMBED] block occasionally inject (1/2)-style segment markers or trailing commas, which broke json.loads and caused the raw marker to be sent. _clean_embed_json() strips both artifacts outside string literals before parsing; string contents are preserved. - 2 new tests: (1/2) + trailing comma regression, string-preservation
Cron deliveries and the send_message tool route through _standalone_send (REST), never adapter.send() — so [EMBED] markers arrived as raw text. _split_embed_from_message() + _embed_payload_from_block() extract the marker into a native embeds[] payload on the plain-text POST, the forum-thread POST, and the caption/media starter. - 3 new tests: standalone split, no-marker passthrough, bad-JSON fallback
|
Verified working end-to-end in production: the [EMBED] marker now renders as a native embed through ALL delivery paths — interactive streaming (edit_message finalize), normal send(), and the standalone REST path used by cron jobs and the send_message tool. Also tolerates LLM artifacts in the JSON block ((1/2) segment markers, trailing commas). 20 unit tests green. |
Summary
Agents can now emit native Discord embeds by including an
[EMBED]{json}[/EMBED]block in a message. The gateway parses the JSON and sends it as a realdiscord.Embed— title, description, colour, inline fields (columns/rows), footer — while keeping any surrounding text as the plain message content.This follows the established
MEDIA:text-marker convention already in the adapter (agent → marker → platform feature).Example
Implementation
_extract_embed(): parses the JSON block into adiscord.Embedjson.loads(strict=False)— literal newlines inside strings work, so the model can write readable multi-line JSONintor#hex; footer acceptsstror{"text": ...}send(): attaches the embed to the first chunk; marker stripped from contentedit_message(): on streaming finalize, swaps the marker for the embed; mid-stream edits show a⏳ rendering embed…placeholder so the raw JSON never flashes on screenTests
11 new unit tests in
tests/gateway/test_discord_adapter_embed.py(parse, preamble/tail preservation, invalid-JSON fallback, newline handling, colour/footer forms, 25-field cap, send-path wiring, marker-gated no-op). Existing Discord adapter tests still pass.