Agora
Packages

@adonis-agora/telescope/ai

AI-assisted exception diagnosis subpath of @adonis-agora/telescope — turns an exception entry (plus its related trace entries) into a structured cause/fix/confidence diagnosis via the Anthropic Claude API, cached by exception family hash so the same error is never diagnosed twice.

When an exception lands in Telescope, the @adonis-agora/telescope/ai subpath can hand it (and the other entries from its trace) to Claude and get back a structured root-cause diagnosis — a likely cause, a concrete fix, and a confidence level — cached by the exception's family hash so each error family is diagnosed once.

Install

AI diagnosis ships inside the one @adonis-agora/telescope package. Add the Anthropic SDK (an optional peer) and enable the feature when configuring telescope:

npm i @adonis-agora/telescope @anthropic-ai/sdk
node ace configure @adonis-agora/telescope   # then pick "AI" at the prompt

@anthropic-ai/sdk is an optional peer (the host owns the version) — install it only if you enable AI. Selecting AI registers @adonis-agora/telescope/ai_provider and publishes config/telescope_ai.ts.

Configuration

config/telescope_ai.ts
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',
  // maxTokens: 1024,
})
KeyDefaultDescription
enabledtrueMaster switch. Treated as disabled when neither an apiKey nor a client resolves.
apiKey(from env)Anthropic API key — env.get('ANTHROPIC_API_KEY'), never hardcoded.
clientunsetA host-supplied model client (any value matching AnthropicMessagesClient) — used verbatim so the package never constructs the SDK.
modelclaude-sonnet-4-6Claude model id.
maxTokens1024Hard cap on generated tokens per diagnosis.
cacheTtlMs24hHow long a cached diagnosis stays fresh (a family is diagnosed once, then served from cache).
timeoutMs8000Per-diagnosis wall-clock timeout the coordinator enforces; <= 0 disables it.

A configured-but-keyless install is treated as disabled — nothing ever calls the API without credentials. Add ANTHROPIC_API_KEY to your .env and validate it in start/env.ts (ANTHROPIC_API_KEY: Env.schema.string.optional()).

The default model is claude-sonnet-4-6 (the best speed/intelligence balance for a short structured triage). Use claude-opus-4-8 for the most capable diagnosis, or claude-haiku-4-5-20251001 for the cheapest.

Use it

The provider binds a TelescopeAiDiagnoser into the container — always resolvable, even when disabled (its diagnose is then a no-op returning null), so you can inject it without a null check.

import { TelescopeAiDiagnoser } from '@adonis-agora/telescope/ai'
import { TelescopeService } from '@adonis-agora/telescope'

const telescope = await app.container.make(TelescopeService)
const diagnoser = await app.container.make(TelescopeAiDiagnoser)

const exception = await telescope.find(entryId)         // an `exception` entry
const related = await telescope.byTrace(exception.traceId ?? '')

const diagnosis = await diagnoser.diagnose(exception, { related })
// → { cause, fix, confidence: 'high' | 'medium' | 'low', model, cached } | null

diagnose(entry, options?):

  • options.related — other entries from the same trace (queries, the request, diagnostics). They're summarized into the prompt as extra context; exception entries are skipped, and each summary is clipped to 500 chars.
  • options.force — bypass the cache and force a fresh API call (still caches the result).

The returned Diagnosis carries cached: true when served from the cache.

How it works

  1. Cache check. If the entry's familyHash was already diagnosed (and force isn't set), the cached diagnosis is returned immediately — no API call.
  2. Prompt build. A system prompt frames Claude as a senior engineer triaging a production exception and pins the output to a strict JSON object (cause / fix / confidence). The user message carries the exception name + message, route, trace id, a stack clipped to the top 25 frames, and the related-entry summaries.
  3. Call + parse. It calls the Claude Messages API once, then defensively extracts the first balanced {...} (surviving markdown fences and stray prose) and parses it, tolerating missing fields.
  4. Cache write. The result is cached by family hash and returned.

Caching. The default cache is a bounded, TTL'd in-process LRU — up to 500 families, 24h TTL, oldest-evicted at the cap, expired-on-read. So re-diagnosing the same error never burns tokens. Swap in a Redis/DB-backed DiagnosisStore (via createDiagnoser) for cross-process sharing.

The diagnosis coordinator

TelescopeAiDiagnoser.diagnose is the low-level call. On top of it sits the DiagnosisCoordinator — the single seam the MCP diagnose_exception tool, the dashboard's Diagnose button and the alerter all call. Every one of them degrades gracefully when AI is not configured — the tool and the alert simply go out without a diagnosis, and the dashboard hides the button. It adds three properties on top of the raw diagnoser:

  • Optional — with AI unconfigured, isConfigured() is false and every call resolves to null rather than throwing, so a caller can ask unconditionally. The Anthropic SDK is never loaded on that path.
  • De-duped — concurrent diagnose calls for the same exception family share one in-flight promise, so an MCP request and an alert firing at once never trigger two model calls (and completed diagnoses are still cached by family hash).
  • Fail-safe — a per-call timeoutMs (default 8s) bounds the wait; any failure or timeout resolves to null and is logged — it never throws into the MCP transport or the alert flush.
import { DiagnosisCoordinator } from '@adonis-agora/telescope/ai'

// The coordinator projects the same diagnosis three ways for its callers:
const diagnosis = await coordinator.diagnose(exception)          // Diagnosis | null
const markdown  = await coordinator.diagnoseMarkdown(exception)  // for the MCP tool
const summary   = await coordinator.diagnoseSummary(exception)   // { cause, fix, confidence, model } | null, for alerts

diagnoseMarkdown renders a compact, agent-facing markdown block (heading, confidence/model line, cause, suggested fix) via the exported formatDiagnosisMarkdown; diagnoseSummary returns the compact object the alerter attaches to an alert payload.

Safe by construction

Diagnosis never throws into your code. Disabled or no client → null. A model call or a parse failure is logged and resolves to null rather than propagating. A missing family hash just skips the cache. So you can call diagnose straight from an error path without guarding it.

Privacy

The exception's name, message, stack (top 25 frames), route/trace id, and the JSON content of related trace entries are sent to the Anthropic API. Nothing else leaves the process, and nothing is sent at all when AI is disabled or no key is configured. Be mindful that recorded content (e.g. query bindings, request data) may contain sensitive values — shape what gets captured upstream if that's a concern.

Notable exports

  • TelescopeAiDiagnoser — the diagnoser; types TelescopeAiDiagnoserOptions, DiagnoseOptions, AnthropicMessagesClient.
  • createDiagnoser(config, options?) — build a diagnoser from resolved config (constructs the real Anthropic client; returns null when disabled).
  • DiagnosisCoordinator — the shared, de-duped, fail-safe coordinator wiring MCP + alerts; formatDiagnosisMarkdown, DEFAULT_TIMEOUT_MS; types DiagnoserLike, DiagnosisCoordinatorOptions, DiagnosisSummary.
  • DiagnosisCache, DEFAULT_DIAGNOSIS_CACHE_MAX, DEFAULT_DIAGNOSIS_TTL_MS; type DiagnosisStore.
  • parseDiagnosis, normalizeConfidence; types Diagnosis, DiagnosisConfidence.
  • buildUserPrompt, SYSTEM_PROMPT, STACK_FRAME_LIMIT; type RelatedEntrySummary.
  • defineConfig, resolveConfig, DEFAULT_MODEL, DEFAULT_MAX_TOKENS.

For a task-oriented walkthrough, see the AI exception diagnosis guide.

On this page