channel

package
v0.68.2 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package channel defines the public contract for channel plugins. Channel plugins are thin platform adapters (Telegram, QQ, Feishu, DingTalk, Weixin) that normalise incoming messages, delegate business logic to a MessageHandler, and render streamed responses back to the platform.

Index

Constants

View Source
const (
	PlatformTelegram = "telegram"
	PlatformDiscord  = "discord"
	PlatformQQ       = "qq"
	PlatformFeishu   = "feishu"
	PlatformDingTalk = "dingtalk"
	PlatformWeixin   = "weixin"
	PlatformCLI      = "cli"

	// MaxInboundAttachmentBytes bounds one attachment before durable publication.
	MaxInboundAttachmentBytes = 32 << 20
)

Platform identifiers for each messaging channel.

View Source
const (
	DefaultGuestMessageLimitPerMinute = 10
	DefaultGuestMaxPerChannel         = 1000
	DefaultGuestRetentionDays         = 30
	MaxGuestMessageLimitPerMinute     = 120
	MaxGuestMaxPerChannel             = 100000
	MaxGuestRetentionDays             = 365
)
View Source
const GroupCompactUnsupportedMessage = "⚠️ Group memory is managed automatically from the shared event log, so manual compaction is not available in group chats."

GroupCompactUnsupportedMessage is the shared reply when a user asks to compact a group chat. Group memory is assembled from the shared event log rather than a per-agent LCM conversation, so manual compaction does not apply. Both the web group endpoint and the shared channel command handler use this text.

View Source
const GroupNewSessionUnsupportedMessage = "⚠️ Group sessions are shared, so `/new` cannot reset them. Use `/new` in a direct message to reset your own session."

GroupNewSessionUnsupportedMessage is the reply when `/new` arrives in a group chat. A group's context is shared by every member, so one member's chat command must not clear it for everyone. The refusal is explicit rather than silent: a `/new` that looked accepted but reset nothing would leave the group believing the context was cleared.

View Source
const NewSessionNotExecutedMessage = "⚠️ Starting a new session did not go through — nothing was reset. Send /new again to retry."

NewSessionNotExecutedMessage is the reply when a rotation's commit acknowledgement was lost but re-reading the session binding proves it never moved: the transaction rolled back, so nothing was reset and the command is safe to send again.

View Source
const NewSessionOutcomeUnknownMessage = "" /* 160-byte string literal not displayed */

NewSessionOutcomeUnknownMessage is the reply when a rotation's commit acknowledgement was lost AND the follow-up read of the session binding also failed, so whether the reset happened is genuinely unknown. It must not invite a retry: if the reset did land, a second `/new` would archive the fresh context instead of the old one.

View Source
const NewSessionStartedMessage = "Started a fresh session. Previous history was archived and removed from memory search."

NewSessionStartedMessage is the shared reply after `/new` rotated the chat onto a fresh session. The previous session is archived and retained for explicit Session inspection, but excluded from memory search.

View Source
const NewSessionUnverifiableMessage = "" /* 132-byte string literal not displayed */

NewSessionUnverifiableMessage is the reply when a `/new` arrives on a delivery that carries no stable message id. Without an identity the receipt cannot tell a redelivery from a new command, and `/new` is destructive, so it fails closed instead of running unguarded.

View Source
const SessionAlreadyResetMessage = "Session was already reset."

SessionAlreadyResetMessage is the shared reply when a `/new` arrives after another one already rotated the same chat, so it has nothing left to do.

View Source
const WelcomeMessage = `` /* 990-byte string literal not displayed */

WelcomeMessage is the shared welcome/help text for all channels. It explains both slash commands and natural-language shortcuts for channels without slash-command menus (like SMS or simple webhooks).

Variables

View Source
var ErrJoinedChatListingUnavailable = errors.New("channel: joined chat listing unavailable")

ErrJoinedChatListingUnavailable means the running channel cannot currently enumerate its joined group chats.

View Source
var ToolEmoji = map[string]string{
	"bash":    "⚡",
	"read":    "📖",
	"write":   "✏️",
	"edit":    "🔧",
	"search":  "🔍",
	"default": "🔧",
}

ToolEmoji maps known tool names to display emoji.

Functions

