Skip to content

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

Merged
MarioCadenas merged 11 commits into
mainfrom
agent/v2/3-plugin-infra
May 7, 2026
Merged

feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303
MarioCadenas merged 11 commits into
mainfrom
agent/v2/3-plugin-infra

Conversation

@MarioCadenas

@MarioCadenasMarioCadenas commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

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:

interfaceBasePlugin{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): tools(plugins) 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

Demo

agent-demo.mp4

This was referenced Apr 21, 2026
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from b328cf2 to 5a7a4dfCompareApril 21, 2026 20:41
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from a5642df to e26795bCompareApril 21, 2026 20:41
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from 5a7a4df to a384b1eCompareApril 22, 2026 08:45
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from e26795b to d73e138CompareApril 22, 2026 08:45
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from a384b1e to 68e05d3CompareApril 22, 2026 09:24
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from d73e138 to 26f43e5CompareApril 22, 2026 09:24
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from 68e05d3 to b765708CompareApril 22, 2026 09:59
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from 26f43e5 to 2ffa31dCompareApril 22, 2026 09:59
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from b765708 to 6712ce7CompareApril 22, 2026 10:21
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from 3a4deb7 to ca9cfcaCompareApril 22, 2026 10:49
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from 6712ce7 to 7077eb0CompareApril 22, 2026 10:50
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from aa95c27 to 71a986dCompareMay 4, 2026 13:15
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from a70bbcc to 3343203CompareMay 4, 2026 16:36
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from 71a986d to 82d8ea0CompareMay 4, 2026 16:36
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from 3343203 to 85663cfCompareMay 4, 2026 17:18
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from a8418ea to c9c986dCompareMay 4, 2026 17:32
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from a63b7a4 to 47c1c68CompareMay 5, 2026 10:09
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from c9c986d to 5fde8aaCompareMay 5, 2026 10:09
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from 47c1c68 to 6f83621CompareMay 5, 2026 10:28
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from 4a388b1 to a7ebc57CompareMay 5, 2026 10:40

@pkosiecpkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but please verify the agentic review findings before merge 👍

