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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"**/*.css",
"packages/appkit/src/plugins/vector-search/**",
"packages/appkit/src/plugin/index.ts",
"packages/appkit/src/plugin/to-plugin.ts",
"packages/appkit/src/plugins/agents/index.ts",
"packages/appkit/src/plugins/agents/tools/index.ts",
"packages/appkit/src/plugins/agents/from-plugin.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@types/semver": "7.7.1",
"dotenv": "16.6.1",
"express": "4.22.0",
"js-yaml": "^4.1.1",
"obug": "2.1.1",
"pg": "8.18.0",
"picocolors": "1.1.1",
Expand All @@ -108,6 +109,7 @@
"@ai-sdk/openai": "4.0.0-beta.27",
"@langchain/core": "^1.1.39",
"@types/express": "4.17.25",
"@types/js-yaml": "^4.0.9",
"@types/json-schema": "7.0.15",
"@types/pg": "8.16.0",
"@types/ws": "8.18.1",
Expand Down
1 change: 1 addition & 0 deletions packages/appkit/src/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from "./files";
export * from "./genie";
export * from "./lakebase";
export * from "./lakebase-v1";
export * from "./mcp";
export * from "./sql-warehouse";
export * from "./vector-search";
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
* transport.
*/
import type { AgentToolDefinition } from "shared";
import { createLogger } from "../../../logging/logger";
import type { McpEndpointConfig } from "./hosted-tools";
import { createLogger } from "../../logging/logger";
import {
assertResolvedHostSafe,
checkMcpUrl,
type DnsLookup,
type McpHostPolicy,
} from "./mcp-host-policy";
} from "./host-policy";
import type { McpEndpointConfig } from "./types";

const logger = createLogger("agent:mcp");
const logger = createLogger("connector:mcp");

interface JsonRpcRequest {
jsonrpc: "2.0";
Expand Down
6 changes: 6 additions & 0 deletions packages/appkit/src/connectors/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { AppKitMcpClient } from "./client";
export {
buildMcpHostPolicy,
type McpHostPolicyConfig,
} from "./host-policy";
export type { McpEndpointConfig } from "./types";
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { AppKitMcpClient } from "../tools/mcp-client";
import type { DnsLookup, McpHostPolicy } from "../tools/mcp-host-policy";
import { AppKitMcpClient } from "../client";
import type { DnsLookup, McpHostPolicy } from "../host-policy";

const WORKSPACE = "https://test-workspace.cloud.databricks.com";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
isLoopbackHost,
type McpHostPolicy,
type McpHostPolicyConfig,
} from "../tools/mcp-host-policy";
} from "../host-policy";

function stubLookup(
addresses: Array<{ address: string; family?: number }>,
Expand Down
12 changes: 12 additions & 0 deletions packages/appkit/src/connectors/mcp/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Input shape consumed by {@link AppKitMcpClient.connect}. Produced by the
* agents plugin from user-facing `HostedTool` declarations (see
* `plugins/agents/tools/hosted-tools.ts`) and accepted directly by the
* connector to keep its surface free of agent-layer concepts.
*/
export interface McpEndpointConfig {
/** Stable logical name used as the `mcp.<name>.*` tool prefix and in logs. */
name: string;
/** Absolute URL (`https://…`) or workspace-relative path (`/api/2.0/mcp/…`). */
url: string;
}
53 changes: 53 additions & 0 deletions packages/appkit/src/core/create-agent-def.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ConfigurationError } from "../errors";
import type { AgentDefinition } from "../plugins/agents/types";

/**
* Pure factory for agent definitions. Returns the passed-in definition after
* cycle-detecting the sub-agent graph. Accepts the full `AgentDefinition` shape
* and is safe to call at module top-level.
*
* The returned value is a plain `AgentDefinition` — no adapter construction,
* no side effects. Register it with `agents({ agents: { name: def } })` or run
* it standalone via `runAgent(def, input)`.
*
* @example
* ```ts
* const support = createAgent({
* instructions: "You help customers.",
* model: "databricks-claude-sonnet-4-5",
* tools: {
* get_weather: tool({ ... }),
* },
* });
* ```
*/
export function createAgent(def: AgentDefinition): AgentDefinition {
detectCycles(def);
return def;
}

/**
* Walks the `agents: { ... }` sub-agent tree via DFS and throws if a cycle is
* found. Cycles would cause infinite recursion at tool-invocation time.
*/
function detectCycles(def: AgentDefinition): void {
const visiting = new Set<AgentDefinition>();
const visited = new Set<AgentDefinition>();

const walk = (current: AgentDefinition, path: string[]): void => {
if (visited.has(current)) return;
if (visiting.has(current)) {
throw new ConfigurationError(
`Agent sub-agent cycle detected: ${path.join(" -> ")}`,
);
}
visiting.add(current);
for (const [childKey, child] of Object.entries(current.agents ?? {})) {
walk(child, [...path, childKey]);
}
visiting.delete(current);
visited.add(current);
};

walk(def, [def.name ?? "(root)"]);
}
226 changes: 226 additions & 0 deletions packages/appkit/src/core/run-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { randomUUID } from "node:crypto";
import type {
AgentAdapter,
AgentEvent,
AgentToolDefinition,
Message,
} from "shared";
import {
type FunctionTool,
functionToolToDefinition,
isFunctionTool,
} from "../plugins/agents/tools/function-tool";
import { isHostedTool } from "../plugins/agents/tools/hosted-tools";
import type {
AgentDefinition,
AgentTool,
ToolkitEntry,
} from "../plugins/agents/types";
import { isToolkitEntry } from "../plugins/agents/types";

