channel

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package channel is the front-door abstraction: every place a user can talk to tomo (the web UI, Telegram, and later Discord, Slack, iMessage) is a Channel. Adapters stay thin. They translate their platform's messages into an Inbound, render replies through a Reply, and answer approvals through an Approver. Everything above that (sessions, the agent turn, policy) is the router's job and is written once.

Index

Constants

View Source
const DefaultWorker = "tomo"

DefaultWorker is the name of the always-present worker: tomo itself. A deployment with no specialists configured routes everything here.

Variables

This section is empty.

Functions

func DecodeDataURL

func DecodeDataURL(s string) (provider.Block, error)

DecodeDataURL turns a browser data: URL into an image block. The webchat UI posts pasted or attached images this way, so it never touches the network.

func Drivers added in v0.2.1

func Drivers() []string

Drivers lists the registered channel names in sorted order, for onboarding and for a scaffold that wants to check a name is free.

func FetchImage

func FetchImage(ctx context.Context, client *http.Client, url string, header http.Header) (provider.Block, error)

FetchImage downloads one image and returns it as a provider image block. It is the shared path every channel uses to turn an attachment into something the model can see. header carries any auth a platform's file URL needs. A non-image response, or one too large, is an error the caller can skip.

func ReadImageFile

func ReadImageFile(path, declaredType string) (provider.Block, error)

ReadImageFile reads an image off disk and returns it as a model-ready block. iMessage stores attachments as files, so this is that channel's path to vision. declaredType is the mime type the database recorded, if any; the file extension is the fallback.

func Register added in v0.2.1

func Register(name string, d Driver)

Register makes a channel driver available by name, from a driver package's init(). A nil driver or a duplicate name panics: both are programmer errors that should surface at process start, not be papered over.

Types

type AgentFunc

type AgentFunc func() (*agent.Agent, error)

AgentFunc returns a ready base agent, minus its gate. It runs once per message so the system prompt reflects the current memory index.

type Attachment

type Attachment struct {
	Name    string // suggested filename, like "chart.png"
	Mime    string // media type, like "image/png"
	Data    []byte // the file bytes
	Caption string // optional note to show alongside it
}

Attachment is a file the agent made for the user: an image, a chart, a screenshot, a document. It rides the send_file tool back out.

type Caps

type Caps struct {
	Media   bool // can carry images in and out
	Buttons bool // can render tap-to-approve controls
	Stream  bool // can update a message as text streams
}

Caps declares what a channel can do, so the router can degrade gracefully: no Buttons means approvals fall back to a yes/no text reply, no Stream means the reply is sent once at the end instead of incrementally.

type Channel

type Channel interface {
	Name() string
	Caps() Caps
	Run(ctx context.Context, h Handler) error
}

Channel receives messages until its context is cancelled.

func Open added in v0.2.1

func Open(name string, s Settings) (Channel, error)

Open constructs the channel registered under name from its settings. An unknown name is an error that names the fix, so a typo in the config points at the driver import that is missing rather than failing silently.

type Clip

type Clip struct {
	Data []byte // the raw audio
	Ext  string // container extension, like ".ogg" or ".m4a"
}

Clip is a speech attachment waiting to be transcribed. The router turns it into text before the turn runs; channels only have to carry the bytes and the container extension.

func DecodeAudioDataURL

func DecodeAudioDataURL(s string) (Clip, error)

DecodeAudioDataURL turns a browser data: URL into a voice clip, the shape the webchat UI posts a recording as.

func FetchAudio

func FetchAudio(ctx context.Context, client *http.Client, url string, header http.Header) (Clip, error)

FetchAudio downloads one voice note and returns it as a clip. header carries any auth the platform's file URL needs. A response that is too large is an error the caller can skip; anything else is taken as audio, since the transcriber decodes by content, not by a strict type check.

func ReadAudioFile

func ReadAudioFile(path string) (Clip, error)

ReadAudioFile reads a voice note off disk, for the iMessage channel whose attachments are files.

type Driver added in v0.2.1

type Driver interface {
	Open(Settings) (Channel, error)
}

Driver opens a channel from its settings. A driver package registers itself under a name in init(), so the core reaches a channel by name and never imports the channel's package. This mirrors how a database driver owns its own connection string: the standard library dispatches by name and stays ignorant of any one driver's dialect.

Open returns (nil, nil) to mean "configured but off", which lets a driver treat an empty or disabled block as a no-op rather than an error.

type Exchange

type Exchange struct {
	Channel  string
	In       Inbound
	Reply    Reply
	Approver policy.Approver
}

Exchange bundles one inbound message with the channel-side handles the router needs to answer it.

type FileReply

type FileReply interface {
	File(a Attachment)
}

FileReply is an optional capability a Reply may implement: it can deliver a file the agent produced. A channel that cannot carry files leaves it unimplemented, and the send_file tool tells the agent where the file is instead.

type Handler

type Handler func(ctx context.Context, x Exchange)

Handler processes one exchange to completion.

type Inbound

type Inbound struct {
	Chat   string           // conversation key within the channel
	User   string           // sender id, for allowlisting
	Text   string           // message text
	Images []provider.Block // any image blocks that came with it
	Audio  []Clip           // any voice notes that came with it, pre-transcription
}

