trigger

package
v0.2.6 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: AGPL-3.0 Imports: 43 Imported by: 0

Documentation

Overview

Package trigger provides services that trigger agent containers in response to external events: webhooks, cron schedules, and channel messages.

Index

Constants

This section is empty.

Variables

View Source
var ErrTranscriptionNotConfigured = errors.New("transcription not configured")

ErrTranscriptionNotConfigured is returned when no system-wide transcription model is set. Callers handle this non-fatally (degrade to file-only delivery).

View Source
var Registry = []SlashCommand{
	{Name: "clear", Description: "Clear conversation context", Access: agentsdk.AccessUser},
	{Name: "compact", Description: "Summarize and compact context", Access: agentsdk.AccessUser},
	{Name: "echo", Description: "Toggle tool output bubbles (on / off / blank=flip)", Access: agentsdk.AccessUser},
}

Registry is the canonical list of commands. Order is preserved when rendered into platform command menus.

Functions

func ResolveAgentAccess

func ResolveAgentAccess(ctx context.Context, q *dbq.Queries, agentID, userID uuid.UUID) agentsdk.Access

ResolveAgentAccess returns the caller's effective per-agent access level from agent_members. Non-members get AccessPublic, members map straight to AccessUser or AccessAdmin. Tenant role is deliberately not consulted — see airlock/CLAUDE.md "Permission Model" for why these axes are separate.

func ResolveEcho

func ResolveEcho(settingsJSON []byte, driverDefault bool) bool

ResolveEcho returns the effective echo flag for a conversation given its raw settings JSON and the driver's default. Explicit conversation settings win over the driver default; missing JSON or missing key falls through to the default.

func StreamNDJSONResponse

func StreamNDJSONResponse(body io.Reader, runID string, events chan<- ResponseEvent) (string, []message.Message, *usageInfo, error)

StreamNDJSONResponse reads NDJSON events from a response stream, forwards ResponseEvents to the events channel for real-time delivery, and collects the full text response. Closes the events channel when done. runID is stamped onto confirmation_required events so drivers can build callback-bound UI (e.g. Telegram inline keyboards).

Types

type BridgeCallback

type BridgeCallback struct {
	Data  string // opaque payload, e.g. "approve:<runID>"
	AckID string // platform-specific ack handle (Telegram callback_query.id)
}

BridgeCallback represents an interactive UI acknowledgement — a button tap on an inline keyboard or similar platform-native affordance. Drivers that don't support rich UI leave this nil.

type BridgeDriver

type BridgeDriver interface {
	// Init is called once when a bridge is first created.
	// Uses pointer so the driver can set initial config (e.g. poll offset).
	Init(ctx context.Context, br *dbq.Bridge) error

	// Activate is called on every startup for active bridges.
	Activate(ctx context.Context, br dbq.Bridge) error

	// Teardown is called when a bridge is deleted or disabled.
	Teardown(ctx context.Context, br dbq.Bridge) error

	// Poll fetches new events from the platform.
	// Uses pointer so the driver can update br.Config (e.g. poll offset).
	Poll(ctx context.Context, br *dbq.Bridge) ([]BridgeEvent, error)

	// SendStream delivers a response, streaming text deltas as they arrive.
	// echo controls whether tool-call / tool-result bubbles are rendered;
	// drivers that collapse tool output some other way may ignore it.
	// Returns the final assembled text.
	SendStream(ctx context.Context, br dbq.Bridge, externalID string, echo bool, events <-chan ResponseEvent) (string, error)

	// DefaultEcho returns whether tool bubbles render by default on this
	// platform. Drivers that display each tool-call/tool-result as its own
	// chat message (Telegram) should return false; drivers whose UI can
	// collapse tool output inline (web) should return true.
	// Used when a conversation has no explicit settings.echo override.
	DefaultEcho() bool
}

BridgeDriver handles platform-specific message parsing and delivery.

type BridgeEvent

type BridgeEvent struct {
	BridgeID   uuid.UUID
	ExternalID string // platform chat_id (Telegram chat ID, etc.)
	SenderID   string // platform user ID of sender (for identity lookup)
	SenderName string
	Text       string
	Files      []BridgeFile // attached files (photos, documents)
	Callback   *BridgeCallback
	RawPayload []byte
}

BridgeEvent represents a normalized incoming event from any platform. Either Text/Files (new user message) or Callback (button tap) is populated.

type BridgeFile

