router

package
v0.17.3 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package router: defensive helpers for attachment IO paths the production caller cannot trigger. Excluded from coverage via the `_must.go` suffix rule in .covignore.

Package router maps Poe conversation_ids to ACP sessions and owns their lifecycle (lazy create / resume, per-conv cwd, serial-per-conv prompting, idle GC).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Agent

type Agent interface {
	Caps() client.Caps
	NewSession(ctx context.Context, cwd string, sink client.SessionUpdateSink, systemPromptBlocks []acp.ContentBlock) (acp.SessionId, error)
	ListSessions(ctx context.Context, cwd string) ([]client.SessionInfo, error)
	ResumeSession(ctx context.Context, cwd string, sid acp.SessionId, sink client.SessionUpdateSink) error
	Prompt(ctx context.Context, sid acp.SessionId, prompt []acp.ContentBlock) (acp.StopReason, error)
	Cancel(ctx context.Context, sid acp.SessionId) error
	SetModel(ctx context.Context, sid acp.SessionId, modelID string) error
	SetConfigOption(ctx context.Context, sid acp.SessionId, configID, value string) error
}

Agent is the subset of client.AgentProc the router needs. Exposed as an interface for testability.

type Attachment

type Attachment struct {
	URL           string
	ContentType   string
	Name          string
	ParsedContent string
}

Attachment is a file attached to a Poe message. The router forwards these to the agent as ACP content blocks alongside the text prompt. Poe URLs are public-signed so no fetch happens on the relay side.

When ParsedContent is non-empty AND the agent advertises promptCapabilities.embeddedContext, the relay emits a ContentBlock::Resource (TextResourceContents) so the agent has the text inline without a fetch round-trip. Otherwise the relay falls back to a ResourceLink block (the mandatory ACP baseline that all agents support).

type ChunkSink

type ChunkSink interface {
	Text(s string) error
	Replace(s string) error
	Error(text, errorType string) error
	Done() error
	// FirstChunk is called by the router once, the first time the sink
	// receives a non-empty update from the agent. Used by the HTTP layer
	// to stop the "still thinking…" heartbeat.
	FirstChunk()
	// SetProviderEmoji conveys the relay-resolved provider emoji for
	// the current turn (dev.acp-kit.status-line/v1). Empty string means
	// the provider is unknown and the segment should be dropped.
	// Called at most once per turn, before any chunks land.
	SetProviderEmoji(emoji string)
	// SetStatus conveys the latest agent-supplied mood/plan labels
	// (dev.acp-kit.status-line/v1). Called whenever a session/update
	// carries the extension's _meta entry. Both fields are opaque
	// strings already trimmed and length-capped by the parser; an
	// empty string means "drop this segment". May be called multiple
	// times per turn — the renderer keeps the latest values.
	SetStatus(mood, plan string)
}

ChunkSink is the interface the HTTP/SSE layer implements to receive assistant output chunks for a single Poe query.

type Config

type Config struct {
	// Agent drives sessions. *client.AgentProc satisfies this.
	Agent Agent
	// StateDir is the root for per-conv working dirs. Each conv gets
	// StateDir/convs/<conv_id>/ as its cwd.
	StateDir string
	// SessionTTL: sessions idle longer than this are dropped from the map.
	SessionTTL time.Duration
	// Defaults are the values the Poe UI shows when the user hasn't
	// touched the Options panel. ParseOptions overlays user-supplied
	// keys on top of these so the agent always converges to the
	// UI-promised state, even on the first turn when Poe sends an
	// empty `parameters` dict.
	Defaults Options
	// SystemPromptProvider, when set, produces durable system-prompt
	// text. It is called once per newly-created (or resumed) router
	// session — never per turn — so callers can rebuild from mutable
	// host state (e.g. an on-disk skills directory) without restarting
	// the relay. Called outside Router.mu; must be safe for concurrent
	// calls. Return "" to disable injection for that session.
	//
	// When the agent advertises the session.systemPrompt capability the
	// text is sent via session/new._meta as one text ContentBlock;
	// otherwise it is prepended to the first session/prompt with a
	// "preserve verbatim" instruction. See acp-spec/rfd-system-prompt.md
	// and docs/skill-injection-plan.md.
	SystemPromptProvider func() string
	// Now overrides the clock for tests. Defaults to time.Now.
	Now func() time.Time
	// HTTPClient is used to fetch attachment bytes (download-to-disk
	// path, plus the inline ImageBlock for vision-capable models).
	// Defaults to http.DefaultClient.
	HTTPClient *http.Client
	// MaxInlineImageBytes caps the raw byte size of an image attachment
	// that the relay will base64-encode into an additive ImageBlock
	// alongside the file:// ResourceLink. Anything larger gets the
	// link-only treatment. Zero falls back to defaultMaxInlineImageBytes
	// (3 MiB — leaves headroom for base64 overhead under the tightest
	// provider cap, Bedrock-Anthropic at 3.75 MB).
	MaxInlineImageBytes int64
	// MaxAttachmentBytes caps how many bytes the relay will download to
	// disk per attachment. Files exceeding the cap are skipped (logged
	// and dropped from the prompt). Zero falls back to
	// defaultMaxAttachmentBytes (100 MiB).
	MaxAttachmentBytes int64
	// AttachmentTTL is how long downloaded attachment files persist on
	// disk before the GC sweep deletes them. Zero falls back to
	// defaultAttachmentTTL (30 days). The router clamps AttachmentTTL
	// to be no shorter than SessionTTL with a warn log so that a live
	// resumed session never points at a swept file.
	AttachmentTTL time.Duration
	// AuthErrorHint, when set, is called if a prompt fails with the
	// agent's "Authentication required" error (JSON-RPC code -32000).
	// Its return value is streamed to the user as ordinary assistant
	// text in place of the raw error, so a bot with no connected
	// provider can offer login instructions instead of a cryptic
	// failure. Called outside Router.mu; must be safe for concurrent
	// use. If nil, or if it returns "", the raw error is surfaced.
	AuthErrorHint func() string
}

