Skip to main content
Version: 0.19.0

AgentSpec

AgentSpec<TConfig> is everything about an agent's domain that the kit needs but doesn't decide: its system prompt, the MCP servers it always has, the folders with its skills and commands, and its subagents, plus a few optional knobs. TConfig is your config type (any type extending BaseSessionConfig); every method receives the full config, so it can depend on your own fields.

import type { AgentSpec, BaseSessionConfig } from "@falkenslab/agent-kit";

interface Config extends BaseSessionConfig {
courseUrl: string;
teacherName: string;
}

const spec: AgentSpec<Config> = {
buildSystemPrompt: (config) => `You help ${config.teacherName} with the course at ${config.courseUrl}.`,
buildMcpServers: () => ({}),
pluginRoots: () => [],
buildSubagents: () => undefined,
};
info
Why not AgentDefinition?

The SDK already uses AgentDefinition for a single subagent's definition. AgentSpec is a level above: it decides, among other things, which subagents get registered.

Required members​

buildSystemPrompt(config): string​

The system prompt for this session. Everything role-, mode- or domain-specific belongs here. The kit appends to it:

Long prompts are easier to maintain as files: see Prompts and createPromptLoader().

buildSystemPrompt: (config) =>
loadPrompt(config.mode === "autonomous" ? "system-autonomous.md" : "system.md", { course: config.courseUrl }),

buildMcpServers(config, runDir): Record<string, McpServerConfig>​

The MCP servers this agent always registers, keyed by server name. The kit adds its own next to them when they apply (approvals, manual intervention, the sources tools, date and time), so don't register those. runDir is this run's folder, useful for a server that writes files (a browser that downloads into it, say).

buildMcpServers: (config, runDir) => ({
inventory, // an in-process server built with createSdkMcpServer()
playwright: {
command: "npx",
args: ["@playwright/mcp@latest", "--output-dir", runDir],
},
}),

Every mcp__<server>__<tool> call is approved automatically (see Permissions); block the ones you don't want with disallowedTools. More in Tools and MCP servers.

pluginRoots(config): string[]​

Absolute paths of local plugins (folders with .claude-plugin/plugin.json, skills/ and commands/) to load, in order. Return [] for none. See Skills and plugins.

pluginRoots: () => [path.join(__dirname, "plugin")],

buildSubagents(config)​

The subagents this configuration registers, and which of them the Agent tool may spawn, or undefined for none:

buildSubagents: () => ({
agents: {
"joke-finder": {
description: "Searches the web for short jokes and returns candidates with their source.",
prompt: "Find 2 or 3 short, clean jokes on the web. Return a numbered list with each joke and its URL.",
tools: ["WebSearch", "WebFetch"],
model: "haiku",
maxTurns: 8,
},
},
allowedSubagentTypes: ["joke-finder"],
}),

Returning something here also gives the session the Agent tool, and Bash when a subagent lists it or has no tools (Bash for subagents only), and registers the three subagent gates. Return undefined when this configuration needs no subagents, so neither tool is granted. See Subagents.

Optional members​

MemberDefaultWhat it does
identity?: AgentIdentitynoneWho the agent is (name, version, description): the awareness extension tells the model, with agent-kit's version. See identity and helpGuide.
helpGuide?: stringnoneAbsolute path of a markdown guide to the agent's own domain, which the awareness extension's about_me gives the agent when the person asks.
disallowedTools?: string[][]Tools blocked whatever else allows them, e.g. an MCP tool you don't trust. Takes precedence over everything. A built-in tool also leaves the context; an MCP tool is blocked but its definition is still sent.
saveToSourcesDescription?: stringa generic descriptionThe description of the save_to_sources tool (registered with sourcesDir), in your domain's words.
extensions?: (string | Extension)[]noneWhat the agent runs with besides the core: the kit's "knowledge" and "sources" by name, its own as objects. See Extensions.
knowledgePageTypes?: PageType[]noneThe agent's own page types, besides the kit's. See Your own page types.
knowledgeStore?(config): KnowledgeStorethe kit's store over knowledgeDir's filesAnother store (a database, a vector store). See Knowledge store.
humanApprovalTexts?generic texts{ description, approved, rejected } of the approval tool (guided mode).
manualInterventionTexts?noneIts texts, and the opt-in, of the manual-intervention tool.
replyInLanguage?: booleantrueThe line asking the agent (and each subagent) to reply in the kit's language.
settingSources?: SettingSource[]["project"]Which Claude Code settings files the session loads.
skills?: string[] | "all" | "plugins""all"Which skills the agent offers: every one found, only its plugins' ("plugins"), or a list.
planMode?: PlanModeSpecnoneIn plan mode, the agent's plan files and its read-only MCP tools.

