From 49c8327cd2b83801363fc2b871ca6346229e6963 Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Tue, 10 Mar 2026 10:38:30 +0100 Subject: [PATCH 1/4] feat(appkit): add AgentPlugin for LangChain/LangGraph agents Signed-off-by: Hubert Zub --- apps/dev-playground/.env.dist | 1 + .../client/src/routeTree.gen.ts | 21 + .../client/src/routes/__root.tsx | 8 + .../client/src/routes/agent.route.tsx | 31 + .../client/src/routes/index.tsx | 26 + apps/dev-playground/server/agent-tools.ts | 37 + apps/dev-playground/server/index.ts | 27 +- .../api/appkit/Interface.AgentInterface.md | 43 + .../api/appkit/Interface.BasePluginConfig.md | 4 + .../docs/api/appkit/Interface.IAgentConfig.md | 138 +++ .../docs/api/appkit/Interface.InvokeParams.md | 38 + .../api/appkit/Interface.StandardAgent.md | 53 ++ .../appkit/TypeAlias.ResponseStreamEvent.md | 11 + docs/docs/api/appkit/Variable.agent.md | 21 + docs/docs/api/appkit/index.md | 10 +- docs/docs/api/appkit/typedoc-sidebar.ts | 30 + docs/static/appkit-ui/styles.gen.css | 52 ++ .../react/agent-chat/agent-chat-message.tsx | 45 + .../src/react/agent-chat/agent-chat-part.tsx | 46 + .../src/react/agent-chat/agent-chat.tsx | 85 ++ .../appkit-ui/src/react/agent-chat/index.ts | 6 + .../appkit-ui/src/react/agent-chat/types.ts | 66 ++ .../src/react/agent-chat/use-agent-chat.ts | 174 ++++ .../appkit-ui/src/react/agent-chat/utils.ts | 32 + packages/appkit-ui/src/react/index.ts | 1 + packages/appkit/package.json | 27 +- packages/appkit/src/index.ts | 9 +- .../src/plugins/agent/agent-interface.ts | 121 +++ packages/appkit/src/plugins/agent/agent.ts | 283 +++++++ packages/appkit/src/plugins/agent/index.ts | 14 + .../src/plugins/agent/invoke-handler.ts | 160 ++++ .../appkit/src/plugins/agent/manifest.json | 23 + .../src/plugins/agent/standard-agent.ts | 225 +++++ .../agent/tests/agent.integration.test.ts | 228 +++++ .../src/plugins/agent/tests/agent.test.ts | 172 ++++ .../agent/tests/invoke-handler.test.ts | 325 +++++++ .../src/plugins/agent/tests/stub-agent.ts | 71 ++ packages/appkit/src/plugins/agent/types.ts | 43 + packages/appkit/src/plugins/index.ts | 1 + pnpm-lock.yaml | 798 +++++++++++++++++- template/appkit.plugins.json | 24 + 41 files changed, 3507 insertions(+), 23 deletions(-) create mode 100644 apps/dev-playground/client/src/routes/agent.route.tsx create mode 100644 apps/dev-playground/server/agent-tools.ts create mode 100644 docs/docs/api/appkit/Interface.AgentInterface.md create mode 100644 docs/docs/api/appkit/Interface.IAgentConfig.md create mode 100644 docs/docs/api/appkit/Interface.InvokeParams.md create mode 100644 docs/docs/api/appkit/Interface.StandardAgent.md create mode 100644 docs/docs/api/appkit/TypeAlias.ResponseStreamEvent.md create mode 100644 docs/docs/api/appkit/Variable.agent.md create mode 100644 packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx create mode 100644 packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx create mode 100644 packages/appkit-ui/src/react/agent-chat/agent-chat.tsx create mode 100644 packages/appkit-ui/src/react/agent-chat/index.ts create mode 100644 packages/appkit-ui/src/react/agent-chat/types.ts create mode 100644 packages/appkit-ui/src/react/agent-chat/use-agent-chat.ts create mode 100644 packages/appkit-ui/src/react/agent-chat/utils.ts create mode 100644 packages/appkit/src/plugins/agent/agent-interface.ts create mode 100644 packages/appkit/src/plugins/agent/agent.ts create mode 100644 packages/appkit/src/plugins/agent/index.ts create mode 100644 packages/appkit/src/plugins/agent/invoke-handler.ts create mode 100644 packages/appkit/src/plugins/agent/manifest.json create mode 100644 packages/appkit/src/plugins/agent/standard-agent.ts create mode 100644 packages/appkit/src/plugins/agent/tests/agent.integration.test.ts create mode 100644 packages/appkit/src/plugins/agent/tests/agent.test.ts create mode 100644 packages/appkit/src/plugins/agent/tests/invoke-handler.test.ts create mode 100644 packages/appkit/src/plugins/agent/tests/stub-agent.ts create mode 100644 packages/appkit/src/plugins/agent/types.ts diff --git a/apps/dev-playground/.env.dist b/apps/dev-playground/.env.dist index 23c3265a..4cbe6f00 100644 --- a/apps/dev-playground/.env.dist +++ b/apps/dev-playground/.env.dist @@ -9,6 +9,7 @@ OTEL_SERVICE_NAME='dev-playground' DATABRICKS_VOLUME_PLAYGROUND= DATABRICKS_VOLUME_OTHER= DATABRICKS_GENIE_SPACE_ID= +DATABRICKS_MODEL= LAKEBASE_ENDPOINT='' # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} — use the `name` field from the output PGHOST= PGUSER= diff --git a/apps/dev-playground/client/src/routeTree.gen.ts b/apps/dev-playground/client/src/routeTree.gen.ts index c4c38d14..c3b807f5 100644 --- a/apps/dev-playground/client/src/routeTree.gen.ts +++ b/apps/dev-playground/client/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as DataVisualizationRouteRouteImport } from './routes/data-visual import { Route as ChartInferenceRouteRouteImport } from './routes/chart-inference.route' import { Route as ArrowAnalyticsRouteRouteImport } from './routes/arrow-analytics.route' import { Route as AnalyticsRouteRouteImport } from './routes/analytics.route' +import { Route as AgentRouteRouteImport } from './routes/agent.route' import { Route as IndexRouteImport } from './routes/index' const TypeSafetyRouteRoute = TypeSafetyRouteRouteImport.update({ @@ -77,6 +78,11 @@ const AnalyticsRouteRoute = AnalyticsRouteRouteImport.update({ path: '/analytics', getParentRoute: () => rootRouteImport, } as any) +const AgentRouteRoute = AgentRouteRouteImport.update({ + id: '/agent', + path: '/agent', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -85,6 +91,7 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/agent': typeof AgentRouteRoute '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/chart-inference': typeof ChartInferenceRouteRoute @@ -99,6 +106,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute + '/agent': typeof AgentRouteRoute '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/chart-inference': typeof ChartInferenceRouteRoute @@ -114,6 +122,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/agent': typeof AgentRouteRoute '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/chart-inference': typeof ChartInferenceRouteRoute @@ -130,6 +139,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/agent' | '/analytics' | '/arrow-analytics' | '/chart-inference' @@ -144,6 +154,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/agent' | '/analytics' | '/arrow-analytics' | '/chart-inference' @@ -158,6 +169,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/agent' | '/analytics' | '/arrow-analytics' | '/chart-inference' @@ -173,6 +185,7 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AgentRouteRoute: typeof AgentRouteRoute AnalyticsRouteRoute: typeof AnalyticsRouteRoute ArrowAnalyticsRouteRoute: typeof ArrowAnalyticsRouteRoute ChartInferenceRouteRoute: typeof ChartInferenceRouteRoute @@ -265,6 +278,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AnalyticsRouteRouteImport parentRoute: typeof rootRouteImport } + '/agent': { + id: '/agent' + path: '/agent' + fullPath: '/agent' + preLoaderRoute: typeof AgentRouteRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -277,6 +297,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AgentRouteRoute: AgentRouteRoute, AnalyticsRouteRoute: AnalyticsRouteRoute, ArrowAnalyticsRouteRoute: ArrowAnalyticsRouteRoute, ChartInferenceRouteRoute: ChartInferenceRouteRoute, diff --git a/apps/dev-playground/client/src/routes/__root.tsx b/apps/dev-playground/client/src/routes/__root.tsx index 5cf74ce3..0cfee693 100644 --- a/apps/dev-playground/client/src/routes/__root.tsx +++ b/apps/dev-playground/client/src/routes/__root.tsx @@ -104,6 +104,14 @@ function RootComponent() { Files + + + diff --git a/apps/dev-playground/client/src/routes/agent.route.tsx b/apps/dev-playground/client/src/routes/agent.route.tsx new file mode 100644 index 00000000..62b8a580 --- /dev/null +++ b/apps/dev-playground/client/src/routes/agent.route.tsx @@ -0,0 +1,31 @@ +import { AgentChat } from "@databricks/appkit-ui/react"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/agent")({ + component: AgentChatRoute, +}); + +function AgentChatRoute() { + return ( +
+
+
+

+ Agent Chat +

+

+ Chat with the agent via POST /invocations (Responses + API SSE stream). +

