messenger

package
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 10, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

Messaging Configuration

Genie supports sending messages to external chat platforms via the pkg/messenger package. When configured, the ReAcTree agent gains a send_message tool that can post updates, alerts, or results to Slack, Telegram, Microsoft Teams, or Google Chat.

Quick Start

Add a [messenger] section to your .genie.toml:

[messenger]
platform = "slack"
buffer_size = 100

[messenger.slack]
app_token = "${SLACK_APP_TOKEN}"
bot_token = "${SLACK_BOT_TOKEN}"

The agent will automatically have access to a send_message tool during ReAcTree execution.

Supported Platforms

Slack

Uses Socket Mode — no public endpoint required.

[messenger]
platform = "slack"

[messenger.slack]
app_token = "${SLACK_APP_TOKEN}"   # xapp-... (App-Level Token)
bot_token = "${SLACK_BOT_TOKEN}"   # xoxb-... (Bot User OAuth Token)

Setup:

  1. Create a Slack App at api.slack.com/apps
  2. Enable Socket Mode and generate an App-Level Token
  3. Add bot scopes: chat:write, channels:read, channels:history
  4. Install the app to your workspace
Telegram

Uses long-polling — no webhook endpoint required.

[messenger]
platform = "telegram"

[messenger.telegram]
token = "${TELEGRAM_BOT_TOKEN}"    # From @BotFather

Setup:

  1. Message @BotFather on Telegram
  2. Create a new bot with /newbot
  3. Copy the token
Microsoft Teams

Uses the Bot Framework — requires an HTTP endpoint for incoming activities.

[messenger]
platform = "teams"

[messenger.teams]
app_id = "${TEAMS_APP_ID}"
app_password = "${TEAMS_APP_PASSWORD}"
listen_addr = ":3978"              # Default: :3978

Setup:

  1. Register a bot in the Azure Bot Service
  2. Note the App ID and generate a password
  3. Configure the messaging endpoint to point to your server's /api/messages
Google Chat

Uses HTTP push for incoming events and the Chat API for outgoing messages.

[messenger]
platform = "googlechat"

[messenger.googlechat]
# No fields required; uses logged-in user token (SecretProvider).

Setup:

  1. Google Chat uses the same logged-in user OAuth token as Gmail/Calendar/Drive. Sign in with genie grant (or set TokenFile / keyring).
  2. Create and configure your Chat app in the Google Chat API console; set the app URL to your server's address.
  3. Ensure the OAuth client used for grant includes the https://www.googleapis.com/auth/chat.messages scope (included in the default browser flow).

Configuration Reference

Field Type Default Description
messenger.platform string "" (disabled) Platform to use: slack, telegram, teams, googlechat
messenger.buffer_size int 100 Incoming message channel buffer size
messenger.slack.app_token string Slack App-Level Token for Socket Mode
messenger.slack.bot_token string Slack Bot User OAuth Token
messenger.telegram.token string Telegram Bot API token
messenger.teams.app_id string Microsoft Bot Framework App ID
messenger.teams.app_password string Microsoft Bot Framework App Password
messenger.teams.listen_addr string :3978 HTTP listen address for Bot Framework
messenger.googlechat No fields; Google Chat uses the logged-in user token (SecretProvider).

Environment Variables

All config values support ${ENV_VAR} expansion. Common variables:

Variable Used by
SLACK_APP_TOKEN Slack adapter
SLACK_BOT_TOKEN Slack adapter
TELEGRAM_BOT_TOKEN Telegram adapter
TEAMS_APP_ID Teams adapter
TEAMS_APP_PASSWORD Teams adapter

How It Works

.genie.toml ──[messenger]──► messenger.NewFromConfig()
                                    │
                                    ▼
                            messenger.Messenger
                                    │
                        ┌───────────┼───────────┐
                        ▼           ▼           ▼
                  ToolDeps      Connect()    Receive()
                      │                        │
                      ▼                        ▼
                send_message tool     ┌──────────────────┐
                (ReAcTree agent)      │  grantWithTUI    │
                                      │  select loop     │
                   TUI ──inputChan──► │                  │──► codeOwner.Chat()
                   TCP ──UserInputMsg─┤                  │──► tui.EmitAgentMessage
                   Messenger ─────────┘                  │──► msgr.Send() (reply)
                                      └──────────────────┘
  1. Config is loaded from .genie.toml via LoadGenieConfig()
  2. If messenger.platform is set, NewFromConfig() creates the adapter
  3. The adapter is connected and injected into codeowner.ToolDeps
  4. The ReAcTree agent receives a send_message tool (outbound only)
  5. grantWithTUI calls messenger.Receive() and multiplexes incoming messages alongside TUI and TCP input
  6. Messenger replies are sent back to the originating channel/thread
  7. On shutdown, the adapter is gracefully disconnected

Programmatic Usage

import (
    "github.com/stackgenhq/genie/pkg/messenger"
    _ "github.com/stackgenhq/genie/pkg/messenger/slack"  // register adapter
)

// Create from config
m, err := messenger.NewFromConfig(cfg.Messenger)

