Skip to content

refactor(appkit): retire toPluginWithInstance; consolidate on fromPlugin + fix schema/routing bugs#300

Closed
MarioCadenas wants to merge 5 commits intoagent/5g-retire-deprecated-agentfrom
agent/5h-retire-toplugininstance
Closed

refactor(appkit): retire toPluginWithInstance; consolidate on fromPlugin + fix schema/routing bugs#300
MarioCadenas wants to merge 5 commits intoagent/5g-retire-deprecated-agentfrom
agent/5h-retire-toplugininstance

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Apr 21, 2026

Summary

Final cleanup layer. With fromPlugin as the canonical path for referencing plugin tools, there's no longer a reason for core plugin factories to eagerly construct an instance at module scope just to expose .toolkit() on the factory return value. This PR retires the eager pattern, consolidates the duplicated resolver logic, and bundles four runtime bug fixes surfaced during end-to-end testing of the full stack.

Retire toPluginWithInstance

  • Swap toPluginWithInstance(X, ["toolkit"] as const) to toPlugin(X) in analytics, files, genie, lakebase. .toolkit() remains as an instance method on each Plugin class — still called by the markdown loader, by AgentsPlugin.applyAutoInherit, and by the new shared resolver.
  • Delete toPluginWithInstance from packages/appkit/src/plugin/to-plugin.ts and its re-export. The pluginName stamp on toPlugin stays — that's what fromPlugin reads.
  • Revert PR refactor(appkit): forward eager plugin instance through preparePlugins #297's plumbing (nothing produces an instance field anymore): drop instance?: BasePlugin from OptionalConfigPluginDef, undo the preparePlugins forwarding and the pluginData.instance reuse branch in core/appkit.ts, delete the instance-reuse test.
  • Rewrite the describe("toolkit()") block in analytics.test.ts to construct AnalyticsPlugin directly.

Extract shared toolkit resolver

New packages/appkit/src/plugins/agents/toolkit-resolver.ts contains resolveToolkitFromProvider — the single source of truth for "turn a ToolProvider into a keyed ToolkitEntry record". Used by:

  1. AgentsPlugin.buildToolIndexfromPlugin marker resolution
  2. AgentsPlugin.applyAutoInherit — markdown auto-inherit path
  3. runAgent's standalone executor

Previously this logic was copy-pasted across all three sites.

Bug fixes (bundled into this PR)

These surfaced during end-to-end testing of dev-playground and are included here rather than as a separate PR because they touch the same files:

  • fix(appkit): strip $schema from Zod-generated tool parameter schemas — Zod v4's toJSONSchema() emits a top-level $schema key. Databricks Mosaic serving forwards tool schemas to Gemini's function_declarations format, which rejects unknown top-level keys. Every tool-using chat on a Gemini-backed endpoint was failing with a 400. New helper toToolJSONSchema() at packages/appkit/src/plugins/agents/tools/json-schema.ts; applied at the three materialization sites (tool(), toolsFromRegistry, buildToolkitEntries).
  • fix(dev-playground): repair broken YAML frontmatter in assistant.md — missing closing --- + ## default: true as a markdown heading → js-yaml parsed no keys → default: true never took effect. Default-agent routing was picked by insertion order rather than intent. Rewritten as valid YAML.
  • docs(appkit): regenerate typedoc API reference after stack cleanup — syncs docs/docs/api/appkit/ with the final surface (17 new pages for fromPlugin / runAgent / AgentDefinition / etc., 2 deleted for the retired shortcut API).
  • chore: sync generated files and refresh biome formatting — seeds the correct baseline for template/appkit.plugins.json, packages/appkit/src/registry/types.generated.ts, packages/shared/src/schemas/plugin-manifest.generated.ts, and biome drift across 4 files. Subsequent pnpm dev runs leave the tree clean.

Net change

~297 lines removed (net), one canonical factory abstraction (toPlugin only), one shared resolver, no deprecated surface left.

