Skip to content

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

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

feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303
MarioCadenas merged 11 commits intomainfrom
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): 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 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/3-plugin-infra branch 2 times, most recently from aa95c27 to 71a986d Compare May 4, 2026 13:15
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from a70bbcc to 3343203 Compare May 4, 2026 16:36
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from 71a986d to 82d8ea0 Compare May 4, 2026 16:36
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 3343203 to 85663cf Compare May 4, 2026 17:18
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from a8418ea to c9c986d Compare May 4, 2026 17:32
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from a63b7a4 to 47c1c68 Compare May 5, 2026 10:09
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from c9c986d to 5fde8aa Compare May 5, 2026 10:09
@MarioCadenas MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from 47c1c68 to 6f83621 Compare May 5, 2026 10:28
@MarioCadenas MarioCadenas force-pushed the agent/v2/3-plugin-infra branch 2 times, most recently from 4a388b1 to a7ebc57 Compare May 5, 2026 10:40
Copy link
Copy Markdown
Member

@pkosiec pkosiec left a comment

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 thread packages/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 MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from dca3d96 to a2c725e Compare May 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 MarioCadenas force-pushed the agent/v2/2-tool-primitives branch from a2c725e to 8b67c5a Compare May 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 MarioCadenas force-pushed the agent/v2/3-plugin-infra branch from a7ebc57 to 91e66e1 Compare May 6, 2026 18:46
Base automatically changed from agent/v2/2-tool-primitives to main May 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 main May 7, 2026
9 checks passed
@MarioCadenas MarioCadenas deleted the agent/v2/3-plugin-infra branch May 7, 2026 09:43
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.

2 participants