// Or create directly
m := slack.New(slack.Config{
    AppToken: os.Getenv("SLACK_APP_TOKEN"),
    BotToken: os.Getenv("SLACK_BOT_TOKEN"),
})

// Connect and send
m.Connect(ctx)
defer m.Disconnect(ctx)

m.Send(ctx, messenger.SendRequest{
    Channel: messenger.Channel{ID: "C1234567890"},
    Content: messenger.MessageContent{Text: "Hello from Genie!"},
})

Known Limitations & Blindspots

[!NOTE] Items marked ✅ have been addressed. Items marked ⚠️ are known but not yet fixed.

✅ 1. Sequential Message Handling

Messenger messages are now dispatched in goroutines, so slow LLM calls don't block the select loop. TUI input remains sequential (expected UX).

✅ 2. No Sender Context

CodeQuestion.SenderContext now carries sender identity (e.g. slack:U12345:C67890), injected into the prompt so the LLM knows who is asking and from which platform.

✅ 3. Duplicate Replies

When a message originates from a messenger, the send_message tool is excluded via CodeQuestion.ExcludeTools since the chat loop handles the reply directly. This prevents double-posting.

✅ 4. Reconnection on Disconnect

ReceiveWithReconnect() in pkg/messenger/reconnect.go wraps Receive() in a goroutine with exponential backoff (1s → 30s). When the platform drops the connection, it automatically retries and forwards messages through a relay channel that stays open until context cancellation.

✅ 5. Loading Indicator for TCP Input

The UserInputMsg handler in model.go now calls SetLoading(true) after forwarding input, so the TUI shows a "Genie is thinking..." spinner for TCP/connect users — matching the keyboard input UX.

✅ 6. Source Attribution in TUI

UserInputMsg now carries a Source field, and ChatMessage stores the Sender. TCP input shows as "user (connect)" and messenger input shows sender name + platform (e.g. "Alice (slack)"). A styled attribution label renders above external user bubbles.

✅ 7. Thread-Isolated Memory

UserKey.UserID now varies per sender/thread (derived from SenderContext), so conversation history is isolated across Slack threads, TUI sessions, and different users.

⚠️ 8. No Rate Limiting or Access Control

Anyone who can message the bot on Slack/Teams can trigger codeOwner.Chat() and LLM calls. There's no auth/rate-limit layer between Receive() and the expert — unlike the TCP listener which has mTLS + audit logging.

Documentation

Overview

Package messenger provides a generic, platform-agnostic interface for bi-directional messaging across chat platforms (Slack, Telegram, Teams, Google Chat, Discord, etc.).

This package is designed to be open-sourceable and has zero dependencies on genie, LLM, or agent-specific code. All types represent pure communication constructs.

Platform adapters implement the Messenger interface and live in sub-packages (e.g., messenger/slack, messenger/telegram, messenger/teams).

Index

Constants

View Source
const ContentCooldown = 2 * time.Minute

ContentCooldown is the minimum interval between successive *content-rich* (meal plan, recipe, long response) deliveries to the same channel. After a content message is delivered, ALL subsequent messages (informational or content) to that channel are suppressed for this window. This prevents sub-agents from sending N copies of the same result across N LLM iterations.

View Source
const DefaultAGUIPort uint32 = 9876
View Source
const MessageCooldown = 15 * time.Second

MessageCooldown is the minimum interval between successive *informational* (short progress/status) send_message calls to the same channel.

View Source
const QuotedMessageID = "quoted_message_id"
View Source
const ToolName = "send_message"

Variables

View Source
var (
	// ErrNotConnected is returned when Send or Receive is called before Connect.
	ErrNotConnected = errors.New("messenger: not connected")

	// ErrAlreadyConnected is returned when Connect is called on an already-connected messenger.
	ErrAlreadyConnected = errors.New("messenger: already connected")

	// ErrChannelNotFound is returned when the target channel does not exist or is inaccessible.
	ErrChannelNotFound = errors.New("messenger: channel not found")

	// ErrSendFailed is returned when a message could not be delivered.
	ErrSendFailed = errors.New("messenger: send failed")

	// ErrRateLimited is returned when the platform's rate limit has been exceeded.
	ErrRateLimited = errors.New("messenger: rate limited")
)

Functions

func GoalFromContext

func GoalFromContext(ctx context.Context) string

GoalFromContext returns the agent's current goal from the context, or an empty string if not set.

func NewSendMessageTool

func NewSendMessageTool(m Messenger, opts ...SendMessageToolOption) tool.Tool

NewSendMessageTool creates a tool.Tool that wraps a Messenger for sending messages. The tool exposes a "send_message" action that the agent can invoke.

func ReceiveWithReconnect

func ReceiveWithReconnect(ctx context.Context, msgr Messenger, initialBackoff, maxBackoff time.Duration) <-chan IncomingMessage

ReceiveWithReconnect wraps Messenger.Receive() in a reconnection loop with exponential backoff. When the platform connection drops (channel closes), it retries Receive() automatically. The returned relay channel stays open until ctx is cancelled.

