feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver#305
Open
MarioCadenas wants to merge 9 commits intoagent/v2/4-agents-pluginfrom
Open
feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver#305MarioCadenas wants to merge 9 commits intoagent/v2/4-agents-pluginfrom
MarioCadenas wants to merge 9 commits intoagent/v2/4-agents-pluginfrom
Conversation
This was referenced Apr 21, 2026
3c7c35e to
cb7fe2b
Compare
162e970 to
29e3534
Compare
cb7fe2b to
0afea5e
Compare
29e3534 to
b462716
Compare
0afea5e to
983461c
Compare
539487e to
dac73b5
Compare
a7b0444 to
623792d
Compare
624f2a0 to
0dd07a4
Compare
623792d to
2f752a0
Compare
8ace826 to
f41317f
Compare
af9b6ee to
e4b1322
Compare
f41317f to
d42173c
Compare
caa6286 to
c2b0f28
Compare
41fa8b0 to
8b0c28e
Compare
c2b0f28 to
fd73087
Compare
22393bb to
fdfd568
Compare
269d1a9 to
c038a77
Compare
fdfd568 to
65178cf
Compare
c038a77 to
ab5b485
Compare
65178cf to
03da825
Compare
ab5b485 to
6378638
Compare
03da825 to
c16fa29
Compare
6378638 to
2640ea4
Compare
c16fa29 to
a8cedbd
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 markerpackages/appkit/src/plugins/agents/from-plugin.ts. Returns a spread-friendly
{ [Symbol()]: FromPluginMarker }record. The symbol key isfreshly 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 acrossmodule boundaries.
resolveToolkitFromProvider(pluginName, provider, opts?)packages/appkit/src/plugins/agents/toolkit-resolver.ts. Single sourceof truth for "turn a ToolProvider into a keyed record of
ToolkitEntrymarkers". Prefers
provider.toolkit(opts)when available (core pluginsimplement it), falls back to walking
getAgentTools()and synthesizingnamespaced keys (
${pluginName}.${localName}) for third-partyproviders, honoring
only/except/rename/prefixthe sameway.
Used by three call sites, previously all copy-pasted:
AgentsPlugin.buildToolIndex— fromPlugin marker resolution passAgentsPlugin.applyAutoInherit— markdown auto-inherit pathrunAgent— standalone-mode plugin tool dispatchAgentsPlugin.buildToolIndex— symbol-key resolution passBefore the existing string-key iteration,
buildToolIndexnow walksObject.getOwnPropertySymbols(def.tools). For eachFromPluginMarker,it looks up the plugin by name in
PluginContext.getToolProviders(),calls
resolveToolkitFromProvider, and merges the resulting entriesinto the per-agent index. Missing plugins throw at setup time with a
clear
Available: ...listing — wiring errors surface on boot, notmid-request.
hasExplicitToolsnow counts symbol keys too, so atools: { ...fromPlugin(x) }record correctly disables auto-inheriton code-defined agents.
Type plumbing
AgentToolstype:{ [key: string]: AgentTool } & { [key: symbol]: FromPluginMarker }. Preserves string-key autocomplete whileaccepting marker spreads under strict TS.
AgentDefinition.toolsswitched toAgentTools.runAgentgainsplugins?: PluginData[]packages/appkit/src/core/run-agent.ts. When an agent def containsfromPluginmarkers, the caller passes plugins viaRunAgentInput.plugins. A local provider cache constructs each pluginand dispatches tool calls via
provider.executeAgentTool(). Runs asservice principal (no OBO — there's no HTTP request). If a def
contains markers but
pluginsis absent, throws with guidance.Exports
fromPlugin,FromPluginMarker,isFromPluginMarker,AgentToolsadded to the main barrel.
Test plan
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
pluginsarg, guidance error whenmissing.
Signed-off-by: MarioCadenas MarioCadenas@users.noreply.github.com
PR Stack
agents()plugin +createAgent(def)+ markdown-driven agents — feat(appkit): agents() plugin, createAgent(def), and markdown-driven agents #304fromPlugin()DX +runAgentplugins arg + toolkit-resolver (this PR)Demo
agent-demo.mp4