Korvus Overview
- cmd/korvus/main.go
- internal/agent/agent.go
- internal/session/manager.go
- internal/transport/transport.go
- internal/store/store.go
- internal/incident/manager.go
Relevant source files · 32
- internal/blob/blob.go 1–30
- internal/workspace/client.go 1–20, 28–50
- internal/agent/prompt.go 17–40
- internal/agent/author.go 13–30
- cmd/korvus/main.go 4–33, 111–118, 120–133, 135–150 +1 more
- internal/telemetry/telemetry.go 1–15, 38–46
- internal/skill/catalog.go 14–43
- internal/store/store.go 1–33, 87–100
- internal/store/postgres.go 14–40
- internal/store/vector.go 1–35
- internal/store/migrate.go 15–40
- internal/session/manager.go 59–100, 342–430
- internal/session/scratch.go 28–40
- internal/session/reopen.go 14–38
- internal/agent/agent.go 34–66, 253–290
- internal/agent/broadcast.go 10–60
- internal/model/provider.go 1–20
- internal/model/errors.go 14–55
- internal/model/embedder.go 6–27
- internal/mcp/client.go 3–8
- internal/mcp/registry.go 13–23
- internal/workspace/sync.go 25–48
- internal/workspace/materialize.go 22–46
- internal/docs/chunk.go 1–12
- internal/docs/index.go 20–42
- internal/skill/skill.go 11–24
- internal/incident/manager.go 40–67, 90–144
- internal/incident/ingest.go 20–60
- internal/transport/transport.go 1–9, 128–190
- internal/telemetry/genai.go 1–35
- demo/checkout/internal/shop/shop.go 1–15
- korvus.example.yaml 1–30
Korvus is an autonomous incident root-cause-analysis agent for Kubernetes-hosted systems. It receives alerts (or is handed an incident by an engineer), opens a session, drives a bounded ReAct loop against real observability data — logs, metrics, traces — and always lands a written RCA report. The whole thing is one Go binary that speaks a Connect API, exposes a health endpoint, and receives Alertmanager webhooks on a single port.
The project's shape is dictated by a handful of rules that show up in almost every package comment. PostgreSQL is the source of truth for incident, session and report state; object storage only ever holds artifact bytes and never their keys or metadata blob.go. The agent may only touch the outside world through MCP tools — never through os/exec, never through direct file reads workspace/client.go. The system prompt is static and assembled once per run, and sessions are stateless with respect to the model, which keeps provider prompt caches warm prompt.go. Everything the agent produces lands on disk first and is read back before it is believed author.go.
How it works
Startup, in dependency order
The process is wired in a single function, and the order is explicitly load-bearing rather than stylistic main.go. Configuration loads first so a bad file fails before any socket is opened or any migration runs; logging follows so every later step reports through the same handler; telemetry is installed before anything instrumented runs, because the agent and the transport's otelconnect interceptor take their tracer from the global provider at construction time telemetry.go. The database pool is opened and proved reachable, then migrations run under a PostgreSQL advisory lock before the store that reads the schema exists main.go.
The embedder is validated against the live database before anything can embed, so a vector-width mismatch fails at startup rather than as an opaque pgvector insert error during the first workspace sync main.go. MCP discovery is best-effort: an unreachable server degrades the skill catalog and is reported through SystemService, but it never fails startup main.go. The skill catalog is loaded next, because skill.Load filters on the set of servers that actually connected catalog.go. Then come the agent, the session manager that drives it, the alert ingestor that starts sessions through it, and finally the transport, which reports on all of the above.
Shutdown runs in reverse, with one deliberate exception: the signal itself stops the alert ingestor and cancels running sessions before the HTTP drain, because a live WatchSession ends only with its session and draining first would wait out the full shutdown timeout main.go. The CLI, flags and process lifecycle are covered in detail in korvus CLI and Process Wiring; the configuration surface, its ${VAR} substitution and its validation rules live in Configuration Loading and Validation.
The domain model
The persistence layer defines the vocabulary every other package speaks: an Incident is a long-lived context space, a Session is one self-contained multi-turn investigation inside it, and a Report is what a session always ends by producing store.go. Everything a session produces or is handed is an Artifact, which makes inherited context one uniform, enumerable thing store.go. The domain types, their status and origin enumerations, and the deliberate nullability rules are set out in Storage Domain Model and Store Interfaces.
The concrete implementation is a pgx/v5 store over a caller-owned pool, with vector encoding written by hand rather than pulling in a pgvector dependency postgres.go vector.go. Schema evolution is handled by embedded goose migrations serialised across replicas with an advisory lock, which needs two pool connections — a constraint the config loader validates migrate.go. Migrations, the vector column and the top-k search path are described in PostgreSQL Persistence, Migrations and Vector Search.
Sessions
A session is created asynchronously: Create returns as soon as the row exists so the caller gets an id it can immediately watch over the streaming RPC, and the actual investigation happens on a background goroutine manager.go. The manager owns the lifecycle rules, the domain errors, and the one-shot guard that refuses new work once shutdown has begun manager.go. The full contract — creation, cancellation, settlement, exit reasons and the shutdown ordering — is in Session Manager and Lifecycle.
What happens after the run returns is split across three concerns. Each session gets a scratch directory seeded from the workspace repository at its pinned commit, and the persist phase harvests that directory into blob storage and writes the report and artifact rows scratch.go. A finished session can be reopened: the next chat iteration reads everything it needs from the store, so a session reopens identically after a restart and in any replica reopen.go. Both halves are described in Session Persistence, Reopen and Scratch Seeding.
The agent loop
The agent is a bounded ReAct loop. The safety rails are compile-time constants rather than configuration knobs — a two-tier iteration ceiling (twenty model calls in one autonomous stretch, sixty across a session), a ten-minute burst timeout, a forty-five-minute session wall clock, and caps on tool calls, tool result size and repeated identical calls agent.go. Run investigates one incident and always lands on a report, whatever the exit agent.go. The loop mechanics — model calls, tool dispatch, retries and failure classification — are in Agent Run Loop, Tool Execution and Provider Calls.
Above the loop sit three more phases. The system prompt is assembled from the skill catalog and never changes mid-session prompt.go; the authoring phase makes the model write the human-readable RCA document into the workspace, which Korvus then reads back off disk before trusting it; and finalization extracts the structured report JSON. Those phases are in Agent Prompts, Report Authoring and Finalization. The JSON contract the model is asked for — summary, key signals, blast radius, ordered hypotheses, next steps, audit summary — is defined in Incident Report Schema.
Per-session state, resume and the live event feed are separate concerns again: the broadcaster is the in-process pub/sub between the agent and WatchSession, publishing a turn only after its database write succeeds, and never blocking the loop on a slow watcher broadcast.go. Session state, resume and event broadcasting are in Agent Session State, Resume and Event Broadcasting.
Models, tools and context
The agent talks to upstream models through a provider-neutral Provider interface; every wire-format quirk lives inside the per-provider adapters, and every failure is wrapped in a classified ProviderError so the loop can decide whether to retry provider.go errors.go. Embeddings are deliberately a separate interface, because Anthropic has no embeddings endpoint and one deployment is expected to run Anthropic for completion and an OpenAI-compatible endpoint for retrieval embedder.go. All of this is covered in Model Providers, Embeddings and Error Kinds.
External I/O goes through MCP. MCPClient is one connection to one server; Registry aggregates the tools of all connected servers, routes calls to the server that owns them, and refuses a duplicate tool name because tool names are the model-facing namespace and must be unambiguous client.go registry.go. The client, registry and connection handling are in MCP Client, Registry and Server Connection. The same MCPClient interface is also satisfied in-process by the workspace sandbox, which serves file tools rooted at a session's scratch directory — path scoping is structural because the root is a field no tool argument can influence workspace/client.go. The sandbox's built-in tools and its read-only prefixes are in Workspace Sandbox Client and Built-in Tools.
Context comes from two more places. The workspace repository is fetched, checked out and materialised into each session's scratch directory, with sessions stamped with the exact 40-character commit SHA so a past session is reproducible sync.go materialize.go — see Workspace Sync, Checkout and Materialization. The repository's documentation is chunked, embedded and served to the agent as a docs_search tool, indexed on sync rather than per session chunk.go index.go; that pipeline is in Documentation Chunking, Indexing and Search. Skills are markdown files with YAML frontmatter, filtered at load time by which MCP servers are actually connected skill.go — see Skill Catalog and Frontmatter Parsing.
Incidents and alerts
An incident is opened manually by an engineer or automatically by an alert group. The manual path is the incident.Manager: it owns the rules that need to read before they write, and it structurally cannot give a manually opened incident a deduplication fingerprint, because the open request type has no field for one manager.go manager.go. Open, list, update and close are in Incident Manager: Open, List, Update, Close. The alert path is separate: the ingestor fingerprints the alert group, debounces it, dedupes against open incidents and starts the first session through the session manager ingest.go — see Alert Ingestion, Fingerprinting and Debounce.
The API surface
Everything is served from one net/http mux behind h2c, so a single port speaks gRPC, gRPC-Web and the Connect protocol without a proxy or a second server transport.go. NewMux mounts one Connect handler per service — Incident, Session, System — plus /healthz and, when configured, the Alertmanager webhook path; every subsystem field is optional, and a nil one leaves that service unimplemented rather than serving a handler that would nil-panic transport.go. The service handlers themselves are in Connect API Server and Service Handlers.
Reads, watch streams, the webhook and the conversion between store rows and protobuf messages are in Watch Streams, Read APIs, Webhooks and Proto Conversion. The wire contract is generated: the protobuf definitions and their Connect stubs live in Generated Korvus v1 Protobuf and Connect API, and the browser-facing types in Generated TypeScript Client Types.
Observability and storage
The agent is instrumented throughout, and telemetry.Setup is where the SDK lights up; spans are best-effort by design, and the audit counters persisted in the report keep the report self-contained regardless telemetry.go. The GenAI semantic attributes are pinned to the last semconv version that still generates gen_ai.*, with a test asserting the literal wire names so a semconv bump fails the build rather than silently emitting an attribute no backend recognises genai.go — see OpenTelemetry Setup and GenAI Semantic Attributes. Artifact bytes live behind a three-method storage seam with a filesystem implementation, deliberately without listing, copying or metadata operations so the bucket never becomes a second source of truth blob.go; the backend and its key rules are in Blob Storage Backend and Key Rules.
The ecosystem around it
Korvus ships three MCP servers that expose the observability backends an investigation actually queries — Bundled MCP Servers Overview, Loki MCP Server, Prometheus MCP Server and Tempo MCP Server. Alongside them is a demo checkout service with an injectable fault — a bad release that lowers the database pool size and produces a latency and error-rate regression — so the whole pipeline can be exercised end to end shop.go; see Checkout Demo Service and Telemetry. Finally, the deployment stack and the annotated example configuration are in Deployment Stack and Example Configuration, with the example file itself documenting every key and its default korvus.example.yaml.
Relationships
flowchart TD
alertmanager["Alertmanager webhook"] --> ingestor["internal/incident/ingest.go"]
engineer["Engineer"] --> incmanager["internal/incident/manager.go"]
ingestor --> incmanager
ingestor --> manager["internal/session/manager.go"]
manager --> agent["internal/agent/agent.go"]
agent --> model["internal/model/provider.go"]
agent --> mcp["internal/mcp/registry.go"]
agent --> store["internal/store/store.go"]
manager --> store
manager --> blob["internal/blob/blob.go"]
manager --> scratch["internal/session/scratch.go"]
scratch --> workspace["internal/workspace/materialize.go"]
workspace --> docs["internal/docs/index.go"]
mcp --> loki["mcp-servers/loki"]
mcp --> prom["mcp-servers/prometheus"]
mcp --> tempo["mcp-servers/tempo"]
mcp --> wsclient["internal/workspace/client.go"]
broadcaster["internal/agent/broadcast.go"] --> transport["internal/transport/transport.go"]
manager --> broadcaster
agent --> broadcaster
transport --> store
transport --> manager
telemetry["internal/telemetry/telemetry.go"] --> agent
telemetry --> transport
main["cmd/korvus/main.go"] --> manager
main --> agent
main --> transport
main --> ingestorReference
| Symbol | Kind | What it does | Location |
|---|---|---|---|
run |
function | Owns the whole process lifecycle: config, telemetry, pool, migrations, discovery, subsystems, HTTP server, ordered shutdown | cmd/korvus/main.go |
setupLogging |
function | Installs the process-wide JSON slog handler |
cmd/korvus/main.go |
openPool |
function | Builds the pgx pool and proves the database is reachable before startup continues | cmd/korvus/main.go |
Agent / New |
struct / function | The bounded ReAct investigation loop; safe for concurrent Run calls |
internal/agent/agent.go |
Agent.Run |
method | Investigates one incident and always lands on a report | internal/agent/agent.go |
Agent.runBurst |
method | One autonomous stretch of the loop: model calls, tool dispatch, guard rails | internal/agent/agent.go |
ToolExecutor |
interface | The agent's only external I/O surface, satisfied by *mcp.Registry |
internal/agent/agent.go |
TurnBroadcaster |
struct | In-process pub/sub feeding the live WatchSession stream |
internal/agent/broadcast.go |
Manager / NewManager |
struct / function | Owns session creation, cancellation, settlement and shutdown | internal/session/manager.go |
Manager.Create |
method | Records a session and starts its run in the background | internal/session/manager.go |
Manager.Reopen |
method | Continues a finished session with the next chat iteration | internal/session/reopen.go |
Runner |
interface | The manager's view of the agent, satisfied by *agent.Agent |
internal/session/manager.go |
Provider |
interface | One request/response completion against an upstream model | internal/model/provider.go |
Embedder |
interface | Text to vectors, separate from Provider by design |
internal/model/embedder.go |
ProviderError / ErrorKind |
struct / type | Classified provider failures the loop switches on for retry policy | internal/model/errors.go |
Registry |
struct | Aggregates MCP tools and routes calls to the owning server | internal/mcp/registry.go |
MCPClient |
interface | One connection to one MCP server | internal/mcp/client.go |
Options / NewMux |
struct / function | Builds the single mux: Connect handlers, /healthz, webhook |
internal/transport/transport.go |
Server.Run |
method | Serves until the context is cancelled, then drains in-flight requests | internal/transport/transport.go |
sessionService |
struct | Store-backed SessionService RPCs, including WatchSession |
internal/transport/watch.go |
Store |
interface | The persistence contract shared across Korvus | internal/store/store.go |
PostgresStore |
struct | The pgx/v5 implementation of Store over a caller-owned pool |
internal/store/postgres.go |
Migrate |
function | Applies embedded goose migrations under an advisory lock | internal/store/migrate.go |
Manager / NewManager |
struct / function | The manual incident lifecycle: open, list, update, close | internal/incident/manager.go |
Manager.Open |
method | Records a manually-opened incident with no fingerprint | internal/incident/manager.go |
Ingestor |
struct | Alert ingestion with fingerprinting, debounce and dedup | internal/incident/ingest.go |
blob.Store |
interface | Three-method storage seam for artifact bytes | internal/blob/blob.go |
workspace.Syncer |
struct | Git fetch/checkout of the workspace repository at a pinned commit | internal/workspace/sync.go |
workspace.Client |
struct | In-process MCP client serving file tools over a scratch directory | internal/workspace/client.go |
docs.Indexer |
struct | Keeps docs_chunks in step with the served workspace revision |
internal/docs/index.go |
skill.Load |
function | Loads and filters the skill catalog by connected MCP servers | internal/skill/catalog.go |
report.ReportData |
struct | The JSON shape the model's finalization step must produce | internal/report/schema.go |
telemetry.Setup |
function | Installs the global tracer provider and returns a flush function | internal/telemetry/telemetry.go |
Generated by astrolabe from commit 0c2dca73 on branch main.