Summary
When the bot spawns a subagent and the subagent later reports its result, the reply is delivered as a plain post message, while a normal reply (triggered directly by a user message) is rendered as an interactive card (with the streaming controller and the Completed · Elapsed · model · tokens footer).
So within the same conversation the two kinds of replies look visibly different: normal turns are cards, subagent/announce turns are plain posts (and are not streamed).
This is a follow-up to #563 (which fixed where subagent replies land — the original topic). This issue is about how they are rendered.
Repro
- In a topic-enabled group,
@bot with a task that spawns a subagent (e.g. "spawn 2 subagents to verify X").
- The bot's immediate reply ("subagents dispatched") renders as an interactive card.
- When the subagents finish, the integrated result reply renders as a plain
post, not a card, and is not streamed.
Root cause (from tracing the compiled core + plugin)
There are two distinct outbound paths that do not share a renderer:
- Normal inbound message → the plugin's
dispatch.ts explicitly creates createFeishuReplyDispatcher (streaming card controller) → rendered as an interactive card.
- Subagent announce / completion → the core wakes or re-runs the parent agent via the announce-delivery path (
queueEmbeddedAgentMessageWithOutcome / dispatchGatewayMethodInProcess("agent")), and its output is delivered through the default sendMessage → feishuOutbound.sendText → im.message.create/reply with msg_type: post. This path bypasses the streaming card dispatcher entirely.
Because the plugin's outbound layer receives only plain text on the announce path, it cannot tell "this is an agent result (should be a card)" from "this is a system/error notification (should stay a post)", so it cannot fix this cleanly on its own.
Impact
- Visual inconsistency within a single conversation; subagent results look like a different sender/format.
- No progressive (streaming) output for subagent results.
- Makes multi-agent flows feel fragmented.
Possible directions
- A — plugin wraps all outbound text in a card. Simplest, but has side effects (system/error notifications would also become cards) and still cannot reproduce the streaming footer (the runtime metadata isn't available on the announce path). Style would not fully match.
- B — let the announce path reuse the channel's reply/card dispatcher. Cleanest and truly consistent, but is a core + plugin change: the announce delivery would need to render through the same
createFeishuReplyDispatcher/streaming controller that normal inbound turns use.
- C — core passes a "render as card" intent on the announce delivery, which the plugin honours by routing through the card sender instead of
sendText. Medium effort; requires the announce delivery to carry render intent (today it carries only { to, threadId, content }).
Direction B (or C) appears to be the right layer, since the discriminator ("is this an agent reply?") lives in the core, not in the plugin's outbound adapter.
Happy to help with a PR once there's agreement on which layer should own the fix.
Summary
When the bot spawns a subagent and the subagent later reports its result, the reply is delivered as a plain
postmessage, while a normal reply (triggered directly by a user message) is rendered as an interactive card (with the streaming controller and theCompleted · Elapsed · model · tokensfooter).So within the same conversation the two kinds of replies look visibly different: normal turns are cards, subagent/announce turns are plain posts (and are not streamed).
This is a follow-up to #563 (which fixed where subagent replies land — the original topic). This issue is about how they are rendered.
Repro
@botwith a task that spawns a subagent (e.g. "spawn 2 subagents to verify X").post, not a card, and is not streamed.Root cause (from tracing the compiled core + plugin)
There are two distinct outbound paths that do not share a renderer:
dispatch.tsexplicitly createscreateFeishuReplyDispatcher(streaming card controller) → rendered as an interactive card.queueEmbeddedAgentMessageWithOutcome/dispatchGatewayMethodInProcess("agent")), and its output is delivered through the defaultsendMessage→feishuOutbound.sendText→im.message.create/replywithmsg_type: post. This path bypasses the streaming card dispatcher entirely.Because the plugin's outbound layer receives only plain text on the announce path, it cannot tell "this is an agent result (should be a card)" from "this is a system/error notification (should stay a post)", so it cannot fix this cleanly on its own.
Impact
Possible directions
createFeishuReplyDispatcher/streaming controller that normal inbound turns use.sendText. Medium effort; requires the announce delivery to carry render intent (today it carries only{ to, threadId, content }).Direction B (or C) appears to be the right layer, since the discriminator ("is this an agent reply?") lives in the core, not in the plugin's outbound adapter.
Happy to help with a PR once there's agreement on which layer should own the fix.