Comment threadpackages/appkit/src/core/plugin-context.ts
Introduces the tool-authoring primitives that peer plugins use to expose
their capabilities as agent tools, and updates analytics, files, genie,
and lakebase to implement the ToolProvider interface.
Tool helpers land in core/agent/ (not plugins/agents/) from day one so
peer plugins can depend on them without reaching across the sibling
boundary:
core/agent/types.ts — ToolkitEntry, AgentDefinition shape
core/agent/build-toolkit.ts — converts ToolRegistry → ToolkitEntry map
core/agent/tools/
define-tool.ts — defineTool() + ToolRegistry
function-tool.ts — FunctionTool interface + helpers
hosted-tools.ts — HostedTool / mcpServer() types
sql-policy.ts — assertReadOnlySql guard
tool.ts — tool() Zod-schema factory
json-schema.ts — Zod → JSON Schema converter
index.ts — public barrel
MCP client (AppKitMcpClient) and host-policy live in
plugins/agents/tools/ at this stage; a later commit promotes them to
connectors/mcp/ once the connector layer exists.
Add a file-level rationale (policy/auth, narrow scope, zero extra deps) and
point the class JSDoc at it to avoid duplicating the same story in two places.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Single pass over volumes: connectors, toolkit tools, and policy warnings.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
… ./plugins barrel
- Keep v2/1 beta.ts comment block; retain Databricks + tool-primitive exports
- Restore JobsConnectorConfig, ga-exports.generated, and jobs plugin types on index
- Remove broken export from ./plugins (no plugins/index.ts on this branch)
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
- organizeImports in core tools barrel, analytics, files, genie, lakebase, mcp-client
- drop stale noExplicitAny biome-ignore (rule is off; suppressions flagged)
- remove unused DownloadResponse import; use vi.mocked + cast in lakebase test
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Vitest mockReturnValueOnce is checked against pg.Pool; connect must return
Promise<PoolClient>. Use a stub client cast to PoolClient for the failure case.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Expose defineTool, MCP client, toolkit helpers alongside existing beta
tool exports so Knip recognizes core/agent/tools/index as used entry surface.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from dca3d96 to a2c725eCompareMay 6, 2026 18:33
The acknowledgement field added no real defense beyond the implicit
opt-in (exposeAsAgentTool is itself an explicit, undefined-by-default
field), and created asymmetry with sibling SP-bound SQL surfaces
(analytics, genie). It would also drift once OBO lands.
Real protections - read-only SQL classifier, BEGIN READ ONLY/ROLLBACK
transaction wrapping, destructive-call HITL approval gate, and the
startup warn log - are unchanged.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenasforce-pushed the agent/v2/2-tool-primitives branch from a2c725e to 8b67c5aCompareMay 6, 2026 18:35
…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.
`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`.
`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.
`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.
`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).
- 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
MarioCadenasforce-pushed the agent/v2/3-plugin-infra branch from a7ebc57 to 91e66e1CompareMay 6, 2026 18:46
Base automatically changed from agent/v2/2-tool-primitives to mainMay 7, 2026 08:17
PR #303 agentic review applied (P1 + cheap P2 + P3 cleanup):
- Snapshot lifecycle hooks before iteration so a callback that registers
another hook for the same event does not re-enter the loop.
- Add attachContext to EXCLUDED_FROM_PROXY so asUser() never proxies
internal binding lifecycle into user context.
- Use SpanStatusCode.OK / .ERROR instead of magic numbers; the previous
code: 0 was UNSET (no-op for setStatus), so the success path was
silently unreported in OTel traces.
- Return getPlugins() as ReadonlyMap to prevent external mutation of
the live plugin registry.
- Strengthen isToolProvider to also require asUser, narrow to a
ToolProviderPlugin shape, and drop the (entry.plugin as any).asUser
cast in executeTool.
- Guard double registerAsRouteTarget with logger.warn + ignore.
- Guard duplicate registerToolProvider name with logger.warn.
- Drop the ToolProviderEntry indirection; store ToolProviderPlugin
directly keyed by name.
Tests cover Set-mutation safety, double registerAsRouteTarget, duplicate
tool-provider, the asUser requirement on isToolProvider, and the
SpanStatusCode assertions on success and failure paths.
Also adds plugin/to-plugin.ts to the knip ignore list. NamedPluginFactory
is consumed only by downstream branches (fromPlugin) and was being flagged
as unused on this branch in isolation.
Findings #8 (configurable executeTool timeout), #9 (double context
injection), and #10 (BasePluginConfig context cast) are advisory and
deferred to a follow-up.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…text
Typedoc reflects the new attachContext lifecycle method and the
PluginContext-typed context field added in 91e66e1.
Fixes the docs:build sync gate failing on agent/v2/3-plugin-infra CI.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas merged commit 9d2920c into mainMay 7, 2026
9 checks passed
@MarioCadenas
MarioCadenas deleted the agent/v2/3-plugin-infra branch May 7, 2026 09:43
pkosiec pushed a commit that referenced this pull request May 21, 2026
…ediator (#303)
* feat(appkit): tool primitives and ToolProvider surfaces on core plugins
Introduces the tool-authoring primitives that peer plugins use to expose
their capabilities as agent tools, and updates analytics, files, genie,
and lakebase to implement the ToolProvider interface.
Tool helpers land in core/agent/ (not plugins/agents/) from day one so
peer plugins can depend on them without reaching across the sibling
boundary:
core/agent/types.ts — ToolkitEntry, AgentDefinition shape
core/agent/build-toolkit.ts — converts ToolRegistry → ToolkitEntry map
core/agent/tools/
define-tool.ts — defineTool() + ToolRegistry
function-tool.ts — FunctionTool interface + helpers
hosted-tools.ts — HostedTool / mcpServer() types
sql-policy.ts — assertReadOnlySql guard
tool.ts — tool() Zod-schema factory
json-schema.ts — Zod → JSON Schema converter
index.ts — public barrel
MCP client (AppKitMcpClient) and host-policy live in
plugins/agents/tools/ at this stage; a later commit promotes them to
connectors/mcp/ once the connector layer exists.
* docs(appkit): explain hand-rolled AppKitMcpClient vs official MCP SDK
Add a file-level rationale (policy/auth, narrow scope, zero extra deps) and
point the class JSDoc at it to avoid duplicating the same story in two places.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* refactor(appkit): merge FilesPlugin ctor volume loops
Single pass over volumes: connectors, toolkit tools, and policy warnings.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* fix(appkit): restore beta header, index GA/jobs exports; drop invalid ./plugins barrel
- Keep v2/1 beta.ts comment block; retain Databricks + tool-primitive exports
- Restore JobsConnectorConfig, ga-exports.generated, and jobs plugin types on index
- Remove broken export from ./plugins (no plugins/index.ts on this branch)
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* chore(appkit): satisfy Biome lint on tool-primitive wiring
- organizeImports in core tools barrel, analytics, files, genie, lakebase, mcp-client
- drop stale noExplicitAny biome-ignore (rule is off; suppressions flagged)
- remove unused DownloadResponse import; use vi.mocked + cast in lakebase test
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* fix(appkit): type lakebase agent-tool test pool mock as PoolClient
Vitest mockReturnValueOnce is checked against pg.Pool; connect must return
Promise<PoolClient>. Use a stub client cast to PoolClient for the failure case.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* fix(appkit): re-export tool barrel from beta for knip parity
Expose defineTool, MCP client, toolkit helpers alongside existing beta
tool exports so Knip recognizes core/agent/tools/index as used entry surface.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* feat(appkit): drop Lakebase iUnderstandRunsAsServicePrincipal flag
The acknowledgement field added no real defense beyond the implicit
opt-in (exposeAsAgentTool is itself an explicit, undefined-by-default
field), and created asymmetry with sibling SP-bound SQL surfaces
(analytics, genie). It would also drift once OBO lands.
Real protections - read-only SQL classifier, BEGIN READ ONLY/ROLLBACK
transaction wrapping, destructive-call HITL approval gate, and the
startup warn log - are unchanged.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* feat(appkit): plugin infrastructure — attachContext lifecycle + PluginContext 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.
`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`.
`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.
`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.
`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).
- 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>
* fix(appkit): apply review feedback to PluginContext and plugin proxy
PR #303 agentic review applied (P1 + cheap P2 + P3 cleanup):
- Snapshot lifecycle hooks before iteration so a callback that registers
another hook for the same event does not re-enter the loop.
- Add attachContext to EXCLUDED_FROM_PROXY so asUser() never proxies
internal binding lifecycle into user context.
- Use SpanStatusCode.OK / .ERROR instead of magic numbers; the previous
code: 0 was UNSET (no-op for setStatus), so the success path was
silently unreported in OTel traces.
- Return getPlugins() as ReadonlyMap to prevent external mutation of
the live plugin registry.
- Strengthen isToolProvider to also require asUser, narrow to a
ToolProviderPlugin shape, and drop the (entry.plugin as any).asUser
cast in executeTool.
- Guard double registerAsRouteTarget with logger.warn + ignore.
- Guard duplicate registerToolProvider name with logger.warn.
- Drop the ToolProviderEntry indirection; store ToolProviderPlugin
directly keyed by name.
Tests cover Set-mutation safety, double registerAsRouteTarget, duplicate
tool-provider, the asUser requirement on isToolProvider, and the
SpanStatusCode assertions on success and failure paths.
Also adds plugin/to-plugin.ts to the knip ignore list. NamedPluginFactory
is consumed only by downstream branches (fromPlugin) and was being flagged
as unused on this branch in isolation.
Findings #8 (configurable executeTool timeout), #9 (double context
injection), and #10 (BasePluginConfig context cast) are advisory and
deferred to a follow-up.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
* docs(appkit): regenerate Plugin API reference for attachContext + context
Typedoc reflects the new attachContext lifecycle method and the
PluginContext-typed context field added in 91e66e1.
Fixes the docs:build sync gate failing on agent/v2/3-plugin-infra CI.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
---------
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Sign up for freeto 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.

2 participants

@MarioCadenas@pkosiec