+
+ + +
+
+ ); +} diff --git a/apps/dev-playground/client/src/routes/index.tsx b/apps/dev-playground/client/src/routes/index.tsx index e331d93c..2765c446 100644 --- a/apps/dev-playground/client/src/routes/index.tsx +++ b/apps/dev-playground/client/src/routes/index.tsx @@ -218,6 +218,32 @@ function IndexRoute() { + + +
+

+ Agent Chat +

+

+ Chat with a AI agent powered by the AppKit Agent Plugin. + Features{" "} + + OpenResponses + + -compatible SSE streaming and tool call rendering. +

+ +
+
diff --git a/apps/dev-playground/server/agent-tools.ts b/apps/dev-playground/server/agent-tools.ts new file mode 100644 index 00000000..5af64a34 --- /dev/null +++ b/apps/dev-playground/server/agent-tools.ts @@ -0,0 +1,37 @@ +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +export const weatherTool = tool( + async ({ location }) => { + const conditions = ["sunny", "partly cloudy", "rainy", "windy"]; + const condition = conditions[Math.floor(Math.random() * conditions.length)]; + const temp = Math.floor(Math.random() * 30) + 50; + return `Weather in ${location}: ${condition}, ${temp}°F`; + }, + { + name: "get_weather", + description: "Get the current weather for a location", + schema: z.object({ + location: z.string().describe("City name, e.g. 'San Francisco'"), + }), + }, +); + +export const timeTool = tool( + async ({ timezone }) => { + const tz = timezone ?? "UTC"; + return `Current time in ${tz}: ${new Date().toLocaleString("en-US", { timeZone: tz })}`; + }, + { + name: "get_current_time", + description: "Get the current date and time in a timezone", + schema: z.object({ + timezone: z + .string() + .optional() + .describe("IANA timezone, e.g. 'America/New_York'. Defaults to UTC"), + }), + }, +); + +export const demoTools = { weatherTool, timeTool }; diff --git a/apps/dev-playground/server/index.ts b/apps/dev-playground/server/index.ts index a4b6a2c6..c21a51ce 100644 --- a/apps/dev-playground/server/index.ts +++ b/apps/dev-playground/server/index.ts @@ -1,6 +1,14 @@ import "reflect-metadata"; -import { analytics, createApp, files, genie, server } from "@databricks/appkit"; +import { + agent, + analytics, + createApp, + files, + genie, + server, +} from "@databricks/appkit"; import { WorkspaceClient } from "@databricks/sdk-experimental"; +import { demoTools } from "./agent-tools"; import { lakebaseExamples } from "./lakebase-examples-plugin"; import { reconnect } from "./reconnect-plugin"; import { telemetryExamples } from "./telemetry-example-plugin"; @@ -26,11 +34,26 @@ createApp({ }), lakebaseExamples(), files(), + agent({ + model: process.env.DATABRICKS_MODEL || "databricks-claude-sonnet-4-5", + systemPrompt: + "You are a helpful assistant. Use tools when appropriate — for example, use get_weather for weather questions, and get_current_time for time queries.", + tools: [demoTools.weatherTool], + }), ], ...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }), -}).then((appkit) => { +}).then(async (appkit) => { + // Add tools (and optionally MCP servers) after app creation + await appkit.agent.addCapabilities({ tools: [demoTools.timeTool] }); + appkit.server .extend((app) => { + // Rewrite to use standard Databricks Apps convention: /invocations at root + app.post("/invocations", (req, res) => { + req.url = "/api/agent"; + app(req, res); + }); + app.get("/sp", (_req, res) => { appkit.analytics .query("SELECT * FROM samples.nyctaxi.trips;") diff --git a/docs/docs/api/appkit/Interface.AgentInterface.md b/docs/docs/api/appkit/Interface.AgentInterface.md new file mode 100644 index 00000000..0736fe32 --- /dev/null +++ b/docs/docs/api/appkit/Interface.AgentInterface.md @@ -0,0 +1,43 @@ +# Interface: AgentInterface + +Contract that agent implementations must fulfil. + +The plugin calls `invoke()` for non-streaming requests and `stream()` for +SSE streaming. Implementations are responsible for translating their SDK's +output into Responses API types. + +## Methods + +### invoke() + +```ts +invoke(params: InvokeParams): Promise; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`Promise`\<`ResponseOutputItem`[]\> + +*** + +### stream() + +```ts +stream(params: InvokeParams): AsyncGenerator; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`AsyncGenerator`\<[`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)\> diff --git a/docs/docs/api/appkit/Interface.BasePluginConfig.md b/docs/docs/api/appkit/Interface.BasePluginConfig.md index a7faffc6..1cf97ca0 100644 --- a/docs/docs/api/appkit/Interface.BasePluginConfig.md +++ b/docs/docs/api/appkit/Interface.BasePluginConfig.md @@ -2,6 +2,10 @@ Base configuration interface for AppKit plugins +## Extended by + +- [`IAgentConfig`](Interface.IAgentConfig.md) + ## Indexable ```ts diff --git a/docs/docs/api/appkit/Interface.IAgentConfig.md b/docs/docs/api/appkit/Interface.IAgentConfig.md new file mode 100644 index 00000000..e3413af0 --- /dev/null +++ b/docs/docs/api/appkit/Interface.IAgentConfig.md @@ -0,0 +1,138 @@ +# Interface: IAgentConfig + +Base configuration interface for AppKit plugins. + +When you do **not** set `agentInstance`, the agent is built from `model`, `tools`, and `mcpServers`. You can then add more tools or MCP servers after app creation via `appkit.agent.addCapabilities()` (see [agent](Variable.agent.md) Plugin API). + +## Extends + +- [`BasePluginConfig`](Interface.BasePluginConfig.md) + +## Indexable + +```ts +[key: string]: unknown +``` + +## Properties + +### agentInstance? + +```ts +optional agentInstance: AgentInterface; +``` + +Pre-built agent implementing AgentInterface. +When provided the plugin skips internal LangGraph setup and delegates +directly to this instance. Use this to bring your own agent +implementation or a different LangChain variant. + +--- + +### host? + +```ts +optional host: string; +``` + +#### Inherited from + +[`BasePluginConfig`](Interface.BasePluginConfig.md).[`host`](Interface.BasePluginConfig.md#host) + +--- + +### maxTokens? + +```ts +optional maxTokens: number; +``` + +Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. + +--- + +### mcpServers? + +```ts +optional mcpServers: DatabricksMCPServer[]; +``` + +MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. You can add more at runtime with `appkit.agent.addCapabilities({ mcpServers: [...] })`. + +--- + +### model? + +```ts +optional model: string; +``` + +Databricks model serving endpoint name (e.g. "databricks-claude-sonnet-4-5"). +Falls back to DATABRICKS_MODEL env var. +Ignored when `agentInstance` is provided. + +--- + +### name? + +```ts +optional name: string; +``` + +#### Inherited from + +[`BasePluginConfig`](Interface.BasePluginConfig.md).[`name`](Interface.BasePluginConfig.md#name) + +--- + +### systemPrompt? + +```ts +optional systemPrompt: string; +``` + +System prompt injected at the start of every conversation + +--- + +### telemetry? + +```ts +optional telemetry: TelemetryOptions; +``` + +#### Inherited from + +[`BasePluginConfig`](Interface.BasePluginConfig.md).[`telemetry`](Interface.BasePluginConfig.md#telemetry) + +--- + +### temperature? + +```ts +optional temperature: number; +``` + +Sampling temperature (0.0-1.0, default 0.1). Ignored when `agentInstance` is provided. + +--- + +### tools? + +```ts +optional tools: StructuredTool[]; +``` + +Additional LangChain tools to register alongside MCP tools. Ignored when `agentInstance` is provided. You can add more at runtime with `appkit.agent.addCapabilities({ tools: [...] })`. + +--- + +### useResponsesApi? + +```ts +optional useResponsesApi: boolean; +``` + +Whether ChatDatabricks calls the upstream model using the Responses API +instead of the Chat Completions API. Default: false. +Ignored when `agentInstance` is provided. diff --git a/docs/docs/api/appkit/Interface.InvokeParams.md b/docs/docs/api/appkit/Interface.InvokeParams.md new file mode 100644 index 00000000..f4f360ea --- /dev/null +++ b/docs/docs/api/appkit/Interface.InvokeParams.md @@ -0,0 +1,38 @@ +# Interface: InvokeParams + +Agent interface types for the AppKit Agent Plugin. + +These types define the contract between the plugin framework and agent +implementations. They mirror the OpenAI Responses API SSE format without +requiring the `openai` package as a dependency. + +## Properties + +### chat\_history? + +```ts +optional chat_history: { + content: string; + role: string; +}[]; +``` + +#### content + +```ts +content: string; +``` + +#### role + +```ts +role: string; +``` + +*** + +### input + +```ts +input: string; +``` diff --git a/docs/docs/api/appkit/Interface.StandardAgent.md b/docs/docs/api/appkit/Interface.StandardAgent.md new file mode 100644 index 00000000..c1d483e1 --- /dev/null +++ b/docs/docs/api/appkit/Interface.StandardAgent.md @@ -0,0 +1,53 @@ +# Interface: StandardAgent + +## Implements + +- [`AgentInterface`](Interface.AgentInterface.md) + +## Methods + +### invoke() + +```ts +invoke(params: InvokeParams): Promise; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`Promise`\<`ResponseOutputItem`[]\> + +#### Implementation of + +```ts +AgentInterface.invoke +``` + +*** + +### stream() + +```ts +stream(params: InvokeParams): AsyncGenerator; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`AsyncGenerator`\<[`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)\> + +#### Implementation of + +```ts +AgentInterface.stream +``` diff --git a/docs/docs/api/appkit/TypeAlias.ResponseStreamEvent.md b/docs/docs/api/appkit/TypeAlias.ResponseStreamEvent.md new file mode 100644 index 00000000..4a4fb290 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.ResponseStreamEvent.md @@ -0,0 +1,11 @@ +# Type Alias: ResponseStreamEvent + +```ts +type ResponseStreamEvent = + | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent + | ResponseTextDeltaEvent + | ResponseCompletedEvent + | ResponseErrorEvent + | ResponseFailedEvent; +``` diff --git a/docs/docs/api/appkit/Variable.agent.md b/docs/docs/api/appkit/Variable.agent.md new file mode 100644 index 00000000..0d22d753 --- /dev/null +++ b/docs/docs/api/appkit/Variable.agent.md @@ -0,0 +1,21 @@ +# Variable: agent + +```ts +const agent: ToPlugin; +``` + +Plugin factory for the AppKit agent (LangChain/LangGraph). Use in `createApp({ plugins: [agent({ ... })] })`. Configuration: [`IAgentConfig`](Interface.IAgentConfig.md). + +## Plugin API (runtime) + +After `const appkit = await createApp({ plugins: [..., agent(config)] })`, `appkit.agent` exposes: + +| Method | Description | +| ------ | ------ | +| `invoke(messages)` | Run the agent (non-streaming). Returns the assistant reply text. | +| `stream(messages)` | Run the agent with streaming. Yields [`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)s. | +| `addCapabilities({ tools?, mcpServers? })` | Batch-add tools and/or MCP servers with a **single** agent rebuild. **Only when not using `agentInstance`.** | +| `addTools(tools)` | Add LangChain tools after app creation. Rebuilds the agent. Convenience wrapper around `addCapabilities`. **Only when not using `agentInstance`.** | +| `addMcpServers(servers)` | Add MCP servers after app creation. Rebuilds the agent and MCP client. Convenience wrapper around `addCapabilities`. **Only when not using `agentInstance`.** | + +When the plugin is configured with `model` and optional `tools` / `mcpServers` (i.e. without `agentInstance`), prefer `addCapabilities` to register both tools and MCP servers in one call instead of sequential `addTools` + `addMcpServers` (which would rebuild the agent twice). diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index 20e6af2d..6e453e81 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -30,18 +30,22 @@ plugin architecture, and React integration. | Interface | Description | | ------ | ------ | +| [AgentInterface](Interface.AgentInterface.md) | Contract that agent implementations must fulfil. | | [BasePluginConfig](Interface.BasePluginConfig.md) | Base configuration interface for AppKit plugins | | [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | | [DatabaseCredential](Interface.DatabaseCredential.md) | Database credentials with OAuth token for Postgres connection | | [GenerateDatabaseCredentialRequest](Interface.GenerateDatabaseCredentialRequest.md) | Request parameters for generating database OAuth credentials | +| [IAgentConfig](Interface.IAgentConfig.md) | Base configuration interface for AppKit plugins | +| [InvokeParams](Interface.InvokeParams.md) | Agent interface types for the AppKit Agent Plugin. | | [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | | [LakebasePoolConfig](Interface.LakebasePoolConfig.md) | Configuration for creating a Lakebase connection pool | -| [PluginManifest](Interface.PluginManifest.md) | Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. Extends the shared PluginManifest with strict resource types. | +| [PluginManifest](Interface.PluginManifest.md) | Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. | | [RequestedClaims](Interface.RequestedClaims.md) | Optional claims for fine-grained Unity Catalog table permissions When specified, the returned token will be scoped to only the requested tables | | [RequestedResource](Interface.RequestedResource.md) | Resource to request permissions for in Unity Catalog | | [ResourceEntry](Interface.ResourceEntry.md) | Internal representation of a resource in the registry. Extends ResourceRequirement with resolution state and plugin ownership. | | [ResourceFieldEntry](Interface.ResourceFieldEntry.md) | Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key). | -| [ResourceRequirement](Interface.ResourceRequirement.md) | Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements(). Narrows the generated base: type → ResourceType enum, permission → ResourcePermission union. | +| [ResourceRequirement](Interface.ResourceRequirement.md) | Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements(). | +| [StandardAgent](Interface.StandardAgent.md) | - | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | Configuration for streaming execution with default and user-scoped settings | | [TelemetryConfig](Interface.TelemetryConfig.md) | OpenTelemetry configuration for AppKit applications | | [ValidationResult](Interface.ValidationResult.md) | Result of validating all registered resources against the environment. | @@ -54,12 +58,14 @@ plugin architecture, and React integration. | [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration | | [PluginData](TypeAlias.PluginData.md) | - | | [ResourcePermission](TypeAlias.ResourcePermission.md) | Union of all possible permission levels across all resource types. | +| [ResponseStreamEvent](TypeAlias.ResponseStreamEvent.md) | - | | [ToPlugin](TypeAlias.ToPlugin.md) | - | ## Variables | Variable | Description | | ------ | ------ | +| [agent](Variable.agent.md) | Agent plugin factory; runtime API includes invoke, stream, addCapabilities, addTools, addMcpServers. | | [sql](Variable.sql.md) | SQL helper namespace | ## Functions diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 2f17b1d2..d4718669 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -82,6 +82,11 @@ const typedocSidebar: SidebarsConfig = { type: "category", label: "Interfaces", items: [ + { + type: "doc", + id: "api/appkit/Interface.AgentInterface", + label: "AgentInterface" + }, { type: "doc", id: "api/appkit/Interface.BasePluginConfig", @@ -102,6 +107,16 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.GenerateDatabaseCredentialRequest", label: "GenerateDatabaseCredentialRequest" }, + { + type: "doc", + id: "api/appkit/Interface.IAgentConfig", + label: "IAgentConfig" + }, + { + type: "doc", + id: "api/appkit/Interface.InvokeParams", + label: "InvokeParams" + }, { type: "doc", id: "api/appkit/Interface.ITelemetry", @@ -142,6 +157,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.ResourceRequirement", label: "ResourceRequirement" }, + { + type: "doc", + id: "api/appkit/Interface.StandardAgent", + label: "StandardAgent" + }, { type: "doc", id: "api/appkit/Interface.StreamExecutionSettings", @@ -183,6 +203,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/TypeAlias.ResourcePermission", label: "ResourcePermission" }, + { + type: "doc", + id: "api/appkit/TypeAlias.ResponseStreamEvent", + label: "ResponseStreamEvent" + }, { type: "doc", id: "api/appkit/TypeAlias.ToPlugin", @@ -194,6 +219,11 @@ const typedocSidebar: SidebarsConfig = { type: "category", label: "Variables", items: [ + { + type: "doc", + id: "api/appkit/Variable.agent", + label: "agent" + }, { type: "doc", id: "api/appkit/Variable.sql", diff --git a/docs/static/appkit-ui/styles.gen.css b/docs/static/appkit-ui/styles.gen.css index eb938427..d59184c5 100644 --- a/docs/static/appkit-ui/styles.gen.css +++ b/docs/static/appkit-ui/styles.gen.css @@ -8,6 +8,12 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-amber-700: oklch(55.5% 0.163 48.998); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-emerald-700: oklch(50.8% 0.118 165.612); --color-sky-500: oklch(68.5% 0.169 237.323); --color-blue-500: oklch(62.3% 0.214 259.815); --color-black: #000; @@ -503,6 +509,9 @@ .inline { display: inline; } + .inline-block { + display: inline-block; + } .inline-flex { display: inline-flex; } @@ -828,6 +837,9 @@ .max-w-\[80\%\] { max-width: 80%; } + .max-w-\[85\%\] { + max-width: 85%; + } .max-w-\[calc\(100\%-2rem\)\] { max-width: calc(100% - 2rem); } @@ -1261,6 +1273,12 @@ .border-\(--color-border\) { border-color: var(--color-border); } + .border-amber-500\/50 { + border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-amber-500) 50%, transparent); + } + } .border-border { border-color: var(--border); } @@ -1270,6 +1288,12 @@ border-color: color-mix(in oklab, var(--border) 50%, transparent); } } + .border-emerald-500\/50 { + border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-emerald-500) 50%, transparent); + } + } .border-input { border-color: var(--input); } @@ -1294,6 +1318,12 @@ .bg-accent { background-color: var(--accent); } + .bg-amber-500\/10 { + background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); + } + } .bg-background { background-color: var(--background); } @@ -1318,6 +1348,12 @@ background-color: color-mix(in oklab, var(--destructive) 10%, transparent); } } + .bg-emerald-500\/10 { + background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-500) 10%, transparent); + } + } .bg-foreground { background-color: var(--foreground); } @@ -1652,6 +1688,9 @@ .text-accent-foreground { color: var(--accent-foreground); } + .text-amber-700 { + color: var(--color-amber-700); + } .text-background { color: var(--background); } @@ -1667,6 +1706,9 @@ .text-destructive { color: var(--destructive); } + .text-emerald-700 { + color: var(--color-emerald-700); + } .text-foreground { color: var(--foreground); } @@ -4194,6 +4236,16 @@ background-color: transparent; } } + .dark\:text-amber-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-amber-400); + } + } + .dark\:text-emerald-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-emerald-400); + } + } .dark\:text-muted-foreground { @media (prefers-color-scheme: dark) { color: var(--muted-foreground); diff --git a/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx b/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx new file mode 100644 index 00000000..a8b3df1f --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx @@ -0,0 +1,45 @@ +import { AgentChatPart } from "./agent-chat-part"; +import type { ChatMessage } from "./types"; + +export interface AgentChatMessageProps { + message: ChatMessage; + isLast?: boolean; + isStreaming?: boolean; +} + +/** Renders a single chat message bubble (user or assistant with parts). */ +export function AgentChatMessage({ + message, + isLast = false, + isStreaming = false, +}: AgentChatMessageProps) { + if (message.role === "user") { + return ( +
+ + You + +
+ {message.content} +
+
+ ); + } + + return ( +
+ + Agent + +
+ {message.parts.map((part, j) => ( + + ))} +
+
+ ); +} diff --git a/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx b/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx new file mode 100644 index 00000000..4da5a1fa --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx @@ -0,0 +1,46 @@ +import type { AssistantPart } from "./types"; +import { tryFormatJson } from "./utils"; + +export interface AgentChatPartProps { + part: AssistantPart; + showCursor?: boolean; +} + +/** Renders a single assistant part: text, function_call, or function_call_output. */ +export function AgentChatPart({ + part, + showCursor = false, +}: AgentChatPartProps) { + if (part.type === "text") { + return ( +
+ {part.content} + {showCursor && |} +
+ ); + } + + if (part.type === "function_call") { + return ( +
+
+ Tool: {part.name} +
+
+          {tryFormatJson(part.arguments)}
+        
+
+ ); + } + + return ( +
+
+ Result +
+
+        {tryFormatJson(part.output)}
+      
+
+ ); +} diff --git a/packages/appkit-ui/src/react/agent-chat/agent-chat.tsx b/packages/appkit-ui/src/react/agent-chat/agent-chat.tsx new file mode 100644 index 00000000..1bda5061 --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/agent-chat.tsx @@ -0,0 +1,85 @@ +import { useEffect, useRef } from "react"; +import { cn } from "../lib/utils"; +import { Button, Card } from "../ui"; +import { AgentChatMessage } from "./agent-chat-message"; +import type { AgentChatProps, ChatMessage } from "./types"; +import { useAgentChat } from "./use-agent-chat"; + +/** Agent chat UI: message list + input, wired to POST /invocations SSE streaming. */ +export function AgentChat({ + invokeUrl = "/invocations", + placeholder = "Type a message...", + emptyMessage = "Send a message to start.", + className, +}: AgentChatProps) { + const scrollRef = useRef(null); + const { + displayMessages, + loading, + input, + setInput, + handleSubmit, + isStreamingText, + } = useAgentChat({ invokeUrl }); + + const contentLength = displayMessages.length; + // biome-ignore lint/correctness/useExhaustiveDependencies: deps used as triggers for auto-scroll + useEffect(() => { + scrollRef.current?.scrollTo({ + top: scrollRef.current.scrollHeight, + behavior: "smooth", + }); + }, [contentLength, isStreamingText]); + + return ( +
+ +
+ {displayMessages.length === 0 && ( +

{emptyMessage}

+ )} + {displayMessages.map((msg, i) => ( + + ))} +
+ +
+ setInput(e.target.value)} + placeholder={placeholder} + className="flex-1 rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + disabled={loading} + /> + +
+
+
+ ); +} + +function MessageItem({ + message, + isLast, + isStreaming, +}: { + message: ChatMessage; + isLast: boolean; + isStreaming: boolean; +}) { + return ( + + ); +} diff --git a/packages/appkit-ui/src/react/agent-chat/index.ts b/packages/appkit-ui/src/react/agent-chat/index.ts new file mode 100644 index 00000000..214204b8 --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/index.ts @@ -0,0 +1,6 @@ +export { AgentChat } from "./agent-chat"; +export { AgentChatMessage } from "./agent-chat-message"; +export { AgentChatPart } from "./agent-chat-part"; +export type * from "./types"; +export { useAgentChat } from "./use-agent-chat"; +export { serializeForApi, tryFormatJson } from "./utils"; diff --git a/packages/appkit-ui/src/react/agent-chat/types.ts b/packages/appkit-ui/src/react/agent-chat/types.ts new file mode 100644 index 00000000..519b4fd8 --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/types.ts @@ -0,0 +1,66 @@ +export type TextPart = { type: "text"; content: string }; +export type FunctionCallPart = { + type: "function_call"; + id: string; + callId: string; + name: string; + arguments: string; +}; +export type FunctionCallOutputPart = { + type: "function_call_output"; + id: string; + callId: string; + output: string; +}; +export type AssistantPart = + | TextPart + | FunctionCallPart + | FunctionCallOutputPart; + +export type ChatMessage = + | { role: "user"; content: string } + | { role: "assistant"; parts: AssistantPart[] }; + +export interface SSEItem { + type?: string; + id?: string; + call_id?: string; + name?: string; + arguments?: string; + output?: string; +} + +export interface SSEEvent { + type?: string; + delta?: string; + error?: string; + item?: SSEItem; +} + +export interface UseAgentChatOptions { + /** POST URL for invocations (Responses API). Default: "/invocations" */ + invokeUrl?: string; +} + +export interface UseAgentChatReturn { + messages: ChatMessage[]; + loading: boolean; + input: string; + setInput: (value: string) => void; + handleSubmit: (e: React.FormEvent) => void; + /** Messages + current streaming state for display */ + displayMessages: ChatMessage[]; + /** True when the last message is still streaming text */ + isStreamingText: boolean; +} + +export interface AgentChatProps { + /** POST URL for invocations. Default: "/invocations" */ + invokeUrl?: string; + /** Placeholder for the message input */ + placeholder?: string; + /** Empty state text when there are no messages */ + emptyMessage?: string; + /** Additional CSS class for the root container */ + className?: string; +} diff --git a/packages/appkit-ui/src/react/agent-chat/use-agent-chat.ts b/packages/appkit-ui/src/react/agent-chat/use-agent-chat.ts new file mode 100644 index 00000000..0e3ba0f7 --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/use-agent-chat.ts @@ -0,0 +1,174 @@ +import { useCallback, useMemo, useState } from "react"; +import type { + AssistantPart, + ChatMessage, + SSEEvent, + UseAgentChatOptions, + UseAgentChatReturn, +} from "./types"; +import { serializeForApi } from "./utils"; + +/** + * Manages agent chat state and streaming via POST /invocations (Responses API SSE). + * Returns messages, loading state, input state, submit handler, and derived display list. + */ +export function useAgentChat( + options: UseAgentChatOptions = {}, +): UseAgentChatReturn { + const { invokeUrl = "/invocations" } = options; + const [messages, setMessages] = useState([]); + const [streamingParts, setStreamingParts] = useState([]); + const [streamingText, setStreamingText] = useState(""); + const [loading, setLoading] = useState(false); + const [input, setInput] = useState(""); + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + const text = input.trim(); + if (!text || loading) return; + + const userMessage: ChatMessage = { role: "user", content: text }; + setInput(""); + setMessages((prev) => [...prev, userMessage]); + setLoading(true); + setStreamingParts([]); + setStreamingText(""); + + try { + const response = await fetch(invokeUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [...messages, userMessage].map(serializeForApi), + stream: true, + }), + }); + + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error( + (err as { error?: string }).error ?? `HTTP ${response.status}`, + ); + } + + const reader = response.body?.getReader(); + if (!reader) throw new Error("No response body"); + + const decoder = new TextDecoder(); + let buffer = ""; + let fullText = ""; + const parts: AssistantPart[] = []; + const seenItemIds = new Set(); + + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.startsWith("data: ")) continue; + const payload = line.slice(6); + if (payload === "[DONE]") continue; + + let data: SSEEvent; + try { + data = JSON.parse(payload); + } catch { + continue; + } + + if (data.type === "response.output_item.added" && data.item) { + const item = data.item; + const id = + item.id ?? `${item.type}_${Date.now()}_${Math.random()}`; + if (seenItemIds.has(id)) continue; + seenItemIds.add(id); + + if (item.type === "function_call" && item.name != null) { + parts.push({ + type: "function_call", + id, + callId: item.call_id ?? id, + name: item.name, + arguments: item.arguments ?? "{}", + }); + } else if ( + item.type === "function_call_output" && + item.call_id != null + ) { + parts.push({ + type: "function_call_output", + id, + callId: item.call_id, + output: item.output ?? "", + }); + } + setStreamingParts([...parts]); + } + + if (data.type === "response.output_text.delta" && data.delta) { + fullText += data.delta; + setStreamingText(fullText); + } + } + } + + const finalParts: AssistantPart[] = [...parts]; + if (fullText) { + finalParts.push({ type: "text", content: fullText }); + } + + setMessages((prev) => [ + ...prev, + { role: "assistant", parts: finalParts }, + ]); + setStreamingParts([]); + setStreamingText(""); + } catch (err) { + const errorText = + err instanceof Error ? err.message : "Something went wrong"; + setMessages((prev) => [ + ...prev, + { + role: "assistant", + parts: [{ type: "text", content: `Error: ${errorText}` }], + }, + ]); + setStreamingParts([]); + setStreamingText(""); + } finally { + setLoading(false); + } + }, + [input, loading, messages, invokeUrl], + ); + + const displayMessages = useMemo(() => { + if (loading && (streamingParts.length > 0 || streamingText)) { + const streamingPartList: AssistantPart[] = [...streamingParts]; + if (streamingText) { + streamingPartList.push({ type: "text", content: streamingText }); + } + return [ + ...messages, + { role: "assistant" as const, parts: streamingPartList }, + ]; + } + return messages; + }, [messages, streamingParts, streamingText, loading]); + + const isStreamingText = Boolean(loading && streamingText); + + return { + messages, + loading, + input, + setInput, + handleSubmit, + displayMessages, + isStreamingText, + }; +} diff --git a/packages/appkit-ui/src/react/agent-chat/utils.ts b/packages/appkit-ui/src/react/agent-chat/utils.ts new file mode 100644 index 00000000..497423f7 --- /dev/null +++ b/packages/appkit-ui/src/react/agent-chat/utils.ts @@ -0,0 +1,32 @@ +import type { ChatMessage } from "./types"; + +/** Serialize chat message for the Responses API request body. */ +export function serializeForApi(msg: ChatMessage): Record { + if (msg.role === "user") { + return { role: "user", content: msg.content }; + } + const content = msg.parts.map((p) => { + if (p.type === "text") + return { type: "output_text" as const, text: p.content }; + if (p.type === "function_call") + return { + type: "function_call" as const, + name: p.name, + arguments: p.arguments, + }; + return { + type: "function_call_output" as const, + call_id: p.callId, + output: p.output, + }; + }); + return { role: "assistant", content }; +} + +export function tryFormatJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} diff --git a/packages/appkit-ui/src/react/index.ts b/packages/appkit-ui/src/react/index.ts index 25aea4a4..005c2d2c 100644 --- a/packages/appkit-ui/src/react/index.ts +++ b/packages/appkit-ui/src/react/index.ts @@ -1,3 +1,4 @@ +export * from "./agent-chat"; export * from "./charts"; export * from "./file-browser"; export * from "./genie"; diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 5b8f01fd..ee3e5164 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -48,6 +48,30 @@ "tarball": "rm -rf tmp && pnpm dist && npm pack ./tmp --pack-destination ./tmp", "typecheck": "tsc --noEmit" }, + "peerDependencies": { + "@arizeai/openinference-instrumentation-langchain": ">=4.0.0", + "@databricks/langchainjs": ">=0.1.0", + "@langchain/core": ">=1.0.0", + "@langchain/langgraph": ">=1.0.0", + "@langchain/mcp-adapters": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@arizeai/openinference-instrumentation-langchain": { + "optional": true + }, + "@databricks/langchainjs": { + "optional": true + }, + "@langchain/core": { + "optional": true + }, + "@langchain/langgraph": { + "optional": true + }, + "@langchain/mcp-adapters": { + "optional": true + } + }, "dependencies": { "@databricks/lakebase": "workspace:*", "@databricks/sdk-experimental": "^0.16.0", @@ -75,7 +99,8 @@ "semver": "^7.7.3", "shared": "workspace:*", "vite": "npm:rolldown-vite@7.1.14", - "ws": "^8.18.3" + "ws": "^8.18.3", + "zod": "^4.3.6" }, "devDependencies": { "@types/express": "^4.17.25", diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 8db7f1d7..b6e42b6a 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -48,7 +48,14 @@ export { } from "./errors"; // Plugin authoring export { Plugin, type ToPlugin, toPlugin } from "./plugin"; -export { analytics, files, genie, lakebase, server } from "./plugins"; +export { agent, analytics, files, genie, lakebase, server } from "./plugins"; +export type { + AgentInterface, + IAgentConfig, + InvokeParams, + ResponseStreamEvent, + StandardAgent, +} from "./plugins/agent"; // Registry types and utilities for plugin manifests export type { ConfigSchema, diff --git a/packages/appkit/src/plugins/agent/agent-interface.ts b/packages/appkit/src/plugins/agent/agent-interface.ts new file mode 100644 index 00000000..2e8a65bc --- /dev/null +++ b/packages/appkit/src/plugins/agent/agent-interface.ts @@ -0,0 +1,121 @@ +/** + * Agent interface types for the AppKit Agent Plugin. + * + * These types define the contract between the plugin framework and agent + * implementations. They mirror the OpenAI Responses API SSE format without + * requiring the `openai` package as a dependency. + */ + +// --------------------------------------------------------------------------- +// Invoke params +// --------------------------------------------------------------------------- + +export interface InvokeParams { + input: string; + chat_history?: Array<{ role: string; content: string }>; +} + +// --------------------------------------------------------------------------- +// Responses API output types (minimal subset) +// --------------------------------------------------------------------------- + +export interface ResponseOutputTextContent { + type: "output_text"; + text: string; + annotations: unknown[]; +} + +export interface ResponseOutputMessage { + id: string; + type: "message"; + role: "assistant"; + status: "in_progress" | "completed"; + content: ResponseOutputTextContent[]; +} + +export interface ResponseFunctionToolCall { + id: string; + type: "function_call"; + call_id: string; + name: string; + arguments: string; + status: "completed"; +} + +export interface ResponseFunctionCallOutput { + id: string; + type: "function_call_output"; + call_id: string; + output: string; +} + +export type ResponseOutputItem = + | ResponseOutputMessage + | ResponseFunctionToolCall + | ResponseFunctionCallOutput; + +// --------------------------------------------------------------------------- +// Responses API SSE event types +// --------------------------------------------------------------------------- + +export interface ResponseOutputItemAddedEvent { + type: "response.output_item.added"; + item: ResponseOutputItem; + output_index: number; + sequence_number: number; +} + +export interface ResponseOutputItemDoneEvent { + type: "response.output_item.done"; + item: ResponseOutputItem; + output_index: number; + sequence_number: number; +} + +export interface ResponseTextDeltaEvent { + type: "response.output_text.delta"; + item_id: string; + output_index: number; + content_index: number; + delta: string; + sequence_number: number; +} + +export interface ResponseCompletedEvent { + type: "response.completed"; + sequence_number: number; + response: Record; +} + +export interface ResponseErrorEvent { + type: "error"; + error: string; +} + +export interface ResponseFailedEvent { + type: "response.failed"; +} + +export type ResponseStreamEvent = + | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent + | ResponseTextDeltaEvent + | ResponseCompletedEvent + | ResponseErrorEvent + | ResponseFailedEvent; + +// --------------------------------------------------------------------------- +// Agent interface +// --------------------------------------------------------------------------- + +/** + * Contract that agent implementations must fulfil. + * + * The plugin calls `invoke()` for non-streaming requests and `stream()` for + * SSE streaming. Implementations are responsible for translating their SDK's + * output into Responses API types. + */ +export interface AgentInterface { + invoke(params: InvokeParams): Promise; + stream(params: InvokeParams): AsyncGenerator; +} diff --git a/packages/appkit/src/plugins/agent/agent.ts b/packages/appkit/src/plugins/agent/agent.ts new file mode 100644 index 00000000..d0d0bf34 --- /dev/null +++ b/packages/appkit/src/plugins/agent/agent.ts @@ -0,0 +1,283 @@ +/** + * AgentPlugin — first-class AppKit plugin for LangChain/LangGraph agents. + * + * Provides: + * - POST /api/agent (standard AppKit namespaced route) + * + * Supports two modes: + * 1. Bring-your-own agent via `config.agentInstance` + * 2. Auto-build a LangGraph ReAct agent from config (model, tools, MCP servers) + * + * When using config (not agentInstance), you can add tools and MCP servers + * after app creation via appkit.agent.addTools() and appkit.agent.addMcpServers(). + */ + +import type { DatabricksMCPServer } from "@databricks/langchainjs"; +import type { StructuredToolInterface } from "@langchain/core/tools"; +import type express from "express"; +import { createLogger } from "../../logging/logger"; +import { Plugin, toPlugin } from "../../plugin"; +import type { AgentInterface } from "./agent-interface"; +import { createInvokeHandler } from "./invoke-handler"; +import manifest from "./manifest.json"; +import { StandardAgent } from "./standard-agent"; +import type { IAgentConfig } from "./types"; + +const logger = createLogger("agent"); + +const DEFAULT_SYSTEM_PROMPT = + "You are a helpful AI assistant with access to various tools."; + +type ChatDatabricksInstance = InstanceType< + Awaited["ChatDatabricks"] +>; + +export class AgentPlugin extends Plugin { + public name = "agent" as const; + + static manifest = manifest; + + protected declare config: IAgentConfig; + + private agentImpl: AgentInterface | null = null; + private systemPrompt = DEFAULT_SYSTEM_PROMPT; + private mcpClient: { + getTools(): Promise; + close(): Promise; + } | null = null; + + /** Only set when building from config (not agentInstance). Used when rebuilding after addTools/addMcpServers. */ + private model: ChatDatabricksInstance | null = null; + /** Mutable list of tools (config + added). Only used when building from config. */ + private toolsList: StructuredToolInterface[] = []; + /** Mutable list of MCP servers (config + added). Only used when building from config. */ + private mcpServersList: DatabricksMCPServer[] = []; + + async setup() { + this.systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; + + // If a pre-built agent is provided, use it directly + if (this.config.agentInstance) { + this.agentImpl = this.config.agentInstance; + logger.info("AgentPlugin initialized with provided agentInstance"); + return; + } + + // Otherwise build a LangGraph ReAct agent from config + const modelName = this.config.model ?? process.env.DATABRICKS_MODEL; + + if (!modelName) { + throw new Error( + "AgentPlugin: model name is required. Set config.model or DATABRICKS_MODEL env var.", + ); + } + + const { ChatDatabricks } = await import("@databricks/langchainjs"); + + this.model = new ChatDatabricks({ + model: modelName, + useResponsesApi: this.config.useResponsesApi ?? false, + temperature: this.config.temperature ?? 0.1, + maxTokens: this.config.maxTokens ?? 2000, + maxRetries: 3, + }); + + this.toolsList = [...(this.config.tools ?? [])]; + this.mcpServersList = [...(this.config.mcpServers ?? [])]; + + await this.buildStandardAgent(); + + logger.info( + "AgentPlugin initialized: model=%s tools=%d mcpServers=%d", + modelName, + this.toolsList.length, + this.mcpServersList.length, + ); + } + + /** + * Builds or rebuilds the LangGraph ReAct agent from current model, toolsList, and mcpServersList. + * Call this after changing toolsList or mcpServersList (e.g. via addTools/addMcpServers). + */ + private async buildStandardAgent(): Promise { + if (!this.model) return; + + // Close existing MCP client before creating a new one + if (this.mcpClient) { + try { + await this.mcpClient.close(); + } catch (err) { + logger.warn("Error closing MCP client during rebuild: %O", err); + } + this.mcpClient = null; + } + + const tools: StructuredToolInterface[] = []; + + if (this.mcpServersList.length > 0) { + try { + const { buildMCPServerConfig } = await import( + "@databricks/langchainjs" + ); + const mcpServerConfigs = await buildMCPServerConfig( + this.mcpServersList, + ); + const { MultiServerMCPClient } = await import( + "@langchain/mcp-adapters" + ); + this.mcpClient = new MultiServerMCPClient({ + mcpServers: mcpServerConfigs, + throwOnLoadError: false, + prefixToolNameWithServerName: true, + }); + const mcpTools = await this.mcpClient.getTools(); + tools.push(...mcpTools); + logger.info( + "Loaded %d MCP tools from %d server(s)", + mcpTools.length, + this.mcpServersList.length, + ); + } catch (err) { + logger.warn( + "Failed to load MCP tools — continuing without them: %O", + err, + ); + } + } + + tools.push(...this.toolsList); + + const { createReactAgent } = await import("@langchain/langgraph/prebuilt"); + const langGraphAgent = createReactAgent({ + llm: this.model, + tools, + }); + + this.agentImpl = new StandardAgent( + langGraphAgent as any, + this.systemPrompt, + ); + } + + /** + * Batch-add tools and/or MCP servers with a single agent rebuild. + * Only supported when the plugin was initialized from config (not agentInstance). + */ + async addCapabilities(options: { + tools?: StructuredToolInterface[]; + mcpServers?: DatabricksMCPServer[]; + }): Promise { + if (this.config.agentInstance) { + throw new Error( + "addCapabilities() is not supported when using a custom agentInstance", + ); + } + if (!this.model) { + throw new Error("AgentPlugin not initialized — call setup() first"); + } + + const { tools, mcpServers } = options; + if (tools?.length) this.toolsList.push(...tools); + if (mcpServers?.length) this.mcpServersList.push(...mcpServers); + + await this.buildStandardAgent(); + + logger.info( + "Configured agent: added %d tool(s), %d MCP server(s); totals tools=%d servers=%d", + tools?.length ?? 0, + mcpServers?.length ?? 0, + this.toolsList.length, + this.mcpServersList.length, + ); + } + + /** + * Add tools to the agent after app creation. Only supported when the plugin + * was initialized from config (not when using agentInstance). Rebuilds the + * underlying LangGraph agent with the new tool set. + */ + async addTools(tools: StructuredToolInterface[]): Promise { + await this.addCapabilities({ tools }); + } + + /** + * Add MCP servers to the agent after app creation. Only supported when the + * plugin was initialized from config (not when using agentInstance). Rebuilds + * the underlying LangGraph agent so new MCP tools are available. + */ + async addMcpServers(servers: DatabricksMCPServer[]): Promise { + await this.addCapabilities({ mcpServers: servers }); + } + + private getAgentImpl(): AgentInterface { + if (!this.agentImpl) { + throw new Error("AgentPlugin not initialized — call setup() first"); + } + return this.agentImpl; + } + + injectRoutes(router: express.Router) { + const handler = createInvokeHandler(() => this.getAgentImpl()); + router.post("/", handler); + this.registerEndpoint("invoke", `/api/${this.name}`); + } + + async abortActiveOperations() { + await super.abortActiveOperations(); + if (this.mcpClient) { + try { + await this.mcpClient.close(); + } catch (err) { + logger.warn("Error closing MCP client: %O", err); + } + } + } + + exports() { + return { + invoke: async ( + messages: { role: string; content: string }[], + ): Promise => { + if (!this.agentImpl) { + throw new Error("AgentPlugin not initialized"); + } + const lastUser = [...messages].reverse().find((m) => m.role === "user"); + const input = lastUser?.content ?? ""; + const chatHistory = messages.slice(0, -1); + const items = await this.agentImpl.invoke({ + input, + chat_history: chatHistory, + }); + const msg = items.find((i) => i.type === "message") as any; + const text = msg?.content?.[0]?.text ?? ""; + return text; + }, + + stream: async function* ( + this: AgentPlugin, + messages: { role: string; content: string }[], + ) { + if (!this.agentImpl) { + throw new Error("AgentPlugin not initialized"); + } + const lastUser = [...messages].reverse().find((m) => m.role === "user"); + const input = lastUser?.content ?? ""; + const chatHistory = messages.slice(0, -1); + yield* this.agentImpl.stream({ + input, + chat_history: chatHistory, + }); + }.bind(this), + + addTools: (tools: StructuredToolInterface[]) => this.addTools(tools), + addMcpServers: (servers: DatabricksMCPServer[]) => + this.addMcpServers(servers), + addCapabilities: (options: { + tools?: StructuredToolInterface[]; + mcpServers?: DatabricksMCPServer[]; + }) => this.addCapabilities(options), + }; + } +} + +export const agent = toPlugin(AgentPlugin); diff --git a/packages/appkit/src/plugins/agent/index.ts b/packages/appkit/src/plugins/agent/index.ts new file mode 100644 index 00000000..616e7f8c --- /dev/null +++ b/packages/appkit/src/plugins/agent/index.ts @@ -0,0 +1,14 @@ +export { AgentPlugin, agent } from "./agent"; +export type { + AgentInterface, + InvokeParams, + ResponseFunctionCallOutput, + ResponseFunctionToolCall, + ResponseOutputItem, + ResponseOutputMessage, + ResponseStreamEvent, +} from "./agent-interface"; +export { createInvokeHandler } from "./invoke-handler"; +export type { LangGraphAgent } from "./standard-agent"; +export { StandardAgent } from "./standard-agent"; +export type { IAgentConfig } from "./types"; diff --git a/packages/appkit/src/plugins/agent/invoke-handler.ts b/packages/appkit/src/plugins/agent/invoke-handler.ts new file mode 100644 index 00000000..20af5798 --- /dev/null +++ b/packages/appkit/src/plugins/agent/invoke-handler.ts @@ -0,0 +1,160 @@ +/** + * Responses API invoke handler for the agent plugin. + * + * Accepts Responses API request format, parses it into InvokeParams, then + * delegates to AgentInterface.stream() / AgentInterface.invoke(). The handler + * is a pure pass-through — all SSE event shaping happens inside the agent. + */ + +import type express from "express"; +import { z } from "zod"; +import type { AgentInterface } from "./agent-interface"; + +const responsesRequestSchema = z.object({ + input: z.union([ + z.string(), + z.array( + z.union([ + z.object({ + role: z.enum(["user", "assistant", "system"]), + content: z.union([ + z.string(), + z.array( + z.union([ + z.object({ type: z.string(), text: z.string() }).passthrough(), + z.object({ type: z.string() }).passthrough(), + ]), + ), + ]), + }), + z.object({ type: z.string() }).passthrough(), + ]), + ), + ]), + stream: z.boolean().optional().default(true), + model: z.string().optional(), +}); + +/** + * Flatten a Responses API message to a plain `{ role, content }` object. + * Handles function_call / function_call_output items and array content. + */ +function flattenHistoryItem(item: any): { role: string; content: string } { + if (item.type === "function_call") { + return { + role: "assistant", + content: `[Tool Call: ${item.name}(${item.arguments})]`, + }; + } + if (item.type === "function_call_output") { + return { role: "assistant", content: `[Tool Result: ${item.output}]` }; + } + + if (Array.isArray(item.content)) { + const textParts = item.content + .filter( + (p: any) => + p.type === "input_text" || + p.type === "output_text" || + p.type === "text", + ) + .map((p: any) => p.text); + + const toolParts = item.content + .filter( + (p: any) => + p.type === "function_call" || p.type === "function_call_output", + ) + .map((p: any) => + p.type === "function_call" + ? `[Tool Call: ${p.name}(${JSON.stringify(p.arguments)})]` + : `[Tool Result: ${p.output}]`, + ); + + const allParts = [...textParts, ...toolParts].filter((p) => p.length > 0); + return { ...item, content: allParts.join("\n") }; + } + + return { role: item.role ?? "user", content: item.content ?? "" }; +} + +/** + * Create an Express handler that invokes the agent via the AgentInterface + * and streams/returns the response in Responses API format. + */ +export function createInvokeHandler( + getAgent: () => AgentInterface, +): express.RequestHandler { + return async (req: express.Request, res: express.Response) => { + try { + const parsed = responsesRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ + error: "Invalid request format", + details: parsed.error.format(), + }); + return; + } + + const { stream } = parsed.data; + + const input = + typeof parsed.data.input === "string" + ? [{ role: "user" as const, content: parsed.data.input }] + : parsed.data.input; + + const userMessages = input.filter((msg: any) => msg.role === "user"); + if (userMessages.length === 0) { + res.status(400).json({ error: "No user message found in input" }); + return; + } + + const lastUserMessage = userMessages[userMessages.length - 1]; + + let userInput: string; + if (Array.isArray(lastUserMessage.content)) { + userInput = lastUserMessage.content + .filter( + (part: any) => part.type === "input_text" || part.type === "text", + ) + .map((part: any) => part.text) + .join("\n"); + } else { + userInput = lastUserMessage.content as string; + } + + const chatHistory = input.slice(0, -1).map(flattenHistoryItem); + + const agentParams = { input: userInput, chat_history: chatHistory }; + const agent = getAgent(); + + if (stream) { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + try { + for await (const event of agent.stream(agentParams)) { + res.write(`data: ${JSON.stringify(event)}\n\n`); + } + res.write("data: [DONE]\n\n"); + res.end(); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + res.write( + `data: ${JSON.stringify({ type: "error", error: message })}\n\n`, + ); + res.write(`data: ${JSON.stringify({ type: "response.failed" })}\n\n`); + res.write("data: [DONE]\n\n"); + res.end(); + } + } else { + const items = await agent.invoke(agentParams); + res.json({ output: items }); + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + res.status(500).json({ error: "Internal server error", message }); + } + }; +} diff --git a/packages/appkit/src/plugins/agent/manifest.json b/packages/appkit/src/plugins/agent/manifest.json new file mode 100644 index 00000000..ee10116e --- /dev/null +++ b/packages/appkit/src/plugins/agent/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "agent", + "displayName": "Agent Plugin", + "description": "LangChain/LangGraph AI agent with streaming Responses API and MCP tool support", + "resources": { + "required": [ + { + "type": "serving_endpoint", + "alias": "Model Endpoint", + "resourceKey": "agent-model-endpoint", + "description": "Databricks model serving endpoint for the agent LLM", + "permission": "CAN_QUERY", + "fields": { + "name": { + "env": "DATABRICKS_MODEL", + "description": "Model serving endpoint name" + } + } + } + ], + "optional": [] + } +} diff --git a/packages/appkit/src/plugins/agent/standard-agent.ts b/packages/appkit/src/plugins/agent/standard-agent.ts new file mode 100644 index 00000000..67c607da --- /dev/null +++ b/packages/appkit/src/plugins/agent/standard-agent.ts @@ -0,0 +1,225 @@ +/** + * StandardAgent — LangGraph wrapper implementing AgentInterface. + * + * Wraps a LangGraph `createReactAgent` instance and translates its stream + * events into Responses API SSE format. If you swap LangGraph for another + * SDK, provide your own AgentInterface implementation instead. + */ + +import { randomUUID } from "node:crypto"; +import type { BaseMessage } from "@langchain/core/messages"; +import { HumanMessage, SystemMessage } from "@langchain/core/messages"; +import type { + AgentInterface, + InvokeParams, + ResponseFunctionToolCall, + ResponseOutputItem, + ResponseOutputMessage, + ResponseStreamEvent, +} from "./agent-interface"; + +/** + * Minimal interface for the LangGraph agent returned by createReactAgent. + */ +export interface LangGraphAgent { + invoke(input: { + messages: BaseMessage[]; + }): Promise<{ messages: BaseMessage[] }>; + streamEvents( + input: { messages: BaseMessage[] }, + options: { version: "v1" | "v2" }, + ): AsyncIterable<{ + event: string; + name: string; + run_id: string; + data?: any; + }>; +} + +function convertToBaseMessages(messages: any[]): BaseMessage[] { + return messages.map((msg) => { + if (msg instanceof HumanMessage || msg instanceof SystemMessage) { + return msg; + } + const content = msg.content || ""; + switch (msg.role) { + case "user": + return new HumanMessage(content); + case "assistant": + return { role: "assistant", content } as any; + case "system": + return new SystemMessage(content); + default: + return new HumanMessage(content); + } + }); +} + +export class StandardAgent implements AgentInterface { + constructor( + private agent: LangGraphAgent, + private systemPrompt: string, + ) {} + + async invoke(params: InvokeParams): Promise { + const { input, chat_history = [] } = params; + + const messages: BaseMessage[] = [ + new SystemMessage(this.systemPrompt), + ...convertToBaseMessages(chat_history), + new HumanMessage(input), + ]; + + const result = await this.agent.invoke({ messages }); + const finalMessages = result.messages || []; + const lastMessage = finalMessages[finalMessages.length - 1]; + const text = + typeof lastMessage?.content === "string" ? lastMessage.content : ""; + + const outputMessage: ResponseOutputMessage = { + id: `msg_${randomUUID()}`, + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text, annotations: [] }], + }; + + return [outputMessage]; + } + + async *stream(params: InvokeParams): AsyncGenerator { + const { input, chat_history = [] } = params; + + const messages: BaseMessage[] = [ + new SystemMessage(this.systemPrompt), + ...convertToBaseMessages(chat_history), + new HumanMessage(input), + ]; + + const toolCallIds = new Map(); + let seqNum = 0; + let outputIndex = 0; + const textItemId = `msg_${randomUUID()}`; + let textOutputIndex = -1; + + const eventStream = this.agent.streamEvents( + { messages }, + { version: "v2" }, + ); + + for await (const event of eventStream) { + if (event.event === "on_tool_start") { + const callId = `call_${randomUUID()}`; + toolCallIds.set(`${event.name}_${event.run_id}`, callId); + + const fcItem: ResponseFunctionToolCall = { + id: `fc_${randomUUID()}`, + call_id: callId, + name: event.name, + arguments: JSON.stringify(event.data?.input || {}), + type: "function_call", + status: "completed", + }; + + const currentIndex = outputIndex++; + + yield { + type: "response.output_item.added", + item: fcItem, + output_index: currentIndex, + sequence_number: seqNum++, + }; + + yield { + type: "response.output_item.done", + item: fcItem, + output_index: currentIndex, + sequence_number: seqNum++, + }; + } + + if (event.event === "on_tool_end") { + const toolKey = `${event.name}_${event.run_id}`; + const callId = toolCallIds.get(toolKey) || `call_${randomUUID()}`; + toolCallIds.delete(toolKey); + + const outputItem = { + id: `fco_${randomUUID()}`, + call_id: callId, + output: JSON.stringify(event.data?.output || ""), + type: "function_call_output" as const, + }; + + const currentIndex = outputIndex++; + + yield { + type: "response.output_item.added", + item: outputItem, + output_index: currentIndex, + sequence_number: seqNum++, + }; + + yield { + type: "response.output_item.done", + item: outputItem, + output_index: currentIndex, + sequence_number: seqNum++, + }; + } + + if (event.event === "on_chat_model_stream") { + const content = event.data?.chunk?.content; + if (content && typeof content === "string") { + if (textOutputIndex === -1) { + textOutputIndex = outputIndex++; + + const msgItem: ResponseOutputMessage = { + id: textItemId, + type: "message", + role: "assistant", + status: "in_progress", + content: [], + }; + yield { + type: "response.output_item.added", + item: msgItem, + output_index: textOutputIndex, + sequence_number: seqNum++, + }; + } + + yield { + type: "response.output_text.delta", + item_id: textItemId, + output_index: textOutputIndex, + content_index: 0, + delta: content, + sequence_number: seqNum++, + }; + } + } + } + + if (textOutputIndex !== -1) { + const msgItem: ResponseOutputMessage = { + id: textItemId, + type: "message", + role: "assistant", + status: "completed", + content: [], + }; + yield { + type: "response.output_item.done", + item: msgItem, + output_index: textOutputIndex, + sequence_number: seqNum++, + }; + } + + yield { + type: "response.completed", + sequence_number: seqNum++, + response: {}, + }; + } +} diff --git a/packages/appkit/src/plugins/agent/tests/agent.integration.test.ts b/packages/appkit/src/plugins/agent/tests/agent.integration.test.ts new file mode 100644 index 00000000..d4c2fdb6 --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/agent.integration.test.ts @@ -0,0 +1,228 @@ +import type { Server } from "node:http"; +import { mockServiceContext, setupDatabricksEnv } from "@tools/test-helpers"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +process.env.DATABRICKS_APP_PORT = "8000"; +process.env.FLASK_RUN_HOST = "0.0.0.0"; +process.env.DATABRICKS_MODEL = "test-model"; + +import { ServiceContext } from "../../../context/service-context"; +import { createApp } from "../../../core"; +import { server as serverPlugin } from "../../server/index"; +import { agent } from "../agent"; +import { StubAgent } from "./stub-agent"; + +function parseSSEStream(text: string) { + const events: any[] = []; + let fullOutput = ""; + const lines = text.split("\n"); + for (const line of lines) { + if (line.startsWith("data: ") && line !== "data: [DONE]") { + try { + const data = JSON.parse(line.slice(6)); + events.push(data); + if (data.type === "response.output_text.delta") { + fullOutput += data.delta; + } + } catch {} + } + } + return { events, fullOutput }; +} + +describe("AgentPlugin Integration", () => { + let server: Server; + let baseUrl: string; + let serviceContextMock: Awaited>; + const TEST_PORT = 9885; + + beforeAll(async () => { + setupDatabricksEnv(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + + const app = await createApp({ + plugins: [ + agent({ agentInstance: new StubAgent() }), + serverPlugin({ + port: TEST_PORT, + host: "127.0.0.1", + autoStart: false, + }), + ], + }); + + await app.server.start(); + server = app.server.getServer(); + baseUrl = `http://127.0.0.1:${TEST_PORT}`; + + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + afterAll(async () => { + serviceContextMock?.restore(); + if (server) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + }); + + describe("POST /api/agent (streaming)", () => { + test("streams SSE events and completes", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [{ role: "user", content: "Hello agent" }], + stream: true, + }), + }); + + expect(response.ok).toBe(true); + expect(response.headers.get("content-type")).toContain( + "text/event-stream", + ); + + const text = await response.text(); + const { events, fullOutput } = parseSSEStream(text); + + expect(fullOutput).toContain("Echo: Hello agent"); + + const hasCompleted = events.some((e) => e.type === "response.completed"); + expect(hasCompleted).toBe(true); + + expect(text).toContain("data: [DONE]"); + }); + }); + + describe("non-streaming mode", () => { + test("returns JSON response", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [{ role: "user", content: "No stream" }], + stream: false, + }), + }); + + expect(response.ok).toBe(true); + const data = (await response.json()) as { + output: { type: string; content: { text: string }[] }[]; + }; + + expect(data.output).toBeDefined(); + expect(data.output).toHaveLength(1); + expect(data.output[0].type).toBe("message"); + expect(data.output[0].content[0].text).toContain("Echo: No stream"); + }); + }); + + describe("string input", () => { + test("accepts plain string as input", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: "Plain string input", + stream: true, + }), + }); + + expect(response.ok).toBe(true); + const text = await response.text(); + const { fullOutput } = parseSSEStream(text); + expect(fullOutput).toContain("Echo: Plain string input"); + }); + }); + + describe("multi-turn conversations", () => { + test("handles chat history", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [ + { role: "user", content: "My name is Alice" }, + { + role: "assistant", + content: "Nice to meet you, Alice", + }, + { + role: "user", + content: "What is my name?", + }, + ], + stream: true, + }), + }); + + expect(response.ok).toBe(true); + const text = await response.text(); + const { fullOutput } = parseSSEStream(text); + expect(fullOutput).toContain("Echo: What is my name?"); + }); + + test("handles function_call items in history", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [ + { role: "user", content: "Look up the answer" }, + { + type: "function_call", + name: "search", + arguments: '{"q":"test"}', + }, + { + type: "function_call_output", + output: '"42"', + }, + { + role: "user", + content: "What did you find?", + }, + ], + stream: true, + }), + }); + + expect(response.ok).toBe(true); + const text = await response.text(); + const { fullOutput } = parseSSEStream(text); + expect(fullOutput.length).toBeGreaterThan(0); + }); + }); + + describe("error responses", () => { + test("returns 400 for malformed input", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stream: true }), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + }); + + test("returns 400 when no user message", async () => { + const response = await fetch(`${baseUrl}/api/agent`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + input: [{ role: "assistant", content: "Only assistant" }], + stream: true, + }), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + }); + }); +}); diff --git a/packages/appkit/src/plugins/agent/tests/agent.test.ts b/packages/appkit/src/plugins/agent/tests/agent.test.ts new file mode 100644 index 00000000..4816b0cb --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/agent.test.ts @@ -0,0 +1,172 @@ +import { + createMockRouter, + mockServiceContext, + setupDatabricksEnv, +} from "@tools/test-helpers"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { ServiceContext } from "../../../context/service-context"; +import { AgentPlugin, agent } from "../agent"; +import type { IAgentConfig } from "../types"; +import { StubAgent } from "./stub-agent"; + +// Mock CacheManager singleton +vi.mock("../../../cache", () => ({ + CacheManager: { + getInstanceSync: vi.fn(() => ({ + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + getOrExecute: vi.fn(async (_key: unknown[], fn: () => Promise) => + fn(), + ), + generateKey: vi.fn(() => "test-key"), + })), + }, +})); + +describe("AgentPlugin", () => { + let serviceContextMock: Awaited>; + + beforeEach(async () => { + setupDatabricksEnv(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + }); + + afterEach(() => { + serviceContextMock?.restore(); + }); + + test("agent factory produces correct plugin data", () => { + const pluginData = agent({ agentInstance: new StubAgent() }); + expect(pluginData.name).toBe("agent"); + }); + + test("plugin has correct manifest", () => { + expect(AgentPlugin.manifest).toBeDefined(); + expect(AgentPlugin.manifest.name).toBe("agent"); + expect(AgentPlugin.manifest.resources.required).toHaveLength(1); + expect(AgentPlugin.manifest.resources.required[0].type).toBe( + "serving_endpoint", + ); + }); + + test("plugin instance has correct name", () => { + const config: IAgentConfig = { agentInstance: new StubAgent() }; + const plugin = new AgentPlugin(config); + expect(plugin.name).toBe("agent"); + }); + + describe("setup()", () => { + test("uses provided agentInstance", async () => { + const stub = new StubAgent(); + const config: IAgentConfig = { agentInstance: stub }; + const plugin = new AgentPlugin(config); + + await plugin.setup(); + + const exported = plugin.exports(); + const result = await exported.invoke([{ role: "user", content: "hi" }]); + expect(result).toContain("Echo: hi"); + }); + + test("throws when no model and no agentInstance", async () => { + const config: IAgentConfig = {}; + const plugin = new AgentPlugin(config); + + await expect(plugin.setup()).rejects.toThrow("model name is required"); + }); + + test("resolves model from env var when not in config", async () => { + process.env.DATABRICKS_MODEL = "test-model"; + + const config: IAgentConfig = {}; + const plugin = new AgentPlugin(config); + + // Will fail because ChatDatabricks isn't available, but it + // should get past the model name check + try { + await plugin.setup(); + } catch (e: any) { + expect(e.message).not.toContain("model name is required"); + } + + delete process.env.DATABRICKS_MODEL; + }); + }); + + describe("injectRoutes()", () => { + test("registers POST handler on router", () => { + const stub = new StubAgent(); + const config: IAgentConfig = { agentInstance: stub }; + const plugin = new AgentPlugin(config); + + const { router } = createMockRouter(); + plugin.injectRoutes(router as any); + + expect(router.post).toHaveBeenCalledWith("/", expect.any(Function)); + }); + }); + + describe("exports()", () => { + test("returns invoke and stream methods", async () => { + const stub = new StubAgent(); + const config: IAgentConfig = { agentInstance: stub }; + const plugin = new AgentPlugin(config); + await plugin.setup(); + + const exported = plugin.exports(); + + expect(typeof exported.invoke).toBe("function"); + expect(typeof exported.stream).toBe("function"); + }); + + test("invoke returns text from agent response", async () => { + const stub = new StubAgent(); + const config: IAgentConfig = { agentInstance: stub }; + const plugin = new AgentPlugin(config); + await plugin.setup(); + + const result = await plugin + .exports() + .invoke([{ role: "user", content: "test message" }]); + + expect(result).toBe("Echo: test message"); + }); + + test("stream yields ResponseStreamEvents", async () => { + const stub = new StubAgent(); + const config: IAgentConfig = { agentInstance: stub }; + const plugin = new AgentPlugin(config); + await plugin.setup(); + + const events: any[] = []; + for await (const event of plugin + .exports() + .stream([{ role: "user", content: "hello" }])) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + const deltaEvent = events.find( + (e) => e.type === "response.output_text.delta", + ); + expect(deltaEvent).toBeDefined(); + expect(deltaEvent.delta).toContain("Echo: hello"); + + const completedEvent = events.find( + (e) => e.type === "response.completed", + ); + expect(completedEvent).toBeDefined(); + }); + + test("throws when not initialized", async () => { + const config: IAgentConfig = { agentInstance: new StubAgent() }; + const plugin = new AgentPlugin(config); + + await expect( + plugin.exports().invoke([{ role: "user", content: "hi" }]), + ).rejects.toThrow("not initialized"); + }); + }); +}); diff --git a/packages/appkit/src/plugins/agent/tests/invoke-handler.test.ts b/packages/appkit/src/plugins/agent/tests/invoke-handler.test.ts new file mode 100644 index 00000000..8bd1cec6 --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/invoke-handler.test.ts @@ -0,0 +1,325 @@ +import { createMockRequest, createMockResponse } from "@tools/test-helpers"; +import { describe, expect, test, vi } from "vitest"; +import { createInvokeHandler } from "../invoke-handler"; +import { StubAgent } from "./stub-agent"; + +function makeReq(body: any) { + return createMockRequest({ body }) as any; +} + +function makeRes() { + const res = createMockResponse() as any; + // Collect all writes for SSE assertions + const chunks: string[] = []; + res.write.mockImplementation((chunk: string) => { + chunks.push(chunk); + return true; + }); + (res as any).__chunks = chunks; + return res; +} + +function parseSSE(res: any): { events: any[]; fullOutput: string } { + const chunks: string[] = res.__chunks; + const events: any[] = []; + let fullOutput = ""; + + for (const chunk of chunks) { + const lines = chunk.split("\n"); + for (const line of lines) { + if (line.startsWith("data: ") && line !== "data: [DONE]") { + try { + const data = JSON.parse(line.slice(6)); + events.push(data); + if (data.type === "response.output_text.delta") { + fullOutput += data.delta; + } + } catch {} + } + } + } + return { events, fullOutput }; +} + +describe("createInvokeHandler", () => { + const stubAgent = new StubAgent(); + const handler = createInvokeHandler(() => stubAgent); + + describe("streaming mode", () => { + test("streams SSE events with correct format", async () => { + const req = makeReq({ + input: [{ role: "user", content: "Hello" }], + stream: true, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + expect(res.setHeader).toHaveBeenCalledWith( + "Content-Type", + "text/event-stream", + ); + + const { events, fullOutput } = parseSSE(res); + + expect(fullOutput).toContain("Echo: Hello"); + + const hasCompleted = events.some((e) => e.type === "response.completed"); + expect(hasCompleted).toBe(true); + + // Last write should be [DONE] + const lastChunk = res.__chunks[res.__chunks.length - 1]; + expect(lastChunk).toContain("[DONE]"); + + expect(res.end).toHaveBeenCalled(); + }); + + test("emits output_item.added and output_item.done events", async () => { + const req = makeReq({ + input: [{ role: "user", content: "Test" }], + stream: true, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + const { events } = parseSSE(res); + const addedEvent = events.find( + (e) => e.type === "response.output_item.added", + ); + const doneEvent = events.find( + (e) => e.type === "response.output_item.done", + ); + + expect(addedEvent).toBeDefined(); + expect(addedEvent.item.type).toBe("message"); + expect(doneEvent).toBeDefined(); + }); + }); + + describe("non-streaming mode", () => { + test("returns JSON with output items", async () => { + const req = makeReq({ + input: [{ role: "user", content: "Hello" }], + stream: false, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.arrayContaining([ + expect.objectContaining({ + type: "message", + content: expect.arrayContaining([ + expect.objectContaining({ + type: "output_text", + text: "Echo: Hello", + }), + ]), + }), + ]), + }), + ); + }); + }); + + describe("input parsing", () => { + test("accepts string input", async () => { + const req = makeReq({ + input: "Hello string", + stream: true, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + const { fullOutput } = parseSSE(res); + expect(fullOutput).toContain("Echo: Hello string"); + }); + + test("accepts array input with multipart content", async () => { + const req = makeReq({ + input: [ + { + role: "user", + content: [ + { type: "input_text", text: "Part one" }, + { type: "input_text", text: "Part two" }, + ], + }, + ], + stream: true, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + const { fullOutput } = parseSSE(res); + expect(fullOutput).toContain("Echo: Part one\nPart two"); + }); + }); + + describe("chat history", () => { + test("passes chat history to agent", async () => { + const spyAgent = { + invoke: vi.fn().mockResolvedValue([ + { + id: "msg_1", + type: "message", + role: "assistant", + status: "completed", + content: [ + { type: "output_text", text: "response", annotations: [] }, + ], + }, + ]), + stream: vi.fn(), + }; + const historyHandler = createInvokeHandler(() => spyAgent as any); + + const req = makeReq({ + input: [ + { role: "user", content: "First message" }, + { role: "assistant", content: "First reply" }, + { role: "user", content: "Second message" }, + ], + stream: false, + }); + const res = makeRes(); + + await historyHandler(req, res, vi.fn()); + + expect(spyAgent.invoke).toHaveBeenCalledWith( + expect.objectContaining({ + input: "Second message", + chat_history: expect.arrayContaining([ + expect.objectContaining({ + role: "user", + content: "First message", + }), + expect.objectContaining({ + role: "assistant", + content: "First reply", + }), + ]), + }), + ); + }); + + test("handles function_call items in history", async () => { + const spyAgent = { + invoke: vi.fn().mockResolvedValue([ + { + id: "msg_1", + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "done", annotations: [] }], + }, + ]), + stream: vi.fn(), + }; + const historyHandler = createInvokeHandler(() => spyAgent as any); + + const req = makeReq({ + input: [ + { role: "user", content: "Look up the answer" }, + { + type: "function_call", + name: "search", + arguments: '{"q":"test"}', + }, + { + type: "function_call_output", + output: '"42"', + }, + { role: "user", content: "What did you find?" }, + ], + stream: false, + }); + const res = makeRes(); + + await historyHandler(req, res, vi.fn()); + + const calledHistory = spyAgent.invoke.mock.calls[0][0].chat_history; + expect(calledHistory).toHaveLength(3); + expect(calledHistory[1].content).toContain("[Tool Call:"); + expect(calledHistory[2].content).toContain("[Tool Result:"); + }); + }); + + describe("error handling", () => { + test("returns 400 for missing input", async () => { + const req = makeReq({ stream: true }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ error: "Invalid request format" }), + ); + }); + + test("returns 400 when no user message is present", async () => { + const req = makeReq({ + input: [{ role: "assistant", content: "I am assistant" }], + stream: true, + }); + const res = makeRes(); + + await handler(req, res, vi.fn()); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: "No user message found in input", + }), + ); + }); + + test("handles agent errors gracefully in streaming mode", async () => { + const errorAgent = { + invoke: vi.fn(), + stream: (_params: any) => { + // Return an async iterable that throws on first next() + return { + async next() { + throw new Error("Agent exploded"); + }, + async return() { + return { done: true, value: undefined }; + }, + async throw(e: unknown) { + throw e; + }, + [Symbol.asyncIterator]() { + return this; + }, + [Symbol.asyncDispose]: undefined, + } as unknown as AsyncGenerator; + }, + }; + const errorHandler = createInvokeHandler(() => errorAgent as any); + + const req = makeReq({ + input: [{ role: "user", content: "boom" }], + stream: true, + }); + const res = makeRes(); + + await errorHandler(req, res, vi.fn()); + + const { events } = parseSSE(res); + const errorEvent = events.find((e) => e.type === "error"); + const failedEvent = events.find((e) => e.type === "response.failed"); + + expect(errorEvent).toBeDefined(); + expect(errorEvent.error).toContain("Agent exploded"); + expect(failedEvent).toBeDefined(); + expect(res.end).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/appkit/src/plugins/agent/tests/stub-agent.ts b/packages/appkit/src/plugins/agent/tests/stub-agent.ts new file mode 100644 index 00000000..540a2fca --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/stub-agent.ts @@ -0,0 +1,71 @@ +/** + * Deterministic stub AgentInterface for framework tests. + * + * Echoes user input as "Echo: {input}" — no LLM or network required. + */ + +import { randomUUID } from "node:crypto"; +import type { + AgentInterface, + InvokeParams, + ResponseOutputItem, + ResponseOutputMessage, + ResponseStreamEvent, +} from "../agent-interface"; + +export class StubAgent implements AgentInterface { + async invoke(params: InvokeParams): Promise { + const text = `Echo: ${params.input}`; + const message: ResponseOutputMessage = { + id: `msg_${randomUUID()}`, + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text, annotations: [] }], + }; + return [message]; + } + + async *stream(params: InvokeParams): AsyncGenerator { + const text = `Echo: ${params.input}`; + const itemId = `msg_${randomUUID()}`; + let seqNum = 0; + + const msgItem: ResponseOutputMessage = { + id: itemId, + type: "message", + role: "assistant", + status: "in_progress", + content: [], + }; + + yield { + type: "response.output_item.added", + item: msgItem, + output_index: 0, + sequence_number: seqNum++, + }; + + yield { + type: "response.output_text.delta", + item_id: itemId, + output_index: 0, + content_index: 0, + delta: text, + sequence_number: seqNum++, + }; + + yield { + type: "response.output_item.done", + item: { ...msgItem, status: "completed" }, + output_index: 0, + sequence_number: seqNum++, + }; + + yield { + type: "response.completed", + sequence_number: seqNum++, + response: {}, + }; + } +} diff --git a/packages/appkit/src/plugins/agent/types.ts b/packages/appkit/src/plugins/agent/types.ts new file mode 100644 index 00000000..0f6fa02c --- /dev/null +++ b/packages/appkit/src/plugins/agent/types.ts @@ -0,0 +1,43 @@ +import type { DatabricksMCPServer } from "@databricks/langchainjs"; +import type { StructuredTool } from "@langchain/core/tools"; +import type { BasePluginConfig } from "shared"; +import type { AgentInterface } from "./agent-interface"; + +export interface IAgentConfig extends BasePluginConfig { + /** + * Pre-built agent implementing AgentInterface. + * When provided the plugin skips internal LangGraph setup and delegates + * directly to this instance. Use this to bring your own agent + * implementation or a different LangChain variant. + */ + agentInstance?: AgentInterface; + + /** + * Databricks model serving endpoint name (e.g. "databricks-claude-sonnet-4-5"). + * Falls back to DATABRICKS_MODEL env var. + * Ignored when `agentInstance` is provided. + */ + model?: string; + + /** + * Whether ChatDatabricks calls the upstream model using the Responses API + * instead of the Chat Completions API. Default: false. + * Ignored when `agentInstance` is provided. + */ + useResponsesApi?: boolean; + + /** System prompt injected at the start of every conversation */ + systemPrompt?: string; + + /** Sampling temperature (0.0-1.0, default 0.1). Ignored when `agentInstance` is provided. */ + temperature?: number; + + /** Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. */ + maxTokens?: number; + + /** MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. */ + mcpServers?: DatabricksMCPServer[]; + + /** Additional LangChain tools to register alongside MCP tools. Ignored when `agentInstance` is provided. */ + tools?: StructuredTool[]; +} diff --git a/packages/appkit/src/plugins/index.ts b/packages/appkit/src/plugins/index.ts index 7caa040f..aa1df929 100644 --- a/packages/appkit/src/plugins/index.ts +++ b/packages/appkit/src/plugins/index.ts @@ -1,3 +1,4 @@ +export * from "./agent"; export * from "./analytics"; export * from "./files"; export * from "./genie"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67171ef7..1ba351a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -239,12 +239,27 @@ importers: packages/appkit: dependencies: + '@arizeai/openinference-instrumentation-langchain': + specifier: '>=4.0.0' + version: 4.0.6(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))) '@databricks/lakebase': specifier: workspace:* version: link:../lakebase + '@databricks/langchainjs': + specifier: '>=0.1.0' + version: 0.1.0(@cfworker/json-schema@4.1.1)(@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) '@databricks/sdk-experimental': specifier: ^0.16.0 version: 0.16.0 + '@langchain/core': + specifier: '>=1.0.0' + version: 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + '@langchain/langgraph': + specifier: '>=1.0.0' + version: 1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6) + '@langchain/mcp-adapters': + specifier: '>=1.0.0' + version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6)) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 @@ -320,6 +335,9 @@ importers: ws: specifier: ^8.18.3 version: 8.18.3(bufferutil@4.0.9) + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/express': specifier: ^4.17.25 @@ -552,16 +570,32 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/gateway@3.0.66': + resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@3.0.19': resolution: {integrity: sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@ai-sdk/react@2.0.115': resolution: {integrity: sha512-Etu7gWSEi2dmXss1PoR5CAZGwGShXsF9+Pon1eRO6EmatjYaBMhq1CfHPyYhGzWrint8jJIK2VaAhiMef29qZw==} engines: {node: '>=18'} @@ -656,6 +690,17 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} + '@arizeai/openinference-core@2.0.5': + resolution: {integrity: sha512-BnufYaFqmG9twkz/9DHX9WTcOs7YvVAYaufau5tdjOT1c0Y8niJwmNWzV36phNPg3c7SmdD5OYLuzeAUN0T3pQ==} + + '@arizeai/openinference-instrumentation-langchain@4.0.6': + resolution: {integrity: sha512-yvA7ObrNUjhUN8y37lO+Cr8Ef7Bq6NKKoChXPOaKG/IufwAAcXUowdEC40gipUelS3k3AOgxcIU2rfP+7f+YyQ==} + peerDependencies: + '@langchain/core': ^1.0.0 || ^0.3.0 || ^0.2.0 + + '@arizeai/openinference-semantic-conventions@2.1.7': + resolution: {integrity: sha512-KyBfwxkSusPvxHBaW/TJ0japEbXCNziW9o6/IRKiPu+gp5TMKIagV2NKvt47rWYa4Jc0Nl+SvAPm+yxkdJqVbg==} + '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} @@ -1411,6 +1456,9 @@ packages: '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -1811,6 +1859,21 @@ packages: peerDependencies: postcss: ^8.4 + '@databricks/ai-sdk-provider@0.3.0': + resolution: {integrity: sha512-KKSeF/vvTeN/YEIzbpPl0tC0uWqXbCU3bjzAlX90aIUdyLjhD+8PviEXuh2g7YYpsDsBdWClu33Z7K+ooudfCA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@ai-sdk/provider': ^3.0.5 + '@ai-sdk/provider-utils': ^4.0.10 + + '@databricks/langchainjs@0.1.0': + resolution: {integrity: sha512-pCAsmoqBxoBOrHP9pxAxWj+jNbqqaD2WfYtnk61xpBpCbgfak1NA5MOZrc56TokidT8kam/f2RNKlFHjsok9aA==} + engines: {node: '>=18.0.0'} + + '@databricks/sdk-experimental@0.15.0': + resolution: {integrity: sha512-HkoMiF7dNDt6WRW0xhi7oPlBJQfxJ9suJhEZRFt08VwLMaWcw2PiF8monfHlkD4lkufEYV6CTxi5njQkciqiHA==} + engines: {node: '>=22.0', npm: '>=10.0.0'} + '@databricks/sdk-experimental@0.16.0': resolution: {integrity: sha512-9c2RxWYoRDFupdt4ZnBc1IPE1XaXgN+/wyV4DVcEqOnIa31ep51OnwAD/3014BImfKdyXg32nmgrB9dwvB6+lg==} engines: {node: '>=22.0', npm: '>=10.0.0'} @@ -2279,6 +2342,12 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2512,6 +2581,48 @@ packages: peerDependencies: tslib: '2' + '@langchain/core@1.1.31': + resolution: {integrity: sha512-FxsgIUONjKaRpjx59sISgmb0OMCbAetPGyhzjGa2kX0y1f8LZ5xm9VB2db7W9HYWyLvzRWcMA51Uu4OSTJmtZQ==} + engines: {node: '>=20'} + + '@langchain/langgraph-checkpoint@1.0.0': + resolution: {integrity: sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.0.1 + + '@langchain/langgraph-sdk@1.6.5': + resolution: {integrity: sha512-JjprmbhgCnoNJ9DUKcvrEU+C9FfKsNGyT3ooqWxAY5Cx2qofhXmDJOpTCqqbxfDHPKG0RjTs5HgVK3WW5M6Big==} + peerDependencies: + '@langchain/core': ^1.1.16 + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + '@langchain/core': + optional: true + react: + optional: true + react-dom: + optional: true + + '@langchain/langgraph@1.2.1': + resolution: {integrity: sha512-OeLMejye1DZeZBPnurus2bqvjRi+pyrqfAXX77hYdUqKeQ3hAG7pLG04xdrvMs0pl/F57ZtwywqoE2oVqcI6JA==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.1.16 + zod: ^3.25.32 || ^4.2.0 + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + + '@langchain/mcp-adapters@1.1.3': + resolution: {integrity: sha512-OPHIQNkTUJjnRj1pr+cp2nguMBZeF3Q1pVT1hCbgU7BrHgV7lov99wbU8po8Cm4zZzmeRtVO/T9X1SrDD1ogtQ==} + engines: {node: '>=20.10.0'} + peerDependencies: + '@langchain/core': ^1.0.0 + '@langchain/langgraph': ^1.0.0 + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -2527,6 +2638,16 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} @@ -2621,6 +2742,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.2.0': resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2951,6 +3078,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.46.0': + resolution: {integrity: sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.208.0': resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3063,6 +3196,10 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.38.0': resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} engines: {node: '>=14'} @@ -4871,6 +5008,9 @@ packages: '@types/serve-static@1.15.9': resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} @@ -4886,6 +5026,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -4964,6 +5107,10 @@ packages: resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} engines: {node: '>= 20'} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitejs/plugin-react@5.0.4': resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5072,6 +5219,16 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-assertions@1.9.0: + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes + peerDependencies: + acorn: ^8 + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -5115,6 +5272,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5355,6 +5518,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -5721,6 +5888,9 @@ packages: console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + console-table-printer@2.15.0: + resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} + content-disposition@0.5.2: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} @@ -5729,6 +5899,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -5784,6 +5958,10 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -5806,6 +5984,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -6182,6 +6364,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -6755,6 +6941,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -6767,10 +6957,20 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.0: + resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.0: resolution: {integrity: sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -6781,6 +6981,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extended-eventsource@1.7.0: + resolution: {integrity: sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -6856,6 +7059,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-cache-dir@4.0.0: resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} engines: {node: '>=14.16'} @@ -6943,6 +7150,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.2: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} @@ -7280,6 +7491,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + hookable@6.0.1: resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} @@ -7436,6 +7651,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.7.1: + resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} + import-in-the-middle@2.0.0: resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} @@ -7652,6 +7870,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regexp@1.0.0: resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} engines: {node: '>=0.10.0'} @@ -7760,6 +7981,12 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7812,6 +8039,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -7866,6 +8096,23 @@ packages: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} + langsmith@0.5.8: + resolution: {integrity: sha512-AsdwxazXXLwbEzVTXB5uo7Fva5MhGhSvIJ9FjBbkWOkgwqC28E9Gmah5SGbPM3CPjN0FdxB6nKzX5GRkkkXDjQ==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + latest-version@7.0.0: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} @@ -8221,6 +8468,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memfs@4.51.1: resolution: {integrity: sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==} @@ -8235,6 +8486,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -8482,6 +8737,10 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -8502,6 +8761,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8635,6 +8898,9 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -8710,14 +8976,26 @@ packages: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} + p-queue@9.1.0: + resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==} + engines: {node: '>=20'} + p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + pac-proxy-agent@7.2.0: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} @@ -8814,6 +9092,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8881,6 +9162,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@7.0.0: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} @@ -9405,6 +9690,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -9430,6 +9719,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -9695,6 +9988,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} @@ -9838,6 +10135,10 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -9932,6 +10233,10 @@ packages: resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + sequelize-pool@7.1.0: resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==} engines: {node: '>= 10.0.0'} @@ -9983,6 +10288,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -10017,6 +10326,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -10043,6 +10355,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-wcswidth@1.1.2: + resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -10575,6 +10890,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -10873,10 +11192,18 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -11199,6 +11526,9 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} @@ -11290,6 +11620,11 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -11299,6 +11634,9 @@ packages: zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zrender@6.0.0: resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} @@ -11310,33 +11648,51 @@ packages: snapshots: - '@ai-sdk/gateway@2.0.21(zod@4.1.13)': + '@ai-sdk/gateway@2.0.21(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@vercel/oidc': 3.0.5 - zod: 4.1.13 + zod: 4.3.6 + + '@ai-sdk/gateway@3.0.66(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 - '@ai-sdk/provider-utils@3.0.19(zod@4.1.13)': + '@ai-sdk/provider-utils@3.0.19(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.1.13 + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.19(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.115(react@19.2.0)(zod@4.1.13)': + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@2.0.115(react@19.2.0)(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) - ai: 5.0.113(zod@4.1.13) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) + ai: 5.0.113(zod@4.3.6) react: 19.2.0 swr: 2.3.8(react@19.2.0) throttleit: 2.1.0 optionalDependencies: - zod: 4.1.13 + zod: 4.3.6 '@algolia/abtesting@1.12.0': dependencies: @@ -11459,6 +11815,25 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 + '@arizeai/openinference-core@2.0.5': + dependencies: + '@arizeai/openinference-semantic-conventions': 2.1.7 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@arizeai/openinference-instrumentation-langchain@4.0.6(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))': + dependencies: + '@arizeai/openinference-core': 2.0.5 + '@arizeai/openinference-semantic-conventions': 2.1.7 + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@arizeai/openinference-semantic-conventions@2.1.7': {} + '@asamuzakjp/css-color@4.0.5': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -12410,6 +12785,8 @@ snapshots: '@braintree/sanitize-url@7.1.1': {} + '@cfworker/json-schema@4.1.1': {} + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -12862,6 +13239,40 @@ snapshots: dependencies: postcss: 8.5.6 + '@databricks/ai-sdk-provider@0.3.0(@ai-sdk/provider-utils@4.0.19(zod@4.3.6))(@ai-sdk/provider@3.0.8)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + zod: 4.3.6 + + '@databricks/langchainjs@0.1.0(@cfworker/json-schema@4.1.1)(@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + '@databricks/ai-sdk-provider': 0.3.0(@ai-sdk/provider-utils@4.0.19(zod@4.3.6))(@ai-sdk/provider@3.0.8) + '@databricks/sdk-experimental': 0.15.0 + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + '@langchain/mcp-adapters': 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6)) + ai: 6.0.116(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@langchain/langgraph' + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - supports-color + + '@databricks/sdk-experimental@0.15.0': + dependencies: + google-auth-library: 10.5.0 + ini: 6.0.0 + reflect-metadata: 0.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + '@databricks/sdk-experimental@0.16.0': dependencies: google-auth-library: 10.5.0 @@ -12885,14 +13296,14 @@ snapshots: '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)': dependencies: - '@ai-sdk/react': 2.0.115(react@19.2.0)(zod@4.1.13) + '@ai-sdk/react': 2.0.115(react@19.2.0)(zod@4.3.6) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) '@docsearch/core': 4.3.1(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@docsearch/css': 4.3.2 - ai: 5.0.113(zod@4.1.13) + ai: 5.0.113(zod@4.3.6) algoliasearch: 5.46.0 marked: 16.4.2 - zod: 4.1.13 + zod: 4.3.6 optionalDependencies: '@types/react': 19.2.7 react: 19.2.0 @@ -13876,6 +14287,10 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -14108,6 +14523,68 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 + '@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.5.8(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + mustache: 4.2.0 + p-queue: 6.6.2 + uuid: 11.1.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + + '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))': + dependencies: + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + uuid: 10.0.0 + + '@langchain/langgraph-sdk@1.6.5(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@types/json-schema': 7.0.15 + p-queue: 9.1.0 + p-retry: 7.1.1 + uuid: 13.0.0 + optionalDependencies: + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))) + '@langchain/langgraph-sdk': 1.6.5(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@standard-schema/spec': 1.1.0 + uuid: 10.0.0 + zod: 4.3.6 + optionalDependencies: + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - react + - react-dom + + '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(@langchain/langgraph@1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6))': + dependencies: + '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)) + '@langchain/langgraph': 1.2.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6) + '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6) + debug: 4.4.3 + zod: 4.3.6 + optionalDependencies: + extended-eventsource: 1.7.0 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + '@leichtgewicht/ip-codec@2.0.5': {} '@mdx-js/mdx@3.1.1': @@ -14150,6 +14627,30 @@ snapshots: dependencies: langium: 3.3.1 + '@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.5 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.7.1 @@ -14309,6 +14810,11 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14774,6 +15280,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.7.1 + require-in-the-middle: 7.5.2 + semver: 7.7.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14921,6 +15438,8 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions@1.28.0': {} + '@opentelemetry/semantic-conventions@1.38.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': @@ -16638,6 +17157,8 @@ snapshots: '@types/node': 24.10.1 '@types/send': 0.17.5 + '@types/shimmer@1.2.0': {} + '@types/sockjs@0.3.36': dependencies: '@types/node': 24.10.1 @@ -16653,6 +17174,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@types/validator@13.15.10': {} '@types/ws@8.18.1': @@ -16760,6 +17283,8 @@ snapshots: '@vercel/oidc@3.0.5': {} + '@vercel/oidc@3.1.0': {} + '@vitejs/plugin-react@5.0.4(vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.4 @@ -16946,6 +17471,15 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-assertions@1.9.0(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -16973,13 +17507,21 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@5.0.113(zod@4.1.13): + ai@5.0.113(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 2.0.21(zod@4.1.13) + '@ai-sdk/gateway': 2.0.21(zod@4.3.6) '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@opentelemetry/api': 1.9.0 - zod: 4.1.13 + zod: 4.3.6 + + ai@6.0.116(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.66(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: @@ -17230,6 +17772,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + bonjour-service@1.3.0: dependencies: fast-deep-equal: 3.1.3 @@ -17636,12 +18192,18 @@ snapshots: console-control-strings@1.1.0: {} + console-table-printer@2.15.0: + dependencies: + simple-wcswidth: 1.1.2 + content-disposition@0.5.2: {} content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} conventional-changelog-angular@7.0.0: @@ -17703,6 +18265,8 @@ snapshots: cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} copy-webpack-plugin@11.0.0(webpack@5.103.0): @@ -17725,6 +18289,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -18137,6 +18706,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} @@ -18640,6 +19211,10 @@ snapshots: eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -18666,6 +19241,11 @@ snapshots: expect-type@1.2.2: {} + express-rate-limit@8.3.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@4.22.0: dependencies: accepts: 1.3.8 @@ -18702,6 +19282,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} extend-shallow@2.0.1: @@ -18710,6 +19323,9 @@ snapshots: extend@3.0.2: {} + extended-eventsource@1.7.0: + optional: true + fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -18789,6 +19405,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-cache-dir@4.0.0: dependencies: common-path-prefix: 3.0.0 @@ -18854,6 +19481,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 @@ -19413,6 +20042,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.12.5: {} + hookable@6.0.1: {} hosted-git-info@8.1.0: @@ -19587,6 +20218,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.7.1: + dependencies: + acorn: 8.15.0 + acorn-import-assertions: 1.9.0(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + import-in-the-middle@2.0.0: dependencies: acorn: 8.15.0 @@ -19737,6 +20375,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regexp@1.0.0: {} is-ssh@1.4.1: @@ -19858,6 +20498,12 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + jose@6.2.1: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -19927,6 +20573,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -19982,7 +20630,7 @@ snapshots: typescript: 5.9.3 unbash: 2.2.0 yaml: 2.8.2 - zod: 4.1.13 + zod: 4.3.6 langium@3.3.1: dependencies: @@ -19992,6 +20640,19 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 + langsmith@0.5.8(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 5.6.2 + console-table-printer: 2.15.0 + p-queue: 6.6.2 + semver: 7.7.3 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/exporter-trace-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + latest-version@7.0.0: dependencies: package-json: 8.1.1 @@ -20437,6 +21098,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memfs@4.51.1: dependencies: '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) @@ -20452,6 +21115,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -20861,6 +21526,8 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 + mustache@4.2.0: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -20871,6 +21538,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} netmask@2.0.2: {} @@ -20988,6 +21657,10 @@ snapshots: on-headers@1.1.0: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -21095,16 +21768,27 @@ snapshots: eventemitter3: 4.0.7 p-timeout: 3.2.0 + p-queue@9.1.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 7.0.1 + p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 is-network-error: 1.3.0 retry: 0.13.1 + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.0 + p-timeout@3.2.0: dependencies: p-finally: 1.0.0 + p-timeout@7.0.1: {} + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -21216,6 +21900,8 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -21269,6 +21955,8 @@ snapshots: pidtree@0.6.0: {} + pkce-challenge@5.0.1: {} + pkg-dir@7.0.0: dependencies: find-up: 6.3.0 @@ -21853,6 +22541,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@1.0.0: {} queue-microtask@1.2.3: {} @@ -21874,6 +22566,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -22259,6 +22958,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + require-in-the-middle@8.0.1: dependencies: debug: 4.4.3 @@ -22456,6 +23163,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.8.0: {} rtlcss@4.3.0: @@ -22570,6 +23287,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + sequelize-pool@7.1.0: {} sequelize@6.37.7(pg@8.18.0): @@ -22630,6 +23363,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -22663,6 +23405,8 @@ snapshots: shell-quote@1.8.3: {} + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -22697,6 +23441,8 @@ snapshots: signal-exit@4.1.0: {} + simple-wcswidth@1.1.2: {} + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 @@ -23169,6 +23915,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -23433,8 +24185,12 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + uuid@11.1.0: {} + uuid@13.0.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -23874,6 +24630,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + write-file-atomic@3.0.3: dependencies: imurmurhash: 0.1.4 @@ -23933,12 +24691,18 @@ snapshots: yoctocolors@2.1.2: {} + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-validation-error@4.0.2(zod@4.1.13): dependencies: zod: 4.1.13 zod@4.1.13: {} + zod@4.3.6: {} + zrender@6.0.0: dependencies: tslib: 2.3.0 diff --git a/template/appkit.plugins.json b/template/appkit.plugins.json index cf60a8af..dcf552e5 100644 --- a/template/appkit.plugins.json +++ b/template/appkit.plugins.json @@ -2,6 +2,30 @@ "$schema": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", "version": "1.0", "plugins": { + "agent": { + "name": "agent", + "displayName": "Agent Plugin", + "description": "LangChain/LangGraph AI agent with streaming Responses API and MCP tool support", + "package": "@databricks/appkit", + "resources": { + "required": [ + { + "type": "serving_endpoint", + "alias": "Model Endpoint", + "resourceKey": "agent-model-endpoint", + "description": "Databricks model serving endpoint for the agent LLM", + "permission": "CAN_QUERY", + "fields": { + "name": { + "env": "DATABRICKS_MODEL", + "description": "Model serving endpoint name" + } + } + } + ], + "optional": [] + } + }, "analytics": { "name": "analytics", "displayName": "Analytics Plugin", From a9568dfc3d6c9f160604f005db53b39dd619dd4a Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Fri, 13 Mar 2026 15:48:38 +0100 Subject: [PATCH 2/4] feat(appkit): modify tools api Signed-off-by: Hubert Zub --- apps/dev-playground/server/agent-tools.ts | 59 +++--- .../docs/api/appkit/Interface.FunctionTool.md | 63 ++++++ .../docs/api/appkit/Interface.IAgentConfig.md | 32 +-- docs/docs/api/appkit/TypeAlias.AgentTool.md | 10 + docs/docs/api/appkit/Variable.agent.md | 18 +- docs/docs/api/appkit/index.md | 4 +- docs/docs/api/appkit/typedoc-sidebar.ts | 10 + packages/appkit/src/index.ts | 2 + packages/appkit/src/plugins/agent/agent.ts | 27 ++- .../appkit/src/plugins/agent/function-tool.ts | 142 +++++++++++++ packages/appkit/src/plugins/agent/index.ts | 4 +- .../plugins/agent/tests/function-tool.test.ts | 188 ++++++++++++++++++ packages/appkit/src/plugins/agent/types.ts | 19 +- 13 files changed, 507 insertions(+), 71 deletions(-) create mode 100644 docs/docs/api/appkit/Interface.FunctionTool.md create mode 100644 docs/docs/api/appkit/TypeAlias.AgentTool.md create mode 100644 packages/appkit/src/plugins/agent/function-tool.ts create mode 100644 packages/appkit/src/plugins/agent/tests/function-tool.test.ts diff --git a/apps/dev-playground/server/agent-tools.ts b/apps/dev-playground/server/agent-tools.ts index 5af64a34..0d8f6667 100644 --- a/apps/dev-playground/server/agent-tools.ts +++ b/apps/dev-playground/server/agent-tools.ts @@ -1,37 +1,44 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; +import type { FunctionTool } from "@databricks/appkit"; -export const weatherTool = tool( - async ({ location }) => { +export const weatherTool: FunctionTool = { + type: "function", + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "City name, e.g. 'San Francisco'", + }, + }, + required: ["location"], + }, + execute: async ({ location }) => { const conditions = ["sunny", "partly cloudy", "rainy", "windy"]; const condition = conditions[Math.floor(Math.random() * conditions.length)]; const temp = Math.floor(Math.random() * 30) + 50; return `Weather in ${location}: ${condition}, ${temp}°F`; }, - { - name: "get_weather", - description: "Get the current weather for a location", - schema: z.object({ - location: z.string().describe("City name, e.g. 'San Francisco'"), - }), - }, -); +}; -export const timeTool = tool( - async ({ timezone }) => { - const tz = timezone ?? "UTC"; - return `Current time in ${tz}: ${new Date().toLocaleString("en-US", { timeZone: tz })}`; +export const timeTool: FunctionTool = { + type: "function", + name: "get_current_time", + description: "Get the current date and time in a timezone", + parameters: { + type: "object", + properties: { + timezone: { + type: "string", + description: "IANA timezone, e.g. 'America/New_York'. Defaults to UTC", + }, + }, }, - { - name: "get_current_time", - description: "Get the current date and time in a timezone", - schema: z.object({ - timezone: z - .string() - .optional() - .describe("IANA timezone, e.g. 'America/New_York'. Defaults to UTC"), - }), + execute: async ({ timezone }) => { + const tz = (timezone as string) ?? "UTC"; + return `Current time in ${tz}: ${new Date().toLocaleString("en-US", { timeZone: tz })}`; }, -); +}; export const demoTools = { weatherTool, timeTool }; diff --git a/docs/docs/api/appkit/Interface.FunctionTool.md b/docs/docs/api/appkit/Interface.FunctionTool.md new file mode 100644 index 00000000..c09cdbb9 --- /dev/null +++ b/docs/docs/api/appkit/Interface.FunctionTool.md @@ -0,0 +1,63 @@ +# Interface: FunctionTool + +## Properties + +### description? + +```ts +optional description: string | null; +``` + +*** + +### execute() + +```ts +execute: (args: Record) => string | Promise; +``` + +Handler invoked when the model calls this tool. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `args` | `Record`\<`string`, `unknown`\> | + +#### Returns + +`string` \| `Promise`\<`string`\> + +*** + +### name + +```ts +name: string; +``` + +*** + +### parameters? + +```ts +optional parameters: Record | null; +``` + +JSON Schema object describing the tool's parameters. + +*** + +### strict? + +```ts +optional strict: boolean | null; +``` + +*** + +### type + +```ts +type: "function"; +``` diff --git a/docs/docs/api/appkit/Interface.IAgentConfig.md b/docs/docs/api/appkit/Interface.IAgentConfig.md index e3413af0..9100521c 100644 --- a/docs/docs/api/appkit/Interface.IAgentConfig.md +++ b/docs/docs/api/appkit/Interface.IAgentConfig.md @@ -1,8 +1,6 @@ # Interface: IAgentConfig -Base configuration interface for AppKit plugins. - -When you do **not** set `agentInstance`, the agent is built from `model`, `tools`, and `mcpServers`. You can then add more tools or MCP servers after app creation via `appkit.agent.addCapabilities()` (see [agent](Variable.agent.md) Plugin API). +Base configuration interface for AppKit plugins ## Extends @@ -27,7 +25,7 @@ When provided the plugin skips internal LangGraph setup and delegates directly to this instance. Use this to bring your own agent implementation or a different LangChain variant. ---- +*** ### host? @@ -39,7 +37,7 @@ optional host: string; [`BasePluginConfig`](Interface.BasePluginConfig.md).[`host`](Interface.BasePluginConfig.md#host) ---- +*** ### maxTokens? @@ -49,7 +47,7 @@ optional maxTokens: number; Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. ---- +*** ### mcpServers? @@ -57,9 +55,9 @@ Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. optional mcpServers: DatabricksMCPServer[]; ``` -MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. You can add more at runtime with `appkit.agent.addCapabilities({ mcpServers: [...] })`. +MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. ---- +*** ### model? @@ -71,7 +69,7 @@ Databricks model serving endpoint name (e.g. "databricks-claude-sonnet-4-5"). Falls back to DATABRICKS_MODEL env var. Ignored when `agentInstance` is provided. ---- +*** ### name? @@ -83,7 +81,7 @@ optional name: string; [`BasePluginConfig`](Interface.BasePluginConfig.md).[`name`](Interface.BasePluginConfig.md#name) ---- +*** ### systemPrompt? @@ -93,7 +91,7 @@ optional systemPrompt: string; System prompt injected at the start of every conversation ---- +*** ### telemetry? @@ -105,7 +103,7 @@ optional telemetry: TelemetryOptions; [`BasePluginConfig`](Interface.BasePluginConfig.md).[`telemetry`](Interface.BasePluginConfig.md#telemetry) ---- +*** ### temperature? @@ -115,17 +113,19 @@ optional temperature: number; Sampling temperature (0.0-1.0, default 0.1). Ignored when `agentInstance` is provided. ---- +*** ### tools? ```ts -optional tools: StructuredTool[]; +optional tools: AgentTool[]; ``` -Additional LangChain tools to register alongside MCP tools. Ignored when `agentInstance` is provided. You can add more at runtime with `appkit.agent.addCapabilities({ tools: [...] })`. +Tools to register with the agent. Accepts OpenResponses-aligned FunctionTool +objects or LangChain StructuredToolInterface instances. +Ignored when `agentInstance` is provided. ---- +*** ### useResponsesApi? diff --git a/docs/docs/api/appkit/TypeAlias.AgentTool.md b/docs/docs/api/appkit/TypeAlias.AgentTool.md new file mode 100644 index 00000000..4fe02425 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.AgentTool.md @@ -0,0 +1,10 @@ +# Type Alias: AgentTool + +```ts +type AgentTool = FunctionTool | StructuredToolInterface; +``` + +A tool that can be registered with the agent plugin. + +- `FunctionTool` (preferred): OpenResponses-aligned plain object with JSON Schema parameters. +- `StructuredToolInterface`: LangChain tool for advanced use cases. diff --git a/docs/docs/api/appkit/Variable.agent.md b/docs/docs/api/appkit/Variable.agent.md index 0d22d753..38a4bfbe 100644 --- a/docs/docs/api/appkit/Variable.agent.md +++ b/docs/docs/api/appkit/Variable.agent.md @@ -1,21 +1,5 @@ # Variable: agent ```ts -const agent: ToPlugin; +const agent: ToPlugin; ``` - -Plugin factory for the AppKit agent (LangChain/LangGraph). Use in `createApp({ plugins: [agent({ ... })] })`. Configuration: [`IAgentConfig`](Interface.IAgentConfig.md). - -## Plugin API (runtime) - -After `const appkit = await createApp({ plugins: [..., agent(config)] })`, `appkit.agent` exposes: - -| Method | Description | -| ------ | ------ | -| `invoke(messages)` | Run the agent (non-streaming). Returns the assistant reply text. | -| `stream(messages)` | Run the agent with streaming. Yields [`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)s. | -| `addCapabilities({ tools?, mcpServers? })` | Batch-add tools and/or MCP servers with a **single** agent rebuild. **Only when not using `agentInstance`.** | -| `addTools(tools)` | Add LangChain tools after app creation. Rebuilds the agent. Convenience wrapper around `addCapabilities`. **Only when not using `agentInstance`.** | -| `addMcpServers(servers)` | Add MCP servers after app creation. Rebuilds the agent and MCP client. Convenience wrapper around `addCapabilities`. **Only when not using `agentInstance`.** | - -When the plugin is configured with `model` and optional `tools` / `mcpServers` (i.e. without `agentInstance`), prefer `addCapabilities` to register both tools and MCP servers in one call instead of sequential `addTools` + `addMcpServers` (which would rebuild the agent twice). diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index 6e453e81..b212a6ab 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -34,6 +34,7 @@ plugin architecture, and React integration. | [BasePluginConfig](Interface.BasePluginConfig.md) | Base configuration interface for AppKit plugins | | [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | | [DatabaseCredential](Interface.DatabaseCredential.md) | Database credentials with OAuth token for Postgres connection | +| [FunctionTool](Interface.FunctionTool.md) | - | | [GenerateDatabaseCredentialRequest](Interface.GenerateDatabaseCredentialRequest.md) | Request parameters for generating database OAuth credentials | | [IAgentConfig](Interface.IAgentConfig.md) | Base configuration interface for AppKit plugins | | [InvokeParams](Interface.InvokeParams.md) | Agent interface types for the AppKit Agent Plugin. | @@ -54,6 +55,7 @@ plugin architecture, and React integration. | Type Alias | Description | | ------ | ------ | +| [AgentTool](TypeAlias.AgentTool.md) | A tool that can be registered with the agent plugin. | | [ConfigSchema](TypeAlias.ConfigSchema.md) | Configuration schema definition for plugin config. Re-exported from the standard JSON Schema Draft 7 types. | | [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration | | [PluginData](TypeAlias.PluginData.md) | - | @@ -65,7 +67,7 @@ plugin architecture, and React integration. | Variable | Description | | ------ | ------ | -| [agent](Variable.agent.md) | Agent plugin factory; runtime API includes invoke, stream, addCapabilities, addTools, addMcpServers. | +| [agent](Variable.agent.md) | - | | [sql](Variable.sql.md) | SQL helper namespace | ## Functions diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index d4718669..a9b0c4ed 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -102,6 +102,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.DatabaseCredential", label: "DatabaseCredential" }, + { + type: "doc", + id: "api/appkit/Interface.FunctionTool", + label: "FunctionTool" + }, { type: "doc", id: "api/appkit/Interface.GenerateDatabaseCredentialRequest", @@ -183,6 +188,11 @@ const typedocSidebar: SidebarsConfig = { type: "category", label: "Type Aliases", items: [ + { + type: "doc", + id: "api/appkit/TypeAlias.AgentTool", + label: "AgentTool" + }, { type: "doc", id: "api/appkit/TypeAlias.ConfigSchema", diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index b6e42b6a..d475ca57 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -51,6 +51,8 @@ export { Plugin, type ToPlugin, toPlugin } from "./plugin"; export { agent, analytics, files, genie, lakebase, server } from "./plugins"; export type { AgentInterface, + AgentTool, + FunctionTool, IAgentConfig, InvokeParams, ResponseStreamEvent, diff --git a/packages/appkit/src/plugins/agent/agent.ts b/packages/appkit/src/plugins/agent/agent.ts index d0d0bf34..1a6605a2 100644 --- a/packages/appkit/src/plugins/agent/agent.ts +++ b/packages/appkit/src/plugins/agent/agent.ts @@ -18,10 +18,11 @@ import type express from "express"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; import type { AgentInterface } from "./agent-interface"; +import { functionToolToStructuredTool, isFunctionTool } from "./function-tool"; import { createInvokeHandler } from "./invoke-handler"; import manifest from "./manifest.json"; import { StandardAgent } from "./standard-agent"; -import type { IAgentConfig } from "./types"; +import type { AgentTool, IAgentConfig } from "./types"; const logger = createLogger("agent"); @@ -49,10 +50,18 @@ export class AgentPlugin extends Plugin { /** Only set when building from config (not agentInstance). Used when rebuilding after addTools/addMcpServers. */ private model: ChatDatabricksInstance | null = null; /** Mutable list of tools (config + added). Only used when building from config. */ - private toolsList: StructuredToolInterface[] = []; + private toolsList: AgentTool[] = []; /** Mutable list of MCP servers (config + added). Only used when building from config. */ private mcpServersList: DatabricksMCPServer[] = []; + /** + * Normalize an AgentTool to a LangChain StructuredToolInterface. + * FunctionTool objects are converted; StructuredToolInterface pass through. + */ + private static toStructuredTool(tool: AgentTool): StructuredToolInterface { + return isFunctionTool(tool) ? functionToolToStructuredTool(tool) : tool; + } + async setup() { this.systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; @@ -145,7 +154,7 @@ export class AgentPlugin extends Plugin { } } - tools.push(...this.toolsList); + tools.push(...this.toolsList.map(AgentPlugin.toStructuredTool)); const { createReactAgent } = await import("@langchain/langgraph/prebuilt"); const langGraphAgent = createReactAgent({ @@ -162,9 +171,11 @@ export class AgentPlugin extends Plugin { /** * Batch-add tools and/or MCP servers with a single agent rebuild. * Only supported when the plugin was initialized from config (not agentInstance). + * + * Tools can be OpenResponses-aligned FunctionTool objects or LangChain StructuredToolInterface. */ async addCapabilities(options: { - tools?: StructuredToolInterface[]; + tools?: AgentTool[]; mcpServers?: DatabricksMCPServer[]; }): Promise { if (this.config.agentInstance) { @@ -195,8 +206,10 @@ export class AgentPlugin extends Plugin { * Add tools to the agent after app creation. Only supported when the plugin * was initialized from config (not when using agentInstance). Rebuilds the * underlying LangGraph agent with the new tool set. + * + * Accepts OpenResponses-aligned FunctionTool objects or LangChain StructuredToolInterface. */ - async addTools(tools: StructuredToolInterface[]): Promise { + async addTools(tools: AgentTool[]): Promise { await this.addCapabilities({ tools }); } @@ -269,11 +282,11 @@ export class AgentPlugin extends Plugin { }); }.bind(this), - addTools: (tools: StructuredToolInterface[]) => this.addTools(tools), + addTools: (tools: AgentTool[]) => this.addTools(tools), addMcpServers: (servers: DatabricksMCPServer[]) => this.addMcpServers(servers), addCapabilities: (options: { - tools?: StructuredToolInterface[]; + tools?: AgentTool[]; mcpServers?: DatabricksMCPServer[]; }) => this.addCapabilities(options), }; diff --git a/packages/appkit/src/plugins/agent/function-tool.ts b/packages/appkit/src/plugins/agent/function-tool.ts new file mode 100644 index 00000000..a1b99ca7 --- /dev/null +++ b/packages/appkit/src/plugins/agent/function-tool.ts @@ -0,0 +1,142 @@ +/** + * OpenResponses-aligned function tool definition and converter. + * + * Users define tools as plain objects matching the OpenResponses FunctionTool + * schema (type, name, description, parameters as JSON Schema). Internally we + * convert them to LangChain StructuredTool instances for LangGraph. + */ + +import type { StructuredToolInterface } from "@langchain/core/tools"; +import { DynamicStructuredTool } from "@langchain/core/tools"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// Public type — matches OpenResponses FunctionToolParam + execute handler +// --------------------------------------------------------------------------- + +export interface FunctionTool { + type: "function"; + name: string; + description?: string | null; + /** JSON Schema object describing the tool's parameters. */ + parameters?: Record | null; + strict?: boolean | null; + /** Handler invoked when the model calls this tool. */ + execute: (args: Record) => Promise | string; +} + +// --------------------------------------------------------------------------- +// Type guard +// --------------------------------------------------------------------------- + +export function isFunctionTool(t: unknown): t is FunctionTool { + return ( + typeof t === "object" && + t !== null && + (t as any).type === "function" && + typeof (t as any).name === "string" && + typeof (t as any).execute === "function" + ); +} + +// --------------------------------------------------------------------------- +// Converter: FunctionTool → LangChain StructuredToolInterface +// --------------------------------------------------------------------------- + +/** + * Converts a JSON Schema properties object to a Zod schema. + * + * Supports the primitive types models actually use for tool parameters: + * string, number, integer, boolean, array, and object. Anything else falls + * back to z.any(). + */ +function jsonSchemaPropertiesToZod( + properties: Record, + required: string[] = [], +): z.ZodObject { + const shape: Record = {}; + + for (const [key, prop] of Object.entries(properties)) { + let field: z.ZodTypeAny; + + switch (prop.type) { + case "string": + field = z.string(); + break; + case "number": + field = z.number(); + break; + case "integer": + field = z.number().int(); + break; + case "boolean": + field = z.boolean(); + break; + case "array": + field = z.array(z.any()); + break; + case "object": + if (prop.properties) { + field = jsonSchemaPropertiesToZod( + prop.properties, + prop.required ?? [], + ); + } else { + field = z.record(z.string(), z.any()); + } + break; + default: + field = z.any(); + } + + if (prop.description) { + field = field.describe(prop.description); + } + + if (prop.enum && Array.isArray(prop.enum) && prop.enum.length > 0) { + field = z.enum(prop.enum as [string, ...string[]]); + if (prop.description) { + field = field.describe(prop.description); + } + } + + if (!required.includes(key)) { + field = field.optional(); + } + + shape[key] = field; + } + + return z.object(shape); +} + +/** + * Convert a single FunctionTool to a LangChain DynamicStructuredTool. + */ +export function functionToolToStructuredTool( + tool: FunctionTool, +): StructuredToolInterface { + const params = tool.parameters as Record | null | undefined; + const schema = params?.properties + ? jsonSchemaPropertiesToZod(params.properties, params.required ?? []) + : z.object({}); + + return new DynamicStructuredTool({ + name: tool.name, + description: tool.description ?? "", + schema, + func: async (args: Record) => { + const result = await tool.execute(args); + return result; + }, + }); +} + +/** + * Convert an array of FunctionTool definitions to LangChain StructuredToolInterface[]. + */ +export function functionToolsToStructuredTools( + tools: FunctionTool[], +): StructuredToolInterface[] { + return tools.map(functionToolToStructuredTool); +} diff --git a/packages/appkit/src/plugins/agent/index.ts b/packages/appkit/src/plugins/agent/index.ts index 616e7f8c..57c6f2bc 100644 --- a/packages/appkit/src/plugins/agent/index.ts +++ b/packages/appkit/src/plugins/agent/index.ts @@ -8,7 +8,9 @@ export type { ResponseOutputMessage, ResponseStreamEvent, } from "./agent-interface"; +export type { FunctionTool } from "./function-tool"; +export { isFunctionTool } from "./function-tool"; export { createInvokeHandler } from "./invoke-handler"; export type { LangGraphAgent } from "./standard-agent"; export { StandardAgent } from "./standard-agent"; -export type { IAgentConfig } from "./types"; +export type { AgentTool, IAgentConfig } from "./types"; diff --git a/packages/appkit/src/plugins/agent/tests/function-tool.test.ts b/packages/appkit/src/plugins/agent/tests/function-tool.test.ts new file mode 100644 index 00000000..c1df58c9 --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/function-tool.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, test } from "vitest"; +import type { FunctionTool } from "../function-tool"; +import { + functionToolsToStructuredTools, + functionToolToStructuredTool, + isFunctionTool, +} from "../function-tool"; + +const weatherTool: FunctionTool = { + type: "function", + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City name" }, + }, + required: ["location"], + }, + execute: async ({ location }) => `Sunny in ${location}`, +}; + +const noParamsTool: FunctionTool = { + type: "function", + name: "get_time", + description: "Get current time", + execute: async () => new Date().toISOString(), +}; + +describe("isFunctionTool", () => { + test("returns true for valid FunctionTool", () => { + expect(isFunctionTool(weatherTool)).toBe(true); + }); + + test("returns true for minimal FunctionTool (no parameters)", () => { + expect(isFunctionTool(noParamsTool)).toBe(true); + }); + + test("returns false for null/undefined", () => { + expect(isFunctionTool(null)).toBe(false); + expect(isFunctionTool(undefined)).toBe(false); + }); + + test("returns false for object missing type", () => { + expect(isFunctionTool({ name: "foo", execute: () => "" })).toBe(false); + }); + + test("returns false for object missing execute", () => { + expect(isFunctionTool({ type: "function", name: "foo" })).toBe(false); + }); + + test("returns false for LangChain StructuredTool-like object", () => { + const lcTool = { + name: "lc_tool", + description: "a langchain tool", + invoke: async () => "result", + }; + expect(isFunctionTool(lcTool)).toBe(false); + }); +}); + +describe("functionToolToStructuredTool", () => { + test("converts FunctionTool to StructuredToolInterface", async () => { + const converted = functionToolToStructuredTool(weatherTool); + + expect(converted.name).toBe("get_weather"); + expect(converted.description).toBe( + "Get the current weather for a location", + ); + + const result = await converted.invoke({ location: "Paris" }); + expect(result).toBe("Sunny in Paris"); + }); + + test("handles tool with no parameters", async () => { + const converted = functionToolToStructuredTool(noParamsTool); + + expect(converted.name).toBe("get_time"); + const result = await converted.invoke({}); + expect(result).toBeTruthy(); + }); + + test("handles tool with null parameters", async () => { + const tool: FunctionTool = { + type: "function", + name: "ping", + parameters: null, + execute: async () => "pong", + }; + const converted = functionToolToStructuredTool(tool); + const result = await converted.invoke({}); + expect(result).toBe("pong"); + }); + + test("handles nested object parameters", async () => { + const tool: FunctionTool = { + type: "function", + name: "search", + description: "Search for items", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "Search query" }, + options: { + type: "object", + properties: { + limit: { type: "integer", description: "Max results" }, + }, + }, + }, + required: ["query"], + }, + execute: async (args) => JSON.stringify(args), + }; + + const converted = functionToolToStructuredTool(tool); + expect(converted.name).toBe("search"); + + const result = await converted.invoke({ + query: "test", + options: { limit: 10 }, + }); + expect(JSON.parse(result)).toEqual({ + query: "test", + options: { limit: 10 }, + }); + }); + + test("handles enum parameters", async () => { + const tool: FunctionTool = { + type: "function", + name: "set_mode", + parameters: { + type: "object", + properties: { + mode: { + type: "string", + enum: ["fast", "slow"], + description: "Speed mode", + }, + }, + required: ["mode"], + }, + execute: async ({ mode }) => `Mode: ${mode}`, + }; + + const converted = functionToolToStructuredTool(tool); + const result = await converted.invoke({ mode: "fast" }); + expect(result).toBe("Mode: fast"); + }); + + test("handles optional parameters", async () => { + const tool: FunctionTool = { + type: "function", + name: "greet", + parameters: { + type: "object", + properties: { + name: { type: "string" }, + greeting: { type: "string" }, + }, + required: ["name"], + }, + execute: async ({ name, greeting }) => `${greeting ?? "Hello"}, ${name}!`, + }; + + const converted = functionToolToStructuredTool(tool); + const result = await converted.invoke({ name: "Alice" }); + expect(result).toBe("Hello, Alice!"); + }); +}); + +describe("functionToolsToStructuredTools", () => { + test("converts array of FunctionTools", () => { + const converted = functionToolsToStructuredTools([ + weatherTool, + noParamsTool, + ]); + + expect(converted).toHaveLength(2); + expect(converted[0].name).toBe("get_weather"); + expect(converted[1].name).toBe("get_time"); + }); + + test("handles empty array", () => { + expect(functionToolsToStructuredTools([])).toEqual([]); + }); +}); diff --git a/packages/appkit/src/plugins/agent/types.ts b/packages/appkit/src/plugins/agent/types.ts index 0f6fa02c..aaac1245 100644 --- a/packages/appkit/src/plugins/agent/types.ts +++ b/packages/appkit/src/plugins/agent/types.ts @@ -1,7 +1,16 @@ import type { DatabricksMCPServer } from "@databricks/langchainjs"; -import type { StructuredTool } from "@langchain/core/tools"; +import type { StructuredToolInterface } from "@langchain/core/tools"; import type { BasePluginConfig } from "shared"; import type { AgentInterface } from "./agent-interface"; +import type { FunctionTool } from "./function-tool"; + +/** + * A tool that can be registered with the agent plugin. + * + * - `FunctionTool` (preferred): OpenResponses-aligned plain object with JSON Schema parameters. + * - `StructuredToolInterface`: LangChain tool for advanced use cases. + */ +export type AgentTool = FunctionTool | StructuredToolInterface; export interface IAgentConfig extends BasePluginConfig { /** @@ -38,6 +47,10 @@ export interface IAgentConfig extends BasePluginConfig { /** MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. */ mcpServers?: DatabricksMCPServer[]; - /** Additional LangChain tools to register alongside MCP tools. Ignored when `agentInstance` is provided. */ - tools?: StructuredTool[]; + /** + * Tools to register with the agent. Accepts OpenResponses-aligned FunctionTool + * objects or LangChain StructuredToolInterface instances. + * Ignored when `agentInstance` is provided. + */ + tools?: AgentTool[]; } From f3cd566180eb7641649576738ae51e366aa52b36 Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Mon, 16 Mar 2026 12:08:16 +0100 Subject: [PATCH 3/4] feat(appkit): update tool/mcp API Signed-off-by: Hubert Zub --- apps/dev-playground/server/index.ts | 4 +- docs/docs/api/appkit/Class.StandardAgent.md | 74 ++++ .../appkit/Function.createInvokeHandler.md | 18 + .../api/appkit/Function.isFunctionTool.md | 15 + docs/docs/api/appkit/Function.isHostedTool.md | 15 + .../api/appkit/Interface.AgentInterface.md | 2 +- .../appkit/Interface.CustomMcpServerTool.md | 32 ++ .../appkit/Interface.ExternalMcpServerTool.md | 25 ++ docs/docs/api/appkit/Interface.GenieTool.md | 31 ++ .../docs/api/appkit/Interface.IAgentConfig.md | 16 +- .../Interface.ResponseFunctionCallOutput.md | 33 ++ .../Interface.ResponseFunctionToolCall.md | 49 +++ .../appkit/Interface.ResponseOutputMessage.md | 41 ++ .../api/appkit/Interface.StandardAgent.md | 53 --- .../appkit/Interface.VectorSearchIndexTool.md | 25 ++ docs/docs/api/appkit/TypeAlias.AgentTool.md | 8 +- docs/docs/api/appkit/TypeAlias.HostedTool.md | 9 + .../appkit/TypeAlias.ResponseOutputItem.md | 8 + docs/docs/api/appkit/index.md | 14 +- docs/docs/api/appkit/typedoc-sidebar.ts | 64 +++- docs/docs/plugins/agent.md | 352 ++++++++++++++++++ docs/docs/plugins/caching.md | 2 +- docs/docs/plugins/custom-plugins.md | 2 +- docs/docs/plugins/index.md | 3 +- knip.json | 9 +- .../react/agent-chat/agent-chat-message.tsx | 2 +- .../src/react/agent-chat/agent-chat-part.tsx | 2 +- packages/appkit/src/index.ts | 14 + packages/appkit/src/plugins/agent/agent.ts | 149 ++++---- .../appkit/src/plugins/agent/hosted-tools.ts | 107 ++++++ packages/appkit/src/plugins/agent/index.ts | 11 +- .../src/plugins/agent/standard-agent.ts | 2 +- .../plugins/agent/tests/hosted-tools.test.ts | 92 +++++ packages/appkit/src/plugins/agent/types.ts | 18 +- 34 files changed, 1126 insertions(+), 175 deletions(-) create mode 100644 docs/docs/api/appkit/Class.StandardAgent.md create mode 100644 docs/docs/api/appkit/Function.createInvokeHandler.md create mode 100644 docs/docs/api/appkit/Function.isFunctionTool.md create mode 100644 docs/docs/api/appkit/Function.isHostedTool.md create mode 100644 docs/docs/api/appkit/Interface.CustomMcpServerTool.md create mode 100644 docs/docs/api/appkit/Interface.ExternalMcpServerTool.md create mode 100644 docs/docs/api/appkit/Interface.GenieTool.md create mode 100644 docs/docs/api/appkit/Interface.ResponseFunctionCallOutput.md create mode 100644 docs/docs/api/appkit/Interface.ResponseFunctionToolCall.md create mode 100644 docs/docs/api/appkit/Interface.ResponseOutputMessage.md delete mode 100644 docs/docs/api/appkit/Interface.StandardAgent.md create mode 100644 docs/docs/api/appkit/Interface.VectorSearchIndexTool.md create mode 100644 docs/docs/api/appkit/TypeAlias.HostedTool.md create mode 100644 docs/docs/api/appkit/TypeAlias.ResponseOutputItem.md create mode 100644 docs/docs/plugins/agent.md create mode 100644 packages/appkit/src/plugins/agent/hosted-tools.ts create mode 100644 packages/appkit/src/plugins/agent/tests/hosted-tools.test.ts diff --git a/apps/dev-playground/server/index.ts b/apps/dev-playground/server/index.ts index c21a51ce..9b98e430 100644 --- a/apps/dev-playground/server/index.ts +++ b/apps/dev-playground/server/index.ts @@ -43,8 +43,8 @@ createApp({ ], ...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }), }).then(async (appkit) => { - // Add tools (and optionally MCP servers) after app creation - await appkit.agent.addCapabilities({ tools: [demoTools.timeTool] }); + // Add tools after app creation + await appkit.agent.addTools([demoTools.timeTool]); appkit.server .extend((app) => { diff --git a/docs/docs/api/appkit/Class.StandardAgent.md b/docs/docs/api/appkit/Class.StandardAgent.md new file mode 100644 index 00000000..a0f065fc --- /dev/null +++ b/docs/docs/api/appkit/Class.StandardAgent.md @@ -0,0 +1,74 @@ +# Class: StandardAgent + +Contract that agent implementations must fulfil. + +The plugin calls `invoke()` for non-streaming requests and `stream()` for +SSE streaming. Implementations are responsible for translating their SDK's +output into Responses API types. + +## Implements + +- [`AgentInterface`](Interface.AgentInterface.md) + +## Constructors + +### Constructor + +```ts +new StandardAgent(agent: LangGraphAgent, systemPrompt: string): StandardAgent; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `agent` | `LangGraphAgent` | +| `systemPrompt` | `string` | + +#### Returns + +`StandardAgent` + +## Methods + +### invoke() + +```ts +invoke(params: InvokeParams): Promise; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`Promise`\<[`ResponseOutputItem`](TypeAlias.ResponseOutputItem.md)[]\> + +#### Implementation of + +[`AgentInterface`](Interface.AgentInterface.md).[`invoke`](Interface.AgentInterface.md#invoke) + +*** + +### stream() + +```ts +stream(params: InvokeParams): AsyncGenerator; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `params` | [`InvokeParams`](Interface.InvokeParams.md) | + +#### Returns + +`AsyncGenerator`\<[`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)\> + +#### Implementation of + +[`AgentInterface`](Interface.AgentInterface.md).[`stream`](Interface.AgentInterface.md#stream) diff --git a/docs/docs/api/appkit/Function.createInvokeHandler.md b/docs/docs/api/appkit/Function.createInvokeHandler.md new file mode 100644 index 00000000..95175db3 --- /dev/null +++ b/docs/docs/api/appkit/Function.createInvokeHandler.md @@ -0,0 +1,18 @@ +# Function: createInvokeHandler() + +```ts +function createInvokeHandler(getAgent: () => AgentInterface): RequestHandler; +``` + +Create an Express handler that invokes the agent via the AgentInterface +and streams/returns the response in Responses API format. + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `getAgent` | () => [`AgentInterface`](Interface.AgentInterface.md) | + +## Returns + +`RequestHandler` diff --git a/docs/docs/api/appkit/Function.isFunctionTool.md b/docs/docs/api/appkit/Function.isFunctionTool.md new file mode 100644 index 00000000..8858641e --- /dev/null +++ b/docs/docs/api/appkit/Function.isFunctionTool.md @@ -0,0 +1,15 @@ +# Function: isFunctionTool() + +```ts +function isFunctionTool(t: unknown): t is FunctionTool; +``` + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `t` | `unknown` | + +## Returns + +`t is FunctionTool` diff --git a/docs/docs/api/appkit/Function.isHostedTool.md b/docs/docs/api/appkit/Function.isHostedTool.md new file mode 100644 index 00000000..ba0c5e2f --- /dev/null +++ b/docs/docs/api/appkit/Function.isHostedTool.md @@ -0,0 +1,15 @@ +# Function: isHostedTool() + +```ts +function isHostedTool(t: unknown): t is HostedTool; +``` + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `t` | `unknown` | + +## Returns + +`t is HostedTool` diff --git a/docs/docs/api/appkit/Interface.AgentInterface.md b/docs/docs/api/appkit/Interface.AgentInterface.md index 0736fe32..e6684cd4 100644 --- a/docs/docs/api/appkit/Interface.AgentInterface.md +++ b/docs/docs/api/appkit/Interface.AgentInterface.md @@ -22,7 +22,7 @@ invoke(params: InvokeParams): Promise; #### Returns -`Promise`\<`ResponseOutputItem`[]\> +`Promise`\<[`ResponseOutputItem`](TypeAlias.ResponseOutputItem.md)[]\> *** diff --git a/docs/docs/api/appkit/Interface.CustomMcpServerTool.md b/docs/docs/api/appkit/Interface.CustomMcpServerTool.md new file mode 100644 index 00000000..0f7a2898 --- /dev/null +++ b/docs/docs/api/appkit/Interface.CustomMcpServerTool.md @@ -0,0 +1,32 @@ +# Interface: CustomMcpServerTool + +## Properties + +### custom\_mcp\_server + +```ts +custom_mcp_server: { + app_name: string; + app_url: string; +}; +``` + +#### app\_name + +```ts +app_name: string; +``` + +#### app\_url + +```ts +app_url: string; +``` + +*** + +### type + +```ts +type: "custom_mcp_server"; +``` diff --git a/docs/docs/api/appkit/Interface.ExternalMcpServerTool.md b/docs/docs/api/appkit/Interface.ExternalMcpServerTool.md new file mode 100644 index 00000000..3bf049e5 --- /dev/null +++ b/docs/docs/api/appkit/Interface.ExternalMcpServerTool.md @@ -0,0 +1,25 @@ +# Interface: ExternalMcpServerTool + +## Properties + +### external\_mcp\_server + +```ts +external_mcp_server: { + connection_name: string; +}; +``` + +#### connection\_name + +```ts +connection_name: string; +``` + +*** + +### type + +```ts +type: "external_mcp_server"; +``` diff --git a/docs/docs/api/appkit/Interface.GenieTool.md b/docs/docs/api/appkit/Interface.GenieTool.md new file mode 100644 index 00000000..c9c32c69 --- /dev/null +++ b/docs/docs/api/appkit/Interface.GenieTool.md @@ -0,0 +1,31 @@ +# Interface: GenieTool + +OpenResponses-style hosted tool definitions for Databricks services. + +These types follow the OpenResponses convention of discriminating on `type`. +Internally, each hosted tool is resolved to a DatabricksMCPServer instance +so the agent can call managed MCP endpoints on the workspace. + +## Properties + +### genie\_space + +```ts +genie_space: { + id: string; +}; +``` + +#### id + +```ts +id: string; +``` + +*** + +### type + +```ts +type: "genie"; +``` diff --git a/docs/docs/api/appkit/Interface.IAgentConfig.md b/docs/docs/api/appkit/Interface.IAgentConfig.md index 9100521c..45fa8928 100644 --- a/docs/docs/api/appkit/Interface.IAgentConfig.md +++ b/docs/docs/api/appkit/Interface.IAgentConfig.md @@ -49,16 +49,6 @@ Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. *** -### mcpServers? - -```ts -optional mcpServers: DatabricksMCPServer[]; -``` - -MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. - -*** - ### model? ```ts @@ -121,8 +111,10 @@ Sampling temperature (0.0-1.0, default 0.1). Ignored when `agentInstance` is pro optional tools: AgentTool[]; ``` -Tools to register with the agent. Accepts OpenResponses-aligned FunctionTool -objects or LangChain StructuredToolInterface instances. +Tools to register with the agent. Accepts: +- OpenResponses-aligned `FunctionTool` objects (local tool with execute handler) +- Databricks hosted tools (`genie`, `vector_search_index`, `custom_mcp_server`, `external_mcp_server`) + Ignored when `agentInstance` is provided. *** diff --git a/docs/docs/api/appkit/Interface.ResponseFunctionCallOutput.md b/docs/docs/api/appkit/Interface.ResponseFunctionCallOutput.md new file mode 100644 index 00000000..2115b51f --- /dev/null +++ b/docs/docs/api/appkit/Interface.ResponseFunctionCallOutput.md @@ -0,0 +1,33 @@ +# Interface: ResponseFunctionCallOutput + +## Properties + +### call\_id + +```ts +call_id: string; +``` + +*** + +### id + +```ts +id: string; +``` + +*** + +### output + +```ts +output: string; +``` + +*** + +### type + +```ts +type: "function_call_output"; +``` diff --git a/docs/docs/api/appkit/Interface.ResponseFunctionToolCall.md b/docs/docs/api/appkit/Interface.ResponseFunctionToolCall.md new file mode 100644 index 00000000..c4563503 --- /dev/null +++ b/docs/docs/api/appkit/Interface.ResponseFunctionToolCall.md @@ -0,0 +1,49 @@ +# Interface: ResponseFunctionToolCall + +## Properties + +### arguments + +```ts +arguments: string; +``` + +*** + +### call\_id + +```ts +call_id: string; +``` + +*** + +### id + +```ts +id: string; +``` + +*** + +### name + +```ts +name: string; +``` + +*** + +### status + +```ts +status: "completed"; +``` + +*** + +### type + +```ts +type: "function_call"; +``` diff --git a/docs/docs/api/appkit/Interface.ResponseOutputMessage.md b/docs/docs/api/appkit/Interface.ResponseOutputMessage.md new file mode 100644 index 00000000..04b4243f --- /dev/null +++ b/docs/docs/api/appkit/Interface.ResponseOutputMessage.md @@ -0,0 +1,41 @@ +# Interface: ResponseOutputMessage + +## Properties + +### content + +```ts +content: ResponseOutputTextContent[]; +``` + +*** + +### id + +```ts +id: string; +``` + +*** + +### role + +```ts +role: "assistant"; +``` + +*** + +### status + +```ts +status: "in_progress" | "completed"; +``` + +*** + +### type + +```ts +type: "message"; +``` diff --git a/docs/docs/api/appkit/Interface.StandardAgent.md b/docs/docs/api/appkit/Interface.StandardAgent.md deleted file mode 100644 index c1d483e1..00000000 --- a/docs/docs/api/appkit/Interface.StandardAgent.md +++ /dev/null @@ -1,53 +0,0 @@ -# Interface: StandardAgent - -## Implements - -- [`AgentInterface`](Interface.AgentInterface.md) - -## Methods - -### invoke() - -```ts -invoke(params: InvokeParams): Promise; -``` - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `params` | [`InvokeParams`](Interface.InvokeParams.md) | - -#### Returns - -`Promise`\<`ResponseOutputItem`[]\> - -#### Implementation of - -```ts -AgentInterface.invoke -``` - -*** - -### stream() - -```ts -stream(params: InvokeParams): AsyncGenerator; -``` - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `params` | [`InvokeParams`](Interface.InvokeParams.md) | - -#### Returns - -`AsyncGenerator`\<[`ResponseStreamEvent`](TypeAlias.ResponseStreamEvent.md)\> - -#### Implementation of - -```ts -AgentInterface.stream -``` diff --git a/docs/docs/api/appkit/Interface.VectorSearchIndexTool.md b/docs/docs/api/appkit/Interface.VectorSearchIndexTool.md new file mode 100644 index 00000000..c935c9c4 --- /dev/null +++ b/docs/docs/api/appkit/Interface.VectorSearchIndexTool.md @@ -0,0 +1,25 @@ +# Interface: VectorSearchIndexTool + +## Properties + +### type + +```ts +type: "vector_search_index"; +``` + +*** + +### vector\_search\_index + +```ts +vector_search_index: { + name: string; +}; +``` + +#### name + +```ts +name: string; +``` diff --git a/docs/docs/api/appkit/TypeAlias.AgentTool.md b/docs/docs/api/appkit/TypeAlias.AgentTool.md index 4fe02425..51ee47bb 100644 --- a/docs/docs/api/appkit/TypeAlias.AgentTool.md +++ b/docs/docs/api/appkit/TypeAlias.AgentTool.md @@ -1,10 +1,12 @@ # Type Alias: AgentTool ```ts -type AgentTool = FunctionTool | StructuredToolInterface; +type AgentTool = + | FunctionTool + | HostedTool; ``` A tool that can be registered with the agent plugin. -- `FunctionTool` (preferred): OpenResponses-aligned plain object with JSON Schema parameters. -- `StructuredToolInterface`: LangChain tool for advanced use cases. +- `FunctionTool`: OpenResponses-aligned plain object with JSON Schema parameters and an execute handler. +- `HostedTool`: Databricks-hosted tool (genie, vector_search_index, custom_mcp_server, external_mcp_server). diff --git a/docs/docs/api/appkit/TypeAlias.HostedTool.md b/docs/docs/api/appkit/TypeAlias.HostedTool.md new file mode 100644 index 00000000..433c0ac8 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.HostedTool.md @@ -0,0 +1,9 @@ +# Type Alias: HostedTool + +```ts +type HostedTool = + | GenieTool + | VectorSearchIndexTool + | CustomMcpServerTool + | ExternalMcpServerTool; +``` diff --git a/docs/docs/api/appkit/TypeAlias.ResponseOutputItem.md b/docs/docs/api/appkit/TypeAlias.ResponseOutputItem.md new file mode 100644 index 00000000..3a4d4577 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.ResponseOutputItem.md @@ -0,0 +1,8 @@ +# Type Alias: ResponseOutputItem + +```ts +type ResponseOutputItem = + | ResponseOutputMessage + | ResponseFunctionToolCall + | ResponseFunctionCallOutput; +``` diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index b212a6ab..0335bd40 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -23,6 +23,7 @@ plugin architecture, and React integration. | [Plugin](Class.Plugin.md) | Base abstract class for creating AppKit plugins. | | [ResourceRegistry](Class.ResourceRegistry.md) | Central registry for tracking plugin resource requirements. Deduplication uses type + resourceKey (machine-stable); alias is for display only. | | [ServerError](Class.ServerError.md) | Error thrown when server lifecycle operations fail. Use for server start/stop issues, configuration conflicts, etc. | +| [StandardAgent](Class.StandardAgent.md) | Contract that agent implementations must fulfil. | | [TunnelError](Class.TunnelError.md) | Error thrown when remote tunnel operations fail. Use for tunnel connection issues, message parsing failures, etc. | | [ValidationError](Class.ValidationError.md) | Error thrown when input validation fails. Use for invalid parameters, missing required fields, or type mismatches. | @@ -33,9 +34,12 @@ plugin architecture, and React integration. | [AgentInterface](Interface.AgentInterface.md) | Contract that agent implementations must fulfil. | | [BasePluginConfig](Interface.BasePluginConfig.md) | Base configuration interface for AppKit plugins | | [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | +| [CustomMcpServerTool](Interface.CustomMcpServerTool.md) | - | | [DatabaseCredential](Interface.DatabaseCredential.md) | Database credentials with OAuth token for Postgres connection | +| [ExternalMcpServerTool](Interface.ExternalMcpServerTool.md) | - | | [FunctionTool](Interface.FunctionTool.md) | - | | [GenerateDatabaseCredentialRequest](Interface.GenerateDatabaseCredentialRequest.md) | Request parameters for generating database OAuth credentials | +| [GenieTool](Interface.GenieTool.md) | OpenResponses-style hosted tool definitions for Databricks services. | | [IAgentConfig](Interface.IAgentConfig.md) | Base configuration interface for AppKit plugins | | [InvokeParams](Interface.InvokeParams.md) | Agent interface types for the AppKit Agent Plugin. | | [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | @@ -46,10 +50,13 @@ plugin architecture, and React integration. | [ResourceEntry](Interface.ResourceEntry.md) | Internal representation of a resource in the registry. Extends ResourceRequirement with resolution state and plugin ownership. | | [ResourceFieldEntry](Interface.ResourceFieldEntry.md) | Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key). | | [ResourceRequirement](Interface.ResourceRequirement.md) | Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements(). | -| [StandardAgent](Interface.StandardAgent.md) | - | +| [ResponseFunctionCallOutput](Interface.ResponseFunctionCallOutput.md) | - | +| [ResponseFunctionToolCall](Interface.ResponseFunctionToolCall.md) | - | +| [ResponseOutputMessage](Interface.ResponseOutputMessage.md) | - | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | Configuration for streaming execution with default and user-scoped settings | | [TelemetryConfig](Interface.TelemetryConfig.md) | OpenTelemetry configuration for AppKit applications | | [ValidationResult](Interface.ValidationResult.md) | Result of validating all registered resources against the environment. | +| [VectorSearchIndexTool](Interface.VectorSearchIndexTool.md) | - | ## Type Aliases @@ -57,9 +64,11 @@ plugin architecture, and React integration. | ------ | ------ | | [AgentTool](TypeAlias.AgentTool.md) | A tool that can be registered with the agent plugin. | | [ConfigSchema](TypeAlias.ConfigSchema.md) | Configuration schema definition for plugin config. Re-exported from the standard JSON Schema Draft 7 types. | +| [HostedTool](TypeAlias.HostedTool.md) | - | | [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration | | [PluginData](TypeAlias.PluginData.md) | - | | [ResourcePermission](TypeAlias.ResourcePermission.md) | Union of all possible permission levels across all resource types. | +| [ResponseOutputItem](TypeAlias.ResponseOutputItem.md) | - | | [ResponseStreamEvent](TypeAlias.ResponseStreamEvent.md) | - | | [ToPlugin](TypeAlias.ToPlugin.md) | - | @@ -76,6 +85,7 @@ plugin architecture, and React integration. | ------ | ------ | | [appKitTypesPlugin](Function.appKitTypesPlugin.md) | Vite plugin to generate types for AppKit queries. Calls generateFromEntryPoint under the hood. | | [createApp](Function.createApp.md) | Bootstraps AppKit with the provided configuration. | +| [createInvokeHandler](Function.createInvokeHandler.md) | Create an Express handler that invokes the agent via the AgentInterface and streams/returns the response in Responses API format. | | [createLakebasePool](Function.createLakebasePool.md) | Create a Lakebase pool with appkit's logger integration. Telemetry automatically uses appkit's OpenTelemetry configuration via global registry. | | [generateDatabaseCredential](Function.generateDatabaseCredential.md) | Generate OAuth credentials for Postgres database connection using the proper Postgres API. | | [getExecutionContext](Function.getExecutionContext.md) | Get the current execution context. | @@ -85,4 +95,6 @@ plugin architecture, and React integration. | [getResourceRequirements](Function.getResourceRequirements.md) | Gets the resource requirements from a plugin's manifest. | | [getUsernameWithApiLookup](Function.getUsernameWithApiLookup.md) | Resolves the PostgreSQL username for a Lakebase connection. | | [getWorkspaceClient](Function.getWorkspaceClient.md) | Get workspace client from config or SDK default auth chain | +| [isFunctionTool](Function.isFunctionTool.md) | - | +| [isHostedTool](Function.isHostedTool.md) | - | | [isSQLTypeMarker](Function.isSQLTypeMarker.md) | Type guard to check if a value is a SQL type marker | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index a9b0c4ed..d44013de 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -66,6 +66,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Class.ServerError", label: "ServerError" }, + { + type: "doc", + id: "api/appkit/Class.StandardAgent", + label: "StandardAgent" + }, { type: "doc", id: "api/appkit/Class.TunnelError", @@ -97,11 +102,21 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.CacheConfig", label: "CacheConfig" }, + { + type: "doc", + id: "api/appkit/Interface.CustomMcpServerTool", + label: "CustomMcpServerTool" + }, { type: "doc", id: "api/appkit/Interface.DatabaseCredential", label: "DatabaseCredential" }, + { + type: "doc", + id: "api/appkit/Interface.ExternalMcpServerTool", + label: "ExternalMcpServerTool" + }, { type: "doc", id: "api/appkit/Interface.FunctionTool", @@ -112,6 +127,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.GenerateDatabaseCredentialRequest", label: "GenerateDatabaseCredentialRequest" }, + { + type: "doc", + id: "api/appkit/Interface.GenieTool", + label: "GenieTool" + }, { type: "doc", id: "api/appkit/Interface.IAgentConfig", @@ -164,8 +184,18 @@ const typedocSidebar: SidebarsConfig = { }, { type: "doc", - id: "api/appkit/Interface.StandardAgent", - label: "StandardAgent" + id: "api/appkit/Interface.ResponseFunctionCallOutput", + label: "ResponseFunctionCallOutput" + }, + { + type: "doc", + id: "api/appkit/Interface.ResponseFunctionToolCall", + label: "ResponseFunctionToolCall" + }, + { + type: "doc", + id: "api/appkit/Interface.ResponseOutputMessage", + label: "ResponseOutputMessage" }, { type: "doc", @@ -181,6 +211,11 @@ const typedocSidebar: SidebarsConfig = { type: "doc", id: "api/appkit/Interface.ValidationResult", label: "ValidationResult" + }, + { + type: "doc", + id: "api/appkit/Interface.VectorSearchIndexTool", + label: "VectorSearchIndexTool" } ] }, @@ -198,6 +233,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/TypeAlias.ConfigSchema", label: "ConfigSchema" }, + { + type: "doc", + id: "api/appkit/TypeAlias.HostedTool", + label: "HostedTool" + }, { type: "doc", id: "api/appkit/TypeAlias.IAppRouter", @@ -213,6 +253,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/TypeAlias.ResourcePermission", label: "ResourcePermission" }, + { + type: "doc", + id: "api/appkit/TypeAlias.ResponseOutputItem", + label: "ResponseOutputItem" + }, { type: "doc", id: "api/appkit/TypeAlias.ResponseStreamEvent", @@ -255,6 +300,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Function.createApp", label: "createApp" }, + { + type: "doc", + id: "api/appkit/Function.createInvokeHandler", + label: "createInvokeHandler" + }, { type: "doc", id: "api/appkit/Function.createLakebasePool", @@ -300,6 +350,16 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Function.getWorkspaceClient", label: "getWorkspaceClient" }, + { + type: "doc", + id: "api/appkit/Function.isFunctionTool", + label: "isFunctionTool" + }, + { + type: "doc", + id: "api/appkit/Function.isHostedTool", + label: "isHostedTool" + }, { type: "doc", id: "api/appkit/Function.isSQLTypeMarker", diff --git a/docs/docs/plugins/agent.md b/docs/docs/plugins/agent.md new file mode 100644 index 00000000..a468a08e --- /dev/null +++ b/docs/docs/plugins/agent.md @@ -0,0 +1,352 @@ +--- +sidebar_position: 7 +--- + +# Agent plugin + +Adds AI agent capabilities to your AppKit application. All requests and responses follow the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) format for both payloads and SSE streaming events. By default, the plugin runs a standard ReAct agent internally, but you can replace it with your own implementation via `AgentInterface`. + +**Key features:** + +- Built-in ReAct agent with tool-use loop — no framework code to write +- OpenResponses-aligned tool definitions (JSON Schema parameters) +- Databricks hosted tools: Genie spaces, Vector Search, custom and external MCP servers +- SSE streaming with real-time text deltas and tool call visibility +- Bring-your-own agent via `AgentInterface` +- Ready-to-use React chat component + +## Supported model endpoints + +The `model` config (or `DATABRICKS_MODEL` env var) should point to a **foundation model or external model endpoint** with the **chat** task type — any endpoint that supports the [chat completion query format](https://docs.databricks.com/aws/en/machine-learning/model-serving/score-foundation-models#chat-completion-model-query). This includes Databricks-hosted foundation models (e.g. `databricks-claude-sonnet-4-5`, `databricks-meta-llama-3-3-70b-instruct`) and external model endpoints. + +## Basic usage + +```ts +import { agent, createApp, server } from "@databricks/appkit"; + +await createApp({ + plugins: [ + server(), + agent({ + model: "databricks-claude-sonnet-4-5", + }), + ], +}); +``` + +## Configuration options + +| Option | Type | Default | Description | +| ----------------- | ---------------- | ------------------------------------- | ----------------------------------------------------------------------------- | +| `model` | `string` | `DATABRICKS_MODEL` env var | Databricks model serving endpoint name | +| `systemPrompt` | `string` | `"You are a helpful AI assistant..."` | System prompt injected at the start of every conversation | +| `tools` | `AgentTool[]` | `[]` | Tools to register (function tools and/or hosted tools) | +| `temperature` | `number` | `0.1` | Sampling temperature (0.0–1.0) | +| `maxTokens` | `number` | `2000` | Max tokens to generate | +| `useResponsesApi` | `boolean` | `false` | Use the Responses API instead of Chat Completions for the upstream model call | +| `agentInstance` | `AgentInterface` | — | Bring-your-own agent (skips built-in agent setup) | + +All options except `agentInstance` and `systemPrompt` are ignored when `agentInstance` is provided. + +## Environment variables + +| Variable | Description | +| ------------------ | --------------------------------------------------------------------- | +| `DATABRICKS_MODEL` | Model serving endpoint name (fallback when `model` config is omitted) | + +## Tools + +Tools are passed via the `tools` config array. There are two kinds: **function tools** (local, with an execute handler) and **hosted tools** (resolved to Databricks-managed MCP servers). + +### Function tools + +Define tools as plain objects following the [OpenResponses FunctionTool](https://platform.openai.com/docs/api-reference/responses/create#responses-create-tools) convention. Parameters use standard JSON Schema: + +```ts +import type { FunctionTool } from "@databricks/appkit"; + +const weatherTool: FunctionTool = { + type: "function", + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "City name, e.g. 'San Francisco'", + }, + }, + required: ["location"], + }, + execute: async ({ location }) => { + return `Weather in ${location}: sunny, 72°F`; + }, +}; + +agent({ + model: "databricks-claude-sonnet-4-5", + tools: [weatherTool], +}); +``` + +### Hosted tools + +Databricks-hosted tools are declared as typed objects and resolved to managed MCP server connections at startup: + +```ts +agent({ + model: "databricks-claude-sonnet-4-5", + tools: [ + // Genie space + { type: "genie", genie_space: { id: "01ABCDEF12345678" } }, + + // Vector Search index (three-part name: catalog.schema.index) + { + type: "vector_search_index", + vector_search_index: { name: "main.default.my_index" }, + }, + + // Custom MCP server (Databricks App) + { + type: "custom_mcp_server", + custom_mcp_server: { app_name: "my-mcp-app", app_url: "my-mcp-app" }, + }, + + // External MCP server (UC Connection) + { + type: "external_mcp_server", + external_mcp_server: { connection_name: "my-connection" }, + }, + ], +}); +``` + +| Hosted tool type | Description | Required fields | +| --------------------- | --------------------------------- | --------------------------------------------------------- | +| `genie` | AI/BI Genie space | `genie_space.id` | +| `vector_search_index` | Unity Catalog Vector Search index | `vector_search_index.name` (catalog.schema.index) | +| `custom_mcp_server` | Databricks App exposing MCP | `custom_mcp_server.app_name`, `custom_mcp_server.app_url` | +| `external_mcp_server` | External MCP via UC Connection | `external_mcp_server.connection_name` | + +### Adding tools after startup + +Tools can also be added after app creation: + +```ts +const appkit = await createApp({ + plugins: [server(), agent({ model: "databricks-claude-sonnet-4-5" })], +}); + +await appkit.agent.addTools([weatherTool, timeTool]); +``` + +This rebuilds the underlying agent with the new tool set. + +## HTTP endpoint + +The agent plugin exposes a single endpoint (mounted under `/api/agent`): + +- `POST /api/agent` — Invoke the agent (streaming or non-streaming) + +### Request format + +The endpoint accepts [Responses API](https://platform.openai.com/docs/api-reference/responses/create) payloads: + +```json +{ + "input": [{ "role": "user", "content": "What's the weather in SF?" }], + "stream": true +} +``` + +`input` can be a plain string (treated as a single user message) or an array of message objects with `role` and `content`. + +### Streaming response (SSE) + +When `stream: true` (default), the response is an SSE stream emitting these event types: + +| Event type | Description | +| ---------------------------- | ------------------------------------------------------ | +| `response.output_item.added` | A new output item (message, tool call, or tool result) | +| `response.output_text.delta` | Incremental text chunk from the assistant | +| `response.output_item.done` | An output item is complete | +| `response.completed` | The full response is done | +| `error` | Error details | +| `response.failed` | The response failed | + +The stream ends with `data: [DONE]`. + +### Non-streaming response + +When `stream: false`, the response is a JSON object: + +```json +{ + "output": [ + { "type": "message", "role": "assistant", "content": [...] } + ] +} +``` + +### Routing convention + +Databricks Apps expects an agent endpoint at `POST /invocations`. Use `server.extend()` to rewrite: + +```ts +const appkit = await createApp({ + plugins: [ + server({ autoStart: false }), + agent({ model: "databricks-claude-sonnet-4-5" }), + ], +}); + +appkit.server + .extend((app) => { + app.post("/invocations", (req, res) => { + req.url = "/api/agent"; + app(req, res); + }); + }) + .start(); +``` + +## Programmatic access + +The plugin exports `invoke` and `stream` for server-side use: + +```ts +const appkit = await createApp({ + plugins: [ + server(), + agent({ model: "databricks-claude-sonnet-4-5", tools: [weatherTool] }), + ], +}); + +// Simple invoke — returns the assistant's text +const reply = await appkit.agent.invoke([ + { role: "user", content: "What's the weather in SF?" }, +]); +console.log(reply); // "Weather in San Francisco: sunny, 72°F" + +// Stream Responses API events +for await (const event of appkit.agent.stream([ + { role: "user", content: "What's the weather in SF?" }, +])) { + if (event.type === "response.output_text.delta") { + process.stdout.write(event.delta); + } +} +``` + +## Bring your own agent + +The built-in agent covers common use cases, but you can replace it entirely. Implement the [`AgentInterface`](../api/appkit/Interface.AgentInterface.md) and pass it as `agentInstance`: + +```ts +import type { + AgentInterface, + InvokeParams, + ResponseOutputItem, + ResponseStreamEvent, +} from "@databricks/appkit"; + +class MyAgent implements AgentInterface { + async invoke(params: InvokeParams): Promise { + // your logic here + } + + async *stream(params: InvokeParams): AsyncGenerator { + // your streaming logic here + } +} + +agent({ agentInstance: new MyAgent() }); +``` + +When `agentInstance` is provided, the plugin acts as a thin HTTP adapter — all model, tool, and prompt configuration is ignored. + +## Frontend components + +The `@databricks/appkit-ui` package provides React components for agent chat. + +### AgentChat + +A full-featured chat interface with SSE streaming, tool call display, and auto-scroll: + +```tsx +import { AgentChat } from "@databricks/appkit-ui/react"; + +function ChatPage() { + return ( +
+ +
+ ); +} +``` + +| Prop | Type | Default | Description | +| -------------- | -------- | ---------------------------- | ------------------------------------------- | +| `invokeUrl` | `string` | `"/invocations"` | POST URL for agent invocations | +| `placeholder` | `string` | `"Type a message..."` | Input field placeholder text | +| `emptyMessage` | `string` | `"Send a message to start."` | Empty state message | +| `className` | `string` | — | Additional CSS class for the root container | + +### useAgentChat hook + +For custom chat UIs, use the `useAgentChat` hook directly: + +```tsx +import { useAgentChat } from "@databricks/appkit-ui/react"; + +function CustomChat() { + const { + displayMessages, + loading, + input, + setInput, + handleSubmit, + isStreamingText, + } = useAgentChat({ invokeUrl: "/invocations" }); + + return ( +
+ {displayMessages.map((msg, i) => ( +
+ {msg.role}: + {msg.role === "user" + ? msg.content + : msg.parts?.map((p, j) => + p.type === "text" ? {p.content} : null + )} +
+ ))} + setInput(e.target.value)} + disabled={loading} + /> + +
+ ); +} +``` + +**Return type:** + +| Field | Type | Description | +| ----------------- | ------------------------- | -------------------------------------------------- | +| `messages` | `ChatMessage[]` | Full message history | +| `displayMessages` | `ChatMessage[]` | Messages including current streaming state | +| `loading` | `boolean` | True while a request is in flight | +| `input` | `string` | Current input field value | +| `setInput` | `(value: string) => void` | Update input | +| `handleSubmit` | `(e: FormEvent) => void` | Submit handler | +| `isStreamingText` | `boolean` | True when the assistant is actively streaming text | diff --git a/docs/docs/plugins/caching.md b/docs/docs/plugins/caching.md index d6cba4c3..d40d6789 100644 --- a/docs/docs/plugins/caching.md +++ b/docs/docs/plugins/caching.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 --- # Caching diff --git a/docs/docs/plugins/custom-plugins.md b/docs/docs/plugins/custom-plugins.md index 8ccd58b4..7a7aac19 100644 --- a/docs/docs/plugins/custom-plugins.md +++ b/docs/docs/plugins/custom-plugins.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- # Creating custom plugins diff --git a/docs/docs/plugins/index.md b/docs/docs/plugins/index.md index f0e4b51d..dece8d4c 100644 --- a/docs/docs/plugins/index.md +++ b/docs/docs/plugins/index.md @@ -13,7 +13,7 @@ For complete API documentation, see the [`Plugin`](../api/appkit/Class.Plugin.md Configure plugins when creating your AppKit instance: ```typescript -import { createApp, server, analytics, genie, files } from "@databricks/appkit"; +import { createApp, server, analytics, genie, files, agent } from "@databricks/appkit"; const AppKit = await createApp({ plugins: [ @@ -21,6 +21,7 @@ const AppKit = await createApp({ analytics(), genie(), files(), + agent({ model: "databricks-claude-sonnet-4-5" }), ], }); ``` diff --git a/knip.json b/knip.json index fae5b9c1..34990bc5 100644 --- a/knip.json +++ b/knip.json @@ -7,7 +7,14 @@ "docs" ], "workspaces": { - "packages/appkit": {}, + "packages/appkit": { + "ignoreDependencies": [ + "@databricks/langchainjs", + "@langchain/core", + "@langchain/langgraph", + "@langchain/mcp-adapters" + ] + }, "packages/appkit-ui": {} }, "ignore": [ diff --git a/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx b/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx index a8b3df1f..1b485302 100644 --- a/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx +++ b/packages/appkit-ui/src/react/agent-chat/agent-chat-message.tsx @@ -1,7 +1,7 @@ import { AgentChatPart } from "./agent-chat-part"; import type { ChatMessage } from "./types"; -export interface AgentChatMessageProps { +interface AgentChatMessageProps { message: ChatMessage; isLast?: boolean; isStreaming?: boolean; diff --git a/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx b/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx index 4da5a1fa..9646372d 100644 --- a/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx +++ b/packages/appkit-ui/src/react/agent-chat/agent-chat-part.tsx @@ -1,7 +1,7 @@ import type { AssistantPart } from "./types"; import { tryFormatJson } from "./utils"; -export interface AgentChatPartProps { +interface AgentChatPartProps { part: AssistantPart; showCursor?: boolean; } diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index d475ca57..5b477845 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -52,10 +52,24 @@ export { agent, analytics, files, genie, lakebase, server } from "./plugins"; export type { AgentInterface, AgentTool, + CustomMcpServerTool, + ExternalMcpServerTool, FunctionTool, + GenieTool, + HostedTool, IAgentConfig, InvokeParams, + ResponseFunctionCallOutput, + ResponseFunctionToolCall, + ResponseOutputItem, + ResponseOutputMessage, ResponseStreamEvent, + VectorSearchIndexTool, +} from "./plugins/agent"; +export { + createInvokeHandler, + isFunctionTool, + isHostedTool, StandardAgent, } from "./plugins/agent"; // Registry types and utilities for plugin manifests diff --git a/packages/appkit/src/plugins/agent/agent.ts b/packages/appkit/src/plugins/agent/agent.ts index 1a6605a2..ec55fba5 100644 --- a/packages/appkit/src/plugins/agent/agent.ts +++ b/packages/appkit/src/plugins/agent/agent.ts @@ -1,24 +1,27 @@ /** - * AgentPlugin — first-class AppKit plugin for LangChain/LangGraph agents. + * AgentPlugin — first-class AppKit plugin for building AI agents. * * Provides: * - POST /api/agent (standard AppKit namespaced route) * * Supports two modes: * 1. Bring-your-own agent via `config.agentInstance` - * 2. Auto-build a LangGraph ReAct agent from config (model, tools, MCP servers) + * 2. Auto-build a ReAct agent from config (model, tools) * - * When using config (not agentInstance), you can add tools and MCP servers - * after app creation via appkit.agent.addTools() and appkit.agent.addMcpServers(). + * Tools can be local (FunctionTool with an execute handler) or hosted on + * Databricks (genie, vector_search_index, custom_mcp_server, external_mcp_server). + * Hosted tools are resolved to managed MCP servers transparently. */ -import type { DatabricksMCPServer } from "@databricks/langchainjs"; -import type { StructuredToolInterface } from "@langchain/core/tools"; import type express from "express"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; +import type { PluginManifest } from "../../registry"; import type { AgentInterface } from "./agent-interface"; -import { functionToolToStructuredTool, isFunctionTool } from "./function-tool"; +import type { FunctionTool } from "./function-tool"; +import { functionToolToStructuredTool } from "./function-tool"; +import type { HostedTool } from "./hosted-tools"; +import { isHostedTool, resolveHostedTools } from "./hosted-tools"; import { createInvokeHandler } from "./invoke-handler"; import manifest from "./manifest.json"; import { StandardAgent } from "./standard-agent"; @@ -36,43 +39,31 @@ type ChatDatabricksInstance = InstanceType< export class AgentPlugin extends Plugin { public name = "agent" as const; - static manifest = manifest; + static manifest = manifest as PluginManifest<"agent">; protected declare config: IAgentConfig; private agentImpl: AgentInterface | null = null; private systemPrompt = DEFAULT_SYSTEM_PROMPT; private mcpClient: { - getTools(): Promise; + getTools(): Promise; close(): Promise; } | null = null; - /** Only set when building from config (not agentInstance). Used when rebuilding after addTools/addMcpServers. */ + /** Only set when building from config (not agentInstance). */ private model: ChatDatabricksInstance | null = null; - /** Mutable list of tools (config + added). Only used when building from config. */ + /** Mutable list of all tools (config + added). Only used when building from config. */ private toolsList: AgentTool[] = []; - /** Mutable list of MCP servers (config + added). Only used when building from config. */ - private mcpServersList: DatabricksMCPServer[] = []; - - /** - * Normalize an AgentTool to a LangChain StructuredToolInterface. - * FunctionTool objects are converted; StructuredToolInterface pass through. - */ - private static toStructuredTool(tool: AgentTool): StructuredToolInterface { - return isFunctionTool(tool) ? functionToolToStructuredTool(tool) : tool; - } async setup() { this.systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; - // If a pre-built agent is provided, use it directly if (this.config.agentInstance) { this.agentImpl = this.config.agentInstance; logger.info("AgentPlugin initialized with provided agentInstance"); return; } - // Otherwise build a LangGraph ReAct agent from config const modelName = this.config.model ?? process.env.DATABRICKS_MODEL; if (!modelName) { @@ -92,26 +83,50 @@ export class AgentPlugin extends Plugin { }); this.toolsList = [...(this.config.tools ?? [])]; - this.mcpServersList = [...(this.config.mcpServers ?? [])]; await this.buildStandardAgent(); + const { localTools, hostedTools } = AgentPlugin.partitionTools( + this.toolsList, + ); logger.info( - "AgentPlugin initialized: model=%s tools=%d mcpServers=%d", + "AgentPlugin initialized: model=%s localTools=%d hostedTools=%d", modelName, - this.toolsList.length, - this.mcpServersList.length, + localTools.length, + hostedTools.length, ); } /** - * Builds or rebuilds the LangGraph ReAct agent from current model, toolsList, and mcpServersList. - * Call this after changing toolsList or mcpServersList (e.g. via addTools/addMcpServers). + * Partition the tools list into local FunctionTools and hosted tools + * (Databricks-managed MCP services). + */ + private static partitionTools(tools: AgentTool[]): { + localTools: FunctionTool[]; + hostedTools: HostedTool[]; + } { + const localTools: FunctionTool[] = []; + const hostedTools: HostedTool[] = []; + + for (const tool of tools) { + if (isHostedTool(tool)) { + hostedTools.push(tool); + } else { + localTools.push(tool); + } + } + + return { localTools, hostedTools }; + } + + /** + * Builds or rebuilds the ReAct agent from current model and toolsList. + * FunctionTools are converted to the internal tool format; hosted tools + * are resolved to MCP server connections. */ private async buildStandardAgent(): Promise { if (!this.model) return; - // Close existing MCP client before creating a new one if (this.mcpClient) { try { await this.mcpClient.close(); @@ -121,16 +136,19 @@ export class AgentPlugin extends Plugin { this.mcpClient = null; } - const tools: StructuredToolInterface[] = []; + const { localTools, hostedTools } = AgentPlugin.partitionTools( + this.toolsList, + ); + + const tools: unknown[] = []; - if (this.mcpServersList.length > 0) { + if (hostedTools.length > 0) { try { + const mcpServers = await resolveHostedTools(hostedTools); const { buildMCPServerConfig } = await import( "@databricks/langchainjs" ); - const mcpServerConfigs = await buildMCPServerConfig( - this.mcpServersList, - ); + const mcpServerConfigs = await buildMCPServerConfig(mcpServers); const { MultiServerMCPClient } = await import( "@langchain/mcp-adapters" ); @@ -142,24 +160,24 @@ export class AgentPlugin extends Plugin { const mcpTools = await this.mcpClient.getTools(); tools.push(...mcpTools); logger.info( - "Loaded %d MCP tools from %d server(s)", + "Loaded %d MCP tools from %d hosted tool(s)", mcpTools.length, - this.mcpServersList.length, + hostedTools.length, ); } catch (err) { logger.warn( - "Failed to load MCP tools — continuing without them: %O", + "Failed to load hosted tools — continuing without them: %O", err, ); } } - tools.push(...this.toolsList.map(AgentPlugin.toStructuredTool)); + tools.push(...localTools.map(functionToolToStructuredTool)); const { createReactAgent } = await import("@langchain/langgraph/prebuilt"); const langGraphAgent = createReactAgent({ llm: this.model, - tools, + tools: tools as any, }); this.agentImpl = new StandardAgent( @@ -169,59 +187,32 @@ export class AgentPlugin extends Plugin { } /** - * Batch-add tools and/or MCP servers with a single agent rebuild. - * Only supported when the plugin was initialized from config (not agentInstance). + * Add tools to the agent after app creation. Only supported when the plugin + * was initialized from config (not when using agentInstance). Rebuilds the + * underlying agent with the new tool set. * - * Tools can be OpenResponses-aligned FunctionTool objects or LangChain StructuredToolInterface. + * Accepts FunctionTool or hosted tool definitions. */ - async addCapabilities(options: { - tools?: AgentTool[]; - mcpServers?: DatabricksMCPServer[]; - }): Promise { + async addTools(tools: AgentTool[]): Promise { if (this.config.agentInstance) { throw new Error( - "addCapabilities() is not supported when using a custom agentInstance", + "addTools() is not supported when using a custom agentInstance", ); } if (!this.model) { throw new Error("AgentPlugin not initialized — call setup() first"); } - const { tools, mcpServers } = options; - if (tools?.length) this.toolsList.push(...tools); - if (mcpServers?.length) this.mcpServersList.push(...mcpServers); - + this.toolsList.push(...tools); await this.buildStandardAgent(); logger.info( - "Configured agent: added %d tool(s), %d MCP server(s); totals tools=%d servers=%d", - tools?.length ?? 0, - mcpServers?.length ?? 0, + "Added %d tool(s); total tools=%d", + tools.length, this.toolsList.length, - this.mcpServersList.length, ); } - /** - * Add tools to the agent after app creation. Only supported when the plugin - * was initialized from config (not when using agentInstance). Rebuilds the - * underlying LangGraph agent with the new tool set. - * - * Accepts OpenResponses-aligned FunctionTool objects or LangChain StructuredToolInterface. - */ - async addTools(tools: AgentTool[]): Promise { - await this.addCapabilities({ tools }); - } - - /** - * Add MCP servers to the agent after app creation. Only supported when the - * plugin was initialized from config (not when using agentInstance). Rebuilds - * the underlying LangGraph agent so new MCP tools are available. - */ - async addMcpServers(servers: DatabricksMCPServer[]): Promise { - await this.addCapabilities({ mcpServers: servers }); - } - private getAgentImpl(): AgentInterface { if (!this.agentImpl) { throw new Error("AgentPlugin not initialized — call setup() first"); @@ -283,12 +274,6 @@ export class AgentPlugin extends Plugin { }.bind(this), addTools: (tools: AgentTool[]) => this.addTools(tools), - addMcpServers: (servers: DatabricksMCPServer[]) => - this.addMcpServers(servers), - addCapabilities: (options: { - tools?: AgentTool[]; - mcpServers?: DatabricksMCPServer[]; - }) => this.addCapabilities(options), }; } } diff --git a/packages/appkit/src/plugins/agent/hosted-tools.ts b/packages/appkit/src/plugins/agent/hosted-tools.ts new file mode 100644 index 00000000..ccc25e12 --- /dev/null +++ b/packages/appkit/src/plugins/agent/hosted-tools.ts @@ -0,0 +1,107 @@ +/** + * OpenResponses-style hosted tool definitions for Databricks services. + * + * These types follow the OpenResponses convention of discriminating on `type`. + * Internally, each hosted tool is resolved to a DatabricksMCPServer instance + * so the agent can call managed MCP endpoints on the workspace. + */ + +// --------------------------------------------------------------------------- +// Hosted tool type definitions +// --------------------------------------------------------------------------- + +export interface GenieTool { + type: "genie"; + genie_space: { id: string }; +} + +export interface VectorSearchIndexTool { + type: "vector_search_index"; + vector_search_index: { name: string }; +} + +export interface CustomMcpServerTool { + type: "custom_mcp_server"; + custom_mcp_server: { app_name: string; app_url: string }; +} + +export interface ExternalMcpServerTool { + type: "external_mcp_server"; + external_mcp_server: { connection_name: string }; +} + +export type HostedTool = + | GenieTool + | VectorSearchIndexTool + | CustomMcpServerTool + | ExternalMcpServerTool; + +// --------------------------------------------------------------------------- +// Type guard +// --------------------------------------------------------------------------- + +const HOSTED_TOOL_TYPES = new Set([ + "genie", + "vector_search_index", + "custom_mcp_server", + "external_mcp_server", +]); + +export function isHostedTool(t: unknown): t is HostedTool { + return ( + typeof t === "object" && + t !== null && + HOSTED_TOOL_TYPES.has((t as any).type) + ); +} + +// --------------------------------------------------------------------------- +// Resolver: HostedTool → DatabricksMCPServer +// --------------------------------------------------------------------------- + +/** + * Resolve an array of HostedTool definitions into DatabricksMCPServer instances. + * + * Uses factory methods from `@databricks/langchainjs` where available, and + * falls back to raw DatabricksMCPServer construction for tool types that + * map to known managed MCP API paths. + */ +export async function resolveHostedTools( + tools: HostedTool[], +): Promise[]> { + const { DatabricksMCPServer } = await import("@databricks/langchainjs"); + + return tools.map((tool) => { + switch (tool.type) { + case "genie": + return DatabricksMCPServer.fromGenieSpace(tool.genie_space.id); + + case "vector_search_index": { + const parts = tool.vector_search_index.name.split("."); + if (parts.length !== 3) { + throw new Error( + `vector_search_index name must be "catalog.schema.index", got "${tool.vector_search_index.name}"`, + ); + } + const [catalog, schema, index] = parts; + return DatabricksMCPServer.fromVectorSearch(catalog, schema, index); + } + + case "custom_mcp_server": + return new DatabricksMCPServer({ + name: `mcp-app-${tool.custom_mcp_server.app_name}`, + path: `/apps/${tool.custom_mcp_server.app_url}`, + }); + + case "external_mcp_server": + return new DatabricksMCPServer({ + name: `mcp-ext-${tool.external_mcp_server.connection_name}`, + path: `/api/2.0/mcp/connections/${tool.external_mcp_server.connection_name}`, + }); + + default: { + throw new Error(`Unknown hosted tool type: ${(tool as any).type}`); + } + } + }); +} diff --git a/packages/appkit/src/plugins/agent/index.ts b/packages/appkit/src/plugins/agent/index.ts index 57c6f2bc..97bae0f6 100644 --- a/packages/appkit/src/plugins/agent/index.ts +++ b/packages/appkit/src/plugins/agent/index.ts @@ -1,4 +1,4 @@ -export { AgentPlugin, agent } from "./agent"; +export { agent } from "./agent"; export type { AgentInterface, InvokeParams, @@ -10,7 +10,14 @@ export type { } from "./agent-interface"; export type { FunctionTool } from "./function-tool"; export { isFunctionTool } from "./function-tool"; +export type { + CustomMcpServerTool, + ExternalMcpServerTool, + GenieTool, + HostedTool, + VectorSearchIndexTool, +} from "./hosted-tools"; +export { isHostedTool } from "./hosted-tools"; export { createInvokeHandler } from "./invoke-handler"; -export type { LangGraphAgent } from "./standard-agent"; export { StandardAgent } from "./standard-agent"; export type { AgentTool, IAgentConfig } from "./types"; diff --git a/packages/appkit/src/plugins/agent/standard-agent.ts b/packages/appkit/src/plugins/agent/standard-agent.ts index 67c607da..290f9dc0 100644 --- a/packages/appkit/src/plugins/agent/standard-agent.ts +++ b/packages/appkit/src/plugins/agent/standard-agent.ts @@ -21,7 +21,7 @@ import type { /** * Minimal interface for the LangGraph agent returned by createReactAgent. */ -export interface LangGraphAgent { +interface LangGraphAgent { invoke(input: { messages: BaseMessage[]; }): Promise<{ messages: BaseMessage[] }>; diff --git a/packages/appkit/src/plugins/agent/tests/hosted-tools.test.ts b/packages/appkit/src/plugins/agent/tests/hosted-tools.test.ts new file mode 100644 index 00000000..9eebffcc --- /dev/null +++ b/packages/appkit/src/plugins/agent/tests/hosted-tools.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "vitest"; +import type { + CustomMcpServerTool, + ExternalMcpServerTool, + GenieTool, + HostedTool, + VectorSearchIndexTool, +} from "../hosted-tools"; +import { isHostedTool } from "../hosted-tools"; + +const genieTool: GenieTool = { + type: "genie", + genie_space: { id: "space-123" }, +}; + +const vectorSearchTool: VectorSearchIndexTool = { + type: "vector_search_index", + vector_search_index: { name: "catalog.schema.my_index" }, +}; + +const customMcpTool: CustomMcpServerTool = { + type: "custom_mcp_server", + custom_mcp_server: { app_name: "my-mcp-app", app_url: "my-mcp-app" }, +}; + +const externalMcpTool: ExternalMcpServerTool = { + type: "external_mcp_server", + external_mcp_server: { connection_name: "my-connection" }, +}; + +describe("isHostedTool", () => { + test("returns true for GenieTool", () => { + expect(isHostedTool(genieTool)).toBe(true); + }); + + test("returns true for VectorSearchIndexTool", () => { + expect(isHostedTool(vectorSearchTool)).toBe(true); + }); + + test("returns true for CustomMcpServerTool", () => { + expect(isHostedTool(customMcpTool)).toBe(true); + }); + + test("returns true for ExternalMcpServerTool", () => { + expect(isHostedTool(externalMcpTool)).toBe(true); + }); + + test("returns false for FunctionTool", () => { + const functionTool = { + type: "function", + name: "test", + execute: async () => "result", + }; + expect(isHostedTool(functionTool)).toBe(false); + }); + + test("returns false for null/undefined", () => { + expect(isHostedTool(null)).toBe(false); + expect(isHostedTool(undefined)).toBe(false); + }); + + test("returns false for object with unknown type", () => { + expect(isHostedTool({ type: "unknown_tool" })).toBe(false); + }); + + test("returns false for non-object", () => { + expect(isHostedTool("genie")).toBe(false); + expect(isHostedTool(42)).toBe(false); + }); +}); + +describe("hosted tool types", () => { + test("all hosted tools satisfy HostedTool union", () => { + const tools: HostedTool[] = [ + genieTool, + vectorSearchTool, + customMcpTool, + externalMcpTool, + ]; + + expect(tools).toHaveLength(4); + for (const tool of tools) { + expect(isHostedTool(tool)).toBe(true); + } + }); + + test("can be mixed in an array with discriminator", () => { + const tools: HostedTool[] = [genieTool, vectorSearchTool]; + const types = tools.map((t) => t.type); + expect(types).toEqual(["genie", "vector_search_index"]); + }); +}); diff --git a/packages/appkit/src/plugins/agent/types.ts b/packages/appkit/src/plugins/agent/types.ts index aaac1245..8ea6f97b 100644 --- a/packages/appkit/src/plugins/agent/types.ts +++ b/packages/appkit/src/plugins/agent/types.ts @@ -1,16 +1,15 @@ -import type { DatabricksMCPServer } from "@databricks/langchainjs"; -import type { StructuredToolInterface } from "@langchain/core/tools"; import type { BasePluginConfig } from "shared"; import type { AgentInterface } from "./agent-interface"; import type { FunctionTool } from "./function-tool"; +import type { HostedTool } from "./hosted-tools"; /** * A tool that can be registered with the agent plugin. * - * - `FunctionTool` (preferred): OpenResponses-aligned plain object with JSON Schema parameters. - * - `StructuredToolInterface`: LangChain tool for advanced use cases. + * - `FunctionTool`: OpenResponses-aligned plain object with JSON Schema parameters and an execute handler. + * - `HostedTool`: Databricks-hosted tool (genie, vector_search_index, custom_mcp_server, external_mcp_server). */ -export type AgentTool = FunctionTool | StructuredToolInterface; +export type AgentTool = FunctionTool | HostedTool; export interface IAgentConfig extends BasePluginConfig { /** @@ -44,12 +43,11 @@ export interface IAgentConfig extends BasePluginConfig { /** Max tokens to generate (default 2000). Ignored when `agentInstance` is provided. */ maxTokens?: number; - /** MCP servers for Databricks tool integration. Ignored when `agentInstance` is provided. */ - mcpServers?: DatabricksMCPServer[]; - /** - * Tools to register with the agent. Accepts OpenResponses-aligned FunctionTool - * objects or LangChain StructuredToolInterface instances. + * Tools to register with the agent. Accepts: + * - OpenResponses-aligned `FunctionTool` objects (local tool with execute handler) + * - Databricks hosted tools (`genie`, `vector_search_index`, `custom_mcp_server`, `external_mcp_server`) + * * Ignored when `agentInstance` is provided. */ tools?: AgentTool[]; From 033192e2d68c223fcea3cbff489d47a78016afcc Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Wed, 11 Mar 2026 16:07:09 +0100 Subject: [PATCH 4/4] feat(appkit): agent plugin tracing Signed-off-by: Hubert Zub --- packages/appkit/src/core/appkit.ts | 29 ++- packages/appkit/src/plugins/agent/agent.ts | 93 ++++++++++ packages/appkit/src/plugins/agent/mlflow.ts | 174 ++++++++++++++++++ packages/appkit/src/plugins/agent/tracing.ts | 47 +++++ packages/appkit/src/plugins/agent/types.ts | 26 +++ packages/appkit/src/plugins/server/index.ts | 10 +- .../src/plugins/server/tests/server.test.ts | 4 +- packages/appkit/src/plugins/server/types.ts | 6 + .../appkit/src/telemetry/instrumentations.ts | 86 +++++---- .../appkit/src/telemetry/telemetry-manager.ts | 56 +++++- .../telemetry/tests/telemetry-manager.test.ts | 14 +- packages/appkit/src/telemetry/types.ts | 6 + packages/shared/src/plugin.ts | 5 + 13 files changed, 491 insertions(+), 65 deletions(-) create mode 100644 packages/appkit/src/plugins/agent/mlflow.ts create mode 100644 packages/appkit/src/plugins/agent/tracing.ts diff --git a/packages/appkit/src/core/appkit.ts b/packages/appkit/src/core/appkit.ts index a2cba994..e334ac6d 100644 --- a/packages/appkit/src/core/appkit.ts +++ b/packages/appkit/src/core/appkit.ts @@ -169,12 +169,35 @@ export class AppKit { client?: WorkspaceClient; } = {}, ): Promise> { + const rawPlugins = config.plugins as T; + + // Collect plugin-contributed trace exporter headers before telemetry init + const traceExporterHeaders: Record = { + ...config?.telemetry?.traceExporterHeaders, + }; + for (const entry of rawPlugins) { + if (typeof entry.plugin.appendTraceHeaders === "function") { + Object.assign( + traceExporterHeaders, + entry.plugin.appendTraceHeaders( + entry.config as Parameters< + typeof entry.plugin.appendTraceHeaders + >[0], + ), + ); + } + } + // Initialize core services - TelemetryManager.initialize(config?.telemetry); + await TelemetryManager.initialize({ + ...config?.telemetry, + traceExporterHeaders: + Object.keys(traceExporterHeaders).length > 0 + ? traceExporterHeaders + : undefined, + }); await CacheManager.getInstance(config?.cache); - const rawPlugins = config.plugins as T; - // Collect manifest resources via registry const registry = new ResourceRegistry(); registry.collectResources(rawPlugins); diff --git a/packages/appkit/src/plugins/agent/agent.ts b/packages/appkit/src/plugins/agent/agent.ts index ec55fba5..16f3d887 100644 --- a/packages/appkit/src/plugins/agent/agent.ts +++ b/packages/appkit/src/plugins/agent/agent.ts @@ -24,8 +24,10 @@ import type { HostedTool } from "./hosted-tools"; import { isHostedTool, resolveHostedTools } from "./hosted-tools"; import { createInvokeHandler } from "./invoke-handler"; import manifest from "./manifest.json"; +import { setupExperimentTraceLocation } from "./mlflow"; import { StandardAgent } from "./standard-agent"; import type { AgentTool, IAgentConfig } from "./types"; +import { instrumentLangChain } from "./tracing"; const logger = createLogger("agent"); @@ -41,6 +43,36 @@ export class AgentPlugin extends Plugin { static manifest = manifest as PluginManifest<"agent">; + /** + * Called by `_createApp` before TelemetryManager initializes. + * + * Resolves the MLflow experiment ID and UC table name from config / env. + * When `ucTableName` is not explicitly set, derives it deterministically + * from catalog + schema so the exporter header is available immediately. + * The actual UC location provisioning happens later in `setup()`. + * + * Returns OTLP headers only when an experiment ID is available. + */ + static appendTraceHeaders(config?: IAgentConfig): Record { + if (config?.tracing === false) return {}; + + const tracing = typeof config?.tracing === "object" ? config.tracing : {}; + + const experimentId = + tracing.experimentId ?? process.env.MLFLOW_EXPERIMENT_ID; + if (!experimentId) return {}; + + const ucTableName = tracing.ucTableName ?? process.env.OTEL_UC_TABLE_NAME; + + const headers: Record = { + "x-mlflow-experiment-id": experimentId, + }; + if (ucTableName) { + headers["X-Databricks-UC-Table-Name"] = ucTableName; + } + return headers; + } + protected declare config: IAgentConfig; private agentImpl: AgentInterface | null = null; @@ -55,10 +87,63 @@ export class AgentPlugin extends Plugin { /** Mutable list of all tools (config + added). Only used when building from config. */ private toolsList: AgentTool[] = []; + /** + * Tracing is active when `tracing !== false` AND an experiment ID is + * available (from config or env). + */ + private isTracingActive(): boolean { + if (this.config.tracing === false) { + return false; + } + const tracing = + typeof this.config.tracing === "object" ? this.config.tracing : {}; + + return Boolean(tracing.experimentId ?? process.env.MLFLOW_EXPERIMENT_ID); + } + + /** + * Resolve tracing config for the location-setup API. + * Catalog and schema are derived from OTEL_UC_TABLE_NAME (catalog.schema.table). + */ + private getTracingConfig() { + const tracing = + typeof this.config.tracing === "object" ? this.config.tracing : {}; + const ucTableName = tracing.ucTableName ?? process.env.OTEL_UC_TABLE_NAME; + const parts = ucTableName?.split(".") ?? []; + return { + experimentId: + tracing.experimentId ?? process.env.MLFLOW_EXPERIMENT_ID ?? "", + ucCatalog: parts.length >= 3 ? parts[0] : undefined, + ucSchema: parts.length >= 3 ? parts[1] : undefined, + warehouseId: tracing.warehouseId, + }; + } + async setup() { this.systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; + let tracingActive = this.isTracingActive(); + + // Ensure the UC trace location exists; skip instrumentation if it fails + if (tracingActive) { + const tracingConfig = this.getTracingConfig(); + const location = await setupExperimentTraceLocation(tracingConfig).catch( + (err) => { + logger.warn("Trace location setup failed: %O", err); + return null; + }, + ); + if (!location) { + tracingActive = false; + } + } + + // If a pre-built agent is provided, use it directly if (this.config.agentInstance) { + if (tracingActive) { + const cbModule = await import("@langchain/core/callbacks/manager"); + await instrumentLangChain(cbModule); + } this.agentImpl = this.config.agentInstance; logger.info("AgentPlugin initialized with provided agentInstance"); return; @@ -74,6 +159,14 @@ export class AgentPlugin extends Plugin { const { ChatDatabricks } = await import("@databricks/langchainjs"); + // Instrument LangChain callbacks using the *same* @langchain/core copy + // that the agent runtime (LangGraph, ChatDatabricks) will use. + // Spans flow through the global tracer provider (TelemetryManager). + if (tracingActive) { + const cbModule = await import("@langchain/core/callbacks/manager"); + await instrumentLangChain(cbModule); + } + this.model = new ChatDatabricks({ model: modelName, useResponsesApi: this.config.useResponsesApi ?? false, diff --git a/packages/appkit/src/plugins/agent/mlflow.ts b/packages/appkit/src/plugins/agent/mlflow.ts new file mode 100644 index 00000000..c6187460 --- /dev/null +++ b/packages/appkit/src/plugins/agent/mlflow.ts @@ -0,0 +1,174 @@ +/** + * MLflow experiment trace location setup. + * + * Auto-provisions Unity Catalog trace storage and links an MLflow experiment + * to it using the Databricks REST API. This mirrors the Python + * `mlflow.set_experiment_trace_location()` behaviour. + * + * Env vars: + * OTEL_UC_TABLE_NAME — fully-qualified UC table (catalog.schema.table); + * catalog and schema are derived by splitting on dots. + * MLFLOW_TRACING_SQL_WAREHOUSE_ID — warehouse used to create the storage (optional) + */ + +import { createLogger } from "../../logging/logger"; + +const logger = createLogger("agent:mlflow"); + +interface TraceLocationOptions { + experimentId: string; + /** Derived from OTEL_UC_TABLE_NAME by the caller (first dot-segment). */ + ucCatalog?: string; + /** Derived from OTEL_UC_TABLE_NAME by the caller (second dot-segment). */ + ucSchema?: string; + warehouseId?: string; +} + +const MLFLOW_TRACE_LOCATION_TABLE_NAME = "mlflow_experiment_trace_otel_spans"; + +/** + * Link an MLflow experiment to an existing UC trace location. + * Returns the fully-qualified table name on success, `null` otherwise. + */ +async function linkExperimentToLocation( + client: { apiClient: { request: (opts: unknown) => Promise } }, + experimentId: string, + catalogName: string, + schemaName: string, +): Promise { + const tableName = `${catalogName}.${schemaName}.${MLFLOW_TRACE_LOCATION_TABLE_NAME}`; + + try { + await client.apiClient.request({ + path: `/api/4.0/mlflow/traces/${experimentId}/link-location`, + method: "POST", + headers: new Headers({ "Content-Type": "application/json" }), + payload: { + experiment_id: experimentId, + uc_schema: { + catalog_name: catalogName, + schema_name: schemaName, + }, + }, + raw: false, + }); + + logger.info("Experiment linked to UC trace location: %s", tableName); + return tableName; + } catch (error: unknown) { + const code = + (error as { error_code?: string }).error_code ?? + (error as Error).name ?? + "UNKNOWN"; + logger.warn( + "Could not link experiment %s to %s (%s)", + experimentId, + tableName, + code, + ); + return null; + } +} + +/** + * Provision a UC trace storage location and link the experiment to it. + * + * If `warehouseId` is not provided, the function attempts to link directly + * (works when the UC table already exists). + * + * Returns the fully-qualified UC table name on success, `null` otherwise. + */ +export async function setupExperimentTraceLocation( + opts: TraceLocationOptions, +): Promise { + const catalogName = opts.ucCatalog; + const schemaName = opts.ucSchema; + + if (!catalogName || !schemaName) { + logger.debug( + "Skipping trace location setup — catalog/schema not available (set OTEL_UC_TABLE_NAME as catalog.schema.table)", + ); + return null; + } + + const warehouseId = + opts.warehouseId ?? process.env.MLFLOW_TRACING_SQL_WAREHOUSE_ID; + + let client: { + apiClient: { request: (opts: unknown) => Promise }; + config: { ensureResolved: () => Promise }; + }; + try { + const { WorkspaceClient } = await import("@databricks/sdk-experimental"); + client = new WorkspaceClient({}) as typeof client; + await client.config.ensureResolved(); + } catch (err) { + logger.warn( + "Cannot set up trace location — Databricks auth unavailable: %O", + err, + ); + return null; + } + + if (!warehouseId) { + const result = await linkExperimentToLocation( + client, + opts.experimentId, + catalogName, + schemaName, + ); + if (!result) { + logger.warn( + "Trace destination does not exist and cannot be created — " + + "set MLFLOW_TRACING_SQL_WAREHOUSE_ID to auto-create it", + ); + } + return result; + } + + try { + logger.debug( + "Creating UC trace location: %s.%s (warehouse=%s)", + catalogName, + schemaName, + warehouseId, + ); + + await client.apiClient.request({ + path: "/api/4.0/mlflow/traces/location", + method: "POST", + headers: new Headers({ "Content-Type": "application/json" }), + payload: { + uc_schema: { + catalog_name: catalogName, + schema_name: schemaName, + }, + sql_warehouse_id: warehouseId, + }, + raw: false, + }); + + return linkExperimentToLocation( + client, + opts.experimentId, + catalogName, + schemaName, + ); + } catch (error: unknown) { + // 409 = location already exists — just link + if ( + error instanceof Error && + (error.message?.includes("409") || + error.message?.includes("ALREADY_EXISTS")) + ) { + return linkExperimentToLocation( + client, + opts.experimentId, + catalogName, + schemaName, + ); + } + logger.warn("Failed to create UC trace location: %O", error); + return null; + } +} diff --git a/packages/appkit/src/plugins/agent/tracing.ts b/packages/appkit/src/plugins/agent/tracing.ts new file mode 100644 index 00000000..e06af5a2 --- /dev/null +++ b/packages/appkit/src/plugins/agent/tracing.ts @@ -0,0 +1,47 @@ +/** + * LangChain tracing integration for the agent plugin. + * + * Instruments LangChain callbacks via @arizeai/openinference-instrumentation-langchain + * so that agent spans are emitted through the global tracer provider + * (set up by AppKit's TelemetryManager). + * + * MLflow-specific headers (experiment ID, UC table name) are added + * to the trace exporter by TelemetryManager — see buildTraceExporterHeaders(). + */ + +import { createLogger } from "../../logging/logger"; + +const logger = createLogger("agent:tracing"); + +/** + * Instrument LangChain callbacks via the Arize/OpenInference library. + * + * The instrumentation creates spans using the **global** tracer provider + * (registered by TelemetryManager's NodeSDK). No separate provider is needed. + * + * IMPORTANT: `callbackManagerModule` must be the module object that the + * agent runtime uses (imported from agent.ts context), NOT a separate + * import inside this file. pnpm strict isolation can resolve different + * physical copies of @langchain/core for different packages in the + * dependency tree, and patching the wrong copy has no effect. + */ +export async function instrumentLangChain( + callbackManagerModule?: typeof import("@langchain/core/callbacks/manager"), +): Promise { + try { + const { LangChainInstrumentation } = await import( + "@arizeai/openinference-instrumentation-langchain" + ); + + const cbModule = + callbackManagerModule ?? + (await import("@langchain/core/callbacks/manager")); + + const inst = new LangChainInstrumentation(); + inst.manuallyInstrument(cbModule); + + logger.debug("LangChain callbacks instrumented (global tracer provider)"); + } catch (err) { + logger.error("Failed to instrument LangChain callbacks: %O", err); + } +} diff --git a/packages/appkit/src/plugins/agent/types.ts b/packages/appkit/src/plugins/agent/types.ts index 8ea6f97b..6a572cca 100644 --- a/packages/appkit/src/plugins/agent/types.ts +++ b/packages/appkit/src/plugins/agent/types.ts @@ -51,4 +51,30 @@ export interface IAgentConfig extends BasePluginConfig { * Ignored when `agentInstance` is provided. */ tools?: AgentTool[]; + + /** + * LangChain tracing configuration. Default: enabled. + * + * When enabled, instruments @langchain/core callbacks so that agent + * spans are emitted through AppKit's global tracer provider (TelemetryManager). + * + * The `experimentId` and `ucTableName` fields (or their corresponding + * env vars MLFLOW_EXPERIMENT_ID / OTEL_UC_TABLE_NAME) are forwarded as + * headers on the OTLP trace exporter via `appendTraceHeaders`. + * + * Pass `false` to disable LangChain tracing instrumentation entirely. + */ + tracing?: + | false + | { + /** MLflow experiment ID. Defaults to MLFLOW_EXPERIMENT_ID env. */ + experimentId?: string; + /** + * UC table name (catalog.schema.table). Defaults to OTEL_UC_TABLE_NAME env. + * Catalog and schema are derived from this value for trace location setup. + */ + ucTableName?: string; + /** SQL warehouse ID for creating UC trace storage. Defaults to MLFLOW_TRACING_SQL_WAREHOUSE_ID env. */ + warehouseId?: string; + }; } diff --git a/packages/appkit/src/plugins/server/index.ts b/packages/appkit/src/plugins/server/index.ts index 22501ce0..464dffe3 100644 --- a/packages/appkit/src/plugins/server/index.ts +++ b/packages/appkit/src/plugins/server/index.ts @@ -58,10 +58,12 @@ export class ServerPlugin extends Plugin { this.serverApplication = express(); this.server = null; this.serverExtensions = []; - this.telemetry.registerInstrumentations([ - instrumentations.http, - instrumentations.express, - ]); + if (config.enableDefaultTelemetry !== false) { + this.telemetry.registerInstrumentations([ + instrumentations.http(), + instrumentations.express(), + ]); + } } /** Setup the server plugin. */ diff --git a/packages/appkit/src/plugins/server/tests/server.test.ts b/packages/appkit/src/plugins/server/tests/server.test.ts index 4015917d..b4da3be3 100644 --- a/packages/appkit/src/plugins/server/tests/server.test.ts +++ b/packages/appkit/src/plugins/server/tests/server.test.ts @@ -75,8 +75,8 @@ vi.mock("../../../telemetry", () => ({ }), }, instrumentations: { - http: {}, - express: {}, + http: () => ({}), + express: () => ({}), }, })); diff --git a/packages/appkit/src/plugins/server/types.ts b/packages/appkit/src/plugins/server/types.ts index e187cacc..381d3ac2 100644 --- a/packages/appkit/src/plugins/server/types.ts +++ b/packages/appkit/src/plugins/server/types.ts @@ -7,4 +7,10 @@ export interface ServerConfig extends BasePluginConfig { staticPath?: string; autoStart?: boolean; host?: string; + /** + * Register HTTP and Express OpenTelemetry instrumentations. + * When false, no HTTP/Express spans are created regardless of other + * telemetry settings. Default: true. + */ + enableDefaultTelemetry?: boolean; } diff --git a/packages/appkit/src/telemetry/instrumentations.ts b/packages/appkit/src/telemetry/instrumentations.ts index 581bdc0e..7b70c38c 100644 --- a/packages/appkit/src/telemetry/instrumentations.ts +++ b/packages/appkit/src/telemetry/instrumentations.ts @@ -4,56 +4,52 @@ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; import { shouldIgnoreRequest } from "../utils/path-exclusions"; /** - * Registry of pre-configured instrumentations for common use cases. - * These can be selectively registered by plugins that need them. - * - * While instrumentations are generally safe to re-register, - * the recommended approach is to register them once in a corresponding plugin constructor. + * Factory functions that create pre-configured instrumentations on demand. + * Lazy creation avoids side-effects at import time (e.g. module patching + * triggered by instrumentation constructors). */ -export const instrumentations: Record = { - http: new HttpInstrumentation({ - // Filter out requests before creating spans - this is the most efficient approach - ignoreIncomingRequestHook: shouldIgnoreRequest, +export const instrumentations = { + http: (): Instrumentation => + new HttpInstrumentation({ + ignoreIncomingRequestHook: shouldIgnoreRequest, - applyCustomAttributesOnSpan(span: any, request: any) { - let spanName: string | null = null; + applyCustomAttributesOnSpan(span: any, request: any) { + let spanName: string | null = null; - if (request.route) { - const baseUrl = request.baseUrl || ""; - const url = request.url?.split("?")[0] || ""; - const fullPath = baseUrl + url; - if (fullPath) { - spanName = `${request.method} ${fullPath}`; + if (request.route) { + const baseUrl = request.baseUrl || ""; + const url = request.url?.split("?")[0] || ""; + const fullPath = baseUrl + url; + if (fullPath) { + spanName = `${request.method} ${fullPath}`; + } + } else if (request.url) { + // No Express route (e.g., static assets) - use the raw URL path + // Remove query string for cleaner trace names + const path = request.url.split("?")[0]; + spanName = `${request.method} ${path}`; } - } else if (request.url) { - // No Express route (e.g., static assets) - use the raw URL path - // Remove query string for cleaner trace names - const path = request.url.split("?")[0]; - spanName = `${request.method} ${path}`; - } - if (spanName) { - span.updateName(spanName); - } - }, - }), - express: new ExpressInstrumentation({ - requestHook: (span: any, info: any) => { - const req = info.request; - - // Only update span name for route handlers (layerType: request_handler) - // This ensures we're not renaming middleware spans - if (info.layerType === "request_handler" && req.route) { - // Combine baseUrl with url to get full path with actual parameter values - // e.g., baseUrl="/api/analytics" + url="/query/spend_data" = "/api/analytics/query/spend_data" - const baseUrl = req.baseUrl || ""; - const url = req.url?.split("?")[0] || ""; - const fullPath = baseUrl + url; - if (fullPath) { - const spanName = `${req.method} ${fullPath}`; + if (spanName) { span.updateName(spanName); } - } - }, - }), + }, + }), + + express: (): Instrumentation => + new ExpressInstrumentation({ + requestHook: (span: any, info: any) => { + const req = info.request; + + if (info.layerType === "request_handler" && req.route) { + const baseUrl = req.baseUrl || ""; + const url = req.url?.split("?")[0] || ""; + const fullPath = baseUrl + url; + if (fullPath) { + const spanName = `${req.method} ${fullPath}`; + span.updateName(spanName); + } + } + }, + }), }; diff --git a/packages/appkit/src/telemetry/telemetry-manager.ts b/packages/appkit/src/telemetry/telemetry-manager.ts index 6660b6b2..9655558a 100644 --- a/packages/appkit/src/telemetry/telemetry-manager.ts +++ b/packages/appkit/src/telemetry/telemetry-manager.ts @@ -60,12 +60,14 @@ export class TelemetryManager { return TelemetryManager.instance; } - static initialize(config: Partial = {}): void { + static async initialize( + config: Partial = {}, + ): Promise { const instance = TelemetryManager.getInstance(); - instance._initialize(config); + await instance._initialize(config); } - private _initialize(config: Partial): void { + private async _initialize(config: Partial): Promise { if (this.sdk) return; if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { @@ -73,11 +75,16 @@ export class TelemetryManager { } try { + const traceHeaders = await this.buildTraceExporterHeaders( + config.headers, + config.traceExporterHeaders, + ); + this.sdk = new NodeSDK({ resource: this.createResource(config), autoDetectResources: false, sampler: new AppKitSampler(), - traceExporter: new OTLPTraceExporter({ headers: config.headers }), + traceExporter: new OTLPTraceExporter({ headers: traceHeaders }), metricReaders: [ new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ headers: config.headers }), @@ -91,7 +98,8 @@ export class TelemetryManager { new OTLPLogExporter({ headers: config.headers }), ), ], - instrumentations: this.getDefaultInstrumentations(), + instrumentations: + config.instrumentations ?? this.getDefaultInstrumentations(), }); this.sdk.start(); @@ -130,6 +138,44 @@ export class TelemetryManager { return initialResource.merge(detectedResource); } + /** + * Builds headers for the trace exporter by merging (in priority order): + * 1. Base `headers` from TelemetryConfig + * 2. Databricks auth (auto-resolved when DATABRICKS_HOST is set) + * 3. Plugin-contributed `traceExporterHeaders` (e.g. MLflow experiment ID) + */ + private async buildTraceExporterHeaders( + configHeaders?: Record, + pluginHeaders?: Record, + ): Promise> { + const headers: Record = { ...configHeaders }; + + if (process.env.DATABRICKS_HOST && !headers.authorization) { + try { + const { WorkspaceClient } = await import( + "@databricks/sdk-experimental" + ); + const client = new WorkspaceClient({}); + const authHeaders = new Headers(); + await client.config.authenticate(authHeaders); + authHeaders.forEach((value, key) => { + headers[key] = value; + }); + } catch (err) { + logger.warn( + "Could not obtain Databricks auth for trace exporter: %O", + err, + ); + } + } + + if (pluginHeaders) { + Object.assign(headers, pluginHeaders); + } + + return headers; + } + private getDefaultInstrumentations(): Instrumentation[] { return [ ...getNodeAutoInstrumentations({ diff --git a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts index 11b85d9b..f28ddaca 100644 --- a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts +++ b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts @@ -51,6 +51,8 @@ describe("TelemetryManager", () => { beforeEach(() => { originalEnv = { ...process.env }; + // Prevent TelemetryManager from attempting Databricks auth during tests + delete process.env.DATABRICKS_HOST; vi.clearAllMocks(); // @ts-expect-error - accessing private static property for testing TelemetryManager.instance = undefined; @@ -75,7 +77,7 @@ describe("TelemetryManager", () => { const { detectResources } = await import("@opentelemetry/resources"); vi.clearAllMocks(); - TelemetryManager.initialize({ + await TelemetryManager.initialize({ serviceName: "test-service-config", }); @@ -85,7 +87,7 @@ describe("TelemetryManager", () => { test("should initialize providers and create telemetry instances", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; - TelemetryManager.initialize({ + await TelemetryManager.initialize({ serviceName: "integration-test", serviceVersion: "1.0.0", }); @@ -118,7 +120,7 @@ describe("TelemetryManager", () => { test("should support disabled telemetry config", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; - TelemetryManager.initialize({ + await TelemetryManager.initialize({ serviceName: "disabled-test", serviceVersion: "1.0.0", }); @@ -161,7 +163,7 @@ describe("TelemetryManager", () => { ); vi.clearAllMocks(); - TelemetryManager.initialize({ + await TelemetryManager.initialize({ headers: { Authorization: "Bearer token", "Custom-Header": "value", @@ -182,7 +184,7 @@ describe("TelemetryManager", () => { test("should create and execute spans with real tracer", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; - TelemetryManager.initialize({ + await TelemetryManager.initialize({ serviceName: "span-test", serviceVersion: "1.0.0", }); @@ -207,7 +209,7 @@ describe("TelemetryManager", () => { test("should handle span errors", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; - TelemetryManager.initialize({ + await TelemetryManager.initialize({ serviceName: "error-test", serviceVersion: "1.0.0", }); diff --git a/packages/appkit/src/telemetry/types.ts b/packages/appkit/src/telemetry/types.ts index abd73e4c..7983c528 100644 --- a/packages/appkit/src/telemetry/types.ts +++ b/packages/appkit/src/telemetry/types.ts @@ -9,6 +9,12 @@ export interface TelemetryConfig { instrumentations?: Instrumentation[]; exportIntervalMs?: number; headers?: Record; + /** + * Additional headers to include on the OTLP **trace** exporter only. + * Merged on top of `headers` and any auto-resolved Databricks auth. + * Typically populated automatically by plugins via `appendTraceHeaders`. + */ + traceExporterHeaders?: Record; } /** diff --git a/packages/shared/src/plugin.ts b/packages/shared/src/plugin.ts index 0020f616..2fafd0c4 100644 --- a/packages/shared/src/plugin.ts +++ b/packages/shared/src/plugin.ts @@ -77,6 +77,11 @@ export type PluginConstructor< * Use this when resource requirements depend on plugin configuration. */ getResourceRequirements?(config: C): ResourceRequirement[]; + /** + * Returns extra headers to append to the OTLP trace exporter. + * Called by `_createApp` before TelemetryManager initializes. + */ + appendTraceHeaders?(config: C): Record; }; /**