Skip to content
Closed
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
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
5 changes: 4 additions & 1 deletion packages/appkit/src/core/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ export class AppKit<TPlugins extends InputPluginMap> {
name,
...extraData,
};
const pluginInstance = new Plugin(baseConfig);
// If the factory eagerly constructed an instance (via
// `toPluginWithInstance`), reuse it; otherwise construct now.
const preBuilt = (pluginData as { instance?: BasePlugin }).instance;
const pluginInstance = preBuilt ?? new Plugin(baseConfig);

if (typeof pluginInstance.attachContext === "function") {
pluginInstance.attachContext({
Expand Down
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)"]);
}
6 changes: 6 additions & 0 deletions packages/appkit/src/core/create-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ export interface AgentHandle {
* });
* ```
*/
/**
* @deprecated Use `createAgent(def)` (pure factory) + `agents()` plugin +
* `createApp()` instead. The new shape separates agent *definition* from
* *app composition*. Re-exported as `createAgentApp` in the main package
* index for migration; will be removed in a future release.
*/
export async function createAgent(
config: CreateAgentConfig = {},
): Promise<AgentHandle> {
Expand Down
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/agent/tools/function-tool";
import { isHostedTool } from "../plugins/agent/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}"`);
}
31 changes: 29 additions & 2 deletions packages/appkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,21 @@ export {
export { getExecutionContext } from "./context";
export { createApp } from "./core";
export type { AgentHandle, CreateAgentConfig } from "./core/create-agent";
export { createAgent } from "./core/create-agent";
/**
* @deprecated Use `createAgent(def)` (pure factory) together with the
* `agents()` plugin and `createApp`. This shortcut composes server +
* agent plugins in a single call; the new shape separates those concerns.
* Import path preserved for backward compatibility during migration.
*/
export { createAgent as createAgentApp } from "./core/create-agent";
// New pure-data agent factory (replaces the old createAgent shortcut once
// callers migrate — they coexist during the deprecation window).
export { createAgent } from "./core/create-agent-def";
export {
type RunAgentInput,
type RunAgentResult,
runAgent,
} from "./core/run-agent";
// Errors
export {
AppKitError,
Expand All @@ -65,6 +79,7 @@ export {
toPlugin,
} from "./plugin";
export {
/** @deprecated Use `agents()` (plural) instead. Kept for migration. */
agent,
analytics,
files,
Expand All @@ -82,7 +97,19 @@ export {
type ToolConfig,
tool,
} from "./plugins/agent/tools";
export type { AgentTool } from "./plugins/agent/types";
export {
type AgentDefinition,
type AgentsPluginConfig,
type AgentTool,
agents,
type BaseSystemPromptOption,
isToolkitEntry,
loadAgentFromFile,
loadAgentsFromDir,
type PromptContext,
type ToolkitEntry,
type ToolkitOptions,
} from "./plugins/agents";
export type {
EndpointConfig,
ServingEndpointEntry,
Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { ToPlugin } from "shared";
export type { ExecutionResult } from "./execution-result";
export { Plugin } from "./plugin";
export { toPlugin } from "./to-plugin";
export { toPlugin, toPluginWithInstance } from "./to-plugin";
Loading