Skip to content

feat(discord): interactive AskUserQuestion with buttons and select menus - #25

Merged
monsterxx03 merged 2 commits into
mainfrom
feat/discord-askuser
Aug 29, 2026
Merged

feat(discord): interactive AskUserQuestion with buttons and select menus#25
monsterxx03 merged 2 commits into
mainfrom
feat/discord-askuser

Conversation

@monsterxx03

Copy link
Copy Markdown
Owner

概要

让 Discord channel 支持 AskUserQuestion 交互式提问。此前该工具在 channel 模式下被 manager 自动取消(auto-cancel),agent 永远无法向用户提问;现在会渲染为 Discord 原生 UI(按钮 / select 菜单),点击后答案直接回传给阻塞中的 agent 轮次。

变更

feat 6599153 — 交互式 AskUserQuestion

  • 实现 channel.InteractiveChannelInteractive() + PresentQuestions):
    • 单选且 ≤5 选项 → 一行按钮
    • 多选或 >5 选项 → select 菜单
    • 无选项 → 文本提示"请直接回复此消息"
  • INTERACTION_CREATE(message component)事件处理:解析 tachi:ask:<token>:q<idx>[:o<optIdx>] CustomID → 构造 AskUserAnswers → 经 manager 现有 handler 路径送回等待中的 agent turn(返回 Steered,不重复发消息)
  • 回答后编辑原消息:组件全部 disable + 标注"✅ 已选择",防重复点击
  • System prompt 告知 LLM 可交互式提问

fix a6fda93 — 交互生命周期加固(review 反馈)

  • 陈旧点击防护:新增 channel.AskUserAcknowledger 钩子,manager 在文字兜底/取消回答投递成功后通知 channel 退役 UI 状态,杜绝"隐形"二次 turn;兜底极罕见路径下非 Steered 回复会被转发而非静默丢弃
  • 注册表重构container.LockedMap 承载 pending 状态,claim 为原子 LoadAndDelete(双击不会双投);messageID 在锁内发布(无撕裂读);同 thread 旧状态在新提问时清理(有界增长)
  • Discord 硬限制前置校验:select 菜单 ≤25 选项(超出提示文字兜底)、消息正文截断至 2000 字符内、空问题列表直接拒绝
  • 交互收尾:优先走 interaction token(InteractionResponseEdit),失败 fallback 消息编辑
  • CustomID 段角色校验(q/o 不混用)、"请逐题回答"文案

测试

  • 新增 19 个单元测试(渲染/截断/解析/claim 原子性/AcknowledgeAskUser 清理/过期 TTL 等)
  • 全项目 go test ./...go test -race ./channel/discord/ ./channel/manager/ 通过

相关文档

  • docs/2026-07-11-discord-channel-design.md(Phase 3 遗留项:AskUserQuestion 组件)

Implements channel.InteractiveChannel for the Discord channel so the LLM's
AskUserQuestion tool renders as native UI instead of being auto-rejected:

- PresentQuestions builds one action row per multiple-choice question:
  buttons for single-select with <=5 options, select menus otherwise;
  questions without options fall back to a text prompt
- Component interactions (INTERACTION_CREATE message component) are routed
  back to the waiting agent turn through the manager's AskUserAnswers path;
  the question message is then edited to disable its components and show
  the chosen answer, preventing double answers
- CustomID namespace tachi:ask:<token>:q<idx>[:o<optIdx>] with a 15-minute
  pending-state TTL; text replies remain a working fallback
- System prompt now tells the LLM it can ask interactive questions
Addresses review findings on the interactive prompt implementation:

- Retire pending UI when a prompt is settled via text fallback or cancel:
  new AskUserAcknowledger hook in pkg/channel, invoked by the manager
  after routing a fallback answer, so stale buttons can never start an
  unintended second turn (and non-steered replies from such clicks are
  now forwarded instead of silently dropped)
- Registry now a container.LockedMap: claims are atomic LoadAndDelete
  (double-clicks can't both deliver), messageID is published under the
  lock after a successful send (no torn reads), stale entries for a
  thread are cleaned when presenting a new prompt (bounded growth)
- Pre-send validation of Discord hard limits: select menus cap at 25
  options (with a typed-fallback hint), message body truncates under
  2000 chars, and an empty question list is rejected up front
- Complete component interactions via the interaction token
  (InteractionResponseEdit) with message-edit fallback
- CustomID parsing validates q/o segment roles
- Prompt copy guides per-question answering; token generation reuses
  strutil.ShortUUID
@monsterxx03
monsterxx03 merged commit 59f01f9 into main Aug 29, 2026
3 checks passed
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.

1 participant