import { z } from "zod"; import { consultationDomainSchema } from "./consultation-domain-registry.ts"; /** * Closed, non-PII observability contract for Agent runs. * * There is intentionally no free-form metadata bag. Raw input/output text, * prompts, messages, birth data, names, email addresses, secrets, API keys, * provider payloads, stack traces and internal filesystem paths are not fields * in this schema. Every object is strict, so unknown fields fail closed. */ const opaqueIdSchema = z.string() .min(1) .max(128) .regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/, "invalid opaque identifier"); const machineValueSchema = z.string() .min(1) .max(128) .regex(/^[A-Za-z0-9][A-Za-z0-9._:+-]*$/, "invalid machine value"); const machineCodeSchema = z.string() .min(1) .max(80) .regex(/^[a-z][a-z0-9._-]*$/, "invalid machine code"); const durationMsSchema = z.number().int().min(0).max(7 * 24 * 60 * 60 * 1000); const countSchema = z.number().int().min(0).max(1_000_000); const tokenCountSchema = z.number().int().min(0).max(1_000_000_000); export const agentObservabilityStepStatuses = [ "completed", "failed", "cancelled", "skipped", ] as const; export const billingSettlementResults = [ "completed", "cancelled", "failed", "not_applicable", ] as const; export type AgentBillingSettlementResult = (typeof billingSettlementResults)[number]; export type AgentSettlementResult = Exclude; /** * Closed vocabulary for why the model stopped stepping. These are the provider * finish reasons plus the two Mastra adds; anything else normalizes to * `unknown` so an unrecognized provider string can never become a log field. * * `tool-calls` on a run that produced no answer means the step budget ran out * while the model still wanted to call a tool, which is otherwise only * inferable from the recorded step list. */ export const agentModelFinishReasons = [ "stop", "length", "content-filter", "tool-calls", "error", "other", "tripwire", "retry", "unknown", ] as const; export type AgentModelFinishReason = (typeof agentModelFinishReasons)[number]; const knownModelFinishReasons = new Set(agentModelFinishReasons); export function toAgentModelFinishReason(value: unknown): AgentModelFinishReason { return typeof value === "string" && knownModelFinishReasons.has(value) ? value as AgentModelFinishReason : "unknown"; } export type AgentSettlementTelemetryOutcome = Readonly<{ billingSettlementResult: AgentSettlementResult; errorCode?: string; }>; export function settlementTelemetryOutcome( settlementResult: AgentSettlementResult, errorCode?: string, ): AgentSettlementTelemetryOutcome { if (settlementResult === "failed") { return { billingSettlementResult: "failed", errorCode: "settlement_failed", }; } return { billingSettlementResult: settlementResult, ...(errorCode === undefined ? {} : { errorCode }), }; } export const agentObservabilityToolCallSchema = z.object({ name: machineCodeSchema, durationMs: durationMsSchema, status: z.enum(agentObservabilityStepStatuses), // Closed classification of why the call failed. Never upstream error text. failureCode: machineCodeSchema.optional(), }).strict().readonly(); export const agentObservabilityContractPhaseSchema = z.object({ phase: machineCodeSchema, durationMs: durationMsSchema.optional(), status: z.enum(agentObservabilityStepStatuses), }).strict().readonly(); export const agentObservabilityEventSchema = z.object({ runId: opaqueIdSchema.optional(), requestId: opaqueIdSchema.optional(), sessionId: opaqueIdSchema.optional(), caseId: opaqueIdSchema.optional(), agentVersion: machineValueSchema.optional(), skillVersion: machineValueSchema.optional(), modelVersion: machineValueSchema.optional(), policyVersion: machineValueSchema.optional(), toolCalls: z.array(agentObservabilityToolCallSchema).max(64).optional(), contractPhases: z.array(agentObservabilityContractPhaseSchema).max(64).optional(), retryCount: z.number().int().min(0).max(100).optional(), errorCode: machineCodeSchema.optional(), // Why the model stopped, and how many model steps the run consumed across // every attempt. Both are enum-like machine values, never provider text. modelFinishReason: z.enum(agentModelFinishReasons).optional(), modelStepCount: countSchema.optional(), // How many reference documents the model opened after loading the skill, and how many strict-method // sections the server delivered with the evidence. Both are needed to read the other: zero reads is // only a gap in the answer's method if nothing was delivered either. skillReferenceReads: countSchema.optional(), methodologySections: countSchema.optional(), inputTokens: tokenCountSchema.optional(), outputTokens: tokenCountSchema.optional(), evidenceCount: countSchema.optional(), claimCount: countSchema.optional(), sectionCount: countSchema.optional(), themeCoverage: z.array(consultationDomainSchema).max(10).optional(), reportJobDurationMs: durationMsSchema.optional(), reportJobPeakMemoryBytes: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(), billingSettlementResult: z.enum(billingSettlementResults).optional(), }).strict().refine( (event) => Boolean(event.runId || event.requestId || event.sessionId || event.caseId), { message: "at least one controlled identifier is required" }, ).readonly(); export type AgentObservabilityEvent = z.infer; export type AgentObservabilitySink = (event: AgentObservabilityEvent) => void; export type AgentObservabilityLogger = (event: unknown) => AgentObservabilityEvent; function consoleSink(event: AgentObservabilityEvent): void { console.info("[agent-observability]", JSON.stringify(event)); } export function createAgentObservabilityLogger( sink: AgentObservabilitySink = consoleSink, ): AgentObservabilityLogger { return (event) => { const parsed = agentObservabilityEventSchema.parse(event); try { sink(parsed); } catch { // Observability transport failure must not change the business response. } return parsed; }; } export const logAgentObservability = createAgentObservabilityLogger(); const knownErrorCodes = new Set([ "runtime_contract_incomplete", "empty_answer", "calculation_failed", "timeout", "cancelled", "settlement_failed", ]); export function toAgentObservabilityErrorCode(error: unknown): string { if (error instanceof DOMException) { if (error.name === "TimeoutError") return "timeout"; if (error.name === "AbortError") return "cancelled"; } if (error instanceof Error && knownErrorCodes.has(error.message)) return error.message; return "calculation_failed"; }