agent

package
v0.70.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package agent computes agent-observability reports over the same generic event log everything else in the engine runs on — no new storage, no schema change. Two reserved event names carry the signal: agent_tool_call (one tool/MCP dispatch, with latency + error) and agent_turn (one conversation turn, with role + time-to-first-token). Every number here is COMPUTED and deterministic — latency percentiles reuse the exact nearest-rank code the p90/p95/p99 measures use (trends.Percentile), so the provably-correct brand holds. The one number we refuse to compute unless the app sends it is resolution rate (the honesty covenant): ResolutionRate stays nil unless a turn actually carries a `resolved` bool — never fabricated.

Index

Constants

View Source
const (
	EventToolCall = "agent_tool_call"
	EventTurn     = "agent_turn"
)

Reserved event names, deliberately NOT $-prefixed so they sidestep the autocapture bot filter at ingest (that filter only drops $-prefixed events from crawler UAs). Documented for the paste-to-install skill so an agent instruments its own observability with these exact names.

View Source
const (
	PropConversationID = "conversation_id"
	PropLabeledBy      = "labeled_by" // the model that inferred the labels — required, never blank
	PropLabeledAt      = "labeled_at" // RFC3339 stamp of when the label was written
)

Reserved properties on an agent_label event. Everything else on it is a label.

View Source
const (
	DefaultSampleLimit = 20
	MaxSampleLimit     = 100
	MaxSampleTurns     = 40
	MaxTextRunes       = 500
)

Sampling caps: a model has to be able to read the result in one go, so the sample is bounded on every axis (conversations, turns per conversation, characters per turn) and the caller is told exactly what was left out.

View Source
const EventLabel = "agent_label"

EventLabel is the third reserved agent event name: one model-written label set for one conversation. Like the other two it is deliberately NOT $-prefixed.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClientSplit

type ClientSplit struct {
	Client string `json:"client"`
	Calls  int    `json:"calls"`
}

ClientSplit is one client's share of a tool's calls (cursor / claude-code / copilot / …).

type ConversationSample added in v0.11.0

type ConversationSample struct {
	Event            string                `json:"event"`
	Conversations    []SampledConversation `json:"conversations"`
	Returned         int                   `json:"returned"`
	Total            int                   `json:"total"` // conversations in the window, before the cap
	Limit            int                   `json:"limit"`
	TurnsWithText    int                   `json:"turns_with_text"`
	TurnsWithoutText int                   `json:"turns_without_text"`
	Note             string                `json:"note"`
}

ConversationSample is what sample_conversations returns: whole conversations plus the counts that keep it honest (how many exist, how many came back, how many turns actually had text).

func SampleConversations added in v0.11.0

func SampleConversations(events []event.Event, from, to time.Time, limit int) ConversationSample

SampleConversations returns whole conversations for the user's own model to read: agent_turn events in [from, to) grouped by conversation_id, turns ordered oldest-first inside each conversation, conversations ordered most-recent-first (last turn descending, conversation_id ascending as the tie-break) so the same input always produces the same sample — no randomness anywhere. limit defaults to 20 and is capped at 100; turns per conversation are capped and long text is truncated so a model can actually consume the result. A turn without a `text` property comes back without text; we never fabricate what was said.

type Conversations

type Conversations struct {
	Event          string   `json:"event"`
	Conversations  int      `json:"conversations"`
	Turns          int      `json:"turns"`
	MedianTurns    float64  `json:"median_turns"`
	P90Turns       float64  `json:"p90_turns"`
	ReAskRate      float64  `json:"reask_rate"`   // fraction with >=2 consecutive user turns (no assistant between)
	AbandonRate    float64  `json:"abandon_rate"` // fraction whose last turn is a user turn
	TTFTP50        float64  `json:"ttft_p50"`     // time-to-first-token percentiles over ttft_ms
	TTFTP90        float64  `json:"ttft_p90"`
	TTFTP99        float64  `json:"ttft_p99"`
	ResolutionRate *float64 `json:"resolution_rate"` // nil unless a turn carries a `resolved` bool
}

Conversations is the conversation-health report over agent_turn events. Every field is computed except ResolutionRate, which stays nil unless the app actually sends a `resolved` bool on a turn — we never invent whether a conversation was resolved.

func ComputeConversations

func ComputeConversations(events []event.Event, from, to time.Time) Conversations

ComputeConversations groups agent_turn events (already filtered + dev-env-scoped) by conversation_id within [from, to). Turns per conversation drive the median/p90 (via trends.Percentile). Ordering each conversation by timestamp yields re-ask (two consecutive user turns with no assistant between) and abandon (ends on a user turn). TTFT percentiles run over every turn's ttft_ms. ResolutionRate is computed ONLY when at least one turn carried a `resolved` bool anywhere in the set — otherwise it is nil (the honesty covenant).

type ErrorTaxonomy

type ErrorTaxonomy struct {
	Event string      `json:"event"`
	Tool  string      `json:"tool,omitempty"`
	Total int         `json:"total"`
	Types []ErrorType `json:"types"`
}

