websocket

package
v0.2.0-alpha.8 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package streamhub provides task-scoped stream buffers for worker-push and client subscribe (SSE).

Package wsconn defines the typed event protocol for WebSocket communication between the portal frontend and the server. All messages are JSON envelopes with a "type" field and a typed "payload".

Index

Constants

View Source
const (
	TypeConversationCreate  = "conversation.create"
	TypeConversationMessage = "conversation.message"
	TypeSubscribeTask       = "subscribe.task"
	TypeUnsubscribeTask     = "unsubscribe.task"
)
View Source
const (
	TypeConversationCreated = "conversation.created"
	TypeMessageDelta        = "conversation.message.delta"
	TypeMessageQueued       = "conversation.message.queued"
	TypeMessageDequeued     = "conversation.message.dequeued"
	TypeMessageCompleted    = "conversation.message.completed"
	TypeConversationError   = "conversation.error"
	TypeTaskStatusChanged   = "task.status.changed"
	TypeTaskStreamDelta     = "task.stream.delta"
	TypeTaskStreamDone      = "task.stream.done"
	TypeSystemError         = "system.error"
)
View Source
const ErrorCodeQueueFull = "queue_full"

ErrorCodeQueueFull marks a conversation error that refused a new message because the conversation's queue is full. It is not a failure of the turn in progress, which is still running — a client must not read it as "the conversation went idle".

View Source
const StreamEventDone = "[[DONE]]"

StreamEventDone is sent on the subscription channel when the run finishes (SUCCEEDED/FAILED).

Variables

This section is empty.

Functions

func DecodePayload

func DecodePayload[T any](env Envelope) (T, error)

DecodePayload unmarshals the envelope's raw payload into the target type T.

func Encode

func Encode(eventType string, payload any) ([]byte, error)

Encode marshals an event type and payload into JSON bytes suitable for WebSocket write.

func Serve

func Serve(w http.ResponseWriter, r *http.Request, userID, spaceID string, deps ConnDeps)

Serve upgrades an authenticated request and runs the connection until it closes. The caller has already decided who this is and which space they are in; this package does not repeat that.

Types

type Conn

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

Conn manages a single WebSocket connection for one authenticated user.

type ConnDeps

type ConnDeps struct {
	Conversations coreconv.Store
	Turns         *turnqueue.Registry
	// Turner runs one conversation turn. An interface so this package does not
	// depend on the service that assembles agents, models, and tools.
	Turner   Turner
	Registry *ConnRegistry
	// CORSOrigin is checked on the upgrade. Empty or "*" accepts any origin,
	// which is what a deployment serving Portal from the same host has.
	CORSOrigin string
}

ConnDeps is everything a live connection needs. It is a struct rather than a Handler because this package should not be able to reach a store it has no use for: a socket creates and reads conversations and runs turns, and that is the whole list.

type ConnRegistry

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

ConnRegistry tracks active WebSocket connections per user.

func NewConnRegistry

func NewConnRegistry() *ConnRegistry

func (*ConnRegistry) Broadcast

func (r *ConnRegistry) Broadcast(spaceID, userID, eventType string, payload any)

Broadcast sends one event to every connection watching the space.

Nothing here picks a single connection. A person reads the Portal from as many tabs as they like and from none at all, and a spacemate reads the same conversation from their own; an event that reaches one of those and not the rest leaves the others showing state that has already changed.

func (*ConnRegistry) BroadcastSink

func (r *ConnRegistry) BroadcastSink(spaceID, userID, conversationID string) llm.StreamSink

BroadcastSink streams one turn's deltas to everyone watching the space.

A turn the server starts on its own — reporting a finished run, say — has no socket of its own to write to. This is what it streams through instead, and it costs nothing when nobody is connected.

func (*ConnRegistry) ForUser

func (r *ConnRegistry) ForUser(userID string) []*Conn

func (*ConnRegistry) Register

func (r *ConnRegistry) Register(userID string, c *Conn)

func (*ConnRegistry) Unregister

func (r *ConnRegistry) Unregister(userID string, c *Conn)

type ConversationCreate

type ConversationCreate struct {
	Channel string `json:"channel,omitempty"`
	Message string `json:"message"`
}

ConversationCreate is the payload for TypeConversationCreate.

type ConversationCreated

