Skip to content

feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver#305

Open
MarioCadenas wants to merge 9 commits intoagent/v2/4-agents-pluginfrom
agent/v2/5-fromplugin-runagent
Open

feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver#305
MarioCadenas wants to merge 9 commits intoagent/v2/4-agents-pluginfrom
agent/v2/5-fromplugin-runagent

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Apr 21, 2026

DX centerpiece. Introduces the symbol-marker pattern that collapses
plugin tool references in code-defined agents from a three-touch dance
to a single line, and extracts the shared resolver that the agents
plugin, auto-inherit, and standalone runAgent all now go through.

fromPlugin(factory, opts?) — the marker

packages/appkit/src/plugins/agents/from-plugin.ts. Returns a spread-
friendly { [Symbol()]: FromPluginMarker } record. The symbol key is
freshly generated per call, so multiple spreads of the same plugin
coexist safely. The marker's brand is a globally-interned
Symbol.for("@databricks/appkit.fromPluginMarker") — stable across
module boundaries.

resolveToolkitFromProvider(pluginName, provider, opts?)

packages/appkit/src/plugins/agents/toolkit-resolver.ts. Single source
of truth for "turn a ToolProvider into a keyed record of ToolkitEntry
markers". Prefers provider.toolkit(opts) when available (core plugins
implement it), falls back to walking getAgentTools() and synthesizing
namespaced keys (${pluginName}.${localName}) for third-party
providers, honoring only / except / rename / prefix the same
way.

Used by three call sites, previously all copy-pasted:

  1. AgentsPlugin.buildToolIndex — fromPlugin marker resolution pass
  2. AgentsPlugin.applyAutoInherit — markdown auto-inherit path
  3. runAgent — standalone-mode plugin tool dispatch

AgentsPlugin.buildToolIndex — symbol-key resolution pass

Before the existing string-key iteration, buildToolIndex now walks
Object.getOwnPropertySymbols(def.tools). For each FromPluginMarker,
it looks up the plugin by name in PluginContext.getToolProviders(),
calls resolveToolkitFromProvider, and merges the resulting entries
into the per-agent index. Missing plugins throw at setup time with a
clear Available: ... listing — wiring errors surface on boot, not
mid-request.

hasExplicitTools now counts symbol keys too, so a
tools: { ...fromPlugin(x) } record correctly disables auto-inherit
on code-defined agents.

Type plumbing

  • AgentTools type: { [key: string]: AgentTool } & { [key: symbol]: FromPluginMarker }. Preserves string-key autocomplete while
    accepting marker spreads under strict TS.
  • AgentDefinition.tools switched to AgentTools.

runAgent gains plugins?: PluginData[]

packages/appkit/src/core/run-agent.ts. When an agent def contains
fromPlugin markers, the caller passes plugins via
RunAgentInput.plugins. A local provider cache constructs each plugin
and dispatches tool calls via provider.executeAgentTool(). Runs as
service principal (no OBO — there's no HTTP request). If a def
contains markers but plugins is absent, throws with guidance.

Exports

fromPlugin, FromPluginMarker, isFromPluginMarker, AgentTools
added to the main barrel.

Test plan

  • 14 new tests: marker shape, symbol uniqueness, type guard,
    factory-without-pluginName error, fromPlugin marker resolution in
    AgentsPlugin, fallback to getAgentTools for providers without
    .toolkit(), symbol-only tools disables auto-inherit, runAgent
    standalone marker resolution via plugins arg, guidance error when
    missing.
  • Full appkit vitest suite: 1311 tests passing.
  • Typecheck clean.

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) — 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 (this PR)
  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/4-agents-plugin branch from 3c7c35e to cb7fe2b Compare April 21, 2026 20:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 162e970 to 29e3534 Compare April 21, 2026 20:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from cb7fe2b to 0afea5e Compare April 22, 2026 08:45
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 29e3534 to b462716 Compare April 22, 2026 08:45
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from 0afea5e to 983461c Compare April 22, 2026 09:24
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch 2 times, most recently from 539487e to dac73b5 Compare April 22, 2026 09:46
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch 2 times, most recently from a7b0444 to 623792d Compare April 22, 2026 09:59
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch 2 times, most recently from 624f2a0 to 0dd07a4 Compare April 22, 2026 10:21
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from 623792d to 2f752a0 Compare April 22, 2026 10:21
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 8ace826 to f41317f Compare May 4, 2026 09:22
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch 2 times, most recently from af9b6ee to e4b1322 Compare May 4, 2026 09:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from f41317f to d42173c Compare May 4, 2026 09:41
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from caa6286 to c2b0f28 Compare May 4, 2026 11:19
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 41fa8b0 to 8b0c28e Compare May 4, 2026 11:19
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from c2b0f28 to fd73087 Compare May 4, 2026 12:59
@MarioCadenas MarioCadenas requested a review from a team as a code owner May 4, 2026 12:59
@MarioCadenas MarioCadenas requested review from calvarjorge and removed request for a team May 4, 2026 12:59
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch 2 times, most recently from 22393bb to fdfd568 Compare May 4, 2026 13:12
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch 2 times, most recently from 269d1a9 to c038a77 Compare May 4, 2026 13:15
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from fdfd568 to 65178cf Compare May 4, 2026 13:15
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from c038a77 to ab5b485 Compare May 4, 2026 16:36
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 65178cf to 03da825 Compare May 4, 2026 16:36
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from ab5b485 to 6378638 Compare May 4, 2026 17:19
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from 03da825 to c16fa29 Compare May 4, 2026 17:19
@MarioCadenas MarioCadenas force-pushed the agent/v2/4-agents-plugin branch from 6378638 to 2640ea4 Compare May 4, 2026 17:32
@MarioCadenas MarioCadenas force-pushed the agent/v2/5-fromplugin-runagent branch from c16fa29 to a8cedbd Compare May 4, 2026 17:32
…esolver

