ADR-020: AgentTrace v1.1 Schema (Glass Box additive fields)
Status
Accepted — delivered as Glass Box v0.11 on branch feat/glass-box-v0.11-chat-boundary-tracing. Companion to ADR-019; extends the trace model of ADR-004.
Date
2026-05-31
Context
Chat-boundary recording (ADR-019) needs to persist evidence the v1.0 AgentTrace schema cannot carry: which recording layer an entry came from, a per-invocation correlation id, the verbatim system prompt, the tool definitions sent to the model, the per-turn finish reason, the request options, and provider metadata. The change must be purely additive — existing v1.0 traces must continue to load and round-trip unchanged, no existing recorder behaviour may change, and no public API may break. The serializer is reflection-based System.Text.Json (PropertyNamingPolicy = CamelCase, DefaultIgnoreCondition = WhenWritingNull), and entries are a flat List<TraceEntry>.
Two distinct AgentTrace types exist and must not be confused: AgentEval.Tracing.AgentTrace (the working trace model, in Core) and AgentEval.Output.AgentTrace (an IOutputStore pipeline record, in Abstractions). This ADR concerns the Core one.
Decision
Bump
AgentTrace.Version"1.0"→"1.1". Version denotes the highest schema version of any content the file may contain; individual entries may still be v1.0-shaped. Consumers branch on per-entry data (e.g.TraceEntry.EffectiveScope), never on the header string.Add additive, nullable fields to
TraceEntry:Scope(TraceEntryScope?),CorrelationId,SystemPrompt,ToolDefinitions(List<TraceToolDefinition>?),FinishReason,RequestOptions,ProviderMetadata, plus a non-serializedEffectiveScopeconvenience (null ⇒AgentInvocation). Add theTraceEntryScopeenum (AgentInvocation = 0 | ChatTurn | ToolExecution,[JsonStringEnumConverter]) and theTraceToolDefinitiontype.Scopeis nullable. WithWhenWritingNull, agent-boundary entries (Scope = null) serialize byte-identically to v1.0 — no snapshot churn — while chat-boundary entries explicitly carry"scope":"ChatTurn". Back-compat is therefore free: v1.0 files load withScope == null(EffectiveScope == AgentInvocation); no serializer code change is required (proven by a round-trip regression test against an inline v1.0 fixture).No subclassing. Because entries serialize as a flat
List<TraceEntry>, layer-specific entries are built by static factory methods onTraceEntry(ForChatRequest/ForChatResponse/ForChatError/ForToolExecution) that return a populated baseTraceEntry.IndexvsCorrelationId.Indexis the request/response pairing key (TraceReplayingAgent.BuildRequestResponsePairsgroups by it, filtering byType); a round-trip's Request and Response share one Index and each round-trip gets a distinct one.CorrelationIdis the per-invocation grouping key. Runtime gate verdicts are recorded into trace-levelMetadata(not as entries) precisely so they never enter the Index pairing/replay path.migratev1.0→v1.1 bumps the header on existing files (cosmetic, since the change is additive);doctorwarns on double-wrapping.
Consequences
Positive
- Existing v1.0 traces, exporters (JUnit/Markdown/JSON/SARIF/PDF — schema-driven),
TraceReplayingAgent, and Mission Control all keep working with no change; richer evidence flows automatically once v1.1 fields are populated. - One schema carries agent-boundary, chat-boundary, and tool-execution entries.
Negative / costs
- A v1.1 header now appears on traces produced by the new recorders (and on
migrated files); documented as "max schema version present." - Consumers must treat
nullscope asAgentInvocation(theEffectiveScopehelper standardises this).
Alternatives Considered
- A new parallel trace type for chat-boundary data. Rejected: would fork the serializer, exporters, replay, and Mission Control; additive fields keep one model.
- Non-nullable
Scopedefaulting toAgentInvocation. Rejected: a value-type field always serialises, changing existing agent-boundary trace JSON (snapshot churn). Nullable +WhenWritingNullkeeps v1.0 output identical. - A
SchemaVersioninteger / breaking rename. Rejected: the field isVersion(string) and must stay back-compatible.