type ConversationCreated struct {
	ConversationID string `json:"conversation_id"`
}

ConversationCreated is the payload for TypeConversationCreated.

type ConversationError

type ConversationError struct {
	ConversationID string `json:"conversation_id,omitempty"`
	Error          string `json:"error"`
	// Code is an optional machine-readable reason. Empty means the turn itself failed.
	Code string `json:"code,omitempty"`
}

ConversationError is the payload for TypeConversationError.

type ConversationMessage

type ConversationMessage struct {
	ConversationID string `json:"conversation_id"`
	Content        string `json:"content"`
}

ConversationMessage is the payload for TypeConversationMessage.

type Envelope

type Envelope struct {
	Type    string          `json:"type"`
	Payload json.RawMessage `json:"payload"`
}

Envelope is the wire format for every WebSocket message (both directions).

func Decode

func Decode(data []byte) (Envelope, error)

Decode unmarshals raw bytes into an Envelope.

type MessageCompleted

type MessageCompleted struct {
	ConversationID string `json:"conversation_id"`
	// QueuedRemaining is how many messages are still waiting for their turn. A
	// client uses it to decide whether the conversation is idle or merely between
	// turns.
	QueuedRemaining int `json:"queued_remaining,omitempty"`
}

MessageCompleted is the payload for TypeMessageCompleted.

type MessageDelta

type MessageDelta struct {
	ConversationID string `json:"conversation_id"`
	Delta          string `json:"delta"`
}

MessageDelta is the payload for TypeMessageDelta.

type MessageDequeued

type MessageDequeued struct {
	ConversationID string `json:"conversation_id"`
	Content        string `json:"content"`
}

MessageDequeued is the payload for TypeMessageDequeued: a queued message is starting its own turn now.

type MessageQueued

type MessageQueued struct {
	ConversationID string `json:"conversation_id"`
	Content        string `json:"content"`
	// Position is 1-based: 1 is the next turn to run after the current one.
	Position int `json:"position"`
}

MessageQueued is the payload for TypeMessageQueued: the message arrived while a turn was running and will run as its own turn once that one finishes.

type StreamHub

type StreamHub interface {
	// Append adds a delta to the buffer and broadcasts to all subscribers for that task.
	Append(taskID, delta string)
	// Buffer returns the current buffered content for the task. Empty string if task has no buffer or was Done.
	Buffer(taskID string) string
	// Done marks the task's current run as finished; sends StreamEventDone to subscribers and clears state.
	Done(taskID string)
	// Subscribe returns a channel of deltas (and finally StreamEventDone) for the task. unsub must be called when done.
	Subscribe(taskID string) (events <-chan string, unsub func())
}

StreamHub is the interface for task-scoped stream buffers. Keys are task_id (one active run per task). Implementations may be in-memory (single instance) or backed by Redis (multi-instance).

func NewStreamHub

func NewStreamHub() StreamHub

NewStreamHub returns an in-memory StreamHub. Multi-instance scaling requires a Redis-backed impl.

type SubscribeTask

type SubscribeTask struct {
	TaskID string `json:"task_id"`
}

SubscribeTask is the payload for TypeSubscribeTask.

type SystemError

type SystemError struct {
	Error string `json:"error"`
}

SystemError is the payload for TypeSystemError.

type TaskStatusChanged

type TaskStatusChanged struct {
	TaskID string `json:"task_id"`
	Status string `json:"status"`
	Title  string `json:"title,omitempty"`
}

TaskStatusChanged is the payload for TypeTaskStatusChanged.

type TaskStreamDelta

type TaskStreamDelta struct {
	TaskID string `json:"task_id"`
	Delta  string `json:"delta"`
}

TaskStreamDelta is the payload for TypeTaskStreamDelta.

type TaskStreamDone

type TaskStreamDone struct {
	TaskID string `json:"task_id"`
}

TaskStreamDone is the payload for TypeTaskStreamDone.

type Turner

type Turner interface {
	HandleTurn(ctx context.Context, cmd conversation.HandleTurnCmd) (conversation.ConversationResult, error)
}

Turner runs one conversation turn to completion, streaming as it goes.

type UnsubscribeTask

type UnsubscribeTask struct {
	TaskID string `json:"task_id"`
}

UnsubscribeTask is the payload for TypeUnsubscribeTask.

Jump to

Keyboard shortcuts

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