docsBuild

Plugin SDK -- the pack extension contract

A pack extends memQL with product-specific behavior: Go integrations plus a .memql DSL bundle. The Go integrations compile into a node-type binary via build tags; the .memql domains load compiled-in or at runtime from disk (see Scope below). A product's DSL bundle plus its client is the reference consumer.

This page is the contract reference for the Go surface a pack targets -- the PluginContext it receives, the PluginFactory it implements, the registration primitives it calls, and the contract version the loader checks at startup. For an end-to-end "build your first pack" walkthrough, see Building a pack -- the developer guide, with the in-tree examples/referencepack reference pack as its worked example.

Scope. A pack has two halves that load differently. (a) The Go integration -- the IntegrationProvider, its PluginFactory, and the registration primitives -- is compiled in via build tags: linking its Go package into a binary runs its init() registrations. There is no runtime (non-compiled) loading of the Go half, by design. (b) The .memql DSL domains load either compiled-in (an embedded tree) or at runtime from disk via MEMQL_DSL_PATH, where a product-agnostic engine image mounts a product's DSL bundle at boot with RegisterTree(domain, os.DirFS(...)) and zero compiled-in product code. The contract below is the stable Go surface that half (a) targets.


Contract version

memql.PluginContractVersion (in component/memql/plugins.go) is the major version of the extension surface: the PluginContext fields, the PluginFactory signature, and the registration primitives.

  • Current: 1.
  • It bumps only on a breaking change to that surface -- a removed/renamed PluginContext field, a changed PluginFactory signature, a changed registration primitive.
  • Additive, backward-compatible changes do NOT bump it -- a new PluginContext field or a new optional primitive leaves existing packs compiling and loading unchanged.

A pack records the version it was built against when it registers (see below). At startup the loader (app.materializePlugins) calls PluginRegistration.ValidateContract for every registered pack and rejects -- fatal, with a descriptive error -- any pack whose declared version is incompatible with the core's PluginContractVersion. Compatibility is exact-major equality: a pack built against a retired older contract (the core may have removed surface it relies on) or one that needs a newer core than the running binary provides (the core lacks surface it relies on) both fail loudly rather than silently mis-binding.

memql.CheckPluginContractCompat(version int) error is the pure, unit-testable form of that check.


PluginFactory

go
type PluginFactory func(pctx PluginContext) (IntegrationProvider, error)

A pack's factory builds its IntegrationProvider (the DSL-callable capability set) from the live PluginContext.

  • Returning an error aborts startup (fatal log). Use this only for a true misconfiguration a degraded mode cannot paper over.
  • Returning (nil, nil) is the documented opt-out signal: the pack is compiled in but its dependencies are not satisfied in this environment (e.g. object storage with no bucket configured). The loader logs plug-in opted out and continues. The factory should log its own warning if the opt-out is worth reporting.

PluginContext -- the surface a pack receives

PluginContext is the narrow, stable Go surface handed to every factory. A pack either finds what it needs here (or on Engine) or it does not reach into app/ internals. Callbacks are lazily evaluated, so a pack that stashes the context still observes live state.

CapabilityTypeUse
Logger*slog.Loggerstructured logging
EngineIntegrationEngineAccessDSL execution, prompt render, tool dispatch, streaming provider lookups
BunDBfunc() *bun.DBpooled DB handle (bulk queries/mutations); nil on a DB-less binary
DirectBunDBfunc() *bun.DBdirect (non-pooled) handle for session-scoped work (advisory locks, leader election) -- never for bulk
VisionProviderfunc() common.VisionAIProviderdefault vision-capable AI provider, or nil
EmbeddingProviderByNamefunc(name string) (EmbeddingAIProvider, error)named embedding provider
ResolvePartitionFromContextfunc(ctx) stringactive partition (the canonical tenant scope) for a request; "default" if unset
ResolveVariablefunc(ctx, name) (string, error)partition-scoped plaintext variable, falling back to the global
ResolveSystemVariablefunc(ctx, name) (string, error)instance-wide plaintext variable
ResolveSecretfunc(ctx, name) (string, error)partition-scoped encrypted secret, falling back to the global
ResolveSystemSecretfunc(ctx, name) (string, error)instance-wide encrypted secret
Providers*ProviderRegistryAI provider registry (stable pointer)
Policies*PolicyRegistryAI Router policy registry (stable pointer)
Agents*AgentRegistryDSL-declared agent registry (stable pointer)