Parameters:

  • initialBackoff: starting delay between reconnection attempts (e.g. 1s)
  • maxBackoff: maximum delay cap (e.g. 30s)

func RegisterAdapter

func RegisterAdapter(platform Platform, factory AdapterFactory)

RegisterAdapter registers an adapter factory for a given platform. Called by adapter sub-packages in their init() functions.

func WithGoal

func WithGoal(ctx context.Context, goal string) context.Context

WithGoal returns a new context carrying the agent's current goal string. The goal flows through to tools like send_message so that the reaction ledger can associate sent message IDs with the goal that produced them.

func WithMessageOrigin

func WithMessageOrigin(ctx context.Context, origin MessageOrigin) context.Context

WithMessageOrigin returns a new context carrying the given MessageOrigin.

Types

type AGUIConfig

type AGUIConfig struct {
	// AppName is used as the session.Key AppName for thread tracking.
	// Defaults to "genie" if empty.
	AppName string `yaml:"app_name,omitempty" toml:"app_name,omitempty"`

	// --- Server settings ---
	CORSOrigins []string `yaml:"cors_origins,omitempty" toml:"cors_origins,omitempty"`
	Port        uint32   `yaml:"port,omitempty" toml:"port,omitempty"`
	// BindAddr is the listen address (e.g. ":9876" for all interfaces, "127.0.0.1:9876" for localhost only).
	// When empty, the server binds to ":port" so HTTP-push messengers (Teams, Google Chat, AGUI) are reachable from other hosts/containers.
	BindAddr      string  `yaml:"bind_addr,omitempty" toml:"bind_addr,omitempty"`
	RateLimit     float64 `yaml:"rate_limit,omitempty" toml:"rate_limit,omitempty"`         // req/sec per IP (0 = disabled)
	RateBurst     int     `yaml:"rate_burst,omitempty" toml:"rate_burst,omitempty"`         // burst allowance per IP
	MaxConcurrent int     `yaml:"max_concurrent,omitempty" toml:"max_concurrent,omitempty"` // max in-flight requests (0 = unlimited)
	MaxBodyBytes  int64   `yaml:"max_body_bytes,omitempty" toml:"max_body_bytes,omitempty"` // max request body in bytes (0 = unlimited)

	// Auth holds authentication settings (password, JWT/OIDC).
	// See auth.Config for all available options.
	Auth auth.Config `yaml:"auth,omitempty" toml:"auth,omitempty"`
}

AGUIConfig holds AG-UI server and messenger adapter configuration. When AGUI is the active messenger (default), these settings configure both the in-process messenger adapter and the HTTP SSE server.

func DefaultAGUIConfig

func DefaultAGUIConfig() AGUIConfig

DefaultAGUIConfig returns sensible defaults for the AG-UI server.

type AdapterConfig

type AdapterConfig struct {
	// MessageBufferSize is the capacity of the incoming message channel.
	// Defaults to DefaultMessageBufferSize.
	MessageBufferSize int
	// SecretProvider is optional; when set, adapters (e.g. Google Chat) can use it
	// to resolve OAuth tokens and credentials (logged-in user) instead of service account files.
	SecretProvider security.SecretProvider
}

AdapterConfig holds common configuration used by platform adapters. Adapters should embed or reference this struct to benefit from shared options.

func ApplyOptions

func ApplyOptions(opts ...Option) AdapterConfig

ApplyOptions applies functional options to the default adapter config.

func DefaultAdapterConfig

func DefaultAdapterConfig() AdapterConfig

DefaultAdapterConfig returns an AdapterConfig with sensible defaults.

type AdapterFactory

type AdapterFactory func(params map[string]string, opts ...Option) (Messenger, error)

AdapterFactory creates a Messenger from generic string params. Used by the registry pattern so the parent package doesn't import adapters.

type ApprovalInfo

type ApprovalInfo struct {
	// ID is the unique approval identifier.
	ID string
	// ToolName is the tool that requires approval.
	ToolName string
	// Args is the pretty-printed JSON arguments.
	Args string
	// Feedback is the optional justification / reason for the call.
	Feedback string
}

ApprovalInfo carries the data needed by adapters to render a rich approval notification. Defined here (not in the hitl package) to avoid a circular dependency between messenger and hitl.

type Attachment

type Attachment struct {
	// Name is the filename or label.
	Name string `json:"name"`
	// URL is the download/access URL for the attachment.
	URL string `json:"url"`
	// ContentType is the MIME type (e.g., "image/png", "application/pdf").
	ContentType string `json:"content_type"`
	// Size is the file size in bytes (0 if unknown).
	Size int64 `json:"size"`
	// LocalPath is the path to the downloaded file on disk. Populated when
	// the adapter downloads the attachment (e.g., WhatsApp encrypted media).
	// Empty if only metadata is available (e.g., Slack URLs that require auth).
	LocalPath string `json:"local_path"`
}

Attachment represents a file or media attachment on a message.

type Channel

type Channel struct {
	// ID is the platform-specific channel identifier.
	ID string
	// Name is the human-readable channel name (may be empty for DMs).
	Name string
	// Type classifies the channel (DM, group, or channel).
	Type ChannelType
}