PR Stack

  1. Shared types + Adapters — feat(appkit): add shared agent types and LLM adapter implementations #282
  2. Tool types + MCP client — feat(appkit): add FunctionTool, HostedTool types and MCP client #283
  3. Agent plugin core + ToolProvider implementations — feat(appkit): add agent plugin core and ToolProvider implementations #284
  4. PluginContext mediator — feat(appkit): add PluginContext mediator for inter-plugin communication #285
  5. createAgent + agent-app + docs (v1) — feat(appkit): add createAgent wrapper, agent-app, and API docs #286
  6. Plugin context-binding separation — refactor(appkit): separate plugin context binding from plugin construction #293
  7. agents() plugin + createAgent(def) + .toolkit()feat(appkit): add agents() plugin, createAgent() factory, and .toolkit() #294
  8. agent-app + docs migrated to agents()feat(appkit): migrate agent-app and docs to the new agents() plugin #295
  9. Relocate shared agent utilities — refactor(appkit): relocate shared agent utilities into plugins/agents #296
  10. preparePlugins forwards eager instance — refactor(appkit): forward eager plugin instance through preparePlugins #297
  11. fromPlugin() API — feat(appkit): add fromPlugin() for referencing plugin tools in code-defined agents #298
  12. Retire deprecated agent() + createAgentAppchore(appkit): remove deprecated agent() plugin and createAgentApp shortcut #299
  13. Retire toPluginWithInstance + bug fixes (this PR)

Test plan

  • Full vitest suite passing — 1,287 tests
  • Typecheck clean across all 8 workspace projects
  • Build clean (build:package + docs:build)
  • End-to-end: dev-playground agent chat streams responses without 400 errors for both the markdown autocomplete agent and the code-defined helper agent; agent-app support agent routes analytics + files tool calls correctly

With `fromPlugin` landed, there is no longer a reason for core plugin
factories to eagerly construct an instance at module scope just to
expose `.toolkit()` on the factory return value. `fromPlugin(factory)`
reads `factory.pluginName` synchronously and defers resolution to
`AgentsPlugin.setup()`, which calls `.toolkit()` as an instance method
on the real plugin.

Changes:

- Swap `toPluginWithInstance(X, ["toolkit"] as const)` to `toPlugin(X)`
  in analytics, files, genie, and lakebase. `.toolkit()` remains as an
  instance method on each Plugin class — it's still called by the
  markdown loader, by `AgentsPlugin.applyAutoInherit`, and by the new
  `resolveToolkitFromProvider` helper.
- Delete `toPluginWithInstance` from
  `packages/appkit/src/plugin/to-plugin.ts` and its re-export from the
  `plugin/` barrel. The `pluginName` stamp on `toPlugin` stays — that's
  what `fromPlugin` reads.
- Revert the PR 5e plumbing it enabled: drop `instance?: BasePlugin`
  from `OptionalConfigPluginDef` in `packages/shared/src/plugin.ts`,
  undo the `preparePlugins` forwarding and the `pluginData.instance`
  reuse branch in `packages/appkit/src/core/appkit.ts`, and delete the
  instance-reuse test. Nothing produces an `instance` field anymore.
- Extract the duplicated provider-resolution logic (previously
  copy-pasted between `AgentsPlugin` and `run-agent`) into a single
  shared helper at
  `packages/appkit/src/plugins/agents/toolkit-resolver.ts`. Both the
  `fromPlugin` marker resolution, the markdown auto-inherit path, and
  the standalone `runAgent` executor now go through it.
- Rewrite the `describe("toolkit()")` block in analytics.test.ts to
  construct `AnalyticsPlugin` directly instead of reaching for the
  factory return value; the behavior being tested lives on the class.
- Docs: drop the "Using `.toolkit()` directly (advanced)" subsection
  from `agents.md` and the "before / after" duplication from the
  migration guide. `fromPlugin` is now the only documented path.

No public-API regression for the new shape: every in-tree migration
already went through `fromPlugin` in PR 5f. Users who were still
spreading `.toolkit()` at module scope (not possible in-tree, but the
escape hatch was documented) need to switch to `fromPlugin(factory,
opts)` — same options, one less object-lifetime concern.

Net: ~370 lines removed, one canonical factory abstraction, one shared
resolver.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Regenerates the generated typedoc markdown for `docs/docs/api/appkit/` so
it matches the final state of the agent stack:

- New pages for the surfaces added in PRs 5b–5h:
  `Function.fromPlugin`, `Function.isFromPluginMarker`,
  `Function.isToolkitEntry`, `Function.loadAgentFromFile`,
  `Function.loadAgentsFromDir`, `Function.runAgent`,
  `Interface.AgentDefinition`, `Interface.AgentsPluginConfig`,
  `Interface.FromPluginMarker`, `Interface.PromptContext`,
  `Interface.RunAgentInput`, `Interface.RunAgentResult`,
  `Interface.ToolkitEntry`, `Interface.ToolkitOptions`,
  `TypeAlias.AgentTools`, `TypeAlias.BaseSystemPromptOption`,
  `Variable.agents`.
- Removed pages for the retired shortcut API:
  `Interface.AgentHandle`, `Interface.CreateAgentConfig`.