ResolvePartitionFromContext is the sanctioned way for a pack to scope work to a tenant: partition is the canonical tenant scope. The dedicated partition-scoping reference lands with issue 2.2.


Registration primitives

A pack wires itself in from init() functions in build-tag-gated .go files. The build tags decide which node-type binaries include the registration.

PrimitivePackageRegisters
RegisterPlugin(name, factory)component/memqla PluginFactory, stamped against the current contract version
RegisterPluginForContract(name, version, factory)component/memqla PluginFactory with an explicit declared contract version (third-party packs SHOULD use this)
RegisterTree(domain, fs.FS)dslan embedded .memql subtree, mounted under domain/ in the unified DSL tree
RegisterRoutingRule(rule)component/nodea cross-node event routing rule (required for any event that must cross a node boundary)
RegisterReadinessCheck(name, check)component/servera readiness probe contributing to /readyz
RegisterSeedDomain(reg)integrations/knowledgea product knowledge domain + optional seed corpus folded into the startup catalog seeder
RegisterAppProfile(profile)component/memqlthe product app profile injected on operator turns + its operator knowledge domains
RegisterChatReplyConcept(c)component/nodea product concept whose events ride the chat-reply delivery substrate (+ optional space-key payload field)
RegisterCapabilitySlug(slug, tools, tags...)component/memqla capability slug -> concrete tool-name bundle for agent tool expansion (tag operator drives the replier's operator fence)

Minimal shape:

go
//go:build mypack
 
package mypack
 
import (
"embed"
 
memqldsl "github.com/znasllc-io/memql/dsl"
"github.com/znasllc-io/memql/component/memql"
)
 
//go:embed all:*.memql all:prompts
var packFS embed.FS
 
func init() {
// Pin the contract version this pack was built against.
memql.RegisterPluginForContract("mypack", memql.PluginContractVersion, newProvider)
memqldsl.RegisterTree("mypack", packFS)
}
 
func newProvider(pctx memql.PluginContext) (memql.IntegrationProvider, error) {
// ... build the IntegrationProvider from pctx ...
}

Pack model + load-time validation

A pack is exactly these three registration primitives, called together from a pack's build-tag-gated init():

  1. RegisterPluginForContract(name, version, factory) -- the Go IntegrationProvider, contract-version-checked by the loader.
  2. RegisterTree(domain, fs.FS) -- the embedded .memql subtree, mounted under domain/ and namespace-validated.
  3. RegisterRoutingRule(rule) -- cross-node event routing (required for any event that must cross a node boundary).

memQL validates both halves at load time and fails loudly on a violation -- a broken pack aborts startup rather than silently mis-binding:

  • Contract version -- app.materializePlugins calls PluginRegistration.ValidateContract for every registered pack and rejects (fatal) any pack whose declared PluginContractVersion is incompatible with the core's (exact-major equality; see "Contract version" above).
  • Namespace ownership -- RegisterTree validates the pack's DSL domain via dsl.ValidatePackDomain(domain, coreDomains, existing) before mounting it. A domain must be non-empty, contain no /, and collide with neither a core embedded domain nor another pack's already-registered domain. A core domain is canonical and owned by memQL -- a pack cannot shadow or extend one. Two packs claiming the same namespace is ambiguous. Either collision panics at init() time (the only caller), consistent with RegisterTree's other input guards, so the conflict surfaces at startup with an actionable message.

dsl.ValidatePackDomain is the pure, unit-testable form of the namespace-ownership check (the analogue of CheckPluginContractCompat for the DSL tree). The core domain set is read from the embedded tree's top-level directories, so it stays in lockstep with the //go:embed directive.

Go plugins stay build-tag-linked. There is no runtime (non-compiled) loading of the Go half -- an IntegrationProvider registers from init() only when its package is linked in, and contract validation runs at startup against that compiled-in set. The DSL half is different: RegisterTree takes any fs.FS, so a product's .memql domains can be mounted at runtime from disk (os.DirFS under MEMQL_DSL_PATH) as well as embedded -- and the namespace-ownership validation below runs identically for either source.


Stability promise

Within a major PluginContractVersion, the surface above is append-only: fields and primitives are added, never removed or repurposed. A pack that compiles and loads against version N keeps compiling and loading against any later core that still reports version N. A breaking change bumps the version and is announced; stale packs then fail closed at startup with a clear, actionable error rather than mis-binding.