Files
hermes-agent/apps/desktop/src/lib/error-surface.ts
T
Teknium 334bcbac93 fix(desktop): error card honors the classifier's retry verdict + failing-session identity (review feedback)
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.
2026-08-21 15:24:03 -07:00

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')
}