Help us shape the future of Go! Take the 2026 Go Developer Survey

export

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package export follows the events table of a Store and hands every committed event, in commit order, to a sink: the JSON Lines writer here, or the OpenTelemetry span exporter in the nested module export/otel.

Events are written in the transaction that makes the state change they explain, so the table is complete and a follower needs no other input (docs/architecture.md, ADR 2). The event's Seq is its cursor: SQLite assigns it from an AUTOINCREMENT key inside the single write transaction that commits the event, so it is strictly increasing in commit order across every run in a database and never reused. A follower that remembers the last Seq it delivered resumes exactly after it.

A Follower only reads. Opened on a Store from agentrt.OpenExisting with readOnly set, it can run as a separate process beside the consumer that owns the database, which needs no code for it. It reads a bounded page at a time through agentrt.Store.ListEventsAfter, with every text column capped at agentrt.MaxPageText, so a huge table, or one crafted row, cannot exhaust the follower's memory.

Index

Examples

Constants

View Source
const (
	// DefaultInterval is how long a Follower waits between polls when it
	// has delivered everything committed so far and Interval is zero.
	DefaultInterval = 250 * time.Millisecond
	// MaxInterval bounds Interval: a follower never waits longer than this
	// between polls, so an event is never more than a minute from its sink.
	MaxInterval = time.Minute
	// DefaultPageSize is how many events a Follower reads per query when
	// PageSize is zero.
	DefaultPageSize = 500
	// MaxPageSize bounds PageSize, so one page holds at most this many
	// payloads of up to agentrt.MaxPageText each.
	MaxPageSize = 10_000
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Cursor

type Cursor interface {
	Load(ctx context.Context) (int64, error)
	Save(ctx context.Context, seq int64) error
}

Cursor persists the Seq of the last event a Follower delivered. Load returns zero when nothing was saved yet.

type FileCursor

type FileCursor struct {
	Path string
}

FileCursor keeps the cursor in a file holding the Seq in decimal, written to a temporary file in the same directory and renamed over the old one, so a crash leaves the previous cursor rather than a torn file. A missing file loads as zero.

func (FileCursor) Load

func (c FileCursor) Load(context.Context) (int64, error)

Load implements Cursor.

func (FileCursor) Save

func (c FileCursor) Save(_ context.Context, seq int64) error

Save implements Cursor.

type Follower

type Follower struct {
	// Store is read and never written. agentrt.OpenExisting(path, true)
	// opens one that cannot write.
	Store *agentrt.Store
	// RunID restricts delivery to one run. Empty delivers every run's
	// events interleaved in commit order.
	RunID string
	// Cursor is where the follower resumes from and saves to. Nil starts
	// at the first event and remembers nothing beyond this call.
	Cursor Cursor
	// Interval is the wait between polls once everything committed has
	// been delivered: DefaultInterval when zero, never more than
	// MaxInterval.
	Interval time.Duration
	// PageSize is how many events one query reads: DefaultPageSize when
	// zero, never more than MaxPageSize.
	PageSize int
	// Once makes Follow return nil as soon as it has delivered every event
	// committed when it last looked, instead of polling for more.
	Once bool
}

Follower delivers a Store's events in commit order, each once per cursor position. The zero value is not usable: Store must be set.

Example

A follower delivers a run's events in commit order to a sink; here the JSON Lines sink, into a buffer, and Once returns when the committed events are delivered. An operator's process would open the database with agentrt.OpenExisting(path, true) and leave Once unset.

store, err := agentrt.OpenStore(":memory:")
if err != nil {
	panic(err)
}
defer store.Close()
echo := tool{spec: agentrt.ToolSpec{Name: "echo", Description: "echo", InputSchema: []byte(`{"type":"object"}`), SideEffect: agentrt.ReadOnly, Timeout: time.Second},
	fn: func(c agentrt.ToolCall) (agentrt.ToolResult, error) {
		return agentrt.ToolResult{Content: c.Args, Summary: "echoed"}, nil
	}}
driver, err := agentrt.NewDriver(agentrt.Config{Store: store, Agent: &scripted.Agent{Decisions: []agentrt.Decision{
	scripted.ToolCall("echo", `{"n":1}`, "look"),
	scripted.Complete(`{"ok":true}`),
}}, Policy: agentrt.DefaultPolicy(), Tools: []agentrt.Tool{echo}})
if err != nil {
	panic(err)
}
if _, err := driver.Start(context.Background(), "echo once", agentrt.DefaultLimits()); err != nil {
	panic(err)
}

var out bytes.Buffer
follower := &export.Follower{Store: store, Cursor: &export.MemCursor{}, Once: true}
if err := follower.Follow(context.Background(), export.JSONL(&out)); err != nil {
	panic(err)
}
for _, line := range bytes.Split(bytes.TrimSpace(out.Bytes()), []byte("\n")) {
	var rec struct {
		Seq  int64  `json:"seq"`
		Type string `json:"type"`
	}
	if err := json.Unmarshal(line, &rec); err != nil {
		panic(err)
	}
	fmt.Println(rec.Seq, rec.Type)
}
Output:
1 run.created
2 run.started
3 step.started
4 step.decided
5 step.policy
6 step.tool_started
7 step.tool_finished
8 step.started
9 step.decided
10 run.finished

func (*Follower) Follow

func (f *Follower) Follow(ctx context.Context, sink Sink) error

Follow reads events after the cursor and calls sink for each, in Seq order, until ctx is done, sink fails, or, with Once, nothing more is committed. Follow returns ctx.Err() when ctx ends it.

The cursor is saved after each page and before Follow returns, so delivery is exactly once across stops that return: a follower stopped by its context, by a sink error, or by Once, and started again on the same cursor, delivers each event once (the event a sink refused is delivered again, since it was never delivered). It is at least once after a hard kill: a process that dies between a sink call and the save that follows it, or whose save fails, starts again from the cursor last saved and delivers again what it delivered since, at most one page of PageSize events. A sink that must not act twice keys on Seq.

type MemCursor

type MemCursor struct {
	Seq int64
}

MemCursor is a Cursor in memory, for a follower that starts over each time it runs or whose caller persists Seq itself.

func (*MemCursor) Load

func (c *MemCursor) Load(context.Context) (int64, error)

Load implements Cursor.

func (*MemCursor) Save

func (c *MemCursor) Save(_ context.Context, seq int64) error

Save implements Cursor.

type Sink

type Sink func(agentrt.Event) error

Sink receives one event. A non-nil error stops the Follower, which returns it; the cursor then names the last event delivered before it, so a restart resumes with the event that failed.

func JSONL

func JSONL(w io.Writer) Sink

JSONL returns a Sink writing one JSON object per event to w, the record trace.JSONL writes: seq, run_id, step_id, at, type, and the payload as recorded. It is trace.JSONL behind a Sink, so the two never differ. A write error stops the follower.

Jump to

Keyboard shortcuts

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