DX centerpiece. Introduces the symbol-marker pattern that collapses
plugin tool references in code-defined agents from a three-touch dance
to a single line, and extracts the shared resolver that the agents
plugin, auto-inherit, and standalone runAgent all now go through.

`packages/appkit/src/plugins/agents/from-plugin.ts`. Returns a spread-
friendly `{ [Symbol()]: FromPluginMarker }` record. The symbol key is
freshly generated per call, so multiple spreads of the same plugin
coexist safely. The marker's brand is a globally-interned
`Symbol.for("@databricks/appkit.fromPluginMarker")` — stable across
module boundaries.

`packages/appkit/src/plugins/agents/toolkit-resolver.ts`. Single source
of truth for "turn a ToolProvider into a keyed record of `ToolkitEntry`
markers". Prefers `provider.toolkit(opts)` when available (core plugins
implement it), falls back to walking `getAgentTools()` and synthesizing
namespaced keys (`${pluginName}.${localName}`) for third-party
providers, honoring `only` / `except` / `rename` / `prefix` the same
way.

Used by three call sites, previously all copy-pasted:
1. `AgentsPlugin.buildToolIndex` — fromPlugin marker resolution pass
2. `AgentsPlugin.applyAutoInherit` — markdown auto-inherit path
3. `runAgent` — standalone-mode plugin tool dispatch

Before the existing string-key iteration, `buildToolIndex` now walks
`Object.getOwnPropertySymbols(def.tools)`. For each `FromPluginMarker`,
it looks up the plugin by name in `PluginContext.getToolProviders()`,
calls `resolveToolkitFromProvider`, and merges the resulting entries
into the per-agent index. Missing plugins throw at setup time with a
clear `Available: ...` listing — wiring errors surface on boot, not
mid-request.

`hasExplicitTools` now counts symbol keys too, so a
`tools: { ...fromPlugin(x) }` record correctly disables auto-inherit
on code-defined agents.

- `AgentTools` type: `{ [key: string]: AgentTool } & { [key: symbol]:
  FromPluginMarker }`. Preserves string-key autocomplete while
  accepting marker spreads under strict TS.
- `AgentDefinition.tools` switched to `AgentTools`.

