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
174 changes: 148 additions & 26 deletions packages/appkit/src/core/run-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import type {
AgentEvent,
AgentToolDefinition,
Message,
PluginConstructor,
PluginData,
ToolProvider,
} from "shared";
import { consumeAdapterStream } from "../plugins/agents/consume-adapter-stream";
import { isFromPluginMarker } from "../plugins/agents/from-plugin";
import { resolveToolkitFromProvider } from "../plugins/agents/toolkit-resolver";
import {
type FunctionTool,
functionToolToDefinition,
Expand All @@ -23,6 +29,14 @@ export interface RunAgentInput {
messages: string | Message[];
/** Abort signal for cancellation. */
signal?: AbortSignal;
/**
* Optional plugin list used to resolve `fromPlugin` markers in `def.tools`.
* Required when the def contains any `...fromPlugin(factory)` spreads;
* ignored otherwise. `runAgent` constructs a fresh instance per plugin
* and dispatches tool calls against it as the service principal (no
* OBO — there is no HTTP request in standalone mode).
*/
plugins?: PluginData<PluginConstructor, unknown, string>[];
}

export interface RunAgentResult {
Expand All @@ -39,19 +53,20 @@ export interface RunAgentResult {
* 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.
* - Hosted tools (MCP) are not supported — they require a live MCP client
* that only exists inside the agents plugin.
* - Sub-agents (`agents: { ... }` on the def) are executed as nested
* `runAgent` calls with no shared thread state.
* - Plugin tools (`fromPlugin` markers or `ToolkitEntry` spreads) require
* passing `plugins: [...]` via `RunAgentInput`.
*/
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 toolIndex = buildStandaloneToolIndex(def, input.plugins ?? []);
const tools = Array.from(toolIndex.values()).map((e) => e.def);

const signal = input.signal;
Expand All @@ -62,6 +77,13 @@ export async function runAgent(
if (entry.kind === "function") {
return entry.tool.execute(args as Record<string, unknown>);
}
if (entry.kind === "toolkit") {
return entry.provider.executeAgentTool(
entry.localName,
args as Record<string, unknown>,
signal,
);
}
if (entry.kind === "subagent") {
const subInput: RunAgentInput = {
messages:
Expand All @@ -71,39 +93,34 @@ export async function runAgent(
? (args as { input: string }).input
: JSON.stringify(args),
signal,
plugins: input.plugins,
};
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(...)] }).",
"Hosted/MCP tools are only usable via createApp({ plugins: [..., agents(...)] }).",
);
};

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

const stream = adapter.run(
const text = await consumeAdapterStream(
adapter.run(
{
messages,
tools,
threadId: randomUUID(),
signal,
},
{ executeTool, signal },
),
{
messages,
tools,
threadId: randomUUID(),
signal,
onEvent: (event) => events.push(event),
},
{ 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 };
}

Expand Down Expand Up @@ -158,20 +175,61 @@ type StandaloneEntry =
| {
kind: "toolkit";
def: AgentToolDefinition;
entry: ToolkitEntry;
provider: ToolProvider;
pluginName: string;
localName: string;
}
| {
kind: "hosted";
def: AgentToolDefinition;
};

/**
* Resolves `def.tools` (string-keyed entries + symbol-keyed `fromPlugin`
* markers) and `def.agents` (sub-agents) into a flat dispatch index.
* Symbol-keyed markers are resolved against `plugins`; missing references
* throw with an `Available: …` listing.
*/
function buildStandaloneToolIndex(
def: AgentDefinition,
plugins: PluginData<PluginConstructor, unknown, string>[],
): Map<string, StandaloneEntry> {
const index = new Map<string, StandaloneEntry>();
const tools = def.tools;

const symbolKeys = tools ? Object.getOwnPropertySymbols(tools) : [];
if (symbolKeys.length > 0) {
const providerCache = new Map<string, ToolProvider>();
for (const sym of symbolKeys) {
const marker = (tools as Record<symbol, unknown>)[sym];
if (!isFromPluginMarker(marker)) continue;

for (const [key, tool] of Object.entries(def.tools ?? {})) {
index.set(key, classifyTool(key, tool));
const provider = resolveStandaloneProvider(
marker.pluginName,
plugins,
providerCache,
);
const entries = resolveToolkitFromProvider(
marker.pluginName,
provider,
marker.opts,
);
for (const [key, entry] of Object.entries(entries)) {
index.set(key, {
kind: "toolkit",
provider,
pluginName: entry.pluginName,
localName: entry.localName,
def: { ...entry.def, name: key },
});
}
}
}

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

for (const [childKey, child] of Object.entries(def.agents ?? {})) {
Expand Down Expand Up @@ -203,7 +261,7 @@ function buildStandaloneToolIndex(

function classifyTool(key: string, tool: AgentTool): StandaloneEntry {
if (isToolkitEntry(tool)) {
return { kind: "toolkit", def: { ...tool.def, name: key }, entry: tool };
return toolkitEntryToStandalone(key, tool);
}
if (isFunctionTool(tool)) {
return {
Expand All @@ -224,3 +282,67 @@ function classifyTool(key: string, tool: AgentTool): StandaloneEntry {
}
throw new Error(`runAgent: unrecognized tool shape at key "${key}"`);
}

/**
* Pre-`fromPlugin` code could reach a `ToolkitEntry` by calling
* `.toolkit()` at module scope (which requires an instance). Those entries
* still flow through `def.tools` but without a provider we can dispatch
* against — runAgent cannot execute them and errors clearly.
*/
function toolkitEntryToStandalone(
key: string,
entry: ToolkitEntry,
): StandaloneEntry {
const def: AgentToolDefinition = { ...entry.def, name: key };
return {
kind: "hosted",
def: {
...def,
description:
`${def.description ?? ""} ` +
`[runAgent: this ToolkitEntry refers to plugin '${entry.pluginName}' but ` +
"runAgent cannot dispatch it without the plugin instance. Pass the " +
"plugin via plugins: [...] and use fromPlugin(factory) instead of " +
".toolkit() spreads.]".trim(),
},
};
}

function resolveStandaloneProvider(
pluginName: string,
plugins: PluginData<PluginConstructor, unknown, string>[],
cache: Map<string, ToolProvider>,
): ToolProvider {
const cached = cache.get(pluginName);
if (cached) return cached;

const match = plugins.find((p) => p.name === pluginName);
if (!match) {
const available = plugins.map((p) => p.name).join(", ") || "(none)";
throw new Error(
`runAgent: agent references plugin '${pluginName}' via fromPlugin(), but ` +
"that plugin is missing from RunAgentInput.plugins. " +
`Available: ${available}.`,
);
}

const instance = new match.plugin({
...(match.config ?? {}),
name: pluginName,
});
const provider = instance as unknown as ToolProvider;
if (
typeof (provider as { getAgentTools?: unknown }).getAgentTools !==
"function" ||
typeof (provider as { executeAgentTool?: unknown }).executeAgentTool !==
"function"
) {
throw new Error(
`runAgent: plugin '${pluginName}' is not a ToolProvider ` +
"(missing getAgentTools/executeAgentTool). Only ToolProvider plugins " +
"are supported via fromPlugin() in runAgent.",
);
}
cache.set(pluginName, provider);
return provider;
}
4 changes: 4 additions & 0 deletions packages/appkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ export {
type AgentDefinition,
type AgentsPluginConfig,
type AgentTool,
type AgentTools,
agentIdFromMarkdownPath,
agents,
type BaseSystemPromptOption,
type FromPluginMarker,
fromPlugin,
isFromPluginMarker,
isToolkitEntry,
loadAgentFromFile,
loadAgentsFromDir,
Expand Down
Loading