Skip to content

feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303

Open
MarioCadenas wants to merge 1 commit intoagent/v2/2-tool-primitivesfrom
agent/v2/3-plugin-infra
Open

feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303
MarioCadenas wants to merge 1 commit intoagent/v2/2-tool-primitivesfrom
agent/v2/3-plugin-infra

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Apr 21, 2026

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:

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

PR Stack

  1. Shared agent types + LLM adapters — feat(appkit): shared agent types and LLM adapter implementations #301
  2. Tool primitives + ToolProvider surfaces — feat(appkit): tool primitives and ToolProvider surfaces on core plugins #302
  3. Plugin infrastructure (attachContext + PluginContext) (this PR)
  4. agents() plugin + createAgent(def) + markdown-driven agents — feat(appkit): agents() plugin, createAgent(def), and markdown-driven agents #304
  5. fromPlugin() DX + runAgent plugins arg + toolkit-resolver — feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver #305
  6. Reference app + dev-playground + docs — feat(appkit): reference agent-app, dev-playground chat UI, docs, and template #306

This 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.

This was referenced Apr 21, 2026
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from b328cf2 to 5a7a4df Compare April 21, 2026 20:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from a5642df to e26795b Compare April 21, 2026 20:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 5a7a4df to a384b1e Compare April 22, 2026 08:45
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from e26795b to d73e138 Compare April 22, 2026 08:45
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from a384b1e to 68e05d3 Compare April 22, 2026 09:24
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from d73e138 to 26f43e5 Compare April 22, 2026 09:24
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 68e05d3 to b765708 Compare April 22, 2026 09:59
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from 26f43e5 to 2ffa31d Compare April 22, 2026 09:59
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from b765708 to 6712ce7 Compare April 22, 2026 10:21
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from 3a4deb7 to ca9cfca Compare April 22, 2026 10:49
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 6712ce7 to 7077eb0 Compare April 22, 2026 10:50
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 7077eb0 to b6ba122 Compare April 22, 2026 15:49
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from ca9cfca to c2a74ac Compare April 22, 2026 15:49
…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>
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from b6ba122 to bab4c0c Compare April 22, 2026 16:19
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from c2a74ac to 14ca97c Compare April 22, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant