visual-journal / src /lib /agent-request-diagnostics.ts
misonL's picture
Deploy 09324ad to Docker Space
b1cfe1b verified
Raw
History Blame Contribute Delete
9.74 kB
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<string, 'string' | 'number' | 'boolean'>;
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<string, DiagnosticContextValue>;
};
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<typeof readAgentStateBackend>;
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<string, string | undefined>;
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<AgentRequestRecord['responseJson']>
): NonNullable<AgentRequestDiagnostics['response']> {
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<AgentRequestRecord['errorJson']>
): NonNullable<AgentRequestDiagnostics['error']> {
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<string, DiagnosticContextValue> | 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<string, DiagnosticContextValue> = {};
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;
}