Channel identifies a conversation in a messaging platform.

type ChannelType

type ChannelType string

ChannelType classifies a conversation channel.

const (
	// ChannelTypeDM represents a direct message / private conversation.
	ChannelTypeDM ChannelType = "dm"
	// ChannelTypeGroup represents a group conversation (e.g., Slack group DM, Discord group).
	ChannelTypeGroup ChannelType = "group"
	// ChannelTypeChannel represents a public or private channel/guild.
	ChannelTypeChannel ChannelType = "channel"
)

type ClarificationInfo

type ClarificationInfo struct {
	// RequestID is the unique clarification request identifier.
	RequestID string
	// Question is the question posed by the agent.
	Question string
	// Context is optional context explaining why the agent needs this info.
	Context string
}

ClarificationInfo carries data for rendering a clarifying-question notification on a chat platform.

type Config

type Config struct {
	// Platform selects which adapter to use (slack, discord, telegram, teams, googlechat).
	// Empty means messaging is disabled.
	Platform Platform `yaml:"platform,omitempty" toml:"platform,omitempty"`

	// BufferSize controls the incoming message channel buffer.
	// Defaults to DefaultMessageBufferSize if zero.
	BufferSize int `yaml:"buffer_size,omitempty" toml:"buffer_size,omitempty"`

	// AllowedSenders is an optional allowlist of sender IDs (phone numbers,
	// usernames, or user IDs depending on platform) that the bot will respond
	// to. When empty, the bot responds to all incoming messages.
	// For WhatsApp: use phone numbers without '+' (e.g., "15551234567").
	AllowedSenders []string `yaml:"allowed_senders,omitempty" toml:"allowed_senders,omitempty"`

	// Slack holds Slack-specific configuration.
	Slack SlackConfig `yaml:"slack,omitempty" toml:"slack,omitempty"`

	// Discord holds Discord-specific configuration.
	Discord DiscordConfig `yaml:"discord,omitempty" toml:"discord,omitempty"`

	// Telegram holds Telegram-specific configuration.
	Telegram TelegramConfig `yaml:"telegram,omitempty" toml:"telegram,omitempty"`

	// Teams holds Microsoft Teams-specific configuration.
	Teams TeamsConfig `yaml:"teams,omitempty" toml:"teams,omitempty"`

	// GoogleChat holds Google Chat-specific configuration.
	GoogleChat GoogleChatConfig `yaml:"googlechat,omitempty" toml:"googlechat,omitempty"`

	// WhatsApp holds WhatsApp Business-specific configuration.
	WhatsApp WhatsAppConfig `yaml:"whatsapp,omitempty" toml:"whatsapp,omitempty"`

	// AGUI holds AG-UI SSE adapter configuration.
	AGUI AGUIConfig `yaml:"agui,omitempty" toml:"agui,omitempty"`
}

Config holds platform-agnostic messenger configuration that can be loaded from TOML or YAML config files (e.g., .genie.toml).

func (Config) InitMessenger

func (c Config) InitMessenger(ctx context.Context, opts ...Option) (Messenger, error)

InitMessenger creates a Messenger for the configured platform. If no platform is configured, it defaults to AGUI. Connect() is NOT called — the caller is responsible for connecting the messenger when ready. Pass WithSecretProvider(sp) when using Google Chat so it uses the logged-in user token.

func (Config) IsSenderAllowed

func (c Config) IsSenderAllowed(senderID string) bool

IsSenderAllowed checks whether the given sender ID is permitted to interact with the bot. When AllowedSenders is empty, all senders are allowed. Entries ending with '*' are treated as prefix matches (e.g., "1555*" matches any sender starting with "1555"). This enables operators to restrict the bot to specific users, country codes, or area codes.

func (Config) Validate

func (c Config) Validate() error

Validate checks that all required secrets are present and appear well-formed for the configured platform. It catches common issues like placeholder values, whitespace-only tokens, or missing prefix conventions (e.g. Slack xapp-/xoxb-). Returns nil when messaging is disabled.

type DiscordConfig

type DiscordConfig struct {
	// BotToken is the Discord bot token from the Developer Portal.
	BotToken string `yaml:"bot_token,omitempty" toml:"bot_token,omitempty"`
}

DiscordConfig holds Discord adapter settings.

type GoogleChatConfig

type GoogleChatConfig struct{}

GoogleChatConfig holds Google Chat adapter settings. Google Chat uses the same logged-in user OAuth token as Gmail/Calendar/Drive; pass WithSecretProvider(sp) when calling InitMessenger so the adapter can resolve it.

type IncomingMessage

