feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303
Open
MarioCadenas wants to merge 1 commit intoagent/v2/2-tool-primitivesfrom
Open
feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303MarioCadenas wants to merge 1 commit intoagent/v2/2-tool-primitivesfrom
MarioCadenas wants to merge 1 commit intoagent/v2/2-tool-primitivesfrom
Conversation
This was referenced Apr 21, 2026
b328cf2 to
5a7a4df
Compare
a5642df to
e26795b
Compare
5a7a4df to
a384b1e
Compare
e26795b to
d73e138
Compare
a384b1e to
68e05d3
Compare
d73e138 to
26f43e5
Compare
68e05d3 to
b765708
Compare
26f43e5 to
2ffa31d
Compare
b765708 to
6712ce7
Compare
3a4deb7 to
ca9cfca
Compare
6712ce7 to
7077eb0
Compare
7077eb0 to
b6ba122
Compare
ca9cfca to
c2a74ac
Compare
…nContext mediator
Third layer: the substrate every downstream PR relies on. No user-
facing API changes here; the surface for this PR is the mediator
pattern, lifecycle semantics, and factory stamping.
### Split Plugin construction from context binding
`Plugin` constructors become pure — no `CacheManager.getInstanceSync()`,
no `TelemetryManager.getProvider()`, no `PluginContext` wiring inside
`constructor()`. That work moves to a new lifecycle method:
```ts
interface BasePlugin {
attachContext?(deps: {
context?: unknown;
telemetryConfig?: TelemetryOptions;
}): void;
}
```
`createApp` calls `attachContext()` on every plugin after all
constructors have run, before `setup()`. This lets factories return
`PluginData` tuples at module scope without pulling core services into
the import graph — a prerequisite for later PRs that construct agent
definitions before `createApp`.
### PluginContext mediator
`packages/appkit/src/core/plugin-context.ts` — new class that mediates
all inter-plugin communication:
- **Route buffering**: `addRoute()` / `addMiddleware()` buffer until
the server plugin calls `registerAsRouteTarget()`, then flush via
`addExtension()`. Eliminates plugin-ordering fragility.
- **ToolProvider registry**: `registerToolProvider(name, plugin)` +
live `getToolProviders()`. Typed discovery of tool-exposing plugins.
- **User-scoped tool execution**: `executeTool(req, pluginName,
localName, args, signal?)` resolves the provider, wraps in
`asUser(req)` for OBO, opens a telemetry span, applies a 30s
timeout, dispatches, returns.
- **Lifecycle hooks**: `onLifecycle('setup:complete' | 'server:ready'
| 'shutdown', cb)` + `emitLifecycle(event)`. Callback errors don't
block siblings.
### `toPlugin` stamps `pluginName`
`packages/appkit/src/plugin/to-plugin.ts` — the factory now attaches a
read-only `pluginName` property to the returned function. Later PRs'
`fromPlugin(factory)` reads it to identify which plugin a factory
refers to without needing to construct an instance. `NamedPluginFactory`
type exported for consumers who want to type-constrain factories.
### Server plugin defers start to `setup:complete`
`ServerPlugin.setup()` no longer calls `extendRoutes()` synchronously.
It subscribes to the `setup:complete` lifecycle event via
`PluginContext` and starts the HTTP server there. This ensures that
any deferred-phase plugin (agents plugin in a later PR) has had a
chance to register routes via `PluginContext.addRoute()` before the
server binds. Removes the `plugins` field from `ServerConfig` (routes
are now discovered via the context, not a config snapshot).
### Test plan
- 25 new PluginContext tests (route buffering, tool provider registry,
executeTool paths, lifecycle hooks, plugin metadata)
- Updated AppKit lifecycle tests to inject `context` instead of
`plugins`
- Full appkit vitest suite: 1237 tests passing
- Typecheck clean across all 8 workspace projects
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
b6ba122 to
bab4c0c
Compare
c2a74ac to
14ca97c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third layer: the substrate every downstream PR relies on. No user-
facing API changes here; the surface for this PR is the mediator
pattern, lifecycle semantics, and factory stamping.
Split Plugin construction from context binding
Pluginconstructors become pure — noCacheManager.getInstanceSync(),no
TelemetryManager.getProvider(), noPluginContextwiring insideconstructor(). That work moves to a new lifecycle method:createAppcallsattachContext()on every plugin after allconstructors have run, before
setup(). This lets factories returnPluginDatatuples at module scope without pulling core services intothe import graph — a prerequisite for later PRs that construct agent
definitions before
createApp.PluginContext mediator
packages/appkit/src/core/plugin-context.ts— new class that mediatesall inter-plugin communication:
addRoute()/addMiddleware()buffer untilthe server plugin calls
registerAsRouteTarget(), then flush viaaddExtension(). Eliminates plugin-ordering fragility.registerToolProvider(name, plugin)+live
getToolProviders(). Typed discovery of tool-exposing plugins.executeTool(req, pluginName, localName, args, signal?)resolves the provider, wraps inasUser(req)for OBO, opens a telemetry span, applies a 30stimeout, dispatches, returns.
onLifecycle('setup:complete' | 'server:ready' | 'shutdown', cb)+emitLifecycle(event). Callback errors don'tblock siblings.
toPluginstampspluginNamepackages/appkit/src/plugin/to-plugin.ts— the factory now attaches aread-only
pluginNameproperty to the returned function. Later PRs'fromPlugin(factory)reads it to identify which plugin a factoryrefers to without needing to construct an instance.
NamedPluginFactorytype exported for consumers who want to type-constrain factories.
Server plugin defers start to
setup:completeServerPlugin.setup()no longer callsextendRoutes()synchronously.It subscribes to the
setup:completelifecycle event viaPluginContextand starts the HTTP server there. This ensures thatany deferred-phase plugin (agents plugin in a later PR) has had a
chance to register routes via
PluginContext.addRoute()before theserver binds. Removes the
pluginsfield fromServerConfig(routesare now discovered via the context, not a config snapshot).
Test plan
executeTool paths, lifecycle hooks, plugin metadata)
contextinstead ofpluginsSigned-off-by: MarioCadenas MarioCadenas@users.noreply.github.com
PR Stack
agents()plugin +createAgent(def)+ markdown-driven agents — feat(appkit): agents() plugin, createAgent(def), and markdown-driven agents #304fromPlugin()DX +runAgentplugins arg + toolkit-resolver — feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver #305This 6-PR stack is a redesign of the earlier 13-PR stack (#282-#286, #293-#300), reorganized to eliminate mid-stack API churn. Reviewers never see code in this stack that later PRs delete. The old 13 PRs have been closed with references back here.