`packages/appkit/src/core/run-agent.ts`. When an agent def contains
`fromPlugin` markers, the caller passes plugins via
`RunAgentInput.plugins`. A local provider cache constructs each plugin
and dispatches tool calls via `provider.executeAgentTool()`. Runs as
service principal (no OBO — there's no HTTP request). If a def
contains markers but `plugins` is absent, throws with guidance.

`fromPlugin`, `FromPluginMarker`, `isFromPluginMarker`, `AgentTools`
added to the main barrel.

- 14 new tests: marker shape, symbol uniqueness, type guard,
  factory-without-pluginName error, fromPlugin marker resolution in
  AgentsPlugin, fallback to getAgentTools for providers without
  .toolkit(), symbol-only tools disables auto-inherit, runAgent
  standalone marker resolution via `plugins` arg, guidance error when
  missing.
- Full appkit vitest suite: 1311 tests passing.
- Typecheck clean.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
runAgent()'s adapter-consumption loop is now the same consumeAdapterStream
helper introduced in the agents-plugin layer. One loop covers all three
execution paths: HTTP streaming (_streamAgent), sub-agents (runSubAgent),
and standalone runAgent. The message_delta + message accumulation rule
(with its LangChain on_chain_end quirk) lives in exactly one place.
…on A rewrite

normalize-result, consume-adapter-stream, tool-dispatch were extracted to
core/agent/ but agents.ts still imported them from plugins/agents/. Update
the import paths to match the final file locations.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Flips the layering: agent types, helpers, and the standalone runner now
live in core/agent/ instead of plugins/agents/. The HTTP-facing agents()
plugin still owns its routes/streaming/threads but no longer re-exports
framework primitives that peer plugins depend on.

Moved (with git mv to preserve history):
- plugins/agents/{types,from-plugin,build-toolkit,toolkit-resolver,
  consume-adapter-stream,normalize-result,tool-dispatch,system-prompt,
  load-agents}.ts -> core/agent/
- plugins/agents/tools/{tool,define-tool,function-tool,hosted-tools,
  sql-policy,json-schema,index}.ts -> core/agent/tools/
- core/{run-agent,create-agent-def}.ts -> core/agent/{run-agent,create-agent}.ts
- 14 corresponding test files -> core/agent/tests/

Stayed in plugins/agents/ (HTTP/route concerns):
- agents.ts, event-channel.ts, event-translator.ts, tool-approval-gate.ts,
  thread-store.ts, schemas.ts, defaults.ts, manifest.json, index.ts

Updated imports across analytics, files, genie, lakebase to source from
core/agent/ directly. plugins/agents/index.ts stays as a back-compat
barrel that re-exports the moved primitives, so the public package
surface (@databricks/appkit) is byte-identical.

Verified: tsc --noEmit clean, 1581/1581 appkit tests pass.
Collapses the two parallel agent loops (`_streamAgent` in the plugin and
`runAgent` in core) onto a single AgentRunner that drives the adapter to
completion and surfaces events. Tool dispatch policy moves behind a
ToolExecutor strategy injected by the caller.

New:
- core/agent/runner.ts (AgentRunner + ToolExecutor interface, ~65 lines)
- core/agent/standalone-tool-executor.ts (in-process dispatch, ~78 lines)
- plugins/agents/http-tool-executor.ts (HTTP-path executor: budget +
  approval gate + OBO dispatch + sub-agent recursion, ~243 lines)
- plugins/agents/tests/http-tool-executor.test.ts (8 focused tests
  including sub-agent approval forwarding — was effectively untestable
  pre-refactor because the logic lived inside a private nested closure)

Refactored:
- core/agent/run-agent.ts: 348 -> 296 lines; the ~120-line executeTool
  closure is now a StandaloneToolExecutor + AgentRunner instantiation
  (~25 lines).
- plugins/agents/agents.ts: 1362 -> 1262 lines; `_streamAgent`
  shrinks from 233 lines (with a 95-line nested executeTool closure)
  to ~150 lines that build an HttpToolExecutor + AgentRunner.

Behaviour preserved:
- Top-level budget enforcement (sub-agents pass budget=null, mirroring
  the original closure that only counted at the outer executeTool)
- Approval gate fires on `effect: write|update|destructive` and the
  legacy `destructive: true` flag
- Sub-agents reuse the parent's checkApproval + outboundEvents +
  translator + abortController so destructive sub-agent tools surface
  approval_pending on the parent's SSE stream
- Sub-agent event forwarding skips `metadata` to avoid clobbering the
  parent thread state, matching the prior closure exactly

Verified: tsc --noEmit clean, knip clean, 1589/1589 appkit tests pass.
Extracts `composePromptForAgent` + `normalizeAutoInherit` into
plugins/agents/prompt.ts and `printRegistry` into
plugins/agents/registry-printer.ts. These were free-function helpers at
the bottom of agents.ts with no dependency on plugin state — pure
candidates for extraction.

Also opens the door for the bigger split (route handlers and
`_streamAgent`/`runSubAgent` extracted into routes/*.ts and
tool-execution.ts) by relaxing the access modifier on plugin members
those modules will need (`agents`, `activeStreams`, `mcpClient`,
`threadStore`, `approvalGate`, `resolvedApprovalPolicy`,
`resolvedLimits`, `countUserStreams`). All marked `@internal` to
keep the public surface unchanged.

Note: the full split into `routes/` and `tool-execution.ts` proposed
in plans/agent-architecture-followup.md is deferred. Route handlers
and `_streamAgent`/`runSubAgent` remain as methods on AgentsPlugin
because they have heavy plugin-state coupling and cross-call patterns
(`runSubAgent` recurses, `_handleChat` calls `_streamAgent`,
etc.) that don't translate cleanly to free functions without a larger
refactor. Tracked as a follow-up.

agents.ts: 1262 -> 1212 lines (-50). The plan's aspirational target
of <=280 isn't met because the per-route extraction pass is deferred,
but the helper extraction + access-modifier relaxation lays the
groundwork.

Verified: tsc --noEmit clean, 1589/1589 appkit tests pass.
…te manifest)

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
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