type IncomingMessage struct {
	// ID is the platform-assigned message identifier.
	ID string
	// Platform identifies which platform the message came from.
	Platform Platform
	// Type distinguishes regular messages from reactions and interactions.
	// Empty string means a normal text/media message.
	Type MessageType
	// Channel is the conversation where the message was posted.
	Channel Channel
	// Sender is the author of the message.
	Sender Sender
	// Content is the message body and any attachments.
	Content MessageContent
	// ThreadID is the thread/reply-chain ID (empty if top-level).
	ThreadID string
	// Timestamp is when the message was sent.
	Timestamp time.Time
	// Metadata holds platform-specific data not captured by common fields.
	Metadata map[string]any

	// ReactionEmoji is the emoji used in a reaction (e.g. "👍", "👎").
	// Only populated when Type == MessageTypeReaction.
	ReactionEmoji string
	// ReactedMessageID is the platform-assigned ID of the message being
	// reacted to. Only populated when Type == MessageTypeReaction.
	ReactedMessageID string

	// Interaction carries structured data when Type == MessageTypeInteraction.
	// Populated by adapters that support interactive UI elements (buttons,
	// menus, card actions). Nil for all other message types.
	Interaction *InteractionData
}

IncomingMessage represents a message received from a platform.

func (IncomingMessage) String

func (msg IncomingMessage) String() string

type InteractionData

type InteractionData struct {
	// ActionID is the platform-specific action identifier.
	// Convention: "{verb}_{resourceID}" (e.g. "approve_abc123",
	// "reject_abc123", "clarify_respond_xyz").
	ActionID string
	// ActionValue is the value associated with the action. For approval
	// buttons, this is the approval ID.
	ActionValue string
	// BlockID is the container block/card identifier
	// (e.g. "approval_abc123" in Slack Block Kit).
	BlockID string
	// ActionType describes the UI element type (e.g. "button", "select").
	ActionType string
	// ResponseURL is an optional, time-limited URL provided by the
	// platform for updating or replacing the message that contained the
	// interactive element. Slack provides this for 30 minutes after a
	// block_actions event. Adapters that don't support it leave this empty.
	ResponseURL string
}

InteractionData carries structured data from an interactive UI element. This is a platform-agnostic representation of a button click, menu selection, or card action. Each adapter converts its native interaction payload (Slack block_actions, Teams Action.Submit, Google Chat card action) into this common type.

Without this type, button clicks from rich approval/clarification notifications would be silently discarded because the Receive channel only delivers text messages.

type LoggingMessenger

type LoggingMessenger struct {
	// contains filtered or unexported fields
}

LoggingMessenger wraps any Messenger implementation and logs all Send and Receive activity. This provides platform-agnostic observability for all outgoing and incoming messages without requiring each adapter to duplicate logging code.

func (*LoggingMessenger) Connect

func (lm *LoggingMessenger) Connect(ctx context.Context) (http.Handler, error)

func (*LoggingMessenger) ConnectionInfo

func (lm *LoggingMessenger) ConnectionInfo() string

func (*LoggingMessenger) Disconnect

func (lm *LoggingMessenger) Disconnect(ctx context.Context) error

func (*LoggingMessenger) FormatApproval

func (lm *LoggingMessenger) FormatApproval(req SendRequest, info ApprovalInfo) SendRequest

func (*LoggingMessenger) FormatClarification

func (lm *LoggingMessenger) FormatClarification(req SendRequest, info ClarificationInfo) SendRequest

func (*LoggingMessenger) Platform

func (lm *LoggingMessenger) Platform() Platform

func (*LoggingMessenger) Receive

func (lm *LoggingMessenger) Receive(ctx context.Context) (<-chan IncomingMessage, error)

func (*LoggingMessenger) Send

func (*LoggingMessenger) Unwrap

func (lm *LoggingMessenger) Unwrap() Messenger

Unwrap returns the underlying Messenger, allowing callers to access the concrete messenger type through the logging wrapper. This follows the standard Go unwrap convention (similar to errors.Unwrap).

func (*LoggingMessenger) UpdateMessage

func (lm *LoggingMessenger) UpdateMessage(ctx context.Context, req UpdateRequest) error

UpdateMessage delegates to the inner Messenger with structured logging. This wraps message updates (e.g. replacing approval buttons with resolved status) with consistent observability.

type MessageContent

type MessageContent struct {
	// Text is the plain-text or markdown message body.
	Text string
	// Attachments are optional file/media attachments.
	Attachments []Attachment
}

MessageContent holds the body of a message.

type MessageLedger

type MessageLedger interface {
	// Record associates a sent message ID with its agent context.
	Record(ctx context.Context, messageID string, goal, output, senderKey string)
}

messageTool wraps a Messenger as a tool.CallableTool so the ReAcTree agent can send messages to configured platforms. MessageLedger records sent message IDs so that future reactions can be correlated back to the agent context that produced them. This interface lives here (not in reactree/memory) to avoid an import cycle between the messenger and reactree/memory packages.

type MessageOrigin

type MessageOrigin struct {
	// Platform identifies which messaging platform (whatsapp, slack, etc).
	Platform Platform
	// Channel is the conversation where the message was posted.
	Channel Channel
	// Sender is the author of the message.
	Sender Sender
	// ThreadID is the thread/reply-chain ID (empty if top-level).
	ThreadID string
	// MessageID is the platform-assigned ID of the original incoming message.
	// Used to quote/reply-to the user's message in outgoing responses.
	MessageID string
}