func AllowsUnlinkedGuestDM added in v0.63.0

func AllowsUnlinkedGuestDM(channelType string, enabled bool, rawConfig string) bool

AllowsUnlinkedGuestDM is the shared fail-closed policy for creating and continuing an unlinked guest session through a persisted channel binding.

func AttachmentReceivedContent added in v0.60.3

func AttachmentReceivedContent(fileName, savedPath string, data []byte) []ai.ContentBlock

AttachmentReceivedContent returns the content blocks for an inbound attachment that has been saved to disk. Images are presented as a saved-path note plus the inline image so the model sees the pixels immediately and can still reach the file later; everything else gets the Xberg extraction hint via FileReceivedContent. data is the raw file content used for image detection and inlining.

func AttachmentSaveFailureContent added in v0.60.3

func AttachmentSaveFailureContent(fileName string, data []byte) []ai.ContentBlock

AttachmentSaveFailureContent returns the blocks to route to the agent when an inbound attachment downloaded successfully but could not be persisted. It keeps the four channel plugins symmetric on the save-failure path: image bytes degrade via InlineImageFallback (attached within the ingestion ceiling, else a text note) and any other file gets an explicit placeholder so the turn is never silently dropped. data is the raw downloaded bytes used for image detection.

func CloneConfigMap

func CloneConfigMap(src map[string]any) map[string]any

func DecodePluginConfig

func DecodePluginConfig[T any](raw map[string]any, label string) (T, error)

func EmojiFor

func EmojiFor(tool string) string

EmojiFor returns the emoji for a tool name, falling back to the default.

func FileReceivedContent

func FileReceivedContent(fileName, savedPath string) []ai.ContentBlock

FileReceivedContent returns the standard content block telling the agent about a file that has been saved to durable storage, with an Xberg extraction hint. savedPath is a portable logical path, never a host path.

func FormatDuration

func FormatDuration(d time.Duration) string

FormatDuration formats a duration as a human-friendly string.

func ImageFileName added in v0.60.3

func ImageFileName(base, mime string) string

ImageFileName builds a file name for an inbound image message that carries no client-supplied name, deriving the extension from the detected MIME type.

func InlineImageFallback added in v0.60.3

func InlineImageFallback(fileName, mime string, data []byte) []ai.ContentBlock

InlineImageFallback returns the content blocks for an inbound image that could not be persisted to the user's assets, so the turn still reaches the agent without a saved path. It honors the same inline ceiling as AttachmentReceivedContent: images within the shared ingestion ceiling are attached for downstream canonical or group handling; larger images degrade to an explicit text note naming the file and its size. Callers pass the mime they already detected; an empty mime is treated as non-inlineable.

func NotificationAgentIDFromContext

func NotificationAgentIDFromContext(ctx context.Context) string

NotificationAgentIDFromContext returns the agent ID associated with a notification-producing tool call, if present.

func ParseCommandArgs

func ParseCommandArgs(text, cmd string) string

ParseCommandArgs extracts arguments after the command token. Example: ParseCommandArgs("/command foo", "/command") returns "foo".

func ParseSlashCommand

func ParseSlashCommand(text string) (string, string)

ParseSlashCommand extracts a slash command and its arguments. The returned command is always lowercased.

func RenderToolRecord

func RenderToolRecord(rec ToolRecord) string

RenderToolRecord formats a single completed tool record.

func SplitMarkdown added in v0.64.0

func SplitMarkdown(text string, maxRunes int) []string

