events

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package events implements the append-only session event log — the single source of truth for session state — plus its live fan-out: per-session seq allocation, list queries, a Postgres LISTEN/NOTIFY broker for SSE subscribers, ephemeral event_start/event_delta preview frames, and the span.* events emitted from the same instrumentation point as OTel spans.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrSessionNotFound = errors.New("session not found")
	ErrSessionArchived = errors.New("session is archived")
)

Sentinel errors the API layer maps onto wire error envelopes.

Functions

func HasUnansweredPlatformToolUse

func HasUnansweredPlatformToolUse(ctx context.Context, q Querier, sessionID domain.ID, extraRefs []string) (bool, error)

HasUnansweredPlatformToolUse reports whether any platform-executed built-in tool use (agent.tool_use) still lacks a result. The executor runs only these, so a confirmation resume enqueues a tool_exec only when one is outstanding: a turn whose remaining unanswered tools are all client-executed (custom) has no platform work and waits on the client's result instead — enqueuing a tool_exec there would provision a sandbox for nothing.

func HasUnansweredToolUse

func HasUnansweredToolUse(ctx context.Context, q Querier, sessionID domain.ID, extraRefs []string) (bool, error)

HasUnansweredToolUse reports whether any tool-use event in the session still lacks a matching result. extraRefs are treated as answered: the ids referenced by results that are validated but not yet inserted, so the API trigger can decide its batch before appending it.

func Previewable

func Previewable(t domain.EventType) bool

PreviewableTypes are the only event types the wire allows previews for.

func ToolConfirmationRefs

func ToolConfirmationRefs(evs []NewEvent) []string

ToolConfirmationRefs collects the tool-use ids a batch's user.tool_confirmation events resolve, in batch order.

func ToolResultRefs

func ToolResultRefs(evs []NewEvent) []string

ToolResultRefs collects the tool-use ids referenced by a batch's inbound tool-result events, in batch order.

func UnconfirmedAskEvents

func UnconfirmedAskEvents(ctx context.Context, q Querier, sessionID domain.ID, extraConfirmed []string) ([]string, error)

UnconfirmedAskEvents returns, in log order, the ids of the session's ask-gated tool-use events that no user.tool_confirmation has resolved yet — the set a requires_action suspension is still blocked on. extraConfirmed are the ids a validated-but-not-yet-inserted confirmation batch resolves, so the API can decide its resume before appending: an empty result means every ask is answered and the session may run; a non-empty result is the remainder to re-emit on session.status_idle.

func ValidateToolConfirmations

func ValidateToolConfirmations(ctx context.Context, q Querier, sessionID domain.ID, evs []NewEvent) error

ValidateToolConfirmations rejects an inbound user.tool_confirmation that does not name a tool use still awaiting confirmation: the id must reference an ask-gated tool-use event (evaluated_permission "ask") in this session that no prior confirmation has resolved, and not appear twice in one request. Like a tool result, an accepted bad confirmation cannot be taken back from the append-only log, so a wrong reference is the client's 400.

func ValidateToolResults

func ValidateToolResults(ctx context.Context, q Querier, sessionID domain.ID, evs []NewEvent) error

ValidateToolResults rejects an inbound tool result that does not reference an outstanding tool call: the id must name an existing tool-use event of the matching kind with no result yet, in the log or earlier in the same batch. The log is append-only — one accepted bad reference would poison every future replay with a request the model protocol rejects, wedging the session permanently.

Types

type AppendOptions

type AppendOptions struct {
	// SetStatus flips sessions.status alongside the append. The batch should
	// carry the matching session.status_* event; this option only moves the
	// resource column.
	SetStatus *domain.SessionStatus
	// AddUsage folds one model turn's usage into sessions.usage.
	AddUsage *domain.ModelUsage
	// MarkProcessedThrough stamps processed_at on still-unprocessed events
	// at seq <= the watermark — the brain recording which inbound events its
	// turn consumed. Zero means no stamping.
	MarkProcessedThrough int64
	// Then runs inside the same transaction after the insert (work enqueue,
	// counters). An error aborts the whole append.
	Then func(ctx context.Context, tx pgx.Tx) error
}

AppendOptions are same-transaction side effects of an append: the session state machine's invariant is that the sessions row (status, usage) and the event log can never disagree, so both change under the one session row lock the append already holds.

type Broker

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

Broker fans Postgres NOTIFY traffic out to in-process stream subscribers. One listening connection per process serves every subscriber (multi-replica control planes each run their own), and it is only held while subscribers exist: the listener starts with the first Subscribe and stops with the last Close, so idle processes and finished tests don't pin a connection.

func NewBroker

func NewBroker(pool *pgxpool.Pool) *Broker

func (*Broker) Ready

func (b *Broker) Ready(ctx context.Context) error

Ready blocks until the shared listener holds an active LISTEN. Subscribers that snapshot their starting log position after Ready returns cannot miss a wake for anything committed after the snapshot; any later coverage lapse re-wakes everyone on reconnect.

func (*Broker) Subscribe

func (b *Broker) Subscribe(sessionID domain.ID) *Subscription

Subscribe registers for one session's live traffic, starting the shared listener if it isn't running. Callers must Close the subscription.

type ListQuery

type ListQuery struct {
	Types                                        []string
	CreatedGT, CreatedGTE, CreatedLT, CreatedLTE *time.Time
	AfterSeq                                     *int64
	Desc                                         bool
	Limit                                        int // 0 = unlimited
}

