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 ¶
- type Agent
- type Attachment
- type ChunkSink
- type Config
- type DebugInfo
- type Options
- type Router
- func (r *Router) Cancel(ctx context.Context, convID string) error
- func (r *Router) Debug() []DebugInfo
- func (r *Router) Defaults() Options
- func (r *Router) Len() int
- func (r *Router) Prompt(ctx context.Context, convID, userID string, query []Turn, opts Options, ...) error
- func (r *Router) ReportReaction(ctx context.Context, convID, userID, messageID, kind string, action string) error
- func (r *Router) RunGC(ctx context.Context, every time.Duration) (stop func())
- type Turn
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 ¶
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 ¶
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):
- `model` — legacy/back-compat single dropdown shape, still honoured if any caller sends it.
- `provider` + `model_<sanitised(provider)>` — cascading shape. The sanitiser matches paramctl.ProviderParamName: lowercase letters/digits/underscore, other bytes → '_'. Empty provider buckets to "other".
- 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 (*Router) Defaults ¶
Defaults returns the per-conversation option defaults configured on this router. The HTTP layer uses this to seed ParseOptions.
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.
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.