type BridgeFile struct {
	FileID      string // platform file ID (e.g. Telegram file_id)
	Filename    string
	ContentType string
	Size        int64
	Data        []byte // file content (downloaded by driver)

	// IsVoiceNote marks a short voice recording (e.g. Telegram "voice")
	// that the bridge layer should auto-transcribe before forwarding to
	// the agent. Plain audio/video/document attachments leave this false.
	IsVoiceNote bool
}

BridgeFile is a file attached to a bridge message.

type BridgeManager

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

BridgeManager manages bridge drivers and routes events to agents.

func NewBridgeManager

func NewBridgeManager(drivers map[string]BridgeDriver, prompter *PromptProxy, database *db.DB, encryptor *crypto.Encryptor, hmacSecret, publicURL string, logger *zap.Logger) *BridgeManager

NewBridgeManager creates a BridgeManager.

func (*BridgeManager) AddBridge

func (m *BridgeManager) AddBridge(bridgeID uuid.UUID)

AddBridge activates a newly created bridge and starts its poller. Idempotent: if a poller is already running for this bridge ID (e.g. the bridge was re-registered after a config change), the existing one is cancelled first so only one poller hits the platform at a time.

func (*BridgeManager) HandleEvent

func (m *BridgeManager) HandleEvent(ctx context.Context, event BridgeEvent) error

HandleEvent processes a parsed BridgeEvent — routes to agent via PromptProxy.

func (*BridgeManager) RemoveBridge

func (m *BridgeManager) RemoveBridge(bridgeID uuid.UUID)

RemoveBridge stops the poller for a bridge. Safe to call for an unknown bridge ID — it's a no-op. The DB row is NOT touched here; callers that want full deletion do the DB work separately (typically by calling q.DeleteBridge alongside this).

func (*BridgeManager) SendMessage

func (m *BridgeManager) SendMessage(ctx context.Context, bridgeID uuid.UUID, externalID, text string) error

SendMessage sends a text message to a bridge conversation. Convenience wrapper.

func (*BridgeManager) SendParts

func (m *BridgeManager) SendParts(ctx context.Context, bridgeID uuid.UUID, externalID string, parts []agentsdk.DisplayPart) error

SendParts sends display parts to a bridge conversation. Looks up the bridge, decrypts the token, and delegates to the driver.

func (*BridgeManager) Start

func (m *BridgeManager) Start(ctx context.Context) error

Start sets up all active bridges and starts pollers.

func (*BridgeManager) Stop

func (m *BridgeManager) Stop()

Stop gracefully shuts down all pollers.

type CommandRegistrar

type CommandRegistrar interface {
	RegisterCommands(ctx context.Context, br dbq.Bridge, cmds []SlashCommand) error
}

CommandRegistrar is an optional BridgeDriver capability: platforms with a native command menu (Telegram setMyCommands, Discord app commands, ...) implement it to receive the slash-command registry on activation. Drivers without such a menu simply don't implement it.

type Dispatcher

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

Dispatcher ensures agent containers are running and forwards HTTP requests to them.

func NewDispatcher

func NewDispatcher(cfg *config.Config, db *db.DB, containers container.ContainerManager, enc *crypto.Encryptor, logger *zap.Logger) *Dispatcher

NewDispatcher creates a Dispatcher.

func (*Dispatcher) EnsureRunning

func (d *Dispatcher) EnsureRunning(ctx context.Context, agentID uuid.UUID) (*container.Container, error)

EnsureRunning looks up the agent, decrypts its DB credentials, and starts (or reconnects to) the agent container. Returns the running container.

func (*Dispatcher) ForwardCron

func (d *Dispatcher) ForwardCron(ctx context.Context, agentID uuid.UUID, cronName string, timeout time.Duration) (io.ReadCloser, uuid.UUID, error)

ForwardCron ensures the agent is running, creates a run record, and POSTs to the agent's cron endpoint. Returns the response body stream and the run ID. The timeout parameter controls the HTTP client timeout.

func (*Dispatcher) ForwardPrompt

func (d *Dispatcher) ForwardPrompt(ctx context.Context, agentID uuid.UUID, input agentsdk.PromptInput, bridgeID *uuid.UUID) (io.ReadCloser, uuid.UUID, error)

ForwardPrompt ensures the agent is running, creates a run record, and POSTs the prompt input to the agent container. Returns the response body stream (NDJSON) and the run ID.

func (*Dispatcher) ForwardWebhook

func (d *Dispatcher) ForwardWebhook(ctx context.Context, agentID uuid.UUID, path string, body []byte, bridgeID *uuid.UUID, timeout time.Duration) (io.ReadCloser, uuid.UUID, error)

ForwardWebhook ensures the agent is running, creates a run record, and POSTs the webhook payload to the agent container. Returns the response body stream and the run ID. The timeout parameter controls the HTTP client timeout.

