334bcbac93
Addresses @helix4u's review on #91493: - conversation_loop now stamps failure_retryable (the real ClassifiedError verdict) next to failure_reason; error_surface prefers it and only falls back to the reason set for older results. Fallback set corrected to match classify_api_error (auth, format_error, billing_unverified now non-retryable). - The descriptor carries the failing session's provider/model captured at classification time; Copy error details prefers them over the foreground composer atoms. - Open logs is labeled 'Open Desktop logs' on remote/cloud connections — the local folder holds transport logs, not the remote runtime's. - API-exception module allowlist widened to botocore/boto3/google/grpc/ requests/aiohttp so other adapter SDKs don't misclassify as gateway.
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
// Structured turn-error descriptor forwarded by the gateway (see
|
|
// agent/error_surface.py). Names WHICH layer of the stack failed so the error
|
|
// card can say "Provider error" / "Gateway error" and offer layer-appropriate
|
|
// recovery actions, instead of toasting an opaque string.
|
|
//
|
|
// Advisory contract: older backends never send this — every consumer must
|
|
// keep working when it is absent (legacy string-sniffing stays as fallback).
|
|
|
|
export const ERROR_SURFACE_LAYERS = [
|
|
'provider',
|
|
'endpoint',
|
|
'streaming',
|
|
'auth',
|
|
'billing',
|
|
'gateway',
|
|
'runtime',
|
|
'disk'
|
|
] as const
|
|
|
|
export type ErrorSurfaceLayer = (typeof ERROR_SURFACE_LAYERS)[number]
|
|
|
|
export interface ErrorSurface {
|
|
layer: ErrorSurfaceLayer
|
|
/** Specific failure code (a FailoverReason value or site-specific code). */
|
|
code: string
|
|
/** False when retrying unchanged reproduces the same failure. */
|
|
retryable: boolean
|
|
/** The failing session's provider/model, captured at classification time —
|
|
* preferred over the foreground composer's atoms, which can point at a
|
|
* different model by the time the user clicks an action. */
|
|
provider?: string
|
|
model?: string
|
|
}
|
|
|
|
/** Validate a wire payload into an ErrorSurface, or null when absent/garbled. */
|
|
export function parseErrorSurface(value: unknown): ErrorSurface | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const raw = value as { code?: unknown; layer?: unknown; model?: unknown; provider?: unknown; retryable?: unknown }
|
|
const layer = typeof raw.layer === 'string' ? (raw.layer as ErrorSurfaceLayer) : null
|
|
|
|
if (!layer || !ERROR_SURFACE_LAYERS.includes(layer)) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
layer,
|
|
code: typeof raw.code === 'string' && raw.code ? raw.code : 'unknown',
|
|
retryable: raw.retryable !== false,
|
|
...(typeof raw.provider === 'string' && raw.provider ? { provider: raw.provider } : {}),
|
|
...(typeof raw.model === 'string' && raw.model ? { model: raw.model } : {})
|
|
}
|
|
}
|
|
|
|
/** Plain-text error-details blob for the error card's "Copy error details". */
|
|
export function formatErrorDiagnostics(input: {
|
|
appVersion?: string
|
|
errorText: string
|
|
model?: string
|
|
provider?: string
|
|
surface?: ErrorSurface | null
|
|
}): string {
|
|
// The descriptor's identity (captured when the turn failed) beats the
|
|
// caller-supplied fallback (typically the foreground composer's atoms).
|
|
const provider = input.surface?.provider || input.provider
|
|
const model = input.surface?.model || input.model
|
|
|
|
const lines = [
|
|
'── Hermes error details ──',
|
|
`time: ${new Date().toISOString()}`,
|
|
input.surface ? `layer: ${input.surface.layer}` : null,
|
|
input.surface ? `code: ${input.surface.code}` : null,
|
|
input.surface ? `retryable: ${input.surface.retryable}` : null,
|
|
provider ? `provider: ${provider}` : null,
|
|
model ? `model: ${model}` : null,
|
|
input.appVersion ? `app: ${input.appVersion}` : null,
|
|
`error: ${input.errorText}`
|
|
]
|
|
|
|
return lines.filter((line): line is string => Boolean(line)).join('\n')
|
|
}
|