ListQuery narrows and pages a session's event log. AfterSeq is a keyset position (exclusive) in the direction of the sort; seq order and created_at order agree because appends serialize per session.

type Log

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

Log is the append-only event store over the shared pool.

func NewLog

func NewLog(pool *pgxpool.Pool) *Log

func (*Log) Append

func (l *Log) Append(ctx context.Context, sessionID domain.ID, evs []NewEvent) ([]domain.Event, error)

Append durably appends events to one session's log in order, allocating the per-session seq under the session row lock (concurrent appends to the same session serialize; different sessions don't contend), and notifies stream subscribers on commit.

func (*Log) AppendInTx

func (l *Log) AppendInTx(ctx context.Context, tx pgx.Tx, sessionID domain.ID, evs []NewEvent, opts AppendOptions) ([]domain.Event, error)

AppendInTx is AppendWith inside a caller-owned transaction, for callers that must decide the batch under the session row lock (the API's state machine reads the current status FOR UPDATE, builds the batch, and appends — all one commit). The NOTIFY still fires only on the caller's commit.

func (*Log) AppendWith

func (l *Log) AppendWith(ctx context.Context, sessionID domain.ID, evs []NewEvent, opts AppendOptions) ([]domain.Event, error)

AppendWith is Append plus atomic session-state side effects.

func (*Log) List

func (l *Log) List(ctx context.Context, sessionID domain.ID, q ListQuery) ([]domain.Event, error)

List returns events for one session in seq order. It does not check that the session exists — callers that need 404 semantics check first.

func (*Log) PublishEventFrame

func (l *Log) PublishEventFrame(ctx context.Context, sessionID domain.ID, event map[string]any) error

PublishEventFrame broadcasts a fully-rendered wire event object (e.g. the session.deleted event, whose row cannot outlive the session) to live stream subscribers without persisting it.

func (*Log) StartModelRequest

func (l *Log) StartModelRequest(ctx context.Context, sessionID domain.ID) (context.Context, *ModelRequest, error)

StartModelRequest emits the span.model_request_start event and opens the matching OTel client span from one instrumentation point, so the wire events and the OTel trace can never drift (CLAUDE.md principle 3). The returned context carries the span for downstream propagation.

func (*Log) StartPreview

func (l *Log) StartPreview(ctx context.Context, sessionID domain.ID, typ domain.EventType) (*Preview, error)

StartPreview broadcasts the event_start frame and pre-allocates the id the buffered event must later be appended under.

type ModelRequest

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

ModelRequest is one in-flight model call being traced.

func (*ModelRequest) EndEvent

func (m *ModelRequest) EndEvent(isError bool, usage domain.ModelUsage) (NewEvent, error)

EndEvent renders the span.model_request_end wire event for the caller to append — the turn's settlement commits it atomically with the rest of the turn's output, so an uncommitted turn leaves no half-told span on the log. Finish then closes the OTel side; both halves live on ModelRequest so the wire event and the OTel span still come from one instrumentation point (CLAUDE.md principle 3).

func (*ModelRequest) Finish

func (m *ModelRequest) Finish(isError bool, commitErr error)

Finish closes the OTel span. commitErr is the fate of the transaction that carried the EndEvent (or the reason no end event was attempted): non-nil records the drift explicitly, so the trace never masks an aborted request as a clean one.

func (*ModelRequest) StartEventID

func (m *ModelRequest) StartEventID() domain.ID

StartEventID is the id of the span.model_request_start event, which the end event references as model_request_start_id.

type NewEvent

type NewEvent struct {
	ID          domain.ID // optional; generated when empty (previews pre-allocate)
	Type        domain.EventType
	Payload     json.RawMessage
	ProcessedAt *time.Time // nil = queued, awaiting in-order processing
}

NewEvent is one event to append. Payload holds the normalized type-specific wire fields only — never id/type/processed_at, which live on the envelope.

func NormalizeInbound

func NormalizeInbound(envKind string, raws []json.RawMessage) ([]NewEvent, error)

NormalizeInbound validates one send batch. envKind is the session's environment kind ("cloud" | "self_hosted"), which gates user.tool_result.

type Preview

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

Preview is one in-flight previewed event.

func (*Preview) Delta

func (p *Preview) Delta(ctx context.Context, index int64, text string) error

Delta broadcasts one content_delta fragment for the content-array entry at index. Only agent.message streams deltas; agent.thinking is start-only. Fragments longer than a NOTIFY payload allows are split into several frames at the same index (append semantics make that equivalent).

func (*Preview) EventID

func (p *Preview) EventID() domain.ID

EventID is the pre-allocated id: append the buffered event under it so subscribers can reconcile deltas.

type Querier

type Querier interface {
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}

Querier is the slice of pgx shared by pools and transactions, so the checks can run inside a caller's transaction.

type Subscription

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

Subscription delivers two kinds of traffic for one session: Wake, a coalesced "new committed events may exist, re-read the log" signal, and Frames, ephemeral broadcast frames (previews, session.deleted) in arrival order. Frames are best-effort by contract — a subscriber that can't keep up loses frames, never log events.

func (*Subscription) Close

func (s *Subscription) Close()

func (*Subscription) Frames

func (s *Subscription) Frames() <-chan json.RawMessage

func (*Subscription) Wake

func (s *Subscription) Wake() <-chan struct{}

Jump to

Keyboard shortcuts

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