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 event | Behaviour |
|---|---|
Register | Creates the engine, builds the API handler, registers the engine in the DI container via vessel.Provide, mounts all HTTP routes (unless disabled) |
Start | Runs store.Migrate(ctx) (unless disabled), then calls engine.Start(ctx) |
Stop | Calls engine.Stop(ctx) which emits OnShutdown to all plugins |
Health | Calls store.Ping(ctx) to verify database connectivity |
Extension options
| Option | Type | Default | Description |
|---|---|---|---|
WithStore(s) | store.Store | — | Required. Composite persistence store |
WithExtension(x) | plugin.Extension | — | Register a Cortex plugin (lifecycle hooks) |
WithEngineOption(o) | engine.Option | — | Pass any engine option through |
WithConfig(c) | extension.Config | — | Set the full extension config |
WithGroveDatabase(name) | string | "" | Name of the grove.DB to resolve from DI; empty uses the default DB |
WithDisableRoutes() | — | false | Skip HTTP route registration |
WithDisableMigrate() | — | false | Skip auto-migration on Start |
WithBasePath(p) | string | "" | URL prefix for all Cortex routes |
WithLogger(l) | *slog.Logger | slog.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: 4Config resolution order
- YAML file values are loaded first (from
extensions.cortexorcortexkey). - Programmatic options (
WithConfig,WithDisableRoutes, etc.) fill in any zero-valued fields. - 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 extensionThe 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:
- Explicit store -- if
WithStore(s)was called, it is used directly and grove is ignored. - Grove database -- if
WithGroveDatabase(name)was called, the named or defaultgrove.DBis resolved from DI. - 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