Documentation
¶
Overview ¶
Package api provides an HTTP API server over the derived sessions/traces/spans read model.
Index ¶
- Constants
- func CompileOpenAPI(ctx context.Context, docs tapesoapi.TypeDocs) (*tapesoapi.CompiledDoc, error)
- func DefaultContractVersions() []cassette.ContractVersion
- func NewOpenAPIParser(docs tapesoapi.TypeDocs) *tapesoapi.Parser
- type Config
- type Discovery
- type DiscoveryDepends
- type DiscoveryEntry
- type DiscoverySetting
- type Evidence
- type EvidenceCassette
- type EvidenceConfiguration
- type EvidenceInstance
- type EvidenceProblem
- type EvidenceUnresolvedSource
- type InternalConfig
- type InternalServer
- type MCPError
- type MCPRequest
- type MCPResponse
- type MainUsage
- type Metrics
- type ModelUsage
- type PayloadMode
- type RawTurnHeaderItem
- type RawTurnListResponse
- type Server
- func (s *Server) NewInternalServer(config InternalConfig) (*InternalServer, error)
- func (s *Server) OpenAPIParser() *tapesoapi.Parser
- func (s *Server) RefreshCassetteSpecs(ctx context.Context) []error
- func (s *Server) Run() error
- func (s *Server) RunWithListener(listener net.Listener) error
- func (s *Server) SetCassetteSources(sources []string)
- func (s *Server) Shutdown() error
- func (s *Server) StartCassetteSpecRefresh(ctx context.Context, interval time.Duration)
- type SessionDetailResponse
- type SessionItem
- type SessionListResponse
- type SessionRollup
- type SessionTracesResponse
- type SessionUsage
- type SpanItem
- type SpanLinkItem
- type StandaloneTraceDetail
- type StatsResponse
- type TraceDetail
- type TraceItem
- type TraceListResponse
- type TraceUsage
- type TreeTask
Constants ¶
const ( // EnvInternalListen carries the internal listener's address. Unset means // the listener is not started at all, which is what keeps a plain // `tapes serve` — the documented Docker-only loop included — unchanged. EnvInternalListen = "TAPES_INTERNAL_LISTEN" // EnvInternalToken carries the bearer token the internal listener // requires. The listener refuses to start without it. EnvInternalToken = "TAPES_INTERNAL_TOKEN" )
Environment variables that configure the internal listener.
They are read from the environment only — never from a flag or config.toml. A bearer token in a config file outlives the process that needed it, and one in a flag is readable from any process table on the host; this pair is delivered by whatever orchestrates the deployment, as a mounted secret and a port assignment, and neither belongs to the configuration surface an operator edits by hand.
const ( EnvPodName = "TAPES_POD_NAME" EnvPodUID = "TAPES_POD_UID" EnvPodIP = "TAPES_POD_IP" EnvNodeName = "TAPES_NODE_NAME" EnvReplicaSet = "TAPES_REPLICA_SET" EnvImageDigest = "TAPES_IMAGE_DIGEST" )
Environment variables carrying this instance's identity.
Core cannot discover any of this for itself without becoming a Kubernetes client, which is exactly the coupling it must not take on: tapes has to stay runnable with no cluster at all. So the deployment supplies them — from the downward API, where a pod's own name, uid, ip and node are available as field references — and an unset variable simply yields an absent field. The evidence is still usable without them; pod_uid is the one a reader correlating an address back to a pod will miss.
const DefaultInternalListen = ":8092"
DefaultInternalListen is the address the internal listener is documented to use when a deployment enables it. It is not a default in the usual sense: the listener is off unless an address is configured, so this is the value to configure rather than the value assumed.
const EvidenceSchema = "tapes.evidence/v1"
EvidenceSchema identifies this payload's shape. A consumer branches on it rather than sniffing fields, so a future shape can be introduced beside this one instead of silently replacing it.
const InternalEvidencePath = "/internal/readiness/evidence"
InternalEvidencePath is where evidence is served on the internal listener.
It is deliberately not a route on the API listener. The tenant-facing gateway rewrites a path prefix onto that listener's root before forwarding, so *any* path added there becomes publicly addressable; and the API process performs no authorization of its own, because tenancy is settled by the gateway in front of it. A second listener is what keeps this reachable by an operator and unreachable by a tenant.
const ProjectionSchema = "2026-06-15"
ProjectionSchema is the compatibility date of the derived projection generation currently served (the dated *_20260615 table family). It is stamped onto the wire `schema` field; a future generation bumps this in lockstep with a new dated table family (derived_projection_schemas).
Variables ¶
This section is empty.
Functions ¶
func CompileOpenAPI ¶ added in v0.30.0
CompileOpenAPI builds the read API's published contract.
It constructs a server purely to make it register its routes, then compiles what that registration produced. Generating from the real construction path rather than from a description of it is the point: there is no second list of routes to fall out of step with the first.
The server is never started and never serves a request, so it is given no driver — the handlers it registers are function values, and nothing calls them.
func DefaultContractVersions ¶ added in v0.30.0
func DefaultContractVersions() []cassette.ContractVersion
DefaultContractVersions returns the tapes contracts this build of core serves.
This is the API server's fact to hold, and it lives next to the handlers that serve the surface it names. It is deliberately not a constant in pkg/cassette: that package is the vocabulary a cassette uses to describe itself, and a cassette declaring "I read tapes v1" must not depend on a package that also asserts which core is running. The dependency goes one way — core reads a cassette's declaration and decides.
It is a set rather than a single value so a deployment can serve a new contract while still admitting cassettes built against the previous one. That is the only way to roll a fleet of cassettes forward without a flag day, and it costs nothing to allow for now.
func NewOpenAPIParser ¶ added in v0.30.0
NewOpenAPIParser returns a parser configured the way both this server and the contract generator need it.
docs may be nil, and is nil in the running server: a deployed binary has no source tree to read prose out of. It is non-nil under `tapes dev openapi`, which points the doc reader at a checkout so the compiled document carries the prose sitting next to each field in the source.
Types ¶
type Config ¶
type Config struct {
// ListenAddr is the address to listen on (e.g., ":8081")
ListenAddr string
// Pricing is the model pricing table used by /v1/sessions/summary to
// compute per-session cost. When nil, sessions.DefaultPricing() is used.
Pricing sessions.PricingTable
// EnableWebUI serves the minimal browser UI at /. It is disabled by default
// so API-only servers do not expose a human-facing development UI unless
// explicitly requested.
EnableWebUI bool
// ContractVersions is the set of tapes contracts this server serves. A
// cassette whose depends.core falls outside the set is refused at
// admission, and the newest entry is what the discovery document
// advertises as current. Empty means DefaultContractVersions().
ContractVersions []cassette.ContractVersion
}
Config is the API server configuration.
type Discovery ¶ added in v0.30.0
type Discovery struct {
ContractVersion string `json:"contract_version"`
Cassettes []DiscoveryEntry `json:"cassettes"`
Problems []cassetterunner.Rejection `json:"problems"`
// Entities is the aggregated entity registry:
// core-native declarations plus every admitted cassette's [[entities]].
// It updates as cassettes are admitted and withdrawn, and a consumer
// keeping a catalog should treat it as advisory — shape-valid entity
// types are usable whether or not they are currently listed.
Entities []cassetterunner.AdvertisedEntity `json:"entities"`
}
Discovery is the document served at GET /v1/cassettes.
It publishes what each cassette *is* — never what it is configured to. Core holds no configuration values—the deployment supplies them directly to the cassette—so there is nothing here to leak.
type DiscoveryDepends ¶ added in v0.30.0
DiscoveryDepends is a cassette's declared dependency on core.
type DiscoveryEntry ¶ added in v0.30.0
type DiscoveryEntry struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
DisplayName string `json:"display_name,omitempty"`
Description string `json:"description,omitempty"`
// Audience is the clients that should offer this cassette, empty meaning
// all of them. Published because the filtering happens in the client, and a
// client cannot filter on a field it never receives.
Audience []string `json:"audience"`
RoutePrefix string `json:"route_prefix"`
Depends *DiscoveryDepends `json:"depends,omitempty"`
Tables []string `json:"tables"`
Config []DiscoverySetting `json:"config"`
OpenAPIPath string `json:"openapi_path"`
OpenAPIStatus tapesoapi.Status `json:"openapi_status"`
ManifestDigest string `json:"manifest_digest"`
}
DiscoveryEntry describes one served cassette.
The OpenAPI document is referenced, not inlined. A single spec runs to tens of kilobytes and clients poll discovery; inlining five of them turns every client boot into a megabyte of mostly-unchanged bytes. The digest is enough to know whether the fetch is worth making.
type DiscoverySetting ¶ added in v0.30.0
type DiscoverySetting struct {
Key string `json:"key"`
Type string `json:"type"`
Required bool `json:"required"`
Secret bool `json:"secret"`
Default any `json:"default,omitempty"`
Description string `json:"description,omitempty"`
}
DiscoverySetting is one configuration key as a schema, never as a value.
type Evidence ¶ added in v0.44.0
type Evidence struct {
Schema string `json:"schema"`
Instance EvidenceInstance `json:"instance"`
Configuration EvidenceConfiguration `json:"configuration"`
// Cassettes is every source with a cassette identity, ordered by name.
// A cassette here is not necessarily serving: Admission says whether it
// is, and a source admitted once keeps its name through a later refusal
// because the route it published is still mounted.
Cassettes []EvidenceCassette `json:"cassettes"`
// UnresolvedSources is every configured source that has produced no
// cassette, ordered by subject — never fetched, unreachable, or refused
// on first sight. There is no name to report for these, only what was
// asked for, which kind of "no" it was, and what went wrong.
UnresolvedSources []EvidenceUnresolvedSource `json:"unresolved_sources"`
// CheckedAt is stamped when the request is served, so it dates the
// answer rather than the state behind it. A reader bounding freshness
// should use its own read time and keep this only for skew diagnosis:
// an instance's clock is not something to make a liveness decision on.
CheckedAt string `json:"checked_at"`
}
Evidence is one serving instance's account of what it loaded and admitted.
Every configured source appears exactly once, in Cassettes when it resolved to a cassette and in UnresolvedSources when it did not. So len(Cassettes) + len(UnresolvedSources) == Configuration.SourceCount, which lets a reader detect a truncated or partially-assembled answer without knowing anything about the individual entries. A served instance always holds a resolution loop, so the equality holds on the wire; only a test that substitutes a bare spec cache can produce empty lists beside a non-zero count, and nothing reads that over HTTP.
type EvidenceCassette ¶ added in v0.44.0
type EvidenceCassette struct {
Name string `json:"name"`
// Source is the configured document URL with any credential redacted.
Source string `json:"source"`
// Admission is the discriminator a registry lookup cannot supply:
// admitted, rejected, or unresolved.
Admission cassetterunner.Admission `json:"admission"`
// ManifestDigest identifies the versioned metadata embedded in the
// admitted document; OpenAPIDigest identifies the document core
// republished from it. Both are always present and may be empty — a
// cassette refused before it ever published has neither.
ManifestDigest string `json:"manifest_digest"`
OpenAPIDigest string `json:"openapi_digest"`
// OpenAPIStatus is how current the cached document is.
OpenAPIStatus tapesoapi.Status `json:"openapi_status"`
// RoutePrefix is where this cassette is mounted. It is reported even for
// a rejected entry, because a cassette whose refresh was refused keeps
// serving the document it published — the route is live, and a reader
// checking that a withdrawn cassette stopped answering needs to see that.
RoutePrefix string `json:"route_prefix"`
// AdmittedAt is when the current admission began, absent when this
// source is not currently admitted.
AdmittedAt string `json:"admitted_at,omitempty"`
// Rejection is the current problem, absent when there is none.
Rejection *EvidenceProblem `json:"rejection,omitempty"`
}
EvidenceCassette is one source that resolved to a cassette, and whether core is currently serving from it.
type EvidenceConfiguration ¶ added in v0.44.0
type EvidenceConfiguration struct {
// SourceListDigest identifies the cassette source list in effect, and
// SourceCount how many entries it held. See
// cassetterunner.SourceListDigest for the construction, which is fixed so
// a reader can compute the same value from its own copy of the intended
// list and compare.
SourceListDigest string `json:"source_list_digest"`
SourceCount int `json:"source_count"`
// LoadedAt is when that list was installed, absent when this process has
// never been told what to serve. It moves on a reconfiguration that does
// not restart the process, which is the case nothing outside the process
// can witness.
LoadedAt string `json:"loaded_at,omitempty"`
// ContractVersion is the tapes contract this core serves — the same one
// the discovery document advertises, so the two cannot disagree about
// which surface this is.
ContractVersion string `json:"contract_version"`
}
EvidenceConfiguration is the identity of what this process loaded.
type EvidenceInstance ¶ added in v0.44.0
type EvidenceInstance struct {
// InstanceID is generated when the process starts. It is the identity
// that needs no cluster: two answers carrying it are from the same
// process, and a restart is visible even where the pod name did not
// change.
InstanceID string `json:"instance_id"`
// StartedAt is when this process built its API server.
StartedAt string `json:"started_at"`
// The remainder are supplied by the deployment and absent when it
// supplies nothing.
PodName string `json:"pod_name,omitempty"`
PodUID string `json:"pod_uid,omitempty"`
PodIP string `json:"pod_ip,omitempty"`
NodeName string `json:"node_name,omitempty"`
ReplicaSet string `json:"replica_set,omitempty"`
ImageDigest string `json:"image_digest,omitempty"`
}
EvidenceInstance identifies the process that answered.
The point of it is that an address is not an identity. A reader fanning out across a set of endpoints has to be able to tell a replacement instance from the one it listed a moment ago, or a pass can silently count a fresh pod as evidence about the pod it replaced.
type EvidenceProblem ¶ added in v0.44.0
type EvidenceProblem struct {
// Subject is what the problem is about: the configured document URL,
// credential redacted.
Subject string `json:"subject"`
// Reason is the human-facing explanation, never parsed. It is empty for
// a configured source no resolution pass has visited yet, which is a
// state with a subject but no failure.
Reason string `json:"reason"`
// ObservedAt is when the reason was last seen, restamped on every
// failing pass so a reader can tell a current problem from a stale one.
ObservedAt string `json:"observed_at,omitempty"`
}
EvidenceProblem is something core could not do, and when it last noticed.
One shape serves both a rejection under a cassette and an entry in UnresolvedSources, because both answer the same three questions and a reader gains nothing from learning two spellings of them.
type EvidenceUnresolvedSource ¶ added in v0.44.0
type EvidenceUnresolvedSource struct {
EvidenceProblem
// Admission is rejected or unresolved, never admitted — a source core
// admitted has a cassette identity and is reported in Cassettes. It is
// always present: an entry here is by construction a source with an
// admission result and no name.
Admission cassetterunner.Admission `json:"admission"`
}
EvidenceUnresolvedSource is a configured source that never earned a cassette identity, and which kind of "no" that was.
The admission is the whole point of the entry existing as its own shape. Without it a document core fetched and refused is on the wire exactly like a source core could not reach at all, and those call for opposite actions: the first is a reachable service serving something wrong — fix the document — and the second is an address, a network, or a service that is not up. A reader could in principle tell them apart by reading Reason, but Reason is prose, documented as never parsed, and free to be reworded.
It stays in UnresolvedSources rather than moving into Cassettes under an empty name, because Cassettes is keyed and ordered by a name these entries do not have. The exact accounting is unaffected either way: len(Cassettes) + len(UnresolvedSources) still equals SourceCount.
type InternalConfig ¶ added in v0.44.0
type InternalConfig struct {
// ListenAddr is the address to listen on. Empty is not a default; it is
// a caller that should not have asked for a listener.
ListenAddr string
// Token is the bearer token every request must present.
Token string
}
InternalConfig configures the internal listener.
type InternalServer ¶ added in v0.44.0
type InternalServer struct {
// contains filtered or unexported fields
}
InternalServer is the operator-facing listener carrying the evidence endpoint and nothing else.
It is a separate server rather than a route group because the separation is the security property: the API listener sits behind a gateway that rewrites a tenant path prefix onto its root, so a path is either on a listener a tenant can reach or on one it cannot, and no middleware ordering makes the first case safe.
func (*InternalServer) Run ¶ added in v0.44.0
func (i *InternalServer) Run() error
Run starts the internal listener and blocks.
func (*InternalServer) RunWithListener ¶ added in v0.44.0
func (i *InternalServer) RunWithListener(listener net.Listener) error
RunWithListener starts the internal listener on an already-bound listener.
func (*InternalServer) Shutdown ¶ added in v0.44.0
func (i *InternalServer) Shutdown() error
Shutdown gracefully stops the internal listener.
type MCPError ¶ added in v0.30.0
type MCPError struct {
// Code is the JSON-RPC error code.
Code int `json:"code" oas:"example=-32600"`
// Message is a short description of the failure.
Message string `json:"message" oas:"example=invalid request"`
}
MCPError is a JSON-RPC 2.0 error object.
type MCPRequest ¶ added in v0.30.0
type MCPRequest struct {
// JSONRPC is the protocol version, always "2.0".
JSONRPC string `json:"jsonrpc" oas:"example=2.0"`
// ID correlates a response with this request. Absent on notifications.
ID string `json:"id,omitempty" oas:"example=1"`
// Method is the MCP method being invoked, such as tools/call.
Method string `json:"method" oas:"example=tools/call"`
// Params are the method's arguments.
Params map[string]any `json:"params,omitempty"`
}
MCPRequest is a JSON-RPC 2.0 request to the streamable MCP endpoint.
type MCPResponse ¶ added in v0.30.0
type MCPResponse struct {
// JSONRPC is the protocol version, always "2.0".
JSONRPC string `json:"jsonrpc" oas:"example=2.0"`
// ID is the id of the request this answers.
ID string `json:"id,omitempty" oas:"example=1"`
// Result is the method's return value on success.
Result map[string]any `json:"result,omitempty"`
// Error describes the failure when the call did not succeed.
Error *MCPError `json:"error,omitempty"`
}
MCPResponse is a JSON-RPC 2.0 response from the streamable MCP endpoint.
Exactly one of Result and Error is set, which is the JSON-RPC contract rather than anything this server adds.
type MainUsage ¶ added in v0.25.0
type MainUsage struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
}
MainUsage is the task token slice of a trace: the main agent and its subagents (call_kind=main across every thread), no cache split or cost (those live on the total Usage). Deliberately not spine-only — a subagent doing the user's work is task spend, not shadow.
type Metrics ¶ added in v0.7.0
type Metrics struct {
// contains filtered or unexported fields
}
Metrics is the Prometheus surface for the Tapes API server. Each Server owns its own registry so tests can scrape in isolation; the production path mounts /metrics on the Fiber app via NewServer.
func NewMetrics ¶ added in v0.7.0
func NewMetrics() *Metrics
NewMetrics constructs the Tapes API server's RED metrics. Labels stay templated (`route`) rather than per-URL so :hash path params don't blow up cardinality.
func (*Metrics) Handler ¶ added in v0.7.0
Handler returns a Fiber handler that serves Prometheus text exposition from this Metrics instance's registry. Mount it at /metrics with no auth. The handler is built once at NewMetrics time and cached — see the scrapeHandler field comment for why.
func (*Metrics) Middleware ¶ added in v0.7.0
Middleware returns a Fiber handler that records request count + duration per (route template, method, status). Templates like /v1/sessions/:hash stay as the label value so the :hash path param never expands cardinality.
Register this OUTSIDE recover.New() (i.e. via app.Use before recover) — see resolveStatus for why.
func (*Metrics) Registry ¶ added in v0.7.0
func (m *Metrics) Registry() *prometheus.Registry
Registry exposes the *prometheus.Registry so tests can scrape against the same registry the middleware writes to.
type ModelUsage ¶ added in v0.16.0
type ModelUsage struct {
Model string `json:"model"`
Calls int64 `json:"calls"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CostUsd float64 `json:"cost_usd"`
}
ModelUsage is one model's contribution to a session in the API: how many llm calls ran on it and what they spent. Cost-weighted (priced at derive time) so a per-model share reflects spend, not call count.
type PayloadMode ¶ added in v0.16.0
type PayloadMode string
PayloadMode selects how much span payload a trace response carries. Full embeds the stored content verbatim; preview truncates long text so list-shaped reads stay O(structure), with the span drill-in endpoint serving the full payload on demand.
const ( PayloadFull PayloadMode = "full" PayloadPreview PayloadMode = "preview" )
type RawTurnHeaderItem ¶ added in v0.16.0
type RawTurnHeaderItem struct {
ID int64 `json:"id"`
Source string `json:"source"`
Provider string `json:"provider,omitempty"`
AgentName string `json:"agent_name,omitempty"`
RequestID string `json:"request_id,omitempty"`
ReceivedAt time.Time `json:"received_at"`
Meta json.RawMessage `json:"meta,omitempty" oas:"type=object"`
RequestBytes int64 `json:"request_bytes"`
ResponseBytes int64 `json:"response_bytes"`
}
RawTurnHeaderItem is one wire-log row: what crossed the wire (or arrived as a transcript push), without the payload blobs. The `source` field is the wire-vs-transcript distinction.
type RawTurnListResponse ¶ added in v0.16.0
type RawTurnListResponse struct {
Items []RawTurnHeaderItem `json:"items"`
}
RawTurnListResponse is a session's wire log.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the API server for managing and querying the Tapes system
func NewServer ¶
NewServer creates a new API server. The storer is injected to allow sharing with other components (e.g., the proxy when not run as a singleton).
func (*Server) NewInternalServer ¶ added in v0.44.0
func (s *Server) NewInternalServer(config InternalConfig) (*InternalServer, error)
NewInternalServer builds this server's internal listener.
It refuses rather than defaults when the token is missing. A listener that came up unauthenticated because a secret failed to mount would be a hole nothing reports: the deployment looks healthy, the endpoint answers, and the only signal is the absence of a rejection nobody is watching for.
func (*Server) OpenAPIParser ¶ added in v0.30.0
OpenAPIParser returns the live parser every route on this server registered itself into.
It is the single source the published contract is generated from, which is what makes "served but undocumented" impossible by construction rather than by a convention someone has to remember.
func (*Server) RefreshCassetteSpecs ¶ added in v0.30.0
RefreshCassetteSpecs refreshes this server's cassette OpenAPI cache once.
func (*Server) RunWithListener ¶
RunWithListener starts the API server using the provided listener.
func (*Server) SetCassetteSources ¶ added in v0.30.0
SetCassetteSources configures exact full OpenAPI document URLs on this server's lifetime-owned runner, and records the identity of what it was handed.
The identity is recorded unconditionally, including for a server whose spec cache is not a runner: what this process was configured with is a fact about the process, not about whether something downstream acted on it.
A runner records the same identity for itself, under the lock that guards its source catalog, and that is the copy evidence publishes. The two records exist because they answer to different owners: this one outlives any particular spec cache, while only the runner's can be read atomically with the per-source state it describes. Nothing reconciles them — they are computed from the same argument by the same function — and a reader is never shown both.
func (*Server) StartCassetteSpecRefresh ¶ added in v0.30.0
StartCassetteSpecRefresh begins this server's source-resolution lifecycle. It retries quickly during startup so sidecars can become ready alongside tapes, then settles onto the configured refresh interval.
type SessionDetailResponse ¶ added in v0.12.0
type SessionDetailResponse struct {
Session SessionItem `json:"session"`
}
SessionDetailResponse is the response for GET /v1/sessions/:id: the session record alone. The conversation content lives on the span model (GET /v1/sessions/:id/traces).
type SessionItem ¶ added in v0.12.0
type SessionItem struct {
// Identity — capture-side facts, ingest-written.
ID string `json:"id"`
HarnessID string `json:"harness_id"`
HarnessSessionID string `json:"harness_session_id"`
Cwd string `json:"cwd,omitempty"`
HarnessVersion string `json:"harness_version,omitempty"`
ParentSessionID string `json:"parent_session_id,omitempty"`
StartedAt time.Time `json:"started_at"`
LastSeenAt time.Time `json:"last_seen_at"`
EndedAt *time.Time `json:"ended_at,omitempty"`
HarnessMetadata map[string]any `json:"harness_metadata,omitempty"`
// AuthSubject is the gateway-stamped JWT subject (WorkOS user id)
// captured at ingest; empty for rows captured before the edge began
// stamping it.
AuthSubject string `json:"auth_subject,omitempty"`
// Name is the harness identity-row label — the harness-supplied session
// name (a plan slug), or the folded title (rollup.title) as a fallback
// when no name was captured. This is capture/deriver provenance, NOT a
// user title: ingest re-sends it every turn. Clients should render
// DisplayTitle, not Name (PCC-970).
Name string `json:"name,omitempty"`
// DisplayName is the user's Console rename (sessions.display_name),
// empty unless a user set one. Written only by PATCH /v1/sessions/:id,
// never by ingest, so it survives a live session. It is the top of the
// DisplayTitle resolution; exposed raw so the edit affordance can seed
// its input from the user's own title (not the resolved fallback).
DisplayName string `json:"display_name,omitempty"`
// DisplayTitle is the server-resolved label clients should render:
// DisplayName -> rollup.title (generated) -> preview -> Name -> id
// slice. Resolving once on the server keeps every client (Console,
// paper CLI) from re-deriving — and diverging on — the precedence
// (PCC-970). Never empty: it falls back to a short harness id slice, then
// the session id (the primary key, always set for a stored row).
DisplayTitle string `json:"display_title"`
// Live is a runtime presence signal, not a projection fact: true when
// the session has no recorded end and was seen within the liveness
// window. Keyed on ended_at + last_seen_at recency (both ingest-fresh),
// never on the derived status: an interactive session folds to a
// terminal status (an end_turn assistant reply reads as "completed")
// after every turn while still open, so status cannot gate liveness.
// Computed at response time so the console renders it directly instead
// of inferring "running" itself.
Live bool `json:"live"`
// Rollup is the deriver-owned projection over the session's spans.
Rollup SessionRollup `json:"rollup"`
}
SessionItem is the per-session shape: capture identity at the top level, the deriver-owned projection nested under `rollup`. The split mirrors the storage rows — identity is ingest-written, rollup is deriver-written — so the wire can't blur which layer owns a field.
type SessionListResponse ¶ added in v0.4.0
type SessionListResponse struct {
Items []SessionItem `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
}
SessionListResponse is the response envelope for GET /v1/sessions.
type SessionRollup ¶ added in v0.25.0
type SessionRollup struct {
Status string `json:"status"`
// Title is the deriver's folded session title (derived_title),
// generated from the conversation. Empty until title generation
// produces one. It never falls back to the identity-row name, so it is
// the stable descriptive title clients prefer for display; the
// identity-row label (harness name or rename) is SessionItem.Name.
Title string `json:"title,omitempty"`
Preview string `json:"preview,omitempty"`
TurnCount int `json:"turn_count"`
// Model is the dominant conversation-spine model; ModelUsage is the
// per-model spend breakdown across every thread (subagent models
// included), cost-ordered so the UI can show "dominant model + share"
// without a cheap-subagent fan-out skewing it.
Model string `json:"model,omitempty"`
ModelUsage []ModelUsage `json:"model_usage,omitempty"`
// KindCounts (spans per call_kind) and Tasks (TaskCreate/TaskUpdate
// folds) are pinned so the rollup shape is uniform across sessions.
KindCounts map[string]int `json:"kind_counts"`
Tasks []TreeTask `json:"tasks"`
Usage SessionUsage `json:"usage"`
}
SessionRollup is the deriver-owned session projection — status, title, counts, and spend, all folded from the span layer at derive time. Every field is 'unknown'/zero/empty until the session first derives.
type SessionTracesResponse ¶ added in v0.16.0
type SessionTracesResponse struct {
Schema string `json:"schema"`
Session SessionItem `json:"session"`
Traces []TraceDetail `json:"traces"`
Links []SpanLinkItem `json:"links"`
}
SessionTracesResponse is the composite session view on the span model. `schema` stamps the projection generation the rows were derived against, so the presentational shape can version independently.
func BuildSessionTraces ¶ added in v0.16.0
func BuildSessionTraces( session SessionItem, turns []storage.SpanTurnRecord, spans []storage.SpanRecord, links []storage.SpanLinkRecord, mode PayloadMode, ) *SessionTracesResponse
BuildSessionTraces assembles the composite response. Pure rendering: every edge and kind here was computed by the deriver. Exported so `tapes dev trace-fixtures` emits byte-identical JSON to the handler.
type SessionUsage ¶ added in v0.25.0
type SessionUsage struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CostUSD float64 `json:"cost_usd"`
}
SessionUsage is the session's total token/cost spend, folded from the span layer. Pinned (no omitempty) for a uniform object shape.
type SpanItem ¶ added in v0.16.0
type SpanItem struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
ParentSpanID string `json:"parent_span_id,omitempty"`
// Seq is the span's presentation ordinal within its trace; spans
// arrive sorted by it (started_at ties inside one llm call — parallel
// tool batches share an instant).
Seq int64 `json:"seq"`
Kind string `json:"kind"`
Name string `json:"name"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
DurationNS int64 `json:"duration_ns"`
// Deriver-written taxonomy, promoted from the old metadata grab-bag.
CallKind string `json:"call_kind"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
ThreadID string `json:"thread_id"`
RawTurnID int64 `json:"raw_turn_id,omitempty"`
// Verdict is the typed security-monitor disposition (null off
// permission-check spans), deriver-written. It is a Verdict object or
// null on the wire; the oas tag states that, because a json.RawMessage
// carries no shape a reflector could recover.
Verdict json.RawMessage `json:"verdict" oas:"type=object,nullable"`
// Input/Output are content-block arrays (llm.ContentBlock), uniform for
// every kind (tool spans included — no unwrapping). Pinned to [] when
// empty.
Input json.RawMessage `json:"input" oas:"type=array:object"`
Output json.RawMessage `json:"output" oas:"type=array:object"`
// Usage (was `metrics`) is an llm.Usage object on the wire — {}-pinned
// for usage-less spans.
Usage json.RawMessage `json:"usage" oas:"type=object"`
// Payload marks a preview-truncated span so the console drills in for
// the full payload; absent in full mode.
Payload string `json:"payload,omitempty"`
}
SpanItem is one observed unit of work. Every field is a deriver output, formatting-only: the harness-taxonomy fields (call_kind, model, stop_reason, thread_id, verdict) are typed rather than bagged in a metadata map, and input/output are uniform content-block arrays for ALL kinds — the console owns per-kind rendering.
type SpanLinkItem ¶ added in v0.16.0
type SpanLinkItem struct {
Kind string `json:"kind"`
FromTraceID string `json:"from_trace_id"`
FromSpanID string `json:"from_span_id"`
FromIO string `json:"from_io,omitempty"`
ToTraceID string `json:"to_trace_id"`
ToSpanID string `json:"to_span_id"`
ToIO string `json:"to_io,omitempty"`
}
SpanLinkItem is a dataflow edge. kind is a typed top-level field (rejoin / verdict / compaction-seam / emits / feeds); from/to trace ids differ on cross-trace causality.
type StandaloneTraceDetail ¶ added in v0.41.0
type StandaloneTraceDetail struct {
SessionID string `json:"session_id" oas:"required"`
TraceDetail
}
StandaloneTraceDetail is the standalone trace lookup's response: a TraceDetail plus the owning session, which THIS caller — unlike the session-scoped composite's — does not already know and needs to navigate. A dedicated type (rather than an optional field on TraceDetail) lets generated clients see the guarantee as required here, while the composite schema advertises no field its payloads never emit.
type StatsResponse ¶ added in v0.4.0
type StatsResponse struct {
SessionCount int `json:"session_count"`
TurnCount int `json:"turn_count"`
CompletedCount int `json:"completed_count"`
TotalCost float64 `json:"total_cost"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
TotalDurationMs int64 `json:"total_duration_ms"`
ToolCalls int `json:"tool_calls"`
}
StatsResponse is the response for GET /v1/stats.
The numbers come from the span-projection trace-grain rollups, so they agree with the session detail and trace views:
- InputTokens / OutputTokens / TotalCost are SUMs of span_turns rollups — delta-only per-call usage, never the re-sent history (each main call re-bills the whole conversation on the wire).
- TotalDurationMs is the SUM of trace durations — agent time. Idle time between turns does not count. Served in milliseconds, not the nanoseconds we store: the summed ns over a wide window overflows a JSON consumer's 2^53 safe-integer range (~104 cumulative days), and sub-ms precision is meaningless for an aggregate agent-time figure.
- TurnCount counts traces (user-visible turns).
- ToolCalls is the SUM of the turn rollups' tool span counts, windowed on the turn's started_at like every other figure here rather than on each tool span's own timestamp (PCC-936).
- CompletedCount counts distinct sessions whose denormalized derived_status is 'completed' (chain-aware, PCC-515).
type TraceDetail ¶ added in v0.16.0
type TraceDetail struct {
Schema string `json:"schema,omitempty"`
Trace TraceItem `json:"trace"`
Spans []SpanItem `json:"spans"`
Links []SpanLinkItem `json:"links,omitempty"`
}
TraceDetail is one trace with its spans. In the composite session response links are session-scoped (top level); the single-trace endpoint sets Links to the edges touching that trace. `schema` stamps the projection generation on the STANDALONE /v1/traces/{id} response (omitempty — the composite embeds TraceDetail and already carries one stamp at the top level, so the embedded copies stay unstamped).
func BuildTraceDetail ¶ added in v0.16.0
func BuildTraceDetail(turn storage.SpanTurnRecord, spans []storage.SpanRecord, links []storage.SpanLinkRecord, mode PayloadMode) TraceDetail
BuildTraceDetail renders one turn with its spans and links. Exported so `tapes dev trace-fixtures` emits byte-identical JSON to the handler.
type TraceItem ¶ added in v0.16.0
type TraceItem struct {
TraceID string `json:"trace_id"`
// UserPrompt is served explicitly (not omitempty): a synthetic opener
// has an empty prompt, and dropping the key turns the empty string
// into `undefined` on the wire, which breaks consumers that expect a
// string (e.g. the console's stripHarnessTags). Empty means synthetic.
UserPrompt string `json:"user_prompt"`
// ResponsePreview is the derive-time fold of the closing
// conversation-spine llm call's text output — the answer line for
// collapsed turn cards, so summary consumers never need spans.
ResponsePreview string `json:"response_preview,omitempty"`
Status string `json:"status"`
// Source is the projection provenance ("wire" | "transcript"). A
// transcript-only session uses fallback until one usable wire call exists;
// then its complete projection is wire-derived and transcripts only
// reconcile structure. Spans inherit this trace-level provenance.
Source string `json:"source"`
StartedAt time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty"`
DurationNS int64 `json:"duration_ns"`
SpanCount int `json:"span_count"`
// Usage is the trace's total token/cost spend over ALL llm spans,
// shadow calls included; MainUsage is the task slice — the main agent
// and its subagents (every call_kind=main span, across threads). The
// difference (Usage − MainUsage) is the harness's shadow spend
// (permission checks, title-gen, web summaries) on the turn.
Usage TraceUsage `json:"usage"`
MainUsage MainUsage `json:"main_usage"`
// Synthetic is a typed deriver signal ("post-compaction" for a
// compaction continuation, "shadow-opener" for a shadow-only opener),
// promoted out of the old metadata grab-bag. Absent for genuine
// prompt-opened turns.
Synthetic string `json:"synthetic,omitempty"`
}
TraceItem is one user-visible turn's header. session_id / harness ids are not duplicated here — they belong to the session. A trace's post-compaction status is the typed Synthetic field below (promoted out of the old metadata grab-bag); the same seam is also recoverable from the session's compaction-seam links.
type TraceListResponse ¶ added in v0.16.0
TraceListResponse is the summaries list for one session. `schema` stamps the projection generation the rows were derived against — the same stamp the composite carries — so every trace-grain response is self-describing, not just the composite.
func BuildTraceList ¶ added in v0.16.0
func BuildTraceList(rows []storage.TraceSummaryRecord) TraceListResponse
BuildTraceList renders the turn-summary rows for one session. Exported so `tapes dev trace-fixtures` emits byte-identical JSON to the handler.
type TraceUsage ¶ added in v0.25.0
type TraceUsage struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheCreationTokens int64 `json:"cache_creation_tokens"`
CostUSD float64 `json:"cost_usd"`
}
TraceUsage is a trace's total token/cost rollup. Fields are pinned (no omitempty) so the object shape is uniform across traces.
Source Files
¶
- admin_handlers.go
- api.go
- cassette_discovery.go
- cassette_stream.go
- cassettes.go
- config.go
- contract.go
- handlers.go
- mcp_types.go
- metrics.go
- openapi.go
- openapi_routes.go
- readiness_evidence.go
- reference.go
- request_id_middleware.go
- sessions_handlers.go
- tenant.go
- trace_browse_handlers.go
- traces_handlers.go
- v1_handlers.go
- web_ui.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cassetterunner resolves and serves the cassettes an API server publishes.
|
Package cassetterunner resolves and serves the cassettes an API server publishes. |
|
Package mcp provides an MCP (Model Context Protocol) server for the Tapes system.
|
Package mcp provides an MCP (Model Context Protocol) server for the Tapes system. |