Status: current core has selected replayable Location-scoped Catalog transforms, aligned with option B. Reload/watch behavior and deferred external plugin activation remain design work; the option comparison below is retained as historical context.
We need to choose where provider/model inputs live and how visible catalog state changes after boot. The designs below compare config, models.dev, auth, plugin activation/disablement, config edits, and policy changes under each option.
Config merges its ordered documents and then runs ordered, replayable plugin transforms. Each transform is a callback receiving Draft<Config.Info> and may mutate any config field.
type ConfigTransform = (config: Draft<Config.Info>) => void
const transform = yield * Config.transform()
yield *
transform((config) => {
config.providers ??= {}
config.providers.acme = {
/* ... */
}
config.model = "acme/code"
config.permissions = [
/* ... */
]
})
Because a transform can mutate any part of config, a transform change cannot safely trigger only Catalog.reload() or any other granular subset. Every service derived from config must reload in place from the newly transformed config.
const transform = yield* Config.transform()
yield* transform((draft) => mutateAnyConfigField(draft))
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Agent.reload()
→ MCP.reload()
→ other config-consuming services reload
Configured plugin installation/updates should not block location readiness. Build an initial snapshot from authored config and fast built-ins, then activate slow plugins in the background and coalesce their resulting reload requests.
LocationServiceMap.get(ref)
→ build location layer
→ Config.layer reads authored documents
→ merge authored documents
→ run currently active Config transforms
→ Policy.layer reads transformed Config
→ Catalog.layer reads transformed Config
→ materialize baseline provider/model catalog
→ PluginBoot baseline ready
→ Frontend.fetchCatalog()
PluginBoot background fiber
→ install/update plugin packages concurrently
→ activate completed plugins
→ Config.transform()
→ transform(updateConfig)
→ ReloadScheduler.request()
→ debounce short burst of completed activations
→ Reload.all()
→ Config.get()
→ run newly active Config transforms
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
The initial layer build is not a reload. Reload.all() only runs after the live location changes, such as a background plugin becoming active or a config source changing. Debouncing reduces repeated full-service reloads when multiple plugins complete near each other; each batch still reloads every config-consuming service because a config transform may mutate any field.
config file loaded
→ config source/watch trigger records new documents
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
timer fires
→ ModelsDevPlugin.refresh()
→ ModelsDev.get()
→ transform(applyModelsDevToConfig)
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
Catalog does not know about ModelsDev; the plugin transforms config before catalog reads it.
Account.switched(providerID)
→ AuthPlugin.refresh(providerID)
→ Account.active(providerID)
→ transform(applyAuthToConfig)
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
Plugin.activate("acme-models")
→ Config.transform()
→ transform(applyAcmeConfig)
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
Plugin.disable("company-naming")
→ close plugin scope
→ Config internally unregisters transform in finalizer
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ sonnet.name = "Sonnet"
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
file watcher sees edit
→ config source/watch trigger records updated documents
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
policy config changes
→ config source/watch trigger records updated documents
→ Reload.all()
→ Policy.reload()
→ Catalog.reload()
→ apply updated policy
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
Draft<Config.Info>, can inspect preceding config state, and can mutate arbitrary config fields through a replayable transform.Config owns merge/order semantics for fields visible to transforms.Catalog depends on provider/model config semantics and is part of that full service reload.Catalog.Event.Updated notification.Plugins register replayable catalog transforms. Each transform receives a Catalog.Editor whose helper methods mutate a private catalog draft; Catalog rematerializes visible records from its active transforms.
interface Catalog {
transform(): Effect.Effect<(update: (catalog: Catalog.Editor) => void) => Effect.Effect<void>, never, Scope.Scope>
}
const transform = yield* Catalog.transform()
yield* transform(update)
→ replace this transform callback
→ apply active transforms in registration order
→ apply policy
→ commit diff
→ Event.publish(Catalog.Event.Updated)
→ Frontend.refetchCatalog()
Configured plugin installation/updates should not block location readiness. Build an initial catalog from immediately available sources, then activate slow plugins in the background and coalesce refresh requests.
LocationServiceMap.get(ref)
→ build location layer
→ Catalog.layer creates empty catalog state
→ PluginBoot.layer activates immediately available plugins
→ ConfigProviderPlugin installs Catalog.transform()
→ ModelsDevPlugin installs Catalog.transform()
→ AuthPlugin installs Catalog.transform()
→ Catalog.layer applies active transforms during boot
→ apply policy
→ materialize baseline provider/model catalog
→ PluginBoot baseline ready
→ Frontend.fetchCatalog()
PluginBoot background fiber
→ install/update plugin packages concurrently
→ activate completed plugins
→ Catalog.transform()
→ transform(updateCatalog)
→ Catalog internally rebuilds
→ Catalog.Event.Updated
→ Frontend.refetchCatalog()
Each completed plugin activation rebuilds catalog when it calls its transform. Debouncing plugin completions would require adding an explicit batch/suspend-rebuild mechanism; it does not arise from the transform interface itself.
config file loaded
→ ConfigProviderAdapter.load()
→ transform(applyConfigToCatalog)
→ Catalog internally rebuilds
timer fires
→ ModelsDevPlugin.refresh()
→ ModelsDev.get()
→ transform(applyModelsDevToCatalog)
→ Catalog internally rebuilds
→ commit diff
Account.switched(providerID)
→ AuthPlugin.refresh()
→ transform(applyAuthToCatalog)
→ Catalog internally rebuilds
→ replay active transforms including current auth
→ apply policy
→ commit diff
Plugin.activate("acme-models")
→ Catalog.transform()
→ transform(applyAcmeToCatalog)
→ Catalog internally rebuilds
→ commit diff
Plugin.disable("company-naming")
→ close plugin scope
→ Catalog internally unregisters transform in finalizer
→ Catalog internally rebuilds
→ sonnet.name = "Sonnet"
→ commit diff
file watcher sees edit
→ ConfigProviderAdapter.load()
→ transform(applyUpdatedConfigToCatalog)
→ Catalog internally rebuilds
policy changes
→ Catalog rebuild trigger
→ replay all active transforms
→ apply updated policy last
→ commit diff
Catalog.Event.Updated notification.