Files
Jyotisha/frontend/src/lib/agent-observability.ts
T
Jesse_Chen 8169bd85f7
Independent Staging Quality Gate / validate (push) Failing after 13m12s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(consult): classify consultation workflow failures for diagnosis
Every workflow fault except abort and timeout collapsed into the single
calculation_failed code, and the upstream message was discarded, so a failing
run left no evidence of whether the API rejected the call or returned a payload
that missed the response contract.

Classify failures into a closed vocabulary carried on ConsultationWorkflowError
and record it as the failureCode of the runtime step. The observability tool
call schema gains one controlled optional field; upstream error text stays out
of logs, as that contract requires. Forward request_id to the API so a run can
be aligned with its access log.

Build public receipts from an explicit allowlist. The internal failure code
must not reach the client contract, whose step schema is strict and would
otherwise reject a successful run.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 12:44:19 +08:00

157 lines
5.1 KiB
TypeScript

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<AgentBillingSettlementResult, "not_applicable">;
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(),
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<typeof agentObservabilityEventSchema>;
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";
}