Cortex

Forge Extension

Mount Cortex into a Forge application as a first-class extension with automatic route registration, migrations, and DI.

Cortex ships a ready-made Forge extension in the extension package. It wires the engine, HTTP API, and lifecycle management into Forge's extension system with one call.

Installation

import "github.com/xraph/cortex/extension"

Registering the extension

package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/cortex/extension"
    pgstore "github.com/xraph/cortex/store/postgres"
)

func main() {
    app := forge.New()

    cortexExt := extension.New(
        extension.WithStore(pgstore.New(bunDB)),
    )

    app.RegisterExtension(cortexExt)
    app.Run()
}

What the extension does

Lifecycle eventBehaviour
RegisterCreates the engine, builds the API handler, registers the engine in the DI container via vessel.Provide, mounts all HTTP routes (unless disabled)
StartRuns store.Migrate(ctx) (unless disabled), then calls engine.Start(ctx)
StopCalls engine.Stop(ctx) which emits OnShutdown to all plugins
HealthCalls store.Ping(ctx) to verify database connectivity

Extension options

OptionTypeDefaultDescription
WithStore(s)store.StoreRequired. Composite persistence store
WithExtension(x)plugin.ExtensionRegister a Cortex plugin (lifecycle hooks)
WithEngineOption(o)engine.OptionPass any engine option through
WithConfig(c)extension.ConfigSet the full extension config
WithGroveDatabase(name)string""Name of the grove.DB to resolve from DI; empty uses the default DB
WithDisableRoutes()falseSkip HTTP route registration
WithDisableMigrate()falseSkip auto-migration on Start
WithBasePath(p)string""URL prefix for all Cortex routes
WithLogger(l)*slog.Loggerslog.Default()Structured logger

Extension config struct

type Config struct {
    DisableRoutes        bool          // Skip HTTP route registration
    DisableMigrate       bool          // Skip auto-migration on start
    BasePath             string        // URL prefix for all cortex routes
    DefaultModel         string        // LLM model (default: "smart")
    DefaultMaxSteps      int           // Max reasoning steps per run (default: 25)
    DefaultMaxTokens     int           // Max output tokens per LLM call (default: 4096)
    DefaultTemperature   float64       // LLM sampling temperature (default: 0.7)
    DefaultReasoningLoop string        // Reasoning loop strategy (default: "react")
    ShutdownTimeout      time.Duration // Graceful shutdown timeout (default: 30s)
    RunConcurrency       int           // Max concurrent runs (default: 4)
    GroveDatabase        string        // Name of the grove.DB to resolve from DI
}

YAML configuration

Cortex can be configured via YAML files under the extensions.cortex or cortex key. YAML configuration is merged with programmatic options (YAML takes precedence for most fields).

# config.yaml
extensions:
  cortex:
    disable_routes: false
    disable_migrate: false
    base_path: "/cortex"
    default_model: "gpt-4o"
    default_max_steps: 50
    default_max_tokens: 8192
    default_temperature: 0.7
    default_reasoning_loop: "react"
    shutdown_timeout: "30s"
    run_concurrency: 8
    grove_database: "cortex"

Alternatively, the legacy top-level key is supported:

# config.yaml
cortex:
  grove_database: "default"
  default_model: "claude-3-opus"
  run_concurrency: 4

Config resolution order

  1. YAML file values are loaded first (from extensions.cortex or cortex key).
  2. Programmatic options (WithConfig, WithDisableRoutes, etc.) fill in any zero-valued fields.
  3. Any remaining zero-valued fields are filled with defaults from DefaultConfig().

Requiring configuration

If you want Cortex to fail when no YAML config is found:

cortexExt := extension.New(
    extension.WithRequireConfig(true),
)

Disabling auto-migration

If you manage migrations separately (e.g. with a migration tool), disable auto-migration:

cortexExt := extension.New(
    extension.WithStore(store),
    extension.WithDisableMigrate(),
)

Disabling routes

If you only need the engine (e.g. using Cortex programmatically from another extension), disable route registration:

cortexExt := extension.New(
    extension.WithStore(store),
    extension.WithDisableRoutes(),
)

Accessing the engine from other extensions

After Register is called by Forge, cortexExt.Engine() returns the fully initialised engine:

eng := cortexExt.Engine()
// Use eng.CreateAgent, eng.ListSkills, etc. from another extension

The engine is also available via Forge's DI container (powered by vessel):

import "github.com/xraph/cortex/engine"

// Inside another extension's Register:
var eng *engine.Engine
if err := vessel.Resolve(fapp.Container(), &eng); err != nil {
    return err
}

Tenant middleware

In a Forge app, tenant scope is typically set by middleware that reads the JWT or API key:

func tenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := r.Header.Get("X-Tenant-ID")
        appID := r.Header.Get("X-App-ID")
        ctx := cortex.WithTenant(r.Context(), tenantID)
        ctx = cortex.WithApp(ctx, appID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

router.Use(tenantMiddleware)

Adding plugins

Register Cortex plugins (observability, audit hooks, etc.) alongside the extension:

import (
    "github.com/xraph/cortex/extension"
    "github.com/xraph/cortex/observability"
    audithook "github.com/xraph/cortex/audit_hook"
)

metricsPlugin := observability.NewMetricsExtension()
auditPlugin := audithook.New(myRecorder)

cortexExt := extension.New(
    extension.WithStore(store),
    extension.WithExtension(metricsPlugin),
    extension.WithExtension(auditPlugin),
)

Health checks

The extension implements forge.Extension's Health method by pinging the store:

func (e *Extension) Health(ctx context.Context) error {
    return e.eng.Store().Ping(ctx)
}

Forge surfaces this automatically in its health endpoint.

Grove database integration

When your Forge app uses the Grove extension to manage database connections, Cortex can automatically resolve a grove.DB from the DI container and construct the correct store backend based on the driver type.

Using the default grove database

If the Grove extension registers a single database (or a default in multi-DB mode), use WithGroveDatabase with an empty name:

ext := extension.New(
    extension.WithGroveDatabase(""),
)

Using a named grove database

In multi-database setups, reference a specific database by name:

ext := extension.New(
    extension.WithGroveDatabase("cortex"),
)

This resolves the grove.DB named "cortex" from the DI container and auto-constructs the matching store. The driver type is detected automatically -- you do not need to import individual store packages.

Store resolution order

The extension resolves its store in this order:

  1. Explicit store -- if WithStore(s) was called, it is used directly and grove is ignored.
  2. Grove database -- if WithGroveDatabase(name) was called, the named or default grove.DB is resolved from DI.
  3. In-memory fallback -- if neither is configured, an in-memory store is used.

Complete example

package main

import (
    "log"

    "github.com/xraph/forge"
    "github.com/xraph/grove"
    "github.com/xraph/grove/drivers/pgdriver"
    "github.com/xraph/cortex/extension"
    "github.com/xraph/cortex/observability"
    pgstore "github.com/xraph/cortex/store/postgres"
)

func main() {
    app := forge.New()

    db := grove.Open(pgdriver.New(
        pgdriver.WithDSN("postgres://user:pass@localhost:5432/cortex?sslmode=disable"),
    ))
    store := pgstore.New(db)

    cortexExt := extension.New(
        extension.WithStore(store),
        extension.WithExtension(observability.NewMetricsExtension()),
    )

    app.RegisterExtension(cortexExt)
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

This gives you a production-ready Cortex deployment with:

  • All 36 REST endpoints mounted
  • Auto-migration on startup
  • Health checks via store.Ping
  • Prometheus-compatible metrics
  • Graceful shutdown with plugin notification

On this page