Skip to content
Merged
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
44 changes: 44 additions & 0 deletions docs/docs/api/appkit/Class.Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ protected config: TConfig;

***

### context?

```ts
protected optional context: PluginContext;
```

***

### devFileReader

```ts
Expand Down Expand Up @@ -244,6 +252,42 @@ AuthenticationError if user token is not available in request headers (productio

***

### attachContext()

```ts
attachContext(deps: {
context?: unknown;
telemetryConfig?: TelemetryOptions;
}): void;
```

Binds runtime dependencies (telemetry provider, cache, plugin context) to
this plugin. Called by `AppKit._createApp` after construction and before
`setup()`. Idempotent: safe to call if the constructor already bound them
eagerly. Kept separate so factories can eagerly construct plugin instances
without running this before `TelemetryManager.initialize()` /
`CacheManager.getInstance()` have run.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `deps` | \{ `context?`: `unknown`; `telemetryConfig?`: `TelemetryOptions`; \} |
| `deps.context?` | `unknown` |
| `deps.telemetryConfig?` | `TelemetryOptions` |

#### Returns

`void`

#### Implementation of

```ts
BasePlugin.attachContext
```

***

### clientConfig()

```ts
Expand Down
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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
39 changes: 39 additions & 0 deletions packages/appkit/src/beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,44 @@
//
// The exports below are auto-generated from each plugin's manifest.json
// "stability" field. See tools/generate-plugin-entries.ts.

// Agent types from shared
export type {
AgentAdapter,
AgentEvent,
AgentInput,
AgentRunContext,
AgentToolDefinition,
Message,
Thread,
ThreadStore,
ToolProvider,
} from "shared";
export { DatabricksAdapter, parseTextToolCalls } from "./agents/databricks";

// Tool authoring primitives
export {
AppKitMcpClient,
defineTool,
executeFromRegistry,
type FunctionTool,
functionToolToDefinition,
type HostedTool,
isFunctionTool,
isHostedTool,
mcpServer,
resolveHostedTools,
type ToolConfig,
type ToolEntry,
type ToolRegistry,
tool,
toolsFromRegistry,
} from "./core/agent/tools";
export {
type AgentTool,
isToolkitEntry,
type ToolkitEntry,
type ToolkitOptions,
} from "./core/agent/types";

export * from "./plugins/beta-exports.generated";
63 changes: 63 additions & 0 deletions packages/appkit/src/core/agent/build-toolkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { AgentToolDefinition } from "shared";
import type { ToolRegistry } from "./tools/define-tool";
import { toToolJSONSchema } from "./tools/json-schema";
import type { ToolkitEntry, ToolkitOptions } from "./types";

/**
* Converts a plugin's internal `ToolRegistry` into a keyed record of
* `ToolkitEntry` markers suitable for spreading into an `AgentDefinition.tools`
* record.
*
* The `opts` record controls shape and filtering:
* - `prefix` — overrides the default `${pluginName}.` prefix; `""` drops it.
* - `only` — allowlist of local tool names to include (post-prefix).
* - `except` — denylist of local names.
* - `rename` — per-tool key remapping (applied after prefix/filter).
*
* Each entry carries `pluginName` + `localName` so the agents plugin can
* dispatch back through `PluginContext.executeTool` for OBO + telemetry.
*/
export function buildToolkitEntries(
pluginName: string,
registry: ToolRegistry,
opts: ToolkitOptions = {},
): Record<string, ToolkitEntry> {
const prefix = opts.prefix ?? `${pluginName}.`;
const only = opts.only ? new Set(opts.only) : null;
const except = opts.except ? new Set(opts.except) : null;
const rename = opts.rename ?? {};

const out: Record<string, ToolkitEntry> = {};

for (const [localName, entry] of Object.entries(registry)) {
if (only && !only.has(localName)) continue;
if (except?.has(localName)) continue;

const keyAfterPrefix = `${prefix}${localName}`;
const key = rename[localName] ?? keyAfterPrefix;

const parameters = toToolJSONSchema(
entry.schema,
) as unknown as AgentToolDefinition["parameters"];

const def: AgentToolDefinition = {
name: key,
description: entry.description,
parameters,
};
if (entry.annotations) {
def.annotations = entry.annotations;
}

out[key] = {
__toolkitRef: true,
pluginName,
localName,
def,
annotations: entry.annotations,
autoInheritable: entry.autoInheritable,
};
}

return out;
}
101 changes: 101 additions & 0 deletions packages/appkit/src/core/agent/tests/build-toolkit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from "vitest";
import { z } from "zod";
import { buildToolkitEntries } from "../build-toolkit";
import { defineTool, type ToolRegistry } from "../tools/define-tool";
import { isToolkitEntry } from "../types";

const registry: ToolRegistry = {
query: defineTool({
description: "Run a query",
schema: z.object({ sql: z.string() }),
handler: () => "ok",
}),
history: defineTool({
description: "Get query history",
schema: z.object({}),
handler: () => [],
}),
};

describe("buildToolkitEntries", () => {
test("produces ToolkitEntry per registry item with default dotted prefix", () => {
const entries = buildToolkitEntries("analytics", registry);
expect(Object.keys(entries).sort()).toEqual([
"analytics.history",
"analytics.query",
]);
for (const entry of Object.values(entries)) {
expect(isToolkitEntry(entry)).toBe(true);
expect(entry.pluginName).toBe("analytics");
}
});

test("respects prefix option (empty drops the namespace)", () => {
const entries = buildToolkitEntries("analytics", registry, { prefix: "" });
expect(Object.keys(entries).sort()).toEqual(["history", "query"]);
});

test("respects custom prefix", () => {
const entries = buildToolkitEntries("analytics", registry, {
prefix: "db.",
});
expect(Object.keys(entries).sort()).toEqual(["db.history", "db.query"]);
});

test("only filter keeps the listed local names", () => {
const entries = buildToolkitEntries("analytics", registry, {
only: ["query"],
});
expect(Object.keys(entries)).toEqual(["analytics.query"]);
});

test("except filter drops the listed local names", () => {
const entries = buildToolkitEntries("analytics", registry, {
except: ["history"],
});
expect(Object.keys(entries)).toEqual(["analytics.query"]);
});

test("rename remaps specific local names (overrides the prefix key)", () => {
const entries = buildToolkitEntries("analytics", registry, {
rename: { query: "sql" },
});
expect(Object.keys(entries).sort()).toEqual(["analytics.history", "sql"]);
});

test("exposes the original plugin+local name so dispatch can route", () => {
const entries = buildToolkitEntries("analytics", registry, {
prefix: "db.",
});
const qEntry = entries["db.query"];
expect(qEntry.pluginName).toBe("analytics");
expect(qEntry.localName).toBe("query");
expect(qEntry.def.name).toBe("db.query");
});

test("propagates autoInheritable from the source registry", () => {
const mixed: ToolRegistry = {
readIt: defineTool({
description: "safe read",
schema: z.object({}),
autoInheritable: true,
handler: () => "ok",
}),
writeIt: defineTool({
description: "unsafe write",
schema: z.object({}),
autoInheritable: false,
handler: () => "ok",
}),
unmarked: defineTool({
description: "default: not auto-inheritable",
schema: z.object({}),
handler: () => "ok",
}),
};
const entries = buildToolkitEntries("p", mixed);
expect(entries["p.readIt"].autoInheritable).toBe(true);
expect(entries["p.writeIt"].autoInheritable).toBe(false);
expect(entries["p.unmarked"].autoInheritable).toBeUndefined();
});
});
Loading
Loading