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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"packages/appkit/src/plugins/vector-search/**",
"packages/appkit/src/plugin/index.ts",
"packages/appkit/src/plugins/agents/index.ts",
"packages/appkit/src/plugins/agents/tools/index.ts",
"packages/appkit/src/plugins/agents/from-plugin.ts",
"packages/appkit/src/plugins/agents/load-agents.ts",
"template/**",
"tools/**",
"docs/**",
".github/scripts/**"
".github/scripts/**",
"packages/appkit/src/core/agent/tools/index.ts",
"packages/appkit/src/core/agent/from-plugin.ts",
"packages/appkit/src/core/agent/load-agents.ts",
"packages/appkit/src/connectors/mcp/index.ts",
"packages/appkit/src/plugin/to-plugin.ts"
],
"ignoreDependencies": ["json-schema-to-typescript"],
"ignoreBinaries": ["tarball"]
Expand Down
2 changes: 2 additions & 0 deletions packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"dotenv": "16.6.1",
"express": "4.22.0",
"get-port": "7.2.0",
"js-yaml": "^4.1.1",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"js-yaml": "^4.1.1",
"js-yaml": "4.1.1",

"obug": "2.1.1",
"pg": "8.18.0",
"picocolors": "1.1.1",
Expand All @@ -88,6 +89,7 @@
"devDependencies": {
"@opentelemetry/context-async-hooks": "2.6.1",
"@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
31 changes: 27 additions & 4 deletions packages/appkit/src/beta.ts
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw maybe we should rename this file as beta.gen.ts? so that we don't review an autogenerated file?

(and add "generated, do not edit" header)

Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ export type {
Message,
Thread,
ThreadStore,
ToolAnnotations,
ToolProvider,
} from "shared";
export { DatabricksAdapter, parseTextToolCalls } from "./agents/databricks";

// Agent runtime
export { createAgent } from "./core/agent/create-agent";
export {
type RunAgentInput,
type RunAgentResult,
runAgent,
} from "./core/agent/run-agent";

// Tool authoring primitives
export {
AppKitMcpClient,
Expand All @@ -37,11 +46,25 @@ export {
tool,
toolsFromRegistry,
} from "./core/agent/tools";

// Agent types
export type {
AgentDefinition,
AgentsPluginConfig,
AgentTool,
AutoInheritToolsConfig,
BaseSystemPromptOption,
PromptContext,
RegisteredAgent,
ResolvedToolEntry,
ToolkitEntry,
ToolkitOptions,
} from "./plugins/agents";
export {
type AgentTool,
agentIdFromMarkdownPath,
isToolkitEntry,
type ToolkitEntry,
type ToolkitOptions,
} from "./core/agent/types";
loadAgentFromFile,
loadAgentsFromDir,
} from "./plugins/agents";

export * from "./plugins/beta-exports.generated";
1 change: 1 addition & 0 deletions packages/appkit/src/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from "./genie";
export * from "./jobs";
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 type { McpEndpointConfig } from "../../../core/agent/tools/hosted-tools";
import { createLogger } from "../../../logging/logger";
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;
}
52 changes: 52 additions & 0 deletions packages/appkit/src/core/agent/consume-adapter-stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { AgentEvent } from "shared";

interface ConsumeAdapterStreamOptions {
/**
* Optional abort signal. When aborted, the loop stops consuming (the caller
* is expected to have forwarded the same signal to `adapter.run` to stop
* upstream work). `undefined` is valid — standalone `runAgent` runs without
* a signal.
*/
signal?: AbortSignal;
/**
* Side-effect callback invoked once per adapter event, after the content
* accumulator has been updated. Use to fan events out to SSE translators,
* collect a raw event list for tests, or emit telemetry.
*/
onEvent?: (event: AgentEvent) => void;
}

/**
* Consume an adapter's event stream and aggregate the assistant's final text.
*
* Accumulation rule (shared across all agent-execution paths in AppKit):
*
* - `message_delta` events append their `content` to the running text.
* - A `message` event *replaces* the running text with its `content`.
*
* The two branches coexist because different adapters emit different shapes:
* streaming adapters (Databricks, Vercel AI) emit deltas chunk-by-chunk,
* while `LangChain`'s `on_chain_end` path emits a single final `message`.
* Without the replace branch, LangChain conversations silently dropped the
* assistant turn from thread history.
*
* Kept pure (no I/O, no mutable external state beyond the caller's `onEvent`
* side effect) so each execution path — HTTP streaming, sub-agents, and the
* standalone `runAgent` — can share one loop.
*/
export async function consumeAdapterStream(
stream: AsyncIterable<AgentEvent>,
opts: ConsumeAdapterStreamOptions = {},
): Promise<string> {
let text = "";
for await (const event of stream) {
if (opts.signal?.aborted) break;
if (event.type === "message_delta") {
text += event.content;
} else if (event.type === "message") {
text = event.content;
}
opts.onEvent?.(event);
}
return text;
}
53 changes: 53 additions & 0 deletions packages/appkit/src/core/agent/create-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ConfigurationError } from "../../errors";
import type { AgentDefinition } from "./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)"]);
}
Loading