Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
"packages/appkit/src/plugin/index.ts",
"packages/appkit/src/plugin/to-plugin.ts",
"packages/appkit/src/plugins/agents/index.ts",
"template/**",
"tools/**",
"docs/**",
".github/scripts/**",
"packages/appkit/src/core/agent/index.ts",
"packages/appkit/src/core/agent/tools/index.ts",
"packages/appkit/src/core/agent/from-plugin.ts",
"packages/appkit/src/core/agent/load-agents.ts",
"packages/appkit/src/connectors/mcp/index.ts",
"packages/appkit/src/plugin/to-plugin.ts"
"template/**",
"tools/**",
"docs/**",
".github/scripts/**"
],
"ignoreDependencies": ["json-schema-to-typescript"],
"ignoreBinaries": ["tarball"]
Expand Down
4 changes: 4 additions & 0 deletions packages/appkit/src/beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ export type {
AgentDefinition,
AgentsPluginConfig,
AgentTool,
AgentTools,
AutoInheritToolsConfig,
BaseSystemPromptOption,
FromPluginMarker,
PromptContext,
RegisteredAgent,
ResolvedToolEntry,
Expand All @@ -62,6 +64,8 @@ export type {
} from "./plugins/agents";
export {
agentIdFromMarkdownPath,
fromPlugin,
isFromPluginMarker,
isToolkitEntry,
loadAgentFromFile,
loadAgentsFromDir,
Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/connectors/mcp/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Input shape consumed by {@link AppKitMcpClient.connect}. Produced by the
* agents plugin from user-facing `HostedTool` declarations (see
* `plugins/agents/tools/hosted-tools.ts`) and accepted directly by the
* `core/agent/tools/hosted-tools.ts`) and accepted directly by the
* connector to keep its surface free of agent-layer concepts.
*/
export interface McpEndpointConfig {
Expand Down
97 changes: 97 additions & 0 deletions packages/appkit/src/core/agent/from-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { NamedPluginFactory } from "../../plugin/to-plugin";
import type { ToolkitOptions } from "./types";

/**
* Symbol brand for the `fromPlugin` marker. Using a globally-interned symbol
* (`Symbol.for`) keeps the brand stable across module boundaries / bundle
* duplicates so `isFromPluginMarker` stays reliable.
*/
export const FROM_PLUGIN_MARKER = Symbol.for(
"@databricks/appkit.fromPluginMarker",
);

/**
* A lazy reference to a plugin's tools, produced by {@link fromPlugin} and
* resolved to concrete `ToolkitEntry`s at `AgentsPlugin.setup()` time.
*
* The marker is spread under a unique symbol key so multiple calls to
* `fromPlugin` (even for the same plugin) coexist in an `AgentDefinition.tools`
* record without colliding.
*/
export interface FromPluginMarker {
readonly [FROM_PLUGIN_MARKER]: true;
readonly pluginName: string;
readonly opts: ToolkitOptions | undefined;
}

/**
* Record shape returned by {@link fromPlugin} — a single symbol-keyed entry
* suitable for spreading into `AgentDefinition.tools`.
*/
export type FromPluginSpread = { readonly [key: symbol]: FromPluginMarker };

/**
* Reference a plugin's tools inside an `AgentDefinition.tools` record without
* naming the plugin instance. The returned spread-friendly object carries a
* symbol-keyed marker that the agents plugin resolves against registered
* `ToolProvider`s at setup time.
*
* The factory argument must come from `toPlugin` (or any function that
* carries a `pluginName` field). `fromPlugin` reads `factory.pluginName`
* synchronously — it does not construct an instance.
*
* If the referenced plugin is also registered in `createApp({ plugins })`, the
* same runtime instance is used for dispatch. If the plugin is missing,
* `AgentsPlugin.setup()` throws with a clear `Available: …` listing.
*
* @example
* ```ts
* import { analytics, createAgent, files, fromPlugin, tool } from "@databricks/appkit";
*
* const support = createAgent({
* instructions: "You help customers.",
* tools: {
* ...fromPlugin(analytics),
* ...fromPlugin(files, { only: ["uploads.read"] }),
* get_weather: tool({ ... }),
* },
* });
* ```
*
* @param factory A plugin factory produced by `toPlugin`. Must expose a
* `pluginName` field.
* @param opts Optional toolkit scoping — `prefix`, `only`, `except`, `rename`.
* Same shape as the `.toolkit()` method.
*/
export function fromPlugin<F extends NamedPluginFactory>(
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.

Honestly, I still don't like the fromPlugin API. Asked an agent if that's just me and looks like the community usually takes a similar approach to the one I suggest 😄 See the report at the end of the comment.

My suggestion is to: instead of

const support = createAgent({                                                                      
  instructions: "You help customers with data and files.",                                         
  model: "databricks-claude-sonnet-4-5",                  // string sugar                          
  tools: {                                                                                         
    ...fromPlugin(analytics),                             // all analytics tools                   
    ...fromPlugin(files, { only: ["uploads.read"] }),     // filtered subset                       
    get_weather: tool({                                                                            
      name: "get_weather",                                                                         
      description: "Weather",                                                                      
      schema: z.object({ city: z.string() }),                                                      
      execute: async ({ city }) => `Sunny in ${city}`,                                             
    }),                                                                                            
  },                                                                                               
});                                                                                                
                                                                                                   
await createApp({                                                                                  
  plugins: [server(), analytics(), files(), agents({ agents: { support } })],                      
});    

go with this:

  const analyticsPlugin = analytics();
  const filesPlugin = files();

  const support = createAgent({
    instructions: "You help customers with data and files.",
    model: "databricks-claude-sonnet-4-5",
    tools: {
      ...analyticsPlugin.toolkit(),
      ...filesPlugin.toolkit({ only: ["uploads.read"] }),
      get_weather: tool({
        name: "get_weather",
        description: "Weather",
        schema: z.object({ city: z.string() }),
        execute: async ({ city }) => `Sunny in ${city}`,
      }),
    },
  });

  await createApp({
    plugins: [server(), analyticsPlugin, filesPlugin, agents({ agents: { support } })],
  });

What do you think?


The chicken-and-egg problem

The tension: agent definitions need plugin tools, but plugins need AppKit context (WorkspaceClient, PluginContext, OBO) to be functional. So at definition time, nothing is "ready."

Common JS patterns for this

1. Two-phase init (your preference)

Create a handle, wire references, then initialize. This is how Express, Fastify, and Koa work — you instantiate middleware/plugins, compose them, then call listen():

const auth = passport();           // create
const app = express();
app.use(auth);                     // wire
app.listen(3000);                  // initialize

In AppKit terms, your preferred API would look like:

const analyticsPlugin = analytics({});
const filesPlugin = files({ volumes: { uploads: {} } });

const agent = createAgent({
  tools: {
    ...analyticsPlugin.toolkit(),
    ...filesPlugin.toolkit({ only: ["uploads.read"] }),
  },
});

createApp({ plugins: [server(), analyticsPlugin, filesPlugin, agents({ agents: { agent } })] });

This is totally achievable — analytics() already returns a data bag. It could also expose .toolkit() that returns the same lazy markers fromPlugin creates internally. Same resolution mechanism, more explicit surface.

2. DI container / token-based (Angular, NestJS, InversifyJS)

Register by class or token, resolve lazily:

@Injectable()
class MyAgent {
  constructor(@Inject(AnalyticsPlugin) private analytics: AnalyticsPlugin) {}
}

This is basically what fromPlugin does — reference-by-name, resolve-at-startup — but dressed up with decorators. Heavy for a non-decorator codebase.

3. Builder / fluent API (Hono, tRPC)

Collect everything, resolve at .build():

const app = AppKit.builder()
  .plugin("analytics", analytics({}))
  .plugin("files", files({ ... }))
  .agent("support", { tools: ["analytics", "files.uploads.read"] })
  .build();

Clean, but loses the "agents are plain objects" composability.

4. String references (Terraform, Spring, Django settings)

Just use names, resolve later. This is what markdown agents already do with toolkits: [analytics] in frontmatter — it's the YAML equivalent of fromPlugin(analytics).

My honest assessment

fromPlugin is pattern #2 disguised as a spread operator. It works, but has two DX friction points:

  1. It's an unusual pattern...fromPlugin(analytics) looks like it's doing something, but it's just creating a { [Symbol]: { pluginName: "analytics" } } marker. The magic is hidden.

  2. It introduces a separate concept — you need to learn fromPlugin as a standalone primitive. In your preferred approach (chore: rework TelemetryManager to use Node SDK #1), .toolkit() is a method on the thing you already have — no new import, no new concept.

The good news: these aren't mutually exclusive. The factory data bag returned by analytics() could grow a .toolkit() method that produces the same markers. fromPlugin could remain as a lower-level escape hatch (e.g., for dynamic plugin names). This is actually a very small surface change — toPlugin just needs to stamp a .toolkit() method alongside the existing .pluginName.

What the broader JS community leans toward

Explicit instance references (your preference) is the dominant pattern. Express, Fastify, Hono, tRPC, Drizzle, Prisma — almost nobody uses magic marker/symbol indirection. The standard expectation is: "I created it, I hold a reference to it, I pass that reference where it's needed."

The fromPlugin pattern is closer to how Terraform or Pulumi work (reference by logical name, resolve in a plan phase), which makes sense for infrastructure-as-code but feels foreign in a JS application SDK.

factory: F,
opts?: ToolkitOptions,
): FromPluginSpread {
if (
!factory ||
typeof factory.pluginName !== "string" ||
!factory.pluginName
) {
throw new Error(
"fromPlugin(): factory is missing pluginName. Pass a factory created by toPlugin().",
);
}
const pluginName = factory.pluginName;
const marker: FromPluginMarker = {
[FROM_PLUGIN_MARKER]: true,
pluginName,
opts,
};
return { [Symbol(`fromPlugin:${pluginName}`)]: marker };
}

/**
* Type guard for {@link FromPluginMarker}.
*/
export function isFromPluginMarker(value: unknown): value is FromPluginMarker {
return (
typeof value === "object" &&
value !== null &&
(value as Record<symbol, unknown>)[FROM_PLUGIN_MARKER] === true
);
}
63 changes: 63 additions & 0 deletions packages/appkit/src/core/agent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Agent runtime primitives. All framework-level agent types, tool helpers,
* and the standalone runner live here. The HTTP-facing `agents()` plugin in
* `plugins/agents/` consumes these but does not own them — peer plugins
* (analytics, files, genie, lakebase) can depend on this module without
* reaching across the sibling boundary.
*/
export { buildToolkitEntries } from "./build-toolkit";
export { consumeAdapterStream } from "./consume-adapter-stream";
export { createAgent } from "./create-agent";
export {
FROM_PLUGIN_MARKER,
type FromPluginMarker,
type FromPluginSpread,
fromPlugin,
isFromPluginMarker,
} from "./from-plugin";
export {
agentIdFromMarkdownPath,
type LoadContext,
type LoadResult,
loadAgentFromFile,
loadAgentsFromDir,
parseFrontmatter,
} from "./load-agents";
export { normalizeToolResult } from "./normalize-result";
export {
type RunAgentInput,
type RunAgentResult,
runAgent,
} from "./run-agent";
export { buildBaseSystemPrompt, composeSystemPrompt } from "./system-prompt";
export { resolveToolkitFromProvider } from "./toolkit-resolver";
export {
defineTool,
executeFromRegistry,
type FunctionTool,
functionToolToDefinition,
type HostedTool,
isFunctionTool,
isHostedTool,
mcpServer,
resolveHostedTools,
type ToolConfig,
type ToolEntry,
type ToolRegistry,
tool,
toolsFromRegistry,
} from "./tools";
export {
type AgentDefinition,
type AgentsPluginConfig,
type AgentTool,
type AgentTools,
type AutoInheritToolsConfig,
type BaseSystemPromptOption,
isToolkitEntry,
type PromptContext,
type RegisteredAgent,
type ResolvedToolEntry,
type ToolkitEntry,
type ToolkitOptions,
} from "./types";
Loading