AI exception diagnosis
Add Claude-powered root-cause diagnosis of Telescope exceptions — install and configure the @adonis-agora/telescope/ai subpath, diagnose an exception with its trace context, read the cached cause/fix/confidence, and plug in a custom diagnosis cache.
When an exception lands, @adonis-agora/telescope/ai can hand it (and the rest of its trace) to
Claude and get back a structured diagnosis — a likely cause, a concrete fix, and a confidence
level — cached so each error family is diagnosed once. This guide wires it up and uses it.
Install
AI diagnosis is the @adonis-agora/telescope/ai subpath of the one telescope package. Add the
Anthropic SDK (an optional peer) and pick AI when configuring telescope:
npm i @adonis-agora/telescope @anthropic-ai/sdk
node ace configure @adonis-agora/telescope # pick "AI"@anthropic-ai/sdk is an optional peer. configure registers the provider and publishes
config/telescope_ai.ts.
Configure
The API key comes from the environment, never source control. Add it to .env and validate
it in start/env.ts:
export default await Env.create(new URL('../', import.meta.url), {
ANTHROPIC_API_KEY: Env.schema.string.optional(),
})import env from '#start/env'
import { defineConfig } from '@adonis-agora/telescope/ai'
export default defineConfig({
apiKey: env.get('ANTHROPIC_API_KEY'),
// model: 'claude-sonnet-4-6', // default; use claude-opus-4-8 or claude-haiku-4-5-20251001
// maxTokens: 1024,
})With no key resolved the diagnoser is disabled — diagnose returns null and never
calls the API. So the package is safe to ship with no key in environments where you don't
want it active.
Diagnose an exception
The provider binds TelescopeAiDiagnoser into the container (even when disabled — its
diagnose is then a no-op), so you can inject it unconditionally. Pull an exception entry,
gather its trace for context, and diagnose:
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { TelescopeService } from '@adonis-agora/telescope'
import { TelescopeAiDiagnoser } from '@adonis-agora/telescope/ai'
@inject()
export default class DiagnoseController {
constructor(
private telescope: TelescopeService,
private diagnoser: TelescopeAiDiagnoser,
) {}
async show({ params, response }: HttpContext) {
const { telescope, diagnoser } = this
const entry = await telescope.find(params.id)
if (!entry || entry.type !== 'exception') {
return response.notFound({ error: 'Not an exception entry' })
}
const related = entry.traceId ? await telescope.byTrace(entry.traceId) : []
const diagnosis = await diagnoser.diagnose(entry as any, { related })
return response.json(diagnosis)
// → { cause, fix, confidence: 'high' | 'medium' | 'low', model, cached } | null
}
}Passing related (the other entries from the same trace — queries, the request, diagnostics)
gives the model the surrounding context; it skips sibling exceptions and clips each summary
to keep the prompt bounded.
What it does under the hood
- Cache first. If the exception's
familyHashwas diagnosed before (and you didn't passforce: true), the cached result comes back instantly withcached: true— no API call, no tokens. - One Claude call. Otherwise it builds a prompt (exception name/message, route, trace, stack clipped to the top 25 frames, related-entry summaries) and calls the Messages API once.
- Defensive parse. It extracts the first balanced JSON object from the response
(surviving markdown fences and stray prose) into
{ cause, fix, confidence }. - Cache + return. The result is cached by family hash.
Diagnosis never throws into your code: disabled → null; an API or parse failure is logged
and resolves to null. So you can call it straight from a request path without a try/catch.
Force a fresh diagnosis
Cached results are great until the underlying code changed and you want a re-read:
const fresh = await diagnoser.diagnose(entry, { related, force: true })
// bypasses the cache, calls the API again, and overwrites the cached entryCross-process cache
The default cache is a per-process LRU (500 families, 24h TTL). For a fleet that should share
diagnoses, supply a Redis/DB-backed DiagnosisStore to createDiagnoser:
import { createDiagnoser, resolveConfig, type DiagnosisStore } from '@adonis-agora/telescope/ai'
const redisCache: DiagnosisStore = {
get: (familyHash) => /* read from redis, parse, TTL-check */ null,
set: (familyHash, diagnosis) => { /* write to redis with a TTL */ },
}
export const diagnoser = createDiagnoser(
resolveConfig({ apiKey: process.env.ANTHROPIC_API_KEY }),
{ cache: redisCache },
)
// returns null when disabled / no key, so guard before useDiagnosisStore is just get(familyHash) → Diagnosis | null and set(familyHash, diagnosis). Now a family diagnosed on one pod is served from cache on all of them.
Privacy
The exception's name, message, stack (top 25 frames), route/trace, and the JSON content of
related trace entries are sent to Anthropic. Nothing is sent when AI is disabled or keyless.
If recorded content (query bindings, request data) may hold secrets, scrub it upstream —
see Tags & redaction.
Full package reference:
@adonis-agora/telescope/ai.
Request context
Correlate Telescope entries to a request, a trace, a tenant, and a user via @adonis-agora/context — how trace correlation works, how to tag entries with the authenticated user, and how to reconstruct everything that happened on one trace.
Configuration
Every configuration key across the Telescope ecosystem in one place — the core, watchers, dashboard UI, alerts, and AI packages, each with its config file, defaults, and the environment variables it expects.