ErrorTaxonomy is the breakdown of failing tool calls by their `error_type`, optionally scoped to a single tool. Total is the number of error calls considered (the taxonomy sums to it).

func ComputeErrorTaxonomy

func ComputeErrorTaxonomy(events []event.Event, tool string, from, to time.Time) ErrorTaxonomy

ComputeErrorTaxonomy runs query.Breakdown over `error_type` for the failing agent_tool_call events (error truthy) in [from, to), optionally restricted to one tool. A typo'd or unseen tool matches nothing and returns an empty taxonomy — honest empty, never an error.

type ErrorType

type ErrorType struct {
	ErrorType string `json:"error_type"`
	Count     int    `json:"count"`
}

ErrorType is one error class and how often it occurred.

type LabelBreakdown added in v0.11.0

type LabelBreakdown struct {
	Event         string       `json:"event"`
	Label         string       `json:"label"`
	Values        []LabelValue `json:"values"`
	Conversations int          `json:"conversations"` // conversations in the window
	Labeled       int          `json:"labeled"`       // ...carrying this label
	Unlabeled     int          `json:"unlabeled"`
	LabeledBy     []string     `json:"labeled_by"` // distinct models behind the counted labels
	Inferred      bool         `json:"inferred"`
	Note          string       `json:"note"`
}

LabelBreakdown is the conversation count per label value, wrapped in the honesty context that makes it safe to read: which model(s) wrote the labels, and how many conversations in the window are still unlabeled. Inferred is always true — the VALUES are a model's judgement; the counts are computed from the event log.

func ComputeLabelBreakdown added in v0.11.0

func ComputeLabelBreakdown(events []event.Event, label string, from, to time.Time) LabelBreakdown

ComputeLabelBreakdown counts conversations per value of one label. Conversations come from agent_turn events in [from, to); labels come from agent_label events joined on conversation_id, regardless of when the label was written (a label is a statement about a conversation, not something that happened at that moment — windowing it would silently drop last week's conversations the moment you label them today). When a conversation was labeled more than once for the same label, the most recent write wins (event id breaks a timestamp tie, so the result is deterministic). Buckets and ordering come from query.Breakdown, the same grouping every other report uses.

Nothing here is invented: an unlabeled set returns an honest empty result that says how to label, and every result carries labeled/unlabeled counts plus the labelling model(s).

type LabelValue added in v0.11.0

type LabelValue struct {
	Value         string `json:"value"`
	Conversations int    `json:"conversations"`
}

LabelValue is one value of a label and how many conversations carry it.

type SampledConversation added in v0.11.0

type SampledConversation struct {
	ConversationID string        `json:"conversation_id"`
	TurnCount      int           `json:"turn_count"` // real number of turns, even if some were dropped
	Start          time.Time     `json:"start"`
	End            time.Time     `json:"end"`
	Labeled        bool          `json:"labeled"` // already carries at least one agent_label event
	Turns          []SampledTurn `json:"turns"`
	TurnsOmitted   int           `json:"turns_omitted,omitempty"`
}

SampledConversation is one whole conversation, turns oldest-first, ready to be read and labeled.

type SampledTurn added in v0.11.0

type SampledTurn struct {
	Role          string    `json:"role"`
	Timestamp     time.Time `json:"timestamp"`
	HasText       bool      `json:"has_text"`
	Text          string    `json:"text,omitempty"`
	TextTruncated bool      `json:"text_truncated,omitempty"`
}

SampledTurn is one turn as the user's model sees it. Text is only ever what the app actually sent on the turn: a turn with no `text` property comes back with role and timing only, and HasText false. We never invent conversation content.

type ToolHealth

type ToolHealth struct {
	Event  string     `json:"event"`
	Tools  []ToolStat `json:"tools"`
	Calls  int        `json:"calls"`
	Errors int        `json:"errors"`
}

ToolHealth is the tool-call report: one row per tool, plus the overall totals.

func ComputeToolHealth

func ComputeToolHealth(events []event.Event, from, to time.Time) ToolHealth

ComputeToolHealth groups agent_tool_call events (already filtered + dev-env-scoped by the caller) by their `tool` property within [from, to), and for each tool computes call/error counts, error rate, latency p50/p90/p99 over `latency_ms` (via trends.Percentile — the same nearest-rank the measures use), and the client split via query.Breakdown over `client`. A tool that doesn't exist simply doesn't appear — an honest empty report, never an error.

type ToolStat

type ToolStat struct {
	Tool       string        `json:"tool"`
	Calls      int           `json:"calls"`
	Errors     int           `json:"errors"`
	ErrorRate  float64       `json:"error_rate"`  // Errors/Calls, 0 when no calls
	LatencyP50 float64       `json:"latency_p50"` // over latency_ms, nearest-rank
	LatencyP90 float64       `json:"latency_p90"`
	LatencyP99 float64       `json:"latency_p99"`
	Clients    []ClientSplit `json:"clients"`
}

ToolStat is one tool's health: volume, error rate, latency tail, and who's calling it.

Jump to

Keyboard shortcuts

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