From c35d9f4d1fae86fec5f0d9e3166002eb73f13cc7 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Apr 2026 12:34:16 +0200 Subject: [PATCH] feat(appkit): migrate agent-app and docs to the new agents() plugin - apps/agent-app/server.ts rewritten to use createApp({ plugins: [ server, analytics, files, agents({...}) ] }) with createAgent() for the code-defined support agent and mcpServer() entries inline. Uses the appkit.agent runtime handle (the agents() plugin registers under the singular name so routes mount at /api/agent/*). - apps/dev-playground/server/index.ts swapped from the deprecated agent() to agents(). Route prefix, client code, and runtime handle are unchanged; the singular "agent" manifest name is preserved in the new plugin so /api/agent/* keeps working. - Normalize assistant.md (both apps) and autocomplete.md frontmatter to proper YAML. The old flat parser tolerated '## key: value' markdown-heading markers and missing closing '---'; the new js-yaml parser requires real YAML. Without this fix, the dev-playground assistant agent silently failed to load and chat requests routed to the autocomplete text-completion model, which rejected them with upstream 400s. - New docs/plugins/agents.md covering all shapes: level 1 (drop a markdown file), level 2 (scope tools in frontmatter), level 3 (code-defined agents), level 4 (sub-agents), level 5 (standalone runAgent). Includes config reference and frontmatter schema table. - New docs/guides/migrating-to-agents-plugin.md with side-by-side before/after for the deprecated createAgent() shortcut and a gradual migration path using the @deprecated aliases. Signed-off-by: MarioCadenas --- apps/agent-app/config/agents/assistant.md | 9 +- apps/agent-app/server.ts | 75 +++++-- .../dev-playground/config/agents/assistant.md | 4 +- .../config/agents/autocomplete.md | 6 +- apps/dev-playground/server/index.ts | 4 +- .../docs/guides/migrating-to-agents-plugin.md | 173 +++++++++++++++ docs/docs/plugins/agents.md | 208 ++++++++++++++++++ 7 files changed, 447 insertions(+), 32 deletions(-) create mode 100644 docs/docs/guides/migrating-to-agents-plugin.md create mode 100644 docs/docs/plugins/agents.md diff --git a/apps/agent-app/config/agents/assistant.md b/apps/agent-app/config/agents/assistant.md index b2fe30fd..bd6e9b7e 100644 --- a/apps/agent-app/config/agents/assistant.md +++ b/apps/agent-app/config/agents/assistant.md @@ -1,11 +1,12 @@ --- - -## default: true +endpoint: databricks-claude-sonnet-4-5 +default: true +--- You are a helpful data assistant running on Databricks. Use the available tools to query data, browse files, and help users with their analysis. -When using analytics.query, write Databricks SQL. When results are large, summarize the key findings rather than dumping raw data. +When using `analytics.query`, write Databricks SQL. When results are large, summarize the key findings rather than dumping raw data. -You also have access to additional tools from MCP servers — use them when relevant. \ No newline at end of file +You also have access to additional tools from MCP servers — use them when relevant. diff --git a/apps/agent-app/server.ts b/apps/agent-app/server.ts index 488ef211..3c853d43 100644 --- a/apps/agent-app/server.ts +++ b/apps/agent-app/server.ts @@ -1,41 +1,72 @@ import { + agents, analytics, createAgent, + createApp, files, mcpServer, + server, tool, } from "@databricks/appkit"; import { z } from "zod"; const port = Number(process.env.DATABRICKS_APP_PORT) || 8003; -createAgent({ - plugins: [analytics(), files()], - tools: [ - tool({ - name: "get_weather", - description: "Get the current weather for a city", - schema: z.object({ - city: z.string().describe("City name"), - }), - execute: async ({ city }) => `The weather in ${city} is sunny, 22°C`, - }), - mcpServer( - "mario-mcp-hello", - "https://mario-mcp-hello-6051921418418893.staging.aws.databricksapps.com/mcp", - ), - mcpServer( +// Shared tool available to any agent that declares `tools: [get_weather]` in +// its markdown frontmatter. +const get_weather = tool({ + name: "get_weather", + description: "Get the current weather for a city", + schema: z.object({ + city: z.string().describe("City name"), + }), + execute: async ({ city }) => `The weather in ${city} is sunny, 22°C`, +}); + +// Code-defined agent. Overrides config/agents/support.md if a file with that +// name exists. Tools here are explicit; defaults are strict (no auto-inherit +// for code-defined agents). +const support = createAgent({ + instructions: + "You help customers with data analysis, file browsing, and general questions. " + + "Use the available tools as needed and summarize results concisely.", + tools: { + get_weather, + "mcp.vector-search": mcpServer( "vector-search", "https://e2-dogfood.staging.cloud.databricks.com/api/2.0/mcp/vector-search/main/default", ), - mcpServer( + "mcp.uc-greet": mcpServer( "uc-greet", "https://e2-dogfood.staging.cloud.databricks.com/api/2.0/mcp/functions/main/mario/greet", ), + "mcp.mario-hello": mcpServer( + "mario-mcp-hello", + "https://mario-mcp-hello-6051921418418893.staging.aws.databricksapps.com/mcp", + ), + }, +}); + +const appkit = await createApp({ + plugins: [ + server({ port }), + analytics(), + files(), + agents({ + // Ambient tool library referenced by markdown frontmatter `tools: [...]`. + tools: { get_weather }, + // Code-defined agents are merged with markdown agents; code wins on key + // collision. Markdown agents still auto-inherit analytics+files tools + // unless their frontmatter says otherwise. + agents: { support }, + }), ], - port, -}).then((agent) => { - console.log( - `Agent running on port ${port} with ${agent.getTools().length} tools`, - ); }); + +const registry = appkit.agent as { + list: () => string[]; + getDefault: () => string | null; +}; +console.log( + `Agent app running on port ${port}. Agents: ${registry.list().join(", ")}. Default: ${registry.getDefault() ?? "(none)"}.`, +); diff --git a/apps/dev-playground/config/agents/assistant.md b/apps/dev-playground/config/agents/assistant.md index aa6701ec..2a116b28 100644 --- a/apps/dev-playground/config/agents/assistant.md +++ b/apps/dev-playground/config/agents/assistant.md @@ -1,5 +1,7 @@ --- -## default: true +## endpoint: databricks-claude-sonnet-4-5 + +default: true You are a helpful data assistant. Use the available tools to query data and help users with their analysis. \ No newline at end of file diff --git a/apps/dev-playground/config/agents/autocomplete.md b/apps/dev-playground/config/agents/autocomplete.md index 3475562a..fafe3330 100644 --- a/apps/dev-playground/config/agents/autocomplete.md +++ b/apps/dev-playground/config/agents/autocomplete.md @@ -1,6 +1,6 @@ --- - -## endpoint: databricks-gemini-3-1-flash-lite +endpoint: databricks-gemini-3-1-flash-lite maxSteps: 1 +--- -You are an autocomplete engine. The user will give you the beginning of a sentence or paragraph. Continue the text naturally, as if you are the same author. Do NOT repeat the input. Only output the continuation. Do NOT use tools. Do NOT explain. Just write the next words. \ No newline at end of file +You are an autocomplete engine. The user will give you the beginning of a sentence or paragraph. Continue the text naturally, as if you are the same author. Do NOT repeat the input. Only output the continuation. Do NOT use tools. Do NOT explain. Just write the next words. diff --git a/apps/dev-playground/server/index.ts b/apps/dev-playground/server/index.ts index bf9207cb..1795321b 100644 --- a/apps/dev-playground/server/index.ts +++ b/apps/dev-playground/server/index.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; import { - agent, + agents, analytics, createApp, files, @@ -33,7 +33,7 @@ createApp({ }), lakebaseExamples(), files(), - agent(), + agents(), ], ...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }), }).then((appkit) => { diff --git a/docs/docs/guides/migrating-to-agents-plugin.md b/docs/docs/guides/migrating-to-agents-plugin.md new file mode 100644 index 00000000..d2ca374a --- /dev/null +++ b/docs/docs/guides/migrating-to-agents-plugin.md @@ -0,0 +1,173 @@ +# Migrating to the `agents()` plugin + +The old `createAgent({ adapter, port, tools, plugins })` shortcut from the agent PR stack is deprecated. The new shape splits agent *definition* (pure data) from app *composition* (plugin registration). + +The old exports still work — they're kept side-by-side until a future removal release. This guide shows how to port code incrementally. + +## Name changes at a glance + +| Old | New | +|---|---| +| `createAgent(config)` (app shortcut) | `createAgentApp(config)` (deprecated) | +| — | `createAgent(def)` (pure factory, **same name, new meaning**) | +| `agent()` plugin | `agents()` plugin (plural) | +| `tools: AgentTool[]` | `tools: Record` | +| Auto-inherit all plugin tools | Asymmetric: markdown yes, code no | + +## Before: old shortcut + +```ts +import { + analytics, + createAgent, + files, + mcpServer, + tool, +} from "@databricks/appkit"; +import { z } from "zod"; + +createAgent({ + plugins: [analytics(), files()], + tools: [ + tool({ + name: "get_weather", + description: "Weather", + schema: z.object({ city: z.string() }), + execute: async ({ city }) => `Sunny in ${city}`, + }), + mcpServer("vector-search", "https://…/mcp/vector-search"), + ], + port: 8000, +}).then((agent) => { + console.log(`Running with ${agent.getTools().length} tools`); +}); +``` + +## After: createApp + agents() + +```ts +import { + agents, + analytics, + createApp, + files, + mcpServer, + server, + tool, +} from "@databricks/appkit"; +import { z } from "zod"; + +const get_weather = tool({ + name: "get_weather", + description: "Weather", + schema: z.object({ city: z.string() }), + execute: async ({ city }) => `Sunny in ${city}`, +}); + +await createApp({ + plugins: [ + server({ port: 8000 }), + analytics(), + files(), + agents({ + tools: { + get_weather, + "mcp.vector-search": mcpServer( + "vector-search", + "https://…/mcp/vector-search", + ), + }, + }), + ], +}); +``` + +Key differences: + +- `plugins` moves to the top level of `createApp`; `agents()` goes into the plugin list. +- `server()` is explicit — required when you want HTTP. +- `tools` is a record on `agents({ tools })`, keyed by the tool-call name the LLM will see. Spread into agent definitions via markdown `tools: [get_weather]` or inline in `createAgent({ tools: { get_weather } })`. +- `port` moves to `server({ port })`. + +## Frontmatter migration + +The old parser was a flat key=value regex. The new parser is real YAML (`js-yaml`). Two things to watch for: + +1. **Remove `##` markers**: `## default: true` was a Markdown heading that the old parser tolerated. YAML requires `default: true`. +2. **Validate structure**: previously typos were silently dropped; the new parser logs warnings for unknown keys and throws on truly invalid YAML. + +Old: + +```md +--- +## default: true +## endpoint: databricks-claude-sonnet-4-5 +--- + +You are helpful. +``` + +New: + +```md +--- +default: true +endpoint: databricks-claude-sonnet-4-5 +--- + +You are helpful. +``` + +## Scoping tools in markdown + +The new schema lets you declare tool scope right in the markdown file: + +```md +--- +endpoint: databricks-claude-sonnet-4-5 +toolkits: + - analytics + - files: [uploads.read, uploads.list] +tools: [get_weather] +--- + +You are a read-only data assistant. +``` + +Engineers declare tools in code; prompt authors pick from a menu in frontmatter. No YAML-as-code ceremony required. + +## Standalone runs + +The old `createAgent` returned a running HTTP app. Sometimes you want to run an agent in a script, cron, or test without HTTP. Use `runAgent`: + +```ts +import { createAgent, runAgent, tool } from "@databricks/appkit"; + +const classifier = createAgent({ + instructions: "Classify tickets.", + model: "databricks-claude-sonnet-4-5", + tools: { /* inline tools only */ }, +}); + +const result = await runAgent(classifier, { messages: "Billing issue please help" }); +console.log(result.text); +``` + +Plugin toolkits (`ToolkitEntry` from `.toolkit()`) require `createApp`; `runAgent` throws a clear error if invoked with one. + +## Gradual migration + +Both APIs coexist. You can land the dependency bump today, keep using `createAgentApp` (the renamed old shortcut), and migrate call sites one at a time: + +```ts +import { createAgentApp, analytics, tool } from "@databricks/appkit"; + +// Old shape still works, just renamed: +createAgentApp({ plugins: [analytics()], tools: [/* ... */] }); +``` + +When you're ready, switch the import to `createApp({ plugins: [..., agents()] })` and remove the `createAgentApp` call. No other code needs to change. + +## Removal timeline + +The old `agent()` and `createAgentApp` exports remain until feedback on the new shape stabilizes. A follow-up PR will remove them in a future release; use of the deprecated exports surfaces via IDE strikethrough (JSDoc `@deprecated`) but does not log runtime warnings. diff --git a/docs/docs/plugins/agents.md b/docs/docs/plugins/agents.md new file mode 100644 index 00000000..45742bbb --- /dev/null +++ b/docs/docs/plugins/agents.md @@ -0,0 +1,208 @@ +# Agents + +The `agents` plugin turns a Databricks AppKit app into an AI-agent host. It loads agent definitions from markdown files (convention: `config/agents/*.md`), from TypeScript (`createAgent(def)`), or both, and exposes them at `POST /invocations` alongside routes for chat, thread management, and cancellation. + +This page covers the full lifecycle. For the hand-written primitives (`tool()`, `mcpServer()`), see [tools](./server.md). + +## Install + +`agents` is a regular plugin. Add it to `plugins[]` alongside `server()` and any ToolProvider plugins whose tools you want agents to reach. + +```ts +import { agents, analytics, createApp, files, server } from "@databricks/appkit"; + +await createApp({ + plugins: [server(), analytics(), files(), agents()], +}); +``` + +That alone gives you a live HTTP server with `POST /invocations` wired to a markdown-driven agent. + +## Level 1: drop a markdown file + +``` +my-app/ + server.ts + config/agents/ + assistant.md +``` + +```md +--- +endpoint: databricks-claude-sonnet-4-5 +default: true +--- + +You are a helpful data assistant running on Databricks. + +Use the available tools to query data, browse files, and help users. +``` + +On startup the plugin: + +1. Discovers the file at `./config/agents/assistant.md`. +2. Parses the YAML frontmatter and markdown body as the agent's `instructions`. +3. Resolves the adapter from `endpoint` (or falls back to `DATABRICKS_AGENT_ENDPOINT`). +4. Auto-inherits every registered ToolProvider plugin's tools (`analytics.*`, `files.*`, …). +5. Mounts the agent at the default name (`assistant`). + +Requests land at `POST /invocations` with an OpenAI Responses-compatible body. Every tool call runs through `asUser(req)` so SQL executes as the requesting user, file access respects Unity Catalog ACLs, and telemetry spans are created automatically. + +## Level 2: scope tools in frontmatter + +```md +--- +endpoint: databricks-claude-sonnet-4-5 +toolkits: + - analytics # all analytics.* tools + - files: [uploads.read, uploads.list] # only these files tools + - genie: { except: [getConversation] } # everything but getConversation +tools: [get_weather] # ambient tool declared in code +default: true +--- + +You are a read-only data analyst. +``` + +When any `toolkits:` or `tools:` is declared the auto-inherit default is turned off — the agent sees exactly the listed tools. Ambient tools (`tools: [get_weather]`) are looked up in the `agents({ tools: { ... } })` config. + +## Level 3: code-defined agents + +```ts +import { + agents, + analytics, + createAgent, + createApp, + files, + server, + tool, +} from "@databricks/appkit"; +import { z } from "zod"; + +const analyticsP = analytics(); +const filesP = files(); + +const support = createAgent({ + instructions: "You help customers with data and files.", + model: "databricks-claude-sonnet-4-5", // string sugar + tools: { + get_weather: tool({ + description: "Weather", + schema: z.object({ city: z.string() }), + execute: async ({ city }) => `Sunny in ${city}`, + }), + ...analyticsP.toolkit(), // spread plugin tools + ...filesP.toolkit({ only: ["uploads.read"] }), // filtered + }, +}); + +await createApp({ + plugins: [server(), analyticsP, filesP, agents({ agents: { support } })], +}); +``` + +Code-defined agents start with no tools by default. Spread `.toolkit()` outputs into `tools: { ... }` explicitly. The asymmetry (file: auto-inherit, code: strict) matches the personas: prompt authors want zero ceremony, engineers want no surprises. + +## Level 4: sub-agents + +```ts +const researcher = createAgent({ + instructions: "Research the question. Return concise bullets.", + model: "databricks-claude-sonnet-4-5", + tools: { search: tool({ /* ... */ }) }, +}); + +const writer = createAgent({ + instructions: "Draft prose from notes.", + model: "databricks-claude-sonnet-4-5", +}); + +const supervisor = createAgent({ + instructions: "Coordinate researcher and writer.", + model: "databricks-claude-sonnet-4-5", + agents: { researcher, writer }, // exposed as agent-researcher, agent-writer +}); + +await createApp({ + plugins: [ + server(), + agents({ agents: { supervisor, researcher, writer } }), + ], +}); +``` + +Each key in `agents: {...}` on an `AgentDefinition` becomes an `agent-` tool on the parent. When invoked, the agents plugin runs the child's adapter with a fresh message list (no shared thread state) and returns the aggregated text. Cycles are rejected at load time. + +## Level 5: standalone (no `createApp`) + +```ts +import { createAgent, runAgent, tool } from "@databricks/appkit"; +import { z } from "zod"; + +const classifier = createAgent({ + instructions: "Classify tickets: billing | bug | feature.", + model: "databricks-claude-sonnet-4-5", + tools: { + lookup_account: tool({ /* ... */ }), + }, +}); + +for (const ticket of tickets) { + const result = await runAgent(classifier, { + messages: [{ role: "user", content: ticket.body }], + }); + await persistClassification(ticket.id, result.text); +} +``` + +`runAgent` drives the adapter without `createApp` or HTTP. Limitation: plugin toolkits (`ToolkitEntry`) require a live `PluginContext`, so they only work when invoked through `agents()` + `createApp`. Inline `tool()` and `mcpServer()` both work standalone. + +## Configuration reference + +```ts +agents({ + dir?: string | false, // "./config/agents" default; false disables + agents?: Record, + defaultAgent?: string, + defaultModel?: AgentAdapter | Promise | string, + tools?: Record, + autoInheritTools?: boolean | { file?: boolean, code?: boolean }, + threadStore?: ThreadStore, // default in-memory + baseSystemPrompt?: false | string | (ctx: PromptContext) => string, +}) +``` + +`autoInheritTools` defaults to `{ file: true, code: false }`. Boolean shorthand applies to both. + +## Runtime API + +After `createApp`, the plugin exposes: + +```ts +appkit.agents.list(); // => ["support", "researcher", ...] +appkit.agents.get("support"); // => RegisteredAgent | null +appkit.agents.getDefault(); // => "support" +appkit.agents.register(name, def); // dynamic registration +appkit.agents.reload(); // re-scan the directory +appkit.agents.getThreads(userId); // list user's threads +``` + +## Frontmatter schema + +| Key | Type | Notes | +|---|---|---| +| `endpoint` | string | Model serving endpoint name. Shortcut for `model`. | +| `model` | string | Same as `endpoint`; either works. | +| `toolkits` | array of string or `{ name: options }` | Spread plugin toolkits. Supports `only`, `except`, `rename`, `prefix`. | +| `tools` | array of string | Keys into `agents({ tools: {...} })`. | +| `default` | boolean | First file with `default: true` becomes the default agent. | +| `maxSteps` | number | Adapter max-step hint. | +| `maxTokens` | number | Adapter max-token hint. | +| `baseSystemPrompt` | false \| string | Per-agent override. `false` disables the AppKit base prompt. | + +Unknown keys are logged and ignored. Invalid YAML and missing plugin/tool references throw at boot. + +## Migration from the old API + +See the [migration guide](../guides/migrating-to-agents-plugin.md).