Config configures a Router.

type DebugInfo

type DebugInfo struct {
	ConvID    string    `json:"conv_id"`
	UserID    string    `json:"user_id"`
	SessionID string    `json:"session_id"`
	Cwd       string    `json:"cwd"`
	LastUsed  time.Time `json:"last_used"`
}

DebugInfo is a snapshot of session state for /debug/sessions.

type Options

type Options struct {
	Model        string // "" = leave as-is
	Thinking     string // "" = leave as-is; one of "off","minimal","low","medium","high","xhigh","max"
	HideThinking bool   // suppress agent_thought_chunk in the SSE stream
}

Options is the per-prompt set of user-selected parameter values extracted from Poe's `parameters` dict.

func ParseOptions

func ParseOptions(params map[string]any, defaults Options) Options

ParseOptions extracts a strongly-typed Options struct from Poe's `parameters` dict, overlaying valid keys on top of defaults. Unknown keys and wrong-type values are silently dropped — Poe documents that other bots calling ours may inject arbitrary parameters, so this is untrusted input.

Defaults matter because Poe materialises `default_value`s into the UI display only; an empty `parameters` dict on the first turn would otherwise leave the agent on its own internal default while the UI promises something else. Overlaying onto defaults keeps UI and agent in sync from turn 1.

Model resolution order (first non-empty wins):

  1. `model` — legacy/back-compat single dropdown shape, still honoured if any caller sends it.
  2. `provider` + `model_<sanitised(provider)>` — cascading shape. The sanitiser matches paramctl.ProviderParamName: lowercase letters/digits/underscore, other bytes → '_'. Empty provider buckets to "other".
  3. defaults.Model — left in place if neither of the above resolved to a non-empty string.

type Router

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

Router is the conv_id → session map.

func New

func New(cfg Config) (*Router, error)

New creates a router.

func (*Router) Cancel

func (r *Router) Cancel(ctx context.Context, convID string) error

Cancel asks the agent to cancel the current prompt for a conv.

func (*Router) Debug

func (r *Router) Debug() []DebugInfo

Debug returns a snapshot of all tracked sessions, sorted by conv_id.

func (*Router) Defaults

func (r *Router) Defaults() Options

Defaults returns the per-conversation option defaults configured on this router. The HTTP layer uses this to seed ParseOptions.

func (*Router) Len

func (r *Router) Len() int

Len returns the number of tracked sessions.

func (*Router) Prompt

func (r *Router) Prompt(ctx context.Context, convID, userID string, query []Turn, opts Options, sink ChunkSink) error

Prompt handles one Poe query end-to-end. Submits the turn onto the session's queue and waits for the per-session runTurns goroutine to drive it to completion (FIFO with any other queued turns, including out-of-band reactions). The query slice is the full Poe-supplied conversation; on a cold-path new session with prior turns, the transcript is flattened so the agent has context.

func (*Router) ReportReaction added in v0.14.0

func (r *Router) ReportReaction(ctx context.Context, convID, userID, messageID, kind string, action string) error

ReportReaction queues an out-of-band reaction turn. Returns as soon as the turn is accepted onto the session queue (or immediately if shed) — the caller does NOT wait for the agent to finish. Poe has no SSE channel for the reaction response, so the agent's reply is discarded.

func (*Router) RunGC

func (r *Router) RunGC(ctx context.Context, every time.Duration) (stop func())

RunGC runs a background goroutine that drops idle sessions. Returns a stop function.

type Turn

type Turn struct {
	Role    string // "user", "bot", or "system"
	Content string
	// MessageID is Poe's per-turn id (unique within a query). The router
	// uses the latest user turn's MessageID to scope downloaded attachments
	// into a per-message subdir under the conv cwd. Empty is tolerated.
	MessageID   string
	Attachments []Attachment
}

Turn is one message in a Poe query, decoupled from the wire type so the router doesn't depend on poeproto.

Jump to

Keyboard shortcuts

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