export interface RunAgentInput {
/** Seed messages for the run. Either a single user string or a full message list. */
messages: string | Message[];
/** Abort signal for cancellation. */
signal?: AbortSignal;
}

export interface RunAgentResult {
/** Aggregated text output from all `message_delta` events. */
text: string;
/** Every event the adapter yielded, in order. Useful for inspection/tests. */
events: AgentEvent[];
}

/**
* Standalone agent execution without `createApp`. Resolves the adapter, binds
* inline tools, and drives the adapter's `run()` loop to completion.
*
* Limitations vs. running through the agents() plugin:
* - No OBO: there is no HTTP request, so plugin tools run as the service
* principal (when they work at all).
* - Plugin tools (`ToolkitEntry`) are not supported — they require a live
* `PluginContext` that only exists when registered in a `createApp`
* instance. This function throws a clear error if encountered.
* - Sub-agents (`agents: { ... }` on the def) are executed as nested
* `runAgent` calls with no shared thread state.
*/
export async function runAgent(
def: AgentDefinition,
input: RunAgentInput,
): Promise<RunAgentResult> {
const adapter = await resolveAdapter(def);
const messages = normalizeMessages(input.messages, def.instructions);
const toolIndex = buildStandaloneToolIndex(def);
const tools = Array.from(toolIndex.values()).map((e) => e.def);

const signal = input.signal;

const executeTool = async (name: string, args: unknown): Promise<unknown> => {
const entry = toolIndex.get(name);
if (!entry) throw new Error(`Unknown tool: ${name}`);
if (entry.kind === "function") {
return entry.tool.execute(args as Record<string, unknown>);
}
if (entry.kind === "subagent") {
const subInput: RunAgentInput = {
messages:
typeof args === "object" &&
args !== null &&
typeof (args as { input?: unknown }).input === "string"
? (args as { input: string }).input
: JSON.stringify(args),
signal,
};
const res = await runAgent(entry.agentDef, subInput);
return res.text;
}
throw new Error(
`runAgent: tool "${name}" is a ${entry.kind} tool. ` +
"Plugin toolkits and MCP tools are only usable via createApp({ plugins: [..., agents(...)] }).",
);
};

const events: AgentEvent[] = [];
let text = "";

const stream = adapter.run(
{
messages,
tools,
threadId: randomUUID(),
signal,
},
{ executeTool, signal },
);

for await (const event of stream) {
if (signal?.aborted) break;
events.push(event);
if (event.type === "message_delta") {
text += event.content;
} else if (event.type === "message") {
text = event.content;
}
}

return { text, events };
}

async function resolveAdapter(def: AgentDefinition): Promise<AgentAdapter> {
const { model } = def;
if (!model) {
const { DatabricksAdapter } = await import("../agents/databricks");
return DatabricksAdapter.fromModelServing();
}
if (typeof model === "string") {
const { DatabricksAdapter } = await import("../agents/databricks");
return DatabricksAdapter.fromModelServing(model);
}
return await model;
}

function normalizeMessages(
input: string | Message[],
instructions: string,
): Message[] {
const systemMessage: Message = {
id: "system",
role: "system",
content: instructions,
createdAt: new Date(),
};
if (typeof input === "string") {
return [
systemMessage,
{
id: randomUUID(),
role: "user",
content: input,
createdAt: new Date(),
},
];
}
return [systemMessage, ...input];
}

type StandaloneEntry =
| {
kind: "function";
def: AgentToolDefinition;
tool: FunctionTool;
}
| {
kind: "subagent";
def: AgentToolDefinition;
agentDef: AgentDefinition;
}
| {
kind: "toolkit";
def: AgentToolDefinition;
entry: ToolkitEntry;
}
| {
kind: "hosted";
def: AgentToolDefinition;
};

function buildStandaloneToolIndex(
def: AgentDefinition,
): Map<string, StandaloneEntry> {
const index = new Map<string, StandaloneEntry>();

for (const [key, tool] of Object.entries(def.tools ?? {})) {
index.set(key, classifyTool(key, tool));
}

for (const [childKey, child] of Object.entries(def.agents ?? {})) {
const toolName = `agent-${childKey}`;
index.set(toolName, {
kind: "subagent",
agentDef: { ...child, name: child.name ?? childKey },
def: {
name: toolName,
description:
child.instructions.slice(0, 120) ||
`Delegate to the ${childKey} sub-agent`,
parameters: {
type: "object",
properties: {
input: {
type: "string",
description: "Message to send to the sub-agent.",
},
},
required: ["input"],
},
},
});
}

return index;
}

function classifyTool(key: string, tool: AgentTool): StandaloneEntry {
if (isToolkitEntry(tool)) {
return { kind: "toolkit", def: { ...tool.def, name: key }, entry: tool };
}
if (isFunctionTool(tool)) {
return {
kind: "function",
tool,
def: { ...functionToolToDefinition(tool), name: key },
};
}
if (isHostedTool(tool)) {
return {
kind: "hosted",
def: {
name: key,
description: `Hosted tool: ${tool.type}`,
parameters: { type: "object", properties: {} },
},
};
}
throw new Error(`runAgent: unrecognized tool shape at key "${key}"`);
}
Loading