trace

package
v0.37.1 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package trace records what a turn actually did.

Factor already knows all of this and keeps none of it. Phase changes go to one live watcher for the status line and are dropped; the registry runs every tool and remembers only the ones that panicked; the meter sees every model call and stores two integers. So "it answered as the wrong person", "it went quiet for ninety seconds" and "why did that turn cost so much" are reconstructed from memory rather than read.

A trace is the trajectory: what came in, which models answered, which tools ran and for how long, what they cost, and how the turn ended. It is the record three things need — a person debugging, the control bands watching for drift, and any eval built from real work rather than imagination.

It stays deliberately small. This is a single-user agent on the user's own machine, so the trace is a local file with a retention limit, it never leaves the box, and it records what a tool was called rather than what was said to it unless asked. Tool arguments hold the user's file paths, their searches and the things they asked to be remembered; the questions this record exists to answer need the shape of a turn, not its contents.

Index

Constants

View Source
const (
	EventFailover   = "failover"
	EventCompaction = "compaction"
	EventSteering   = "steering"
	EventBudget     = "budget"
	EventOverflow   = "overflow"
	EventBargeIn    = "barge_in"
	EventForget     = "forget"
	EventAsk        = "ask"
)

Event kinds. Named rather than free text because the control bands count them and a typo would read as a metric that never fires.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Enabled bool
	// RecordArgs adds argument values to each tool call. Off by default: the
	// shape of a turn answers the questions traces exist for, and the values
	// are the user's paths, searches and secrets-adjacent text.
	RecordArgs bool
	KeepDays   int
}

Config is what the user gets to decide.

type Event

type Event struct {
	At     float64 `json:"at_s"`
	Kind   string  `json:"kind"`
	Detail string  `json:"detail,omitempty"`
}

Event is something that happened to the turn rather than in it: a provider failover, a compaction, a message steered in mid-flight, a budget refusal, a user correcting the answer.

type ModelCall

type ModelCall struct {
	Model    string  `json:"model"`
	Input    int     `json:"input"`
	Cached   int     `json:"cached,omitempty"`
	Written  int     `json:"cache_write,omitempty"`
	Output   int     `json:"output"`
	USD      float64 `json:"usd,omitempty"`
	Duration float64 `json:"duration_s,omitempty"`
}

ModelCall is one provider round trip.

type Record

type Record struct {
	ID       string    `json:"id"`
	Started  time.Time `json:"started"`
	Session  string    `json:"session"`
	Channel  string    `json:"channel"`
	Trigger  string    `json:"trigger,omitempty"` // user | cron | job | heartbeat
	Speaker  string    `json:"speaker,omitempty"`
	Duration float64   `json:"duration_s"`

	Models []ModelCall `json:"models,omitempty"`
	Tools  []ToolCall  `json:"tools,omitempty"`
	Events []Event     `json:"events,omitempty"`

	// Outcome is how the turn ended: "ok", "error", "interrupted", "budget".
	Outcome string `json:"outcome"`
	Error   string `json:"error,omitempty"`

	InputTokens  int     `json:"input_tokens,omitempty"`
	CachedTokens int     `json:"cached_input_tokens,omitempty"`
	OutputTokens int     `json:"output_tokens,omitempty"`
	USD          float64 `json:"usd,omitempty"`
}

Record is one turn, written as a single JSON line when the turn ends.

func Since

func Since(dir string, t time.Time) ([]Record, error)

Since reads the records written on or after t, oldest first.

The reader is deliberately forgiving: a half-written final line is what a crash mid-write leaves behind, and a watcher that refused to read anything because of it would go blind exactly when something has gone wrong.

func (Record) CacheHitRate

func (r Record) CacheHitRate() float64

CacheHitRate is the share of this turn's input that was served from cache.

func (Record) Count

func (r Record) Count(kind string) int

Count returns how many events of a kind the turn carried.

func (Record) Failed

func (r Record) Failed() bool

Failed reports whether the turn ended in something that needs looking at. An interruption does not: the user talked over the answer, which is the system working.

func (Record) ToolErrors

func (r Record) ToolErrors() int

ToolErrors counts the tool calls that came back as failures.

type Recorder

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

Recorder writes turn records and prunes old ones. Every method is safe on a nil receiver, so the loop calls them unconditionally and a disabled tracer costs a nil check.

func NewRecorder

func NewRecorder(dir string, cfg Config, filter func(string) string) *Recorder

NewRecorder opens the trace directory, or returns nil when tracing is off. filter is the same secret scrubber the tool registry uses; nil means none.

func (*Recorder) Begin

func (r *Recorder) Begin(sessionKey, trigger, speaker string) *Turn

Begin opens a turn. The returned handle is nil when tracing is off.

func (*Recorder) Charge

func (r *Recorder) Charge(sessionKey string, call ModelCall)

Charge attributes one priced model call to the turn running on a session. Calls with no turn open — an idle compaction, an induction verdict — are written as records of their own, because spend nobody asked for is exactly the spend worth being able to find.

func (*Recorder) Close

func (r *Recorder) Close() error

Close releases the day's file.

func (*Recorder) Dir

func (r *Recorder) Dir() string

Dir is where records are written, for the readers that scan them.

func (*Recorder) Event

func (r *Recorder) Event(sessionKey, kind, detail string)

Event records something against whatever turn is running on a session, for callers that know the session but not the turn — the provider chain failing over, a channel reporting that the user talked over the answer. Nothing is recorded when no turn is open: an event with no turn to belong to is noise.

type ToolCall

type ToolCall struct {
	Name     string   `json:"name"`
	Duration float64  `json:"duration_s"`
	Bytes    int      `json:"bytes"`
	Error    bool     `json:"error,omitempty"`
	ArgKeys  []string `json:"arg_keys,omitempty"`
	// Args is present only when trace.record_args is on. It holds the
	// arguments after the same secret filter tool results pass through,
	// bounded, because a browser call can carry a page of them.
	Args string `json:"args,omitempty"`
}

ToolCall is one tool execution: what ran, for how long, how much it returned, and whether it failed.

type Turn

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

Turn is one in-flight turn. Methods are safe on a nil receiver.

func (*Turn) End

func (t *Turn) End(outcome string, err error)

End closes the turn and writes it.

func (*Turn) Event

func (t *Turn) Event(kind, detail string)

Event records something that happened to the turn.

func (*Turn) Tool

func (t *Turn) Tool(name string, args map[string]any, d time.Duration, bytes int, isErr bool)

Tool records one execution.

Jump to

Keyboard shortcuts

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