This research report outlines the architecture patterns, workflow designs, and project structures for bridging a local AI coding agent (specifically Google Antigravity SDK) to Telegram or Discord for remote execution, status updates, and interactive "Yes/No" tool approvals (Human-in-the-Loop).
discord.py and the google-antigravity Python SDK. Discord is selected over Telegram because its native Channels (mapping to different Projects) and Threads (mapping to different Conversations) eliminate the need to write custom UI/session-switching code.pre_tool_call_decide) to intercept tool execution, pause the agent, send interactive Discord buttons ([Approve]/[Deny]), and resume once clicked.This research synthesized patterns from:
1. LangGraph State Interrupts: Analyzing how LangGraph uses checkpointers and interrupts to pause agents for human approval.
2. LobeHub Telegram HITL MCP: Standardized model context protocol patterns for piping questions to messaging apps.
3. Community Discord Bridges: Analyzing how developers route tmux sessions and CLI outputs to Discord channels.
4. Google Antigravity SDK: Reviewing the native hooks and LocalAgentConfig APIs for tool interception.
The following diagram illustrates how a command is routed from Discord, executed by the agent, paused for approval, and completed.
sequenceDiagram
autonumber
actor User as User (Phone/Discord)
participant Discord as Discord Server
participant Bot as Python Bot (Host)
participant Agent as Antigravity Agent
participant System as Local OS / Filesystem
User->>Discord: Create Thread & Send: "Refactor main.py"
Discord->>Bot: Event: on_message (in Thread)
Note over Bot: Identify Project from Channel Path<br/>Identify Session from Thread ID
Bot->>Agent: Initialize/Retrieve Agent & Send Prompt
Agent->>Agent: Process prompt & decide to use tool: "write_file"
rect rgb(240, 240, 255)
Note over Agent, Bot: HITL Approval Gate
Agent->>Bot: Trigger Hook: pre_tool_call_decide(write_file)
Note over Agent: Agent Pauses (Async Wait)
Bot->>Discord: Send Message with Buttons: [Approve] / [Deny]
User->>Discord: Taps [Approve]
Discord->>Bot: Event: on_interaction (Button Click)
Bot->>Agent: Resume Hook with HookResult(allow=True)
end
Agent->>System: Execute write_file tool
System-->>Agent: Tool Result (Success)
Agent-->>Bot: Return Final Answer
Bot->>Discord: Send Final Response to Thread
Discord-->>User: View complete refactoring
To implement this on your Mac, we will structure a lightweight Python project.
antigravity-messenger-bridge/
├── .env # BOT_TOKEN, AUTHORIZED_USER_IDS, WORKSPACE_ROOT
├── requirements.txt # discord.py, google-antigravity, python-dotenv
├── main.py # Bot entrypoint and event loop
├── bridge/
│ ├── __init__.py
│ ├── bot.py # Discord client & event handlers
│ ├── manager.py # Session & Project routing manager
│ └── agent_wrapper.py # Antigravity agent wrapper & hook definitions
└── config.json # Maps Discord Channel IDs to local project paths
bridge/manager.py)Responsible for routing messages.
* It reads config.json to map Discord Channels to local directory paths.
* Example: Channel #website-frontend $\rightarrow$ /Users/keira/Documents/GitHub/my-frontend.
* It tracks active Discord Threads. Each Thread ID represents an isolated conversation context.
* When a message arrives in a Thread, it retrieves the corresponding AgentWrapper or creates a new one.
bridge/agent_wrapper.py)Wraps the google-antigravity SDK and implements the approval gate.
import asyncio
from google.antigravity import types
from google.antigravity.connections.local import LocalAgentConfig, LocalAgent
class AgentWrapper:
def __init__(self, project_path, thread_id, bot_client):
self.project_path = project_path
self.thread_id = thread_id
self.bot = bot_client
# Future used to block the hook until the user clicks a button
self.approval_future = None
# Configure the Agent with local hooks
self.config = LocalAgentConfig(
workspace=self.project_path,
hooks=[
self.pre_tool_call_hook,
self.post_tool_call_hook
]
)
self.agent = LocalAgent(self.config)
async def pre_tool_call_hook(self, tool_call: types.ToolCall) -> types.HookResult:
# 1. Create the approval future
self.approval_future = asyncio.get_event_loop().create_future()
# 2. Send the approval request to Discord
await self.bot.send_approval_request(
thread_id=self.thread_id,
tool_name=tool_call.name,
arguments=tool_call.arguments
)
# 3. Wait for the Discord button click event to resolve the future
approved = await self.approval_future
return types.HookResult(allow=approved)
def resolve_approval(self, approved: bool):
if self.approval_future and not self.approval_future.done():
self.approval_future.set_result(approved)
| Inherit (Ground Truth) | Takeaway (Actionable Insights) |
|---|---|
| Secure User Validation: Messengers are public networks; we must strictly validate the sender's ID. | Implement a hard check: if message.author.id != AUTHORIZED_USER_ID: return on every event. |
Stateful Asynchrony: AI agents run asynchronously and cannot be blocked using synchronous input(). |
Use asyncio.Future to pause the SDK's async hook execution cleanly without freezing the bot's event loop. |
| Workspace Isolation: Operating on multiple projects requires changing the working directory of the agent. | Dynamically instantiate LocalAgent with a different workspace path depending on the Discord channel. |
Guilds, Guild Messages, and Message Content intents, and invite it to your private server.main.py: Write the integration code connecting discord.py with google-antigravity.