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
- Variables
- func AllowsUnlinkedGuestDM(channelType string, enabled bool, rawConfig string) bool
- func AttachmentReceivedContent(fileName, savedPath string, data []byte) []ai.ContentBlock
- func AttachmentSaveFailureContent(fileName string, data []byte) []ai.ContentBlock
- func CloneConfigMap(src map[string]any) map[string]any
- func DecodePluginConfig[T any](raw map[string]any, label string) (T, error)
- func EmojiFor(tool string) string
- func FileReceivedContent(fileName, savedPath string) []ai.ContentBlock
- func FormatDuration(d time.Duration) string
- func ImageFileName(base, mime string) string
- func InlineImageFallback(fileName, mime string, data []byte) []ai.ContentBlock
- func NotificationAgentIDFromContext(ctx context.Context) string
- func ParseCommandArgs(text, cmd string) string
- func ParseSlashCommand(text string) (string, string)
- func RenderToolRecord(rec ToolRecord) string
- func SplitMarkdown(text string, maxRunes int) []string
- func SplitMessage(text string, maxLen int) []string
- func TextContent(text string) []ai.ContentBlock
- func ToolLine(t *ToolUseEvent) string
- func Truncate(s string, maxLen int) string
- func WithNotificationAgentID(ctx context.Context, agentID string) context.Context
- type AgentInfo
- type AssetSaveAdmitter
- type AssetSaver
- type BotNameRegistrar
- type BotRegistrar
- type Channel
- type ChatStream
- type DingTalkConfig
- type DiscordConfig
- type Event
- type FeishuConfig
- type FeishuGroup
- type FileEvent
- type GuestConfig
- type Handler
- type ImageEvent
- type IncomingMessage
- type Mention
- type MessageHandler
- type ModelOption
- type Notification
- type ProvisionRequest
- type Provisioner
- type QQConfig
- type TelegramConfig
- type ToolRecord
- type ToolTracker
- func (tt *ToolTracker) Finish(t *ToolUseEvent)
- func (tt *ToolTracker) Handle(t *ToolUseEvent) bool
- func (tt *ToolTracker) HasHistory() bool
- func (tt *ToolTracker) IsDisplaying() bool
- func (tt *ToolTracker) Render() string
- func (tt *ToolTracker) RenderFinal() string
- func (tt *ToolTracker) Start(t *ToolUseEvent)
- type ToolUseEvent
- type WeixinConfig
Constants ¶
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.
const ( DefaultGuestMessageLimitPerMinute = 10 DefaultGuestMaxPerChannel = 1000 DefaultGuestRetentionDays = 30 MaxGuestMessageLimitPerMinute = 120 MaxGuestMaxPerChannel = 100000 MaxGuestRetentionDays = 365 )
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.
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.
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.
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.
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.
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.
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.
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 ¶
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
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 DecodePluginConfig ¶
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 ¶
FormatDuration formats a duration as a human-friendly string.
func ImageFileName ¶ added in v0.60.3
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 ¶
NotificationAgentIDFromContext returns the agent ID associated with a notification-producing tool call, if present.
func ParseCommandArgs ¶
ParseCommandArgs extracts arguments after the command token. Example: ParseCommandArgs("/command foo", "/command") returns "foo".
func ParseSlashCommand ¶
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
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 ¶
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).
Types ¶
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 ¶
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"`
}
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 ¶
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 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 ¶
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.