MessageOrigin captures where an incoming message originated from. It replaces the string-based senderContext ("platform:senderID:channelID") with structured data, providing reply routing without string parsing.

func MessageOriginFrom

func MessageOriginFrom(ctx context.Context) MessageOrigin

MessageOriginFrom returns the MessageOrigin from the context. Every inbound request (AG-UI, messenger, webhook) should have an origin. A zero-value return indicates a bug in the request pipeline and is logged as a warning.

func SystemMessageOrigin

func SystemMessageOrigin() MessageOrigin

func (MessageOrigin) DeriveConversationKey added in v0.1.6

func (origin MessageOrigin) DeriveConversationKey() string

DeriveConversationKey returns a memory key for conversation history isolation. Unlike DeriveVisibility (which scopes by sender for privacy/access-control), this method includes the Channel.ID so each chat thread gets isolated conversation memory. Without this, all AG-UI threads from the same sender would share conversation history, causing the agent to confuse questions across separate chat sessions.

Conversation key values:

  • "private:{senderID}:{channelID}" — per-thread history (AG-UI, DMs with channel)
  • "private:{senderID}" — fallback when no channel is set (TUI)
  • "group:{channelID}" — shared history within a group/channel
  • "global" — no restriction (system content)

func (MessageOrigin) DeriveVisibility

func (origin MessageOrigin) DeriveVisibility() string

DeriveVisibility determines the memory visibility scope from a MessageOrigin. The returned string is stored as metadata on vector store entries and used as a filter key during retrieval to enforce memory isolation.

Visibility values:

  • "private:{senderID}" — only the sender can retrieve this memory
  • "group:{channelID}" — anyone in the channel/group can retrieve this memory
  • "global" — no restriction (e.g. runbooks, system content)

func (MessageOrigin) IsGroupContext

func (origin MessageOrigin) IsGroupContext() bool

IsGroupContext returns true when the message is from a shared context (channel, group) where memories should be accessible to other members. This covers WhatsApp group chats, Slack channels, Teams channels, etc.

func (MessageOrigin) IsPrivateContext

func (origin MessageOrigin) IsPrivateContext() bool

IsPrivateContext returns true when the message is from a 1:1 / DM context where memory should not be shared with other users.

Uses Channel.Type which is set by each platform adapter:

  • whatsapp: DM for 1:1, Group for group chats
  • slack/teams/discord/googlechat: DM or Channel/Group
  • agui/tui: defaults to private (no channel type set)

func (MessageOrigin) IsSystem

func (o MessageOrigin) IsSystem() bool

IsSystem reports whether the origin is the system message origin.

func (MessageOrigin) IsZero

func (o MessageOrigin) IsZero() bool

IsZero reports whether the origin is the zero value (no origin set).

func (MessageOrigin) String

func (o MessageOrigin) String() string

String returns the sender context format "platform:senderID:channelID" matching IncomingMessage.String() for backward compatibility with HITL DB storage and pending approval keys.

type MessageType

type MessageType string

MessageType distinguishes incoming message kinds on the Receive channel.

const (
	// MessageTypeDefault is a normal text/media message.
	MessageTypeDefault MessageType = ""
	// MessageTypeReaction is an emoji reaction to an existing message.
	// When Type == MessageTypeReaction, ReactionEmoji and ReactedMessageID
	// are populated. This allows the system to use reactions as human
	// feedback signals for episodic memory (e.g. 👍 = positive, 👎 = negative).
	MessageTypeReaction MessageType = "reaction"
	// MessageTypeInteraction is a structured action from an interactive UI
	// element (e.g. Slack Block Kit button click, Teams Adaptive Card
	// Action.Submit, Google Chat card action). When Type ==
	// MessageTypeInteraction, the Interaction field is populated with
	// action metadata. This allows the system to resolve approvals and
	// clarifications via button clicks rather than requiring text replies.
	MessageTypeInteraction MessageType = "interaction"
)

type Messenger

