Documentation
¶
Overview ¶
Package trigger provides services that trigger agent containers in response to external events: webhooks, cron schedules, and channel messages.
Index ¶
- Variables
- func ResolveAgentAccess(ctx context.Context, q *dbq.Queries, agentID, userID uuid.UUID) agentsdk.Access
- func ResolveEcho(settingsJSON []byte, driverDefault bool) bool
- func StreamNDJSONResponse(body io.Reader, runID string, events chan<- ResponseEvent) (string, []message.Message, *usageInfo, error)
- type BridgeCallback
- type BridgeDriver
- type BridgeEvent
- type BridgeFile
- type BridgeManager
- func (m *BridgeManager) AddBridge(bridgeID uuid.UUID)
- func (m *BridgeManager) HandleEvent(ctx context.Context, event BridgeEvent) error
- func (m *BridgeManager) RemoveBridge(bridgeID uuid.UUID)
- func (m *BridgeManager) SendMessage(ctx context.Context, bridgeID uuid.UUID, externalID, text string) error
- func (m *BridgeManager) SendParts(ctx context.Context, bridgeID uuid.UUID, externalID string, ...) error
- func (m *BridgeManager) Start(ctx context.Context) error
- func (m *BridgeManager) Stop()
- type CommandRegistrar
- type Dispatcher
- func (d *Dispatcher) EnsureRunning(ctx context.Context, agentID uuid.UUID) (*container.Container, error)
- func (d *Dispatcher) ForwardCron(ctx context.Context, agentID uuid.UUID, cronName string, timeout time.Duration) (io.ReadCloser, uuid.UUID, error)
- func (d *Dispatcher) ForwardPrompt(ctx context.Context, agentID uuid.UUID, input agentsdk.PromptInput, ...) (io.ReadCloser, uuid.UUID, error)
- func (d *Dispatcher) ForwardWebhook(ctx context.Context, agentID uuid.UUID, path string, body []byte, ...) (io.ReadCloser, uuid.UUID, error)
- func (d *Dispatcher) RefreshAgent(ctx context.Context, agentID uuid.UUID) error
- type PromptProxy
- type ResponseEvent
- type Scheduler
- type SlashCommand
- type SlashCommandResult
- type TelegramChatInfo
- type TelegramDriver
- func (d *TelegramDriver) Activate(ctx context.Context, br dbq.Bridge) error
- func (d *TelegramDriver) AnswerCallbackQuery(ctx context.Context, token, callbackID, text string) error
- func (d *TelegramDriver) DefaultEcho() bool
- func (d *TelegramDriver) GetChat(ctx context.Context, token, chatID string) (TelegramChatInfo, error)
- func (d *TelegramDriver) GetMe(ctx context.Context, token string) (string, error)
- func (d *TelegramDriver) Init(ctx context.Context, br *dbq.Bridge) error
- func (d *TelegramDriver) Poll(ctx context.Context, br *dbq.Bridge) ([]BridgeEvent, error)
- func (d *TelegramDriver) RegisterCommands(ctx context.Context, br dbq.Bridge, cmds []SlashCommand) error
- func (d *TelegramDriver) SendMessage(ctx context.Context, token string, chatID int64, text string) error
- func (d *TelegramDriver) SendParts(ctx context.Context, token string, chatID int64, parts []agentsdk.DisplayPart) error
- func (d *TelegramDriver) SendStream(ctx context.Context, br dbq.Bridge, externalID string, echo bool, ...) (string, error)
- func (d *TelegramDriver) Teardown(_ context.Context, _ dbq.Bridge) error
- type TranscriptionResolver
Constants ¶
This section is empty.
Variables ¶
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).
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 ¶
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.
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 ¶
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 ¶
NewScheduler creates a Scheduler.
func (*Scheduler) ReloadAgent ¶
ReloadAgent reloads cron entries for a specific agent after a sync.
type SlashCommand ¶
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 ¶
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 ¶
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) 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) 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)
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.