SplitMarkdown splits text into chunks of at most maxRunes Unicode runes, preferring to break at line boundaries. If a break falls inside a ``` or ~~~ code fence, the fence is closed at the end of the chunk and reopened with the same marker and info string at the start of the next chunk, so every chunk is independently valid Markdown.

func SplitMessage

func SplitMessage(text string, maxLen int) []string

SplitMessage splits text into chunks that fit within maxLen. It tries to split at newline boundaries and avoids cutting multi-byte UTF-8 characters.

func TextContent

func TextContent(text string) []ai.ContentBlock

TextContent wraps a plain string as a single-element []ai.ContentBlock.

func ToolLine

func ToolLine(t *ToolUseEvent) string

ToolLine returns a short status line for a tool-use event. Used by channels with simple tool display (no full tracker).

func Truncate

func Truncate(s string, maxLen int) string

Truncate shortens s to maxLen bytes, appending "..." if truncated. Cuts at a valid UTF-8 boundary to avoid producing invalid strings.

func WithNotificationAgentID

func WithNotificationAgentID(ctx context.Context, agentID string) context.Context

WithNotificationAgentID annotates tool execution contexts with the agent that produced a notification.

Types

type AgentInfo

type AgentInfo struct {
	ID   string
	Name string
}

AgentInfo is agent metadata for display in channel UIs.

type AssetSaveAdmitter added in v0.64.0

type AssetSaveAdmitter interface {
	AdmitAssetSave(ctx context.Context, msg IncomingMessage) error
}

AssetSaveAdmitter authorizes attachment ingestion before a plugin downloads untrusted bytes. It deliberately exposes no workspace or host path.

type AssetSaver added in v0.60.0

type AssetSaver interface {
	// SaveAsset returns a portable $STELLA_ASSETS_DIR expression, never a host path.
	SaveAsset(ctx context.Context, msg IncomingMessage, fileName string, data []byte) (string, error)
}

AssetSaver is an optional capability that a Handler may implement. Channel plugins assert for it to persist inbound bytes. Identity and workspace selection remain entirely host-owned.

type BotNameRegistrar added in v0.65.0

type BotNameRegistrar interface {
	RegisterBotName(platform, displayName, channelID string)
}

BotNameRegistrar is an optional capability that a Handler may implement. It exists for platforms whose bot ids are scoped to the receiving app (Feishu open_id), where no id lets one app recognise another app's bot and the display name is the only shared identity.

type BotRegistrar added in v0.42.0

type BotRegistrar interface {
	RegisterBotIdentity(platform, platformBotID, channelID string)
}

BotRegistrar is an optional capability that a Handler may implement. Channel adapters call RegisterBotIdentity at startup to record their bot's platform identity (e.g., Telegram username), enabling the group dispatcher to resolve @mentions to Stella agents.

type Channel

type Channel interface {
	// Name returns a unique identifier (e.g. "telegram", "qq").
	Name() string

	// Start begins listening for messages. Blocks until ctx is cancelled.
	Start(ctx context.Context) error

	// Stop gracefully shuts down the channel.
	Stop()

	// Notify sends a push notification to a target within this channel.
	Notify(ctx context.Context, n Notification) error
}

Channel is a messaging platform adapter.

type ChatStream

type ChatStream struct {
	Events    <-chan Event
	SessionID string
}

ChatStream holds the event channel and session metadata returned by HandleMessage.

type DingTalkConfig added in v0.64.0

type DingTalkConfig struct {
	InstanceID                 string `json:"-"`
	ClientID                   string `json:"client_id"`
	ClientSecret               string `json:"client_secret"`
	AllowGroup                 bool   `json:"allow_group"`
	AllowDM                    bool   `json:"allow_dm"`
	AllowUnlinkedDM            bool   `json:"allow_unlinked_dm"`
	GuestMessageLimitPerMinute int    `json:"guest_message_limit_per_minute"`
	GuestMaxPerChannel         int    `json:"guest_max_per_channel"`
	GuestRetentionDays         int    `json:"guest_retention_days"`
	RequireMention             bool   `json:"require_mention"`
}

DingTalkConfig is the persisted DingTalk channel plugin configuration.

func DecodeDingTalkConfig added in v0.64.0

func DecodeDingTalkConfig(data []byte) (DingTalkConfig, error)

DecodeDingTalkConfig applies stable admission defaults before decoding JSON.

type DiscordConfig added in v0.63.0

type DiscordConfig struct {
	InstanceID                 string   `json:"-"`
	Token                      string   `json:"token"`
	AllowGroup                 bool     `json:"allow_group"`
	AllowAllGuilds             bool     `json:"allow_all_guilds"`
	AllowedGuildIDs            []string `json:"allowed_guild_ids"`
	AllowedChannelIDs          []string `json:"allowed_channel_ids"`
	AllowedUserIDs             []string `json:"allowed_user_ids"`
	AllowedRoleIDs             []string `json:"allowed_role_ids"`
	AllowDM                    bool     `json:"allow_dm"`
	AllowUnlinkedDM            bool     `json:"allow_unlinked_dm"`
	GuestMessageLimitPerMinute int      `json:"guest_message_limit_per_minute"`
	GuestMaxPerChannel         int      `json:"guest_max_per_channel"`
	GuestRetentionDays         int      `json:"guest_retention_days"`
	RequireMention             bool     `json:"require_mention"`
}

DiscordConfig is the persisted Discord channel plugin configuration.

func DecodeDiscordConfig added in v0.63.0

func DecodeDiscordConfig(data []byte) (DiscordConfig, error)

DecodeDiscordConfig applies stable defaults before decoding persisted JSON.

type Event

type Event struct {
	Text       string
	Reasoning  string
	Image      *ImageEvent
	File       *FileEvent
	ToolUse    *ToolUseEvent
	References []renderrefs.Reference
	Err        error
}

Event is a stream event from the agent, consumed by channel plugins to render responses on the platform.

type FeishuConfig

type FeishuConfig struct {
	InstanceID                 string                 `json:"-"`
	AppID                      string                 `json:"app_id"`
	AppSecret                  string                 `json:"app_secret"`
	EncryptKey                 string                 `json:"encrypt_key"`
	VerificationToken          string                 `json:"verification_token"`
	Groups                     map[string]FeishuGroup `json:"groups"`
	AllowGroup                 bool                   `json:"allow_group"`
	AllowDM                    bool                   `json:"allow_dm"`
	AllowUnlinkedDM            bool                   `json:"allow_unlinked_dm"`
	GuestMessageLimitPerMinute int                    `json:"guest_message_limit_per_minute"`
	GuestMaxPerChannel         int                    `json:"guest_max_per_channel"`
	GuestRetentionDays         int                    `json:"guest_retention_days"`
	RequireMention             bool                   `json:"require_mention"`
	EnableNotify               bool                   `json:"enable_notify"`
	TenantKey                  string                 `json:"tenant_key"`
	AutoProvision              bool                   `json:"auto_provision"`
}

FeishuConfig is the persisted Feishu channel plugin configuration.

func DecodeFeishuConfig added in v0.63.0

func DecodeFeishuConfig(data []byte) (FeishuConfig, error)

DecodeFeishuConfig applies stable admission defaults before decoding JSON.

type FeishuGroup

type FeishuGroup struct {
	SystemPrompt    string   `json:"system_prompt"`
	ToolAllow       []string `json:"tool_allow"`
	ToolDeny        []string `json:"tool_deny"`
	Enabled         *bool    `json:"enabled,omitempty"`
	RequireMention  *bool    `json:"require_mention,omitempty"`
	AllowedUsers    []string `json:"allowed_users,omitempty"`
	DisallowedUsers []string `json:"disallowed_users,omitempty"`
}

FeishuGroup is a per-chat override in the persisted Feishu channel config.

type FileEvent

type FileEvent struct {
	Path string // absolute path on disk
	Name string // display filename (with extension)
}

FileEvent carries a local file path to send to the user.

type GuestConfig added in v0.63.0

type GuestConfig struct {
	AllowDM                    bool
	AllowUnlinkedDM            bool
	GuestMessageLimitPerMinute int
	GuestMaxPerChannel         int
	GuestRetentionDays         int
}

GuestConfig is the platform-independent subset controlling restricted guest admission and resource bounds.

func DecodeGuestConfig added in v0.63.0

func DecodeGuestConfig(channelType, rawConfig string) (GuestConfig, error)

DecodeGuestConfig returns the shared guest policy for a supported channel.

type Handler

type Handler interface {
	MessageHandler
}

Handler is the message-routing contract used by channel plugins.

type ImageEvent

type ImageEvent struct {
	Data     string // base64 encoded
	MimeType string // e.g. "image/jpeg"
}

ImageEvent carries a base64-encoded image.

type IncomingMessage

type IncomingMessage struct {
	Platform   string   // "telegram", "qq", etc.
	ChannelID  string   // configured channel instance ID; defaults to Platform.
	SenderID   string   // preferred platform-specific user ID
	SenderIDs  []string // ordered candidate sender IDs, most stable first
	SenderName string   // display name
	ChatID     string   // group/channel ID (empty for DMs)
	IsGroup    bool
	ThreadID   string // platform sub-thread/topic id within ChatID (e.g. Telegram forum topic); empty if none
	Content    []ai.ContentBlock

	// Group-chat metadata (D3). Adapters fill what they can; empty/zero is allowed.
	MessageID         string    // platform-native message ID (stable delivery/update id), empty if unavailable
	Timestamp         time.Time // platform-reported send time, zero if unavailable
	ReplyTo           string    // platform message ID this message replies to, empty if none
	Mentions          []Mention // @-mentions, normalized; AgentID is resolved later by the dispatcher
	LifecycleFeedback bool      // platform adapter should show addressed-turn completion feedback
}

IncomingMessage is the normalised input from any platform.

type JoinedChat added in v0.68.0

type JoinedChat struct {
	ID   string
	Name string
}

JoinedChat is a group chat the channel's bot currently belongs to. It deliberately contains only display metadata safe to expose in channel configuration, not platform-specific credentials or member data.

type JoinedChatLister added in v0.68.0

type JoinedChatLister interface {
	ListJoinedChats(ctx context.Context, pageSize int, pageToken string) (JoinedChatPage, error)
}

JoinedChatLister is an optional capability for channel adapters whose platform API can enumerate the groups the bot currently belongs to.

type JoinedChatPage added in v0.68.0

type JoinedChatPage struct {
	Chats         []JoinedChat
	NextPageToken string
}

JoinedChatPage is one opaque-token page of the bot's currently joined chats.

type Mention added in v0.42.0

type Mention struct {
	Raw        string `json:"Raw"`        // raw @ text (@username / <at open_id> ...), for audit/fallback
	PlatformID string `json:"PlatformID"` // platform-side mentioned id (username / open_id / qq number)
	AgentID    string `json:"AgentID"`    // resolved Stella agent; empty if unresolved
}

Mention is a normalized @-mention. Adapters fill Raw and PlatformID; the dispatcher resolves AgentID by looking up group membership. @-routing honors only mentions whose AgentID is non-empty. The tags are deliberately the Go field names, not snake_case: mentions are persisted inside the ctx_group_outbox envelope JSON, and the AgentMentionedSinceCursor query in internal/db/queries/ctx_group_dispatch.sql matches `AgentID` inside that stored JSON. The tags freeze the wire names so renaming a field here can never silently break that query against old rows.

type MessageHandler

type MessageHandler interface {
	// HandleIncoming resolves the user once, tries command handling, and if the
	// command is not handled, streams a chat response.
	// Returns (commandResponse, handled, stream, err).
	// If handled is true, commandResponse contains the reply and stream is nil.
	// If handled is false, stream contains the chat response.
	HandleIncoming(ctx context.Context, msg IncomingMessage, command, args string) (string, bool, *ChatStream, error)
}

MessageHandler is the core coordinator interface injected into channel plugins. It owns user resolution, agent routing, session management, and command handling. The single HandleIncoming entry point resolves the user once, tries command handling, and falls through to chat streaming — eliminating double resolution.

type ModelOption

type ModelOption struct {
	Provider string
	Model    string
}

ModelOption represents a selectable provider/model combination.

type Notification

type Notification struct {
	Channel     string // optional: route to a specific backend
	ChatID      string // target chat/channel within the backend
	RecipientID string // linked platform user identity; empty for explicit ChatID targets
	AgentID     string // optional: agent that produced the notification
	Text        string // markdown content
	Silent      bool   // send without notification sound
}

Notification is a push message to send to a chat.

type ProvisionRequest

type ProvisionRequest struct {
	Platform   string
	ExternalID string
	TenantKey  string
	Email      string
	Name       string
	AvatarURL  string
}

ProvisionRequest carries verified platform evidence for enrollment. Plugins remain independent of internal auth types; ExternalID is the platform's canonical identity subject (Feishu union_id for Feishu enrollment).

type Provisioner

type Provisioner interface {
	ProvisionUser(ctx context.Context, req ProvisionRequest) error
}

Provisioner is an optional capability that a Handler may implement. Channel plugins assert for this interface when they want to auto-provision users on first contact, without adding the method to every channel's Handler.

type QQConfig

type QQConfig struct {
	InstanceID   string `json:"-"`
	AppID        string `json:"app_id"`
	AppSecret    string `json:"app_secret"`
	EnableNotify bool   `json:"enable_notify"`
}

QQConfig is the persisted QQ channel plugin configuration.

type TelegramConfig

type TelegramConfig struct {
	InstanceID                 string   `json:"-"`
	Token                      string   `json:"token"`
	ChannelID                  string   `json:"channel_id"`
	AllowGroup                 bool     `json:"allow_group"`
	AllowedChatIDs             []string `json:"allowed_chat_ids"`
	AllowedTopicIDs            []string `json:"allowed_topic_ids"`
	AllowDM                    bool     `json:"allow_dm"`
	AllowUnlinkedDM            bool     `json:"allow_unlinked_dm"`
	GuestMessageLimitPerMinute int      `json:"guest_message_limit_per_minute"`
	GuestMaxPerChannel         int      `json:"guest_max_per_channel"`
	GuestRetentionDays         int      `json:"guest_retention_days"`
	RequireMention             bool     `json:"require_mention"`
	EnableNotify               bool     `json:"enable_notify"`
}

TelegramConfig is the persisted Telegram channel plugin configuration.

func DecodeTelegramConfig added in v0.63.0

func DecodeTelegramConfig(data []byte) (TelegramConfig, error)

DecodeTelegramConfig applies stable admission defaults before decoding JSON.

type ToolRecord

type ToolRecord struct {
	Tool     string
	Input    string
	Status   string // "done" or "error"
	Detail   string
	Duration time.Duration
}

ToolRecord holds a completed tool invocation for summary display.

type ToolTracker

type ToolTracker struct {
	History      []ToolRecord
	ActiveTool   string
	ActiveInput  string
	ActiveStart  time.Time
	DisplayUntil time.Time // minimum time to keep showing the last-finished tool

	// MinDisplayDuration controls how long a finished tool stays visible.
	// Zero means no minimum.
	MinDisplayDuration time.Duration
}

ToolTracker tracks active and completed tool invocations during streaming.

func (*ToolTracker) Finish

func (tt *ToolTracker) Finish(t *ToolUseEvent)

Finish records the active tool as completed.

func (*ToolTracker) Handle

func (tt *ToolTracker) Handle(t *ToolUseEvent) bool

Handle processes a tool event, returning true if a display refresh is needed.

func (*ToolTracker) HasHistory

func (tt *ToolTracker) HasHistory() bool

HasHistory returns true if any tools were tracked.

func (*ToolTracker) IsDisplaying

func (tt *ToolTracker) IsDisplaying() bool

IsDisplaying returns true if there is an active tool or the minimum display duration for the last finished tool has not yet elapsed.

func (*ToolTracker) Render

func (tt *ToolTracker) Render() string

Render builds the tool section for real-time streaming display, showing completed tools and the active tool with elapsed time.

func (*ToolTracker) RenderFinal

func (tt *ToolTracker) RenderFinal() string

RenderFinal builds a compact tool summary for the final message. Shows a one-liner with tool counts and total time, plus individual lines for any failed tool calls.

func (*ToolTracker) Start

func (tt *ToolTracker) Start(t *ToolUseEvent)

Start registers a new tool as running.

type ToolUseEvent

type ToolUseEvent struct {
	ID         string
	Tool       string // tool name, e.g. "bash", "read"
	Status     string // "running", "done", "error"
	Input      string // short summary of the tool input
	Arguments  map[string]any
	Detail     string // error detail or result summary
	Content    string
	References []renderrefs.Reference
}

ToolUseEvent describes a tool invocation in progress or completed.

type WeixinConfig

type WeixinConfig struct {
	InstanceID   string `json:"-"`
	BotToken     string `json:"bot_token"`
	BaseURL      string `json:"base_url"`
	BotID        string `json:"bot_id"`
	UserID       string `json:"user_id"`
	SKRouteTag   string `json:"sk_route_tag"`
	EnableNotify bool   `json:"enable_notify"`
}

WeixinConfig is the persisted Weixin channel plugin configuration.

Jump to

Keyboard shortcuts

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