func (*Dispatcher) RefreshAgent

func (d *Dispatcher) RefreshAgent(ctx context.Context, agentID uuid.UUID) error

RefreshAgent triggers a synchronous re-sync on the agent container. Used after server-side state changes the cached system prompt depends on (typically MCP OAuth completion) so the running agent picks up new tools without a restart. If the container isn't running, returns nil — there's nothing to refresh; the agent will sync fresh on its next startup.

type PromptProxy

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

PromptProxy manages conversation history and forwards prompts to agent containers.

func NewPromptProxy

func NewPromptProxy(dispatcher *Dispatcher, database *db.DB, s3 *storage.S3Client, resolveTranscription TranscriptionResolver, logger *zap.Logger) *PromptProxy

NewPromptProxy creates a PromptProxy. The resolver is invoked to obtain the admin-configured transcription model when a voice note arrives; pass nil to disable auto-transcription (voice notes flow through as plain attachments).

func (*PromptProxy) HandleCallback

func (p *PromptProxy) HandleCallback(
	ctx context.Context,
	agentID, bridgeID, userID uuid.UUID,
	externalID, data string,
	events chan<- ResponseEvent,
) (staleRun bool, err error)

HandleCallback resolves a suspended run based on a bridge UI callback (inline-keyboard tap). data is the opaque platform payload — expected format:

"approve:<runID>"  — resume with Approved=true, no prompt
"deny:<runID>"     — resume with Approved=false + a "Rejected by user." prompt

If the referenced run is no longer suspended (e.g. already resolved via web), emits a single "info" event and returns — the driver's AnswerCallbackQuery should still fire to clear the spinner.

func (*PromptProxy) HandleMessage

func (p *PromptProxy) HandleMessage(
	ctx context.Context,
	agentID, bridgeID, userID uuid.UUID,
	externalID string,
	storeHistory bool,
	userMessage string,
	files []BridgeFile,
	events chan<- ResponseEvent,
) (string, error)

HandleMessage processes an incoming DM for an agent via a bridge. Manages conversation history, forwards to agent, streams response events to the provided channel, stores response when complete. The events channel is closed when streaming completes.

type ResponseEvent

type ResponseEvent struct {
	Type       string // "text-delta", "tool-call", "tool-result", "confirmation_required", "info"
	Text       string // for text-delta / info: the delta text or info message
	ToolCallID string // for tool_call/tool_result
	ToolName   string // for tool_call/tool_result
	ToolInput  string // for tool_call: the tool arguments
	ToolOutput string // for tool_result: the tool output
	ToolError  string // for tool_result: error message if failed
	Raw        []byte // full NDJSON line (for non-text events drivers may need)

	// Populated when Type == "confirmation_required":
	RunID      string
	Permission string
	Patterns   []string
	Code       string
}

ResponseEvent represents an NDJSON event from the agent response stream, forwarded to the bridge driver for progressive delivery.

type Scheduler

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

Scheduler manages cron schedules for all active agents.

func NewScheduler

func NewScheduler(dispatcher *Dispatcher, db *db.DB, logger *zap.Logger) *Scheduler

NewScheduler creates a Scheduler.

func (*Scheduler) ReloadAgent

func (s *Scheduler) ReloadAgent(ctx context.Context, agentID uuid.UUID) error

ReloadAgent reloads cron entries for a specific agent after a sync.

func (*Scheduler) Start

func (s *Scheduler) Start(ctx context.Context) error

Start loads all enabled crons from the database and starts the scheduler.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop gracefully stops the scheduler.

type SlashCommand

type SlashCommand struct {
	Name        string
	Description string
	Access      agentsdk.Access
}

SlashCommand describes a user-typable command. Registry is the single source of truth: TrySlashCommand dispatches from it, and bridge drivers that support native command menus (Telegram setMyCommands, etc.) expose it verbatim. Name is stored without the leading slash.

type SlashCommandResult

type SlashCommandResult struct {
	Handled          bool
	Reply            string
	ForwardAsCompact bool
}

SlashCommandResult is the outcome of handling a slash command. Handled=false means the message was not a slash command — callers should forward it to the agent as usual. Handled=true means the command was recognized and processed. Reply is the short message to show the user, unless ForwardAsCompact is set — in that case the caller should invoke the agent with PromptInput.ForceCompact=true so Sol's summarization produces the user-visible text via a normal run.

func TrySlashCommand

func TrySlashCommand(
	ctx context.Context,
	q *dbq.Queries,
	convID pgtype.UUID,
	agentID uuid.UUID,
	access agentsdk.Access,
	message string,
	logger *zap.Logger,
) (SlashCommandResult, error)

