import { buildAgentRequestDiagnosticsRetention, readAgentStateBackend, type AgentImageResponse, type AgentRequestDiagnosticsRetention } from './agent-api-contracts'; import type { AgentArtifactRecord, AgentRequestRecord } from './agent-state-store'; import type { AgentErrorBody } from './api-error-response'; import type { AppLogEntry } from './app-logger'; import type { FeedbackResponse } from './feedback-store'; import { buildLogScopeDiagnostics, filterLogsByScope, resolveLogClientRequestIds } from './log-filter'; const MAX_DIAGNOSTIC_EVENTS = 30; const DIAGNOSTIC_CONTEXT_FIELD_TYPES = { providerDialect: 'string', normalizedEventCount: 'number', reason: 'string', channel_id: 'string', image_backend: 'string', operation: 'string', stream_mode: 'string', streamingStrategy: 'string', streaming_strategy: 'string', partialImages: 'number', partial_images: 'number', upstream_status: 'number', upstream_event_type: 'string', transport_error: 'boolean' } as const satisfies Record; type DiagnosticContextFieldType = (typeof DIAGNOSTIC_CONTEXT_FIELD_TYPES)[keyof typeof DIAGNOSTIC_CONTEXT_FIELD_TYPES]; type DiagnosticContextValue = string | number | boolean; export type AgentLogDiagnosticEvent = { id: number; at: string; level: AppLogEntry['level']; message: string; client_request_id?: string; filenames?: string[]; diagnostics?: Record; }; export type AgentRequestDiagnostics = { request: { request_id: string; idempotency_key: string; mode: AgentRequestRecord['mode']; status: AgentRequestRecord['status']; cached: boolean; created_at: string; updated_at: string; expires_at: string; locked_until?: string; }; timeline: Array<{ at: string; event: 'created' | 'updated' | 'locked_until' | 'expires'; }>; artifacts: Array<{ id: string; filename: string; content_url: string; metadata_url: string; output_format: string; mime_type: string; size_bytes: number; width: number | null; height: number | null; model: string; created_at: string; }>; response?: { request_id: string; idempotency_key: string; cached: boolean; image_count: number; artifact_ids: string[]; content_urls: string[]; created_at: string; timing?: AgentImageResponse['timing']; execution?: AgentImageResponse['execution']; }; error?: { code: string; message: string; retryable: boolean; upstream_status?: number; diagnostics?: AgentErrorBody['error']['diagnostics']; }; feedback?: FeedbackResponse; state_backend: ReturnType; diagnostics_retention: AgentRequestDiagnosticsRetention; diagnostics_boundary: { source: 'agent_state'; not_page_request_log: true; raw_request_json_redacted: true; api_key_redacted: true; }; }; export function buildAgentRequestDiagnostics(input: { record: AgentRequestRecord; artifacts: AgentArtifactRecord[]; env: Record; feedback?: FeedbackResponse; }): AgentRequestDiagnostics { return { request: { request_id: input.record.requestId, idempotency_key: input.record.idempotencyKey, mode: input.record.mode, status: input.record.status, cached: Boolean(input.record.responseJson), created_at: input.record.createdAt, updated_at: input.record.updatedAt, expires_at: input.record.expiresAt, ...(input.record.lockedUntil ? { locked_until: input.record.lockedUntil } : {}) }, timeline: buildAgentRequestTimeline(input.record), artifacts: input.artifacts.map(agentArtifactToDiagnostics), ...(input.record.responseJson ? { response: agentResponseToDiagnostics(input.record.responseJson) } : {}), ...(input.record.errorJson ? { error: agentErrorToDiagnostics(input.record.errorJson) } : {}), ...(input.feedback ? { feedback: input.feedback } : {}), state_backend: readAgentStateBackend(input.env), diagnostics_retention: buildAgentRequestDiagnosticsRetention(input.env), diagnostics_boundary: { source: 'agent_state', not_page_request_log: true, raw_request_json_redacted: true, api_key_redacted: true } }; } export function buildPageRequestDiagnostics(input: { logs: AppLogEntry[]; clientRequestId: string; filenames?: string[]; }): { scope: { request_ids: string[]; filenames: string[]; filename_matched_request_ids: string[]; copy_text: string; }; matched_log_count: number; events: AgentLogDiagnosticEvent[]; } { const filenames = input.filenames ?? []; const resolvedClientRequestIds = resolveLogClientRequestIds({ logs: input.logs, clientRequestIds: [input.clientRequestId], filenames }); const matchedLogs = filterLogsByScope({ logs: input.logs, clientRequestIds: [input.clientRequestId], filenames }); const scope = buildLogScopeDiagnostics({ clientRequestIds: [input.clientRequestId], filenames, resolvedClientRequestIds }); return { scope: { request_ids: scope.requestIds, filenames: scope.filenames, filename_matched_request_ids: scope.filenameMatchedRequestIds, copy_text: scope.copyText }, matched_log_count: matchedLogs.length, events: matchedLogs.slice(-MAX_DIAGNOSTIC_EVENTS).map(logEntryToDiagnosticEvent) }; } function buildAgentRequestTimeline(record: AgentRequestRecord): AgentRequestDiagnostics['timeline'] { const timeline: AgentRequestDiagnostics['timeline'] = [{ at: record.createdAt, event: 'created' }]; if (record.lockedUntil) timeline.push({ at: record.lockedUntil, event: 'locked_until' }); timeline.push({ at: record.updatedAt, event: 'updated' }); timeline.push({ at: record.expiresAt, event: 'expires' }); return timeline.sort((left, right) => left.at.localeCompare(right.at)); } function agentArtifactToDiagnostics(artifact: AgentArtifactRecord): AgentRequestDiagnostics['artifacts'][number] { return { id: artifact.id, filename: artifact.filename, content_url: artifact.contentUrl, metadata_url: artifact.metadataUrl, output_format: artifact.outputFormat, mime_type: artifact.mimeType, size_bytes: artifact.sizeBytes, width: artifact.width, height: artifact.height, model: artifact.model, created_at: artifact.createdAt }; } function agentResponseToDiagnostics( response: NonNullable ): NonNullable { return { request_id: response.request_id, idempotency_key: response.idempotency_key, cached: response.cached, image_count: response.images.length, artifact_ids: response.images.map((image) => image.id), content_urls: response.images.map((image) => image.content_url), created_at: response.created_at, ...(response.timing ? { timing: response.timing } : {}), ...(response.execution ? { execution: response.execution } : {}) }; } function agentErrorToDiagnostics( errorBody: NonNullable ): NonNullable { return { code: errorBody.error.code, message: errorBody.error.message, retryable: errorBody.error.retryable, ...(errorBody.error.upstream_status !== undefined ? { upstream_status: errorBody.error.upstream_status } : {}), ...(errorBody.error.diagnostics ? { diagnostics: errorBody.error.diagnostics } : {}) }; } function logEntryToDiagnosticEvent(entry: AppLogEntry): AgentLogDiagnosticEvent { const diagnostics = sanitizeContext(entry.context); return { id: entry.id, at: entry.at, level: entry.level, message: entry.message, ...(entry.clientRequestId ? { client_request_id: entry.clientRequestId } : {}), ...(entry.filenames ? { filenames: entry.filenames } : {}), ...(diagnostics ? { diagnostics } : {}) }; } function sanitizeContext(context: string | undefined): Record | undefined { if (!context) return undefined; let parsed: unknown; try { parsed = JSON.parse(context); } catch { return undefined; } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined; const sanitized: Record = {}; for (const [key, value] of Object.entries(parsed)) { const expectedType = DIAGNOSTIC_CONTEXT_FIELD_TYPES[key as keyof typeof DIAGNOSTIC_CONTEXT_FIELD_TYPES]; if (!expectedType) continue; if (isDiagnosticContextValue(value, expectedType)) { sanitized[key] = value; } } return Object.keys(sanitized).length > 0 ? sanitized : undefined; } function isDiagnosticContextValue( value: unknown, expectedType: DiagnosticContextFieldType ): value is DiagnosticContextValue { if (expectedType === 'number') return typeof value === 'number' && Number.isFinite(value); return typeof value === expectedType; }