HKUDS/Vibe-Trading · warning · ValueError
Feishu message content must not be empty
Error message
Feishu message content must not be empty
What it means
Raised by FeishuChannel.send_with_receipt when sending a plain text message (no media, reply, or metadata) whose content strips to empty. It is a client-side validation before any HTTP call, because the Feishu im/v1/messages API rejects empty text payloads. Uses ValueError rather than RuntimeError since it's caller input, not provider state.
Source
Thrown at agent/src/channels/feishu.py:2209
except Exception:
self.logger.exception("Error sending message")
raise
async def send_with_receipt(self, msg: OutboundMessage) -> DeliveryReceipt:
"""Send a non-reply message and retain Feishu's provider message id.
Scheduled briefings use this path. Rich reply/media sends keep the
generic adapter contract because Feishu's reply and upload APIs do not
expose one uniform receipt shape.
"""
if msg.media or msg.reply_to or msg.metadata:
return await super().send_with_receipt(msg)
if not self._client:
raise RuntimeError("Feishu client is not initialized")
content = (msg.content or "").strip()
if not content:
raise ValueError("Feishu message content must not be empty")
receive_id_type = "chat_id" if msg.chat_id.startswith("oc_") else "open_id"
loop = asyncio.get_running_loop()
payloads: list[tuple[str, str]] = []
fmt = self._detect_msg_format(content)
if fmt == "text":
payloads.append(("text", json.dumps({"text": content}, ensure_ascii=False)))
elif fmt == "post":
payloads.append(("post", self._markdown_to_post(content)))
else:
elements = self._build_card_elements(content)
for chunk in self._split_elements_by_table_limit(elements):
payloads.append(
(
"interactive",
json.dumps(
{"config": {"wide_screen_mode": True}, "elements": chunk},
ensure_ascii=False,View on GitHub (pinned to 80ffdda44c)
Solutions
- Check/trim msg.content before sending and skip or substitute a placeholder when empty
- Fix the upstream producer (prompt/LLM output) so it never yields empty content
- If you intended to send media or a reply, populate msg.media or msg.reply_to so the base channel path is used
Example fix
// before
await channel.send_with_receipt(OutboundMessage(chat_id=chat, content=" "))
// after
text = (content or "").strip()
if not text:
text = "(empty response)"
await channel.send_with_receipt(OutboundMessage(chat_id=chat, content=text)) Defensive patterns
Strategy: validation
Validate before calling
content = (msg.content or "").strip()
if not content and not (msg.media or msg.reply_to):
skip_or_placeholder = True # don't call send_with_receipt as-is Type guard
def has_sendable_text(msg: OutboundMessage) -> bool:
return bool((msg.content or "").strip()) or bool(msg.media or msg.reply_to or msg.metadata) Try / catch
null
Prevention
- Normalize/trim LLM output before constructing OutboundMessage
- Add an empty-output guard in the agent reply pipeline
- Skip sending rather than letting ValueError abort the whole handler
When it happens
Trigger: Calling send_with_receipt with an OutboundMessage whose content is '', None, or only whitespace, while msg.media, msg.reply_to, and msg.metadata are all falsy (those routes delegate to the base channel instead).
Common situations: Upstream LLM/agent produced an empty completion, templating rendered an empty string, whitespace-only content after trimming user input, or a bug where content was assigned to the wrong field.
Related errors
- invalid alpha_id
- alpha_id not found
- invalid period: {exc}
- too many running benches; wait for one to finish
- invalid job_id
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/968fb3728ce233d7.
Report an issue: GitHub.