- Refreshed sidebar, index, and a handful of pages whose rendered
  signatures changed as fields were added/removed upstream.

These are derived artifacts — they don't carry narrative-appropriate
home branches, so they land here at the tip, in sync with the final
code state.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Zod v4's `toJSONSchema()` emits a top-level `$schema` annotation (e.g.
`"https://json-schema.org/draft/2020-12/schema"`) on every schema it
produces. When an agent runs against a Databricks Model Serving endpoint
backed by Google Gemini (e.g. `databricks-gemini-3-1-flash-lite`),
Mosaic forwards tool schemas as Gemini `function_declarations[i].parameters`.
That format accepts only a restricted JSON Schema subset and rejects any
unrecognized top-level key, including `$schema`, with a 400:

```
Databricks API error (400): Invalid JSON payload received. Unknown name
"$schema" at 'tools[0].function_declarations[0].parameters': Cannot find field.
```

Every chat turn that exposed tools failed with 9x (one per tool) field
violations. The bug existed in all three call sites that materialize tool
parameters from Zod:

- `tool()` — the user-facing inline tool factory
- `defineTool()` registry + `toolsFromRegistry()` — used by every plugin's
  `getAgentTools()` (analytics, files, genie, lakebase)
- `buildToolkitEntries()` — the internal `.toolkit()` path used by
  markdown frontmatter resolution and auto-inherit

Adds a small shared helper `toToolJSONSchema(schema)` at
`packages/appkit/src/plugins/agents/tools/json-schema.ts` that wraps
`toJSONSchema()` and strips the top-level `$schema` key. Swaps all three
call sites to use it. The generated `parameters` record is otherwise
unchanged (`type`, `properties`, `required`, `additionalProperties` all
pass through), so no downstream semantics shift.

Confirmed via runtime instrumentation that every outbound tool after the
fix has `paramKeys: ["type", "properties", ...]` with
`hasDollarSchema: false`, and that the autocomplete agent (which gets
all 9 auto-inherited plugin tools under its markdown defaults) now
streams completions without a 400.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The file was committed with a malformed frontmatter block: the opening
`---` was followed by `## endpoint: databricks-claude-sonnet-4-5` (a
Markdown heading, not a YAML key) and the closing `---` was missing
entirely, so `js-yaml` parsed no keys at all.

Effect: the `assistant` agent still registered (via the body-only path
and the plugin's adapter fallback), but `default: true` never took
effect. `fileDefault` resolved to `null`, and the default agent was
picked by insertion order instead of intent. Chat requests without an
explicit `agent:` field landed wherever that fallback happened to
point, producing confusing "which agent am I actually talking to"
behavior.

Rewrites the frontmatter as valid YAML with a closing `---`. Confirmed
via runtime instrumentation that `loadAgents` now reports
`fileDefault: "assistant"` and default-agent routing is stable.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The working tree was persistently dirty across the stack because four
independent generators / formatters kept rewriting committed content on
every `pnpm dev`:

- `template/appkit.plugins.json` is regenerated by
  `pnpm sync:template`, which scans `packages/appkit/src/plugins/*/manifest.json`
  for the canonical plugin name. The agents plugin's manifest declares
  `"name": "agent"` (singular, so routes mount at `/api/agent/*` to match
  the client's expectations), so the template's key should be `"agent"`.
  An earlier commit in the stack had it as `"agents"`, which `sync:template`
  corrected on every run.
- `packages/appkit/src/registry/types.generated.ts` is regenerated by
  `tools/generate-registry-types.ts` (fires via `appkit#build:package` →
  `build:watch`). The committed version had stale formatting.
- `packages/shared/src/schemas/plugin-manifest.generated.ts` is
  regenerated by `tools/generate-schema-types.ts` (same mechanism on
  the shared package). Same stale-formatting situation.
- `run-agent.ts`, `agents.ts`, `from-plugin.ts`, `run-agent.test.ts`
  — biome format drift. A `pnpm format` run touched committed whitespace
  / line-wrap that no longer matched current rules.

Committing all of these so the working tree settles. No semantic
changes. Subsequent `pnpm dev` runs should leave the tree clean.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
This was referenced Apr 21, 2026
@MarioCadenas
Copy link
Copy Markdown
Collaborator Author

Superseded by the v2 6-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) — feat(appkit): plugin infrastructure — attachContext + PluginContext mediator #303
  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

The v2 stack reorganizes the same work so no PR ships API that a later PR deletes. Start at #301 for the new entry point. Branches from this older stack are preserved unchanged.

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