fix: 修正 logo 中 N 和 G 字母造型
N 添加对角线笔画(█▄ █),G 添加内横杠(█ ▀█), 避免与 O 字母造型雷同。同步更新 ui.ts 中的硬编码 wordmark。
This commit is contained in:
324
specs/v2/catalog-config-plugin-lifecycle.md
Normal file
324
specs/v2/catalog-config-plugin-lifecycle.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Catalog / Config / Plugin Lifecycle Options
|
||||
|
||||
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.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- Initial load: a location opens, built-in/configured plugins activate, and the first visible catalog is constructed.
|
||||
- Config: authored provider/model definitions and overrides.
|
||||
- models.dev: remote provider/model data refreshed on a timer.
|
||||
- Auth: active credentials enable/configure providers and can later disappear.
|
||||
- Plugin activation: a plugin starts contributing while the location is open.
|
||||
- Plugin disablement: a plugin stops contributing and its influence must disappear.
|
||||
- Config edit: authored configuration changes while the location is open.
|
||||
- Policy: allowed/denied provider selection changes after providers exist.
|
||||
|
||||
## A. Config Transforms, Service Reload
|
||||
|
||||
`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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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
|
||||
```
|
||||
|
||||
### Initial Load
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
config file loaded
|
||||
→ config source/watch trigger records new documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### models.dev
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
### Auth
|
||||
|
||||
```ts
|
||||
Account.switched(providerID)
|
||||
→ AuthPlugin.refresh(providerID)
|
||||
→ Account.active(providerID)
|
||||
→ transform(applyAuthToConfig)
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Plugin Activation
|
||||
|
||||
```ts
|
||||
Plugin.activate("acme-models")
|
||||
→ Config.transform()
|
||||
→ transform(applyAcmeConfig)
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Plugin Disablement
|
||||
|
||||
```ts
|
||||
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()
|
||||
```
|
||||
|
||||
### Config Edit
|
||||
|
||||
```ts
|
||||
file watcher sees edit
|
||||
→ config source/watch trigger records updated documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Policy
|
||||
|
||||
```ts
|
||||
policy config changes
|
||||
→ config source/watch trigger records updated documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ apply updated policy
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- A plugin receives `Draft<Config.Info>`, can inspect preceding config state, and can mutate arbitrary config fields through a replayable transform.
|
||||
- Plugin disablement removes its config transform and lets services rematerialize without manual undo.
|
||||
- models.dev and auth become config transforms rather than catalog dependencies.
|
||||
- `Config` owns merge/order semantics for fields visible to transforms.
|
||||
- Granular service reload is not safe because a config transform can mutate anything; every config-consuming service reloads after any transform change.
|
||||
- `Catalog` depends on provider/model config semantics and is part of that full service reload.
|
||||
- One reload produces at most one `Catalog.Event.Updated` notification.
|
||||
- Deferred plugin activation avoids blocking readiness, but plugin completions may cause repeated full-service reload batches during startup.
|
||||
|
||||
## B. Catalog Transforms
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
interface Catalog {
|
||||
transform(): Effect.Effect<(update: (catalog: Catalog.Editor) => void) => Effect.Effect<void>, never, Scope.Scope>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
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()
|
||||
```
|
||||
|
||||
### Initial Load
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
config file loaded
|
||||
→ ConfigProviderAdapter.load()
|
||||
→ transform(applyConfigToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
```
|
||||
|
||||
### models.dev
|
||||
|
||||
```ts
|
||||
timer fires
|
||||
→ ModelsDevPlugin.refresh()
|
||||
→ ModelsDev.get()
|
||||
→ transform(applyModelsDevToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Auth
|
||||
|
||||
```ts
|
||||
Account.switched(providerID)
|
||||
→ AuthPlugin.refresh()
|
||||
→ transform(applyAuthToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ replay active transforms including current auth
|
||||
→ apply policy
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Plugin Activation
|
||||
|
||||
```ts
|
||||
Plugin.activate("acme-models")
|
||||
→ Catalog.transform()
|
||||
→ transform(applyAcmeToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Plugin Disablement
|
||||
|
||||
```ts
|
||||
Plugin.disable("company-naming")
|
||||
→ close plugin scope
|
||||
→ Catalog internally unregisters transform in finalizer
|
||||
→ Catalog internally rebuilds
|
||||
→ sonnet.name = "Sonnet"
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Config Edit
|
||||
|
||||
```ts
|
||||
file watcher sees edit
|
||||
→ ConfigProviderAdapter.load()
|
||||
→ transform(applyUpdatedConfigToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
```
|
||||
|
||||
### Policy
|
||||
|
||||
```ts
|
||||
policy changes
|
||||
→ Catalog rebuild trigger
|
||||
→ replay all active transforms
|
||||
→ apply updated policy last
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- Disablement, source refresh, and policy re-evaluation are transform replay operations.
|
||||
- Auth does not need to be represented as config.
|
||||
- Config remains one catalog source rather than a catalog dependency.
|
||||
- The API shape matches A, but the mutable draft is catalog state instead of configuration state.
|
||||
- Catalog needs transform ordering and internal rebuild behavior in addition to reads.
|
||||
- Recompute ordering, serialization, and diff events must be specified.
|
||||
- One internal rebuild produces at most one `Catalog.Event.Updated` notification.
|
||||
- Deferred plugin activation avoids blocking readiness and only rebuilds catalog for catalog transform changes.
|
||||
- Debouncing those rebuilds needs an additional batching interface or an activation coordinator that installs multiple transforms before exposing updates.
|
||||
Reference in New Issue
Block a user