agentscope-ai / agentscope-ai/agentscope

[Bug]: Discord tool approval buttons stop responding after direct reply delivery

Offen
#2,522 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Python
Sterne
31.5k
Forks
3.5k
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
95

Beschreibung

### Prerequisites

- [x] I have searched the existing [issues](https://github.com/agentscope-ai/agentscope/issues) and [discussions](https://github.com/agentscope-ai/agentscope/discussions), and this is not a duplicate.
- [x] This is a bug, not a usage question. (For questions, please use [Discussions](https://github.com/agentscope-ai/agentscope/discussions/new?category=general) instead.)

### Background / Description

While reproducing #2373 (restricting group-chat tool approval to the requester), I found a separate Discord regression on my local checkout of `main`: the bot posts the tool-confirmation card, but clicking either **Approve** or **Deny** does not invoke the corresponding callback. The run remains waiting for confirmation.

This blocks testing requester authorization on Discord without an additional patch. I have a local `on_interaction` patch that restores the approval flow, and would like feedback on the preferred implementation before preparing a contribution.

**Expected:** the click is acknowledged and the pending tool call is approved or denied.

**Actual:** neither button callback runs, and the tool call remains waiting for confirmation.

### Root-cause analysis

The send and receive paths use different `DiscordChannel` instances, each with its own `discord.Client`:

- `ChannelLifecycleDispatcher` creates the listening instance. `start_listening()` establishes the Gateway connection and sets `_emit`.
- `ChannelClients` creates a separate sending instance. `_ensure_client()` calls `login()` for REST access without opening a Gateway connection.
- `_build_view()` still creates a local `discord.ui.View` with decorated `approve()` / `deny()` callbacks.
- When the View is sent, discord.py stores its callbacks in the **sending Client's** ViewStore.
- The **listening Client** receives the component interaction, but its ViewStore has no matching entry, so the SDK cannot dispatch to those callbacks.

`_ensure_client()` caches a Client per `DiscordChannel` object; it does not share one Client between the two instances. This can happen even in a single-process deployment.

```text
Sending Client B: posts card -> stores View callbacks in B
Discord: user clicks -> delivers interaction to listening Client A
Listening Client A: no matching View callback -> approval is not handled
```

The original implementation at `efba74ed` forwarded replies through the same `inst.channel` that was listening, so View callbacks and incoming interactions shared a Client. The delivery change in `801dd1ef` (#2395), following the worker separation in #2390, moved reply sending to `ChannelClients` without adapting this Discord callback mechanism.

Relevant source at the reproduced revision:

- [Listener construction](https://github.com/agentscope-ai/agentscope/blob/41ba0216b3291b085d964bbf1c52d05a9b9d4c41/src/agentscope/app/channel/_dispatcher.py#L127)
- [Sender construction](https://github.com/agentscope-ai/agentscope/blob/41ba0216b3291b085d964bbf1c52d05a9b9d4c41/src/agentscope/app/channel/_clients.py#L139)
- [Approval View callbacks](https://github.com/agentscope-ai/agentscope/blob/41ba0216b3291b085d964bbf1c52d05a9b9d4c41/src/agentscope/app/channel/_discord/_channel.py#L478)

### Two possible fixes

Both approaches preserve separate listening and REST-sending instances.

**Option 1: handle component interactions in the listener.** Register `on_interaction` inside `start_listening()`. Give approval buttons an explicit, recognizable `custom_id`, and let the listener decode it, locate the pending approval, and emit the decision through its own `_emit`. This is the approach in my local patch and avoids synchronizing a Python View for every card between processes.

**Option 2: register SDK component handlers on the listener.** Use discord.py's View/component dispatch mechanism on the listening Client, with matching explicit `custom_id` values. A `DynamicItem` handler registered at listener startup could match approval IDs without per-card registration, subject to the supported SDK version. Alternatively, per-card persistent Views could use `client.add_view()`, but their identifiers/context would need to reach the listener and be restored after restart. Callbacks must bind to the listening channel; simply moving the sender's existing View would retain its closure over the sending channel.

For either option, approval lookup must identify the correct run/tool call, respect Discord's custom-ID length limit, and retain stale/duplicate-click handling. Restoring interaction delivery does not itself solve requester authorization in #2373.

**Which approach would the community prefer? I am happy to implement either and coordinate this fix with the work on #2373.**

### Error Messages

```shell
Discord displays: The application didn't respond in time

Neither approve() nor deny() is invoked; the pending tool call remains waiting for confirmation.
```

### Steps to Reproduce

1. Start Agent Service from the main revision listed below and configure a Discord channel.
2. Configure the agent's Bash tool to require user confirmation.
3. Mention the bot and ask it to execute `touch approval-2373.txt`.
4. Wait for the tool-approval card.
5. Click **Approve**, or repeat with **Deny**.
6. Observe the interaction timeout and the tool call remaining in the waiting state.

### Environment

- AgentScope Version: 2.0.8, source checkout at `41ba0216b3291b085d964bbf1c52d05a9b9d4c41` (local main)
- Python Version: 3.11.4
- discord.py Version: 2.7.1
- OS: macOS

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.