type Messenger interface {
	// Connect establishes a connection to the messaging platform and returns
	// an optional http.Handler for receiving inbound webhook/push events.
	//
	// HTTP-push adapters (Teams, Google Chat, Slack Events API, Telegram
	// webhook) return a non-nil handler that the caller mounts on a shared
	// HTTP mux at the desired context path (e.g., /agents/{name}/{platform}/events).
	// The adapter MUST NOT start its own http.Server.
	//
	// Outbound-only adapters (Slack Socket Mode, Discord WebSocket, Telegram
	// long-polling, WhatsApp) return a nil handler because they initiate
	// connections to the platform rather than receiving inbound HTTP.
	//
	// Calling Connect on an already-connected Messenger returns
	// (nil, ErrAlreadyConnected).
	Connect(ctx context.Context) (http.Handler, error)

	// Disconnect gracefully shuts down the platform connection.
	// After Disconnect, the Receive channel will be closed and further
	// Send calls will return ErrNotConnected.
	Disconnect(ctx context.Context) error

	// Send delivers a message to a specific channel/conversation.
	// Returns ErrNotConnected if Connect has not been called.
	Send(ctx context.Context, req SendRequest) (SendResponse, error)

	// Receive returns a read-only channel that delivers incoming messages.
	// The channel is closed when the context is cancelled or Disconnect is called.
	// Returns ErrNotConnected if Connect has not been called.
	Receive(ctx context.Context) (<-chan IncomingMessage, error)

	// Platform returns the platform identifier for this adapter.
	Platform() Platform

	// FormatApproval enriches a SendRequest with platform-specific rich
	// formatting (e.g. Slack Block Kit, Google Chat Cards v2, Teams Adaptive
	// Cards) for the given approval. Adapters that do not support rich
	// formatting should return the request unchanged.
	FormatApproval(req SendRequest, info ApprovalInfo) SendRequest

	ConnectionInfo() string

	// FormatClarification enriches a SendRequest with platform-specific rich
	// formatting for a clarifying question posed by the agent.
	// Adapters that do not support rich formatting should return the request unchanged.
	FormatClarification(req SendRequest, info ClarificationInfo) SendRequest

	// UpdateMessage replaces the content of a previously sent message.
	// Used to disarm interactive buttons after resolution (e.g. replacing
	// approval buttons with "✅ Approved by @user") and to update
	// progress messages. Adapters that do not support message editing
	// should return nil (a no-op).
	// Returns ErrNotConnected if Connect has not been called.
	UpdateMessage(ctx context.Context, req UpdateRequest) error
}

Messenger is the core interface for bi-directional communication with a messaging platform. Platform adapters (Slack, Telegram, Teams, Google Chat) implement this interface.

func WithLogging

func WithLogging(ctx context.Context, m Messenger) Messenger

WithLogging wraps a Messenger with structured logging for Send/Receive calls. Returns the original Messenger unmodified if it is nil.

type Option

type Option func(*AdapterConfig)

Option is a functional option for configuring a platform adapter.

func WithMessageBuffer

func WithMessageBuffer(size int) Option

WithMessageBuffer sets the capacity of the incoming message channel. Values less than 1 are ignored.

func WithSecretProvider

func WithSecretProvider(sp security.SecretProvider) Option

WithSecretProvider sets the secret provider for adapters that support it (e.g. Google Chat). When set, the adapter uses the logged-in user OAuth token (from TokenFile/keyring) instead of a service account credentials file.

type Platform

type Platform string

Platform identifies a messaging platform.

const (
	// PlatformSlack represents the Slack messaging platform.
	PlatformSlack Platform = "slack"
	// PlatformDiscord represents the Discord messaging platform.
	PlatformDiscord Platform = "discord"
	// PlatformTelegram represents the Telegram messaging platform.
	PlatformTelegram Platform = "telegram"
	// PlatformTeams represents the Microsoft Teams messaging platform.
	PlatformTeams Platform = "teams"
	// PlatformGoogleChat represents the Google Chat messaging platform.
	PlatformGoogleChat Platform = "googlechat"
	// PlatformWhatsApp represents the WhatsApp Business messaging platform.
	PlatformWhatsApp Platform = "whatsapp"
	// PlatformAGUI represents the AG-UI SSE server (in-process adapter).
	PlatformAGUI Platform = "agui"
)

func (Platform) String

func (p Platform) String() string

String returns a human-friendly display name for the platform.

type SendMessageToolOption

type SendMessageToolOption func(*messageTool)

SendMessageToolOption configures optional behaviour for the send_message tool.

func WithReactionLedger

func WithReactionLedger(l MessageLedger) SendMessageToolOption

WithReactionLedger attaches a MessageLedger so that every successfully sent content message is recorded for later reaction-based learning.

type SendRequest

type SendRequest struct {
	// Type selects the action. Default ("") sends a normal message.
	// Use SendTypeReaction to react to an existing message with an emoji.
	Type SendType
	// Channel is the target channel/conversation.
	Channel Channel
	// Content is the message body (ignored for reactions).
	Content MessageContent
	// ThreadID is an optional thread/reply-chain identifier for threaded replies.
	// Leave empty to post at top level.
	ThreadID string
	// ReplyToMessageID, if set, quotes/replies to the specified message.
	// For reactions, this is the message ID to react to.
	ReplyToMessageID string
	// Emoji is the reaction emoji (e.g. "👍"). Only used when Type is SendTypeReaction.
	Emoji string
	// Metadata holds platform-specific key-value pairs that adapters can use
	// for features not covered by the common interface (e.g., Slack blocks,
	// Discord embeds).
	Metadata map[string]any
}

SendRequest contains all parameters needed to send a message.

type SendResponse

type SendResponse struct {
	// MessageID is the platform-assigned identifier for the sent message.
	MessageID string
	// Timestamp is when the platform recorded the message.
	Timestamp time.Time
}

SendResponse is returned after a message is successfully sent.

type SendType

type SendType string

SendType distinguishes message actions routed through Messenger.Send.

const (
	// SendTypeMessage is the default: deliver a text (or rich) message.
	SendTypeMessage SendType = ""
	// SendTypeReaction adds an emoji reaction to an existing message.
	// Requires ReplyToMessageID (the message to react to) and Emoji.
	SendTypeReaction SendType = "reaction"
	// SendTypeUpdate replaces an existing message with new content.
	// Requires ReplyToMessageID (the message ID to update). Used to
	// disarm interactive buttons after resolution (e.g. replacing
	// approval buttons with "✅ Approved by @user").
	SendTypeUpdate SendType = "update"
)