Inbound is one message arriving from a channel.

func (Inbound) Message

func (in Inbound) Message() provider.Message

Message builds the provider message for an inbound, text first then images.

type Reply

type Reply interface {
	Chunk(text string)
	Notice(text string)
	Done()
}

Reply is how the router talks back during one turn. Chunk carries streamed assistant text, Notice carries out-of-band status (tool activity, errors), and Done finalizes the turn.

type Router

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

Router turns an inbound message into a persisted, policy-gated agent turn and streams the reply back. It is channel-agnostic: HandlerFor binds it to a named channel and returns the Handler that channel drives. A Workforce decides which worker handles each message and builds that worker's agent.

func NewRouter

func NewRouter(st *store.Store, work Workforce, auditor policy.Auditor, transcriber voice.Transcriber, synth voice.Synthesizer) *Router

NewRouter wires a router. transcriber and synth may each be nil: without a transcriber a voice note is acknowledged but not understood, and without a synth a reply is never spoken back.

func (*Router) Background

func (r *Router) Background(ctx context.Context, ch, chat, prompt string) (string, error)

Background runs a one-off prompt as a turn in the chat's session and returns the assistant's text, for the scheduler to deliver. No one is watching, so any tool call that would ask for approval is declined; the rest of the policy stays in force.

func (*Router) HandlerFor

func (r *Router) HandlerFor(name string) Handler

HandlerFor returns the Handler for a channel by name.

func (*Router) WaitIdle

func (r *Router) WaitIdle()

WaitIdle blocks until every in-flight curation has finished. Used on shutdown so a reflection is not cut off mid-write, and by tests.

type Settings added in v0.2.1

type Settings map[string]any

Settings is one channel's configuration, decoded from the YAML block under channels.<name>. The core is deliberately blind to what any single key means: a Telegram token, a Discord allow-list, an iMessage database path are all just entries here, and only the channel's own driver knows how to read them. That is the whole point of the split. A new channel is a new package that registers a driver; the core never grows a field for it.

The accessors coerce the loose types a YAML decode produces (a number may arrive as int, int64, or float64; a list as []any) into the shapes a driver asks for, so a driver states what it wants and does not repeat the coercion.

func (Settings) Bool added in v0.2.1

func (s Settings) Bool(key string) bool

Bool returns the value at key as a bool, defaulting to false.

func (Settings) Int64s added in v0.2.1

func (s Settings) Int64s(key string) []int64

Int64s returns the value at key as a list of int64. It accepts the several numeric types a YAML decode can hand back, and a lone number as a one-element list.

func (Settings) String added in v0.2.1

func (s Settings) String(key string) string

String returns the value at key as a string, or "" if it is missing or not a string.

func (Settings) Strings added in v0.2.1

func (s Settings) Strings(key string) []string

Strings returns the value at key as a list of strings. A single string is treated as a one-element list, so allow: "C123" and allow: ["C123"] both work.

type VoiceReply

type VoiceReply interface {
	Voice(clip Clip)
}

VoiceReply is an optional capability a Reply may also implement: it can send a synthesized audio clip back. The router calls it when it has spoken a reply and the channel can carry audio out. A channel that cannot just leaves it unimplemented and the reply stays text only.

type Workforce

type Workforce interface {
	// Route picks the worker for a message. An explicit @name prefix wins, then
	// a channel:chat binding, otherwise the default worker. It returns the
	// worker name and the text with any @name prefix stripped.
	Route(channel, chat, text string) (worker, cleaned string)
	// Agent builds a fresh agent for one worker, so its system prompt reflects
	// the worker's current memory.
	Agent(worker string) (*agent.Agent, error)
	// Engine returns the policy engine for one worker.
	Engine(worker string) *policy.Engine
	// Curator returns the reflection pass for one worker, or nil if none runs.
	Curator(worker string) *curator.Curator
	// Names lists every known worker, the default included.
	Names() []string
}

Workforce resolves an inbound message to the worker that should handle it and builds that worker's agent, policy, and curator. A worker is a named specialist with its own persona, memory, and gate; the default worker is tomo. The router owns none of this itself, so how workers are configured stays out of pkg/channel.

func Solo

func Solo(newAgent AgentFunc, engine *policy.Engine, cur *curator.Curator) Workforce

Solo is the one-worker workforce: everything routes to the default worker. It wraps the pieces the router would otherwise hold directly, so a deployment with no specialists behaves exactly as a single agent. cur may be nil.

Directories

Path Synopsis
Package discord is tomo's Discord channel.
Package discord is tomo's Discord channel.
Package imessage is a no-op everywhere but macOS.
Package imessage is a no-op everywhere but macOS.
Package slack is tomo's Slack channel.
Package slack is tomo's Slack channel.
Package telegram is tomo's Telegram channel.
Package telegram is tomo's Telegram channel.
Package webchat is tomo's built-in channel: a small single-page UI served on localhost and a streaming endpoint behind it.
Package webchat is tomo's built-in channel: a small single-page UI served on localhost and a streaming endpoint behind it.

Jump to

Keyboard shortcuts

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