identity and helpGuide​

Who the agent is (its name, version and what it is) and a guide to its own domain (its commands, configuration, folders). The awareness extension uses them: it tells the model who it is, with agent-kit's version, answers what it is right now with about_me, gives it the guide when the person asks, and explains the kit's chat with its help skill. It needs identity; without the extension, neither field reaches the model.

const spec: AgentSpec<Config> = {
// …
extensions: ["awareness"],
identity: { name: "padawan", version, description: "an agent that takes a Moodle course as a student" },
helpGuide: path.join(__dirname, "guide.md"),
};

Only name is required. Write the guide in English, for the person's questions, and leave out the chat and what changes during a session: see Awareness.

disallowedTools​

// A browser-driving agent: this Playwright tool runs arbitrary code, so it's never allowed.
disallowedTools: ["mcp__playwright__browser_run_code_unsafe"],

The SDK checks disallowedTools before anything else: a disallowed tool never reaches the kit's permission callback, let alone runs. Built-in tools can be disallowed too (Captain Whiskers disallows Read, Write and Glob).

humanApprovalTexts​

The approval tool exists outside autonomous mode. Its description tells the model when to call it, so write it for your domain:

humanApprovalTexts: {
description:
"Call this right before publishing anything students will see (a grade, a forum post, a page). " +
"Not for browsing or reading.",
approved: "Approved by the teacher. Go ahead.",
rejected: "The teacher rejected it. Don't publish it; ask what to change.",
},

See Approvals.

manualInterventionTexts​

Setting it is what registers the request_manual_login tool (outside autonomous mode). It only makes sense for an agent that drives a live interface a person can step into, such as a browser window:

manualInterventionTexts: {
toolDescription: "Call this when you can't log in by yourself; a person will log in by hand in the browser window.",
confirmedMessage: "The person says they logged in. Check the page and continue.",
checkpointTitle: "Manual login needed",
checkpointLines: ["Log in in the browser window the agent opened, then confirm here."],
},

See Manual intervention.

settingSources and skills​

settingSources: [], // load no Claude Code settings or CLAUDE.md at all
skills: "plugins", // only the skills of the plugins it loads are listed to the model

See Permissions and isolation and Context and cost.

A complete spec​

spec.ts
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createPromptLoader, createSdkMcpServer, tool, type AgentSpec, type BaseSessionConfig } from "@falkenslab/agent-kit";
import { z } from "zod";

const here = path.dirname(fileURLToPath(import.meta.url));
const loadPrompt = createPromptLoader(path.join(here, "prompts"));

export interface Config extends BaseSessionConfig {
audience: string;
}

const inventory = createSdkMcpServer({
name: "inventory",
version: "1.0.0",
tools: [
tool("find_product", "Find products in the inventory by name.", { query: z.string() }, async ({ query }) => ({
content: [{ type: "text" as const, text: JSON.stringify(await db.products.search(query)) }],
}), { annotations: { readOnlyHint: true } }),
],
});

export const spec: AgentSpec<Config> = {
buildSystemPrompt: (config) => loadPrompt("system.md", { audience: config.audience }),
buildMcpServers: () => ({ inventory }),
pluginRoots: () => [path.join(here, "plugin")],
buildSubagents: () => ({
agents: {
researcher: {
description: "Researches a question on the web and returns a short, sourced answer.",
prompt: "Research the question with WebSearch and WebFetch. Answer in five lines at most, with sources.",
tools: ["WebSearch", "WebFetch"],
model: "haiku",
},
},
allowedSubagentTypes: ["researcher"],
}),
disallowedTools: ["Write"],
skills: ["my-agent:summaries"],
settingSources: [],
};

planMode​

In plan mode the agent only reads and plans. Two callbacks say what else it may do there:

  • isPlanFile(filePath, config): whether Write/Edit may touch this file (absolute path). It must also be writable under the file scope.
  • isReadOnlyTool(toolName, input): whether one of the agent's MCP tools only reads. Every MCP tool it doesn't vouch for is denied in plan mode.
planMode: { isReadOnlyTool: (toolName) => toolName === "mcp__inventory__find_product" },

Without planMode, the agent can read, search, ask a person and delegate in plan mode, writes nothing and presents its plan in its reply.