TrySlashCommand parses message for a leading slash command and, if recognized, executes it against the conversation. Returns Handled=false when the message is a plain user prompt. Both web (api.conversationsHandler.Prompt) and bridge (PromptProxy) paths call this before forwarding to the agent.

access is the caller's resolved agent access (see ResolveAgentAccess) and is compared against the command's required level. agentID is used by /clear to resolve any pending suspended run so the pending-confirmation dialog doesn't linger after context clear.

type TelegramChatInfo

type TelegramChatInfo struct {
	Username  string
	FirstName string
	LastName  string
}

TelegramChatInfo holds the subset of getChat fields we expose.

type TelegramDriver

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

TelegramDriver handles Telegram bridges via long-polling.

func NewTelegramDriver

func NewTelegramDriver(logger *zap.Logger) *TelegramDriver

NewTelegramDriver creates a TelegramDriver. logger is used to surface Telegram API errors (notably the silent-failure Markdown/HTML parse rejections that otherwise vanish). Pass zap.NewNop() in tests.

func NewTelegramDriverWithBaseURL

func NewTelegramDriverWithBaseURL(baseURL string, client *http.Client) *TelegramDriver

NewTelegramDriverWithBaseURL creates a TelegramDriver with a custom base URL and HTTP client (for testing).

func (*TelegramDriver) Activate

func (d *TelegramDriver) Activate(ctx context.Context, br dbq.Bridge) error

func (*TelegramDriver) AnswerCallbackQuery

func (d *TelegramDriver) AnswerCallbackQuery(ctx context.Context, token, callbackID, text string) error

AnswerCallbackQuery clears the loading spinner on a tapped inline-keyboard button. If text is non-empty, Telegram shows it as a transient toast.

func (*TelegramDriver) DefaultEcho

func (d *TelegramDriver) DefaultEcho() bool

DefaultEcho reports that Telegram defaults to hiding tool bubbles: each tool-call / tool-result is rendered as its own chat message and a chatty agent quickly swamps the conversation. Users can opt in per-chat with `/echo on`.

func (*TelegramDriver) GetChat

func (d *TelegramDriver) GetChat(ctx context.Context, token, chatID string) (TelegramChatInfo, error)

GetChat calls the Telegram getChat API for a private chat. Because private chat IDs equal the user ID, this resolves a telegram user's public username and display name as long as that user has DM'd the bot.

func (*TelegramDriver) GetMe

func (d *TelegramDriver) GetMe(ctx context.Context, token string) (string, error)

GetMe calls the Telegram getMe API and returns the bot username.

func (*TelegramDriver) Init

func (d *TelegramDriver) Init(ctx context.Context, br *dbq.Bridge) error

func (*TelegramDriver) Poll

func (d *TelegramDriver) Poll(ctx context.Context, br *dbq.Bridge) ([]BridgeEvent, error)

func (*TelegramDriver) RegisterCommands

func (d *TelegramDriver) RegisterCommands(ctx context.Context, br dbq.Bridge, cmds []SlashCommand) error

RegisterCommands publishes the slash-command registry to Telegram's global command menu via setMyCommands. Telegram stores names without the leading slash.

func (*TelegramDriver) SendMessage

func (d *TelegramDriver) SendMessage(ctx context.Context, token string, chatID int64, text string) error

SendMessage sends a text message to a Telegram chat.

func (*TelegramDriver) SendParts

func (d *TelegramDriver) SendParts(ctx context.Context, token string, chatID int64, parts []agentsdk.DisplayPart) error

SendParts sends display parts to a Telegram chat. Renders each part appropriately: text → sendMessage, image → sendPhoto, file → sendDocument.

func (*TelegramDriver) SendStream

func (d *TelegramDriver) SendStream(ctx context.Context, br dbq.Bridge, externalID string, echo bool, events <-chan ResponseEvent) (string, error)

func (*TelegramDriver) Teardown

func (d *TelegramDriver) Teardown(_ context.Context, _ dbq.Bridge) error

type TranscriptionResolver

type TranscriptionResolver func(ctx context.Context) (model.TranscriptionModel, error)

TranscriptionResolver returns the admin-configured transcription model or ErrTranscriptionNotConfigured when no model is set.

func NewTranscriptionResolver

func NewTranscriptionResolver(database *db.DB, encryptor *crypto.Encryptor) TranscriptionResolver

NewTranscriptionResolver returns a resolver that reads system_settings.default_transcription_model and looks up the associated provider credentials.

Jump to

Keyboard shortcuts

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