type Sender

type Sender struct {
	// ID is the platform-specific user identifier.
	ID string
	// Username is the unique handle (e.g., Slack member ID, Discord username).
	Username string
	// DisplayName is the user's friendly name (e.g. "John Doe").
	// Adapters should populate both if possible; otherwise DisplayName generally falls back to Username.
	DisplayName string
}

Sender represents the author of an incoming message.

type SlackConfig

type SlackConfig struct {
	// AppToken is the Slack app-level token (xapp-...) for Socket Mode.
	AppToken string `yaml:"app_token,omitempty" toml:"app_token,omitempty"`
	// BotToken is the Slack bot user OAuth token (xoxb-...).
	BotToken string `yaml:"bot_token,omitempty" toml:"bot_token,omitempty"`
}

SlackConfig holds Slack adapter settings.

type TeamsConfig

type TeamsConfig struct {
	// AppID is the Microsoft Bot Framework App ID.
	AppID string `yaml:"app_id,omitempty" toml:"app_id,omitempty"`
	// AppPassword is the Microsoft Bot Framework App Password.
	AppPassword string `yaml:"app_password,omitempty" toml:"app_password,omitempty"`
	// ListenAddr is the address for incoming Bot Framework activities (e.g., ":3978").
	ListenAddr string `yaml:"listen_addr,omitempty" toml:"listen_addr,omitempty"`
}

TeamsConfig holds Microsoft Teams adapter settings.

type TelegramConfig

type TelegramConfig struct {
	// Token is the Telegram Bot API token from BotFather.
	Token string `yaml:"token,omitempty" toml:"token,omitempty"`
}

TelegramConfig holds Telegram adapter settings.

type ToolProvider

type ToolProvider struct {
	// contains filtered or unexported fields
}

ToolProvider wraps a Messenger and satisfies the tools.ToolProviders interface so the send_message tool can be passed directly to tools.NewRegistry. Without this, messenger tool construction would be inlined in the registry.

func NewToolProvider

func NewToolProvider(msgr Messenger) *ToolProvider

NewToolProvider creates a ToolProvider for the send_message tool.

func (*ToolProvider) GetTools

func (p *ToolProvider) GetTools() []tool.Tool

GetTools returns the send_message tool wired to the underlying messenger.

type UpdateRequest

type UpdateRequest struct {
	// MessageID is the platform-assigned identifier of the message to update.
	MessageID string
	// Channel is the channel/conversation containing the message.
	Channel Channel
	// Content is the replacement message body.
	Content MessageContent
	// Metadata holds platform-specific key-value pairs for the update
	// (e.g. replacement Slack blocks, updated Adaptive Card).
	Metadata map[string]any
}

UpdateRequest contains all parameters needed to update an existing message. Used after resolving an approval or clarification to replace interactive buttons with a resolved status (e.g. "✅ Approved by @user").

Without this type, interactive buttons would remain active after resolution, allowing other users to click them and causing confusion.

type WhatsAppConfig

type WhatsAppConfig struct {
}

WhatsAppConfig holds WhatsApp adapter settings.

Directories

Path Synopsis
Package agui provides a Messenger adapter for the AG-UI SSE server.
Package agui provides a Messenger adapter for the AG-UI SSE server.
aguifakes
Code generated by counterfeiter.
Code generated by counterfeiter.
Package discord provides a Messenger adapter for the Discord platform using the WebSocket gateway for bi-directional communication.
Package discord provides a Messenger adapter for the Discord platform using the WebSocket gateway for bi-directional communication.
Package googlechat provides a Messenger adapter for Google Chat using HTTP push for incoming events and the Chat API for outgoing messages.
Package googlechat provides a Messenger adapter for Google Chat using HTTP push for incoming events and the Chat API for outgoing messages.
Package media provides shared utility functions for building and describing messenger.Attachment values across platform adapters.
Package media provides shared utility functions for building and describing messenger.Attachment values across platform adapters.
Code generated by counterfeiter.
Code generated by counterfeiter.
Package slack provides a DataSource connector that enumerates Slack channel messages for vectorization.
Package slack provides a DataSource connector that enumerates Slack channel messages for vectorization.
Package teams provides a Messenger adapter for Microsoft Teams using the Bot Framework protocol for bi-directional communication.
Package teams provides a Messenger adapter for Microsoft Teams using the Bot Framework protocol for bi-directional communication.
Package telegram provides a Messenger adapter for the Telegram platform using long-polling for bi-directional communication.
Package telegram provides a Messenger adapter for the Telegram platform using long-polling for bi-directional communication.
Package whatsapp provides a Messenger adapter for WhatsApp using the WhatsApp Web multi-device protocol via whatsmeow.
Package whatsapp provides a Messenger adapter for WhatsApp using the WhatsApp Web multi-device protocol via whatsmeow.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL