Agent - Modes API
The Modes API allows developers to register custom modes and contexts programmatically. This is a powerful API for creating AI assistants tailored to specific workflows.
To demonstrate how powerful the Modes API is, we will dissect the built-in Block Editor mode, which was created entirely using it.
Registering a Mode
Use Agent::registerMode() to register a custom mode from PHP.
Mode Properties
id (string, required)
A unique identifier for the mode. Use lowercase letters and hyphens.
label (string)
The display name shown in the mode selector dropdown.
description (string)
A brief description of what the mode does. Shown in the mode selector.
category (string)
Category for grouping modes in the UI. Built-in categories are General and
WordPress.
detection (array)
Optional auto-detection configuration. When set, the mode is automatically selected based on the current context.
requiresCapability (string)
Optional capability required to use this mode.
toolbar (string)
Optional toolbar type. Set to plugin to show the plugin mode toolbar.
agent.systemInstruction (string)
The system instruction sent to the AI. Can also be a function in JavaScript.
agent.canvas (boolean)
Enables the virtual file system for this mode. Default is false.
agent.executeJs (boolean)
Whether the AI can execute JavaScript. Default is false.
script (string)
URL to a JavaScript file that extends the mode with file systems and tools.
Case Study: Block Editor Mode
The Block Editor mode allows the AI to read and edit WordPress blocks. It was built entirely with the Modes API and demonstrates how to create a powerful, context-aware assistant.
Step 1: PHP Registration
The mode is registered in PHP with basic configuration:
use SleekAi\Agent\Agent;
Agent::registerMode([
'id' => 'block-editor',
'label' => 'Block Editor',
'description' => 'AI assistant for the WordPress block editor',
'category' => 'WordPress',
'detection' => [
'context' => 'block-editor',
],
'agent' => [
'canvas' => true,
'executeJs' => true,
],
'script' => plugins_url('script.js', __FILE__),
]);
Copy
This tells Agent mode to:
- Auto-select this mode when the block editor context is detected
- Enable the virtual file system (
canvas: true) - Allow JavaScript execution for operations like inserting blocks
- Load a script that defines the file system and tools
Step 2: Helper Functions
The script starts with helper functions to safely access WordPress APIs:
const getWpBlockEditor = () => {
const wp = window.wp;
if (!wp?.data) return null;
return wp.data.select("core/block-editor");
};
const getWpDispatch = () => {
const wp = window.wp;
if (!wp?.data?.dispatch) return null;
return wp.data.dispatch("core/block-editor");
};
Copy
These ensure the script does not crash if WordPress APIs are unavailable.
Step 3: Transforming Blocks to Virtual Files
Each WordPress block becomes a JSON file. The blockToJson function creates a
clean representation of a block:
const blockToJson = (block, path) => ({
path,
clientId: block.clientId,
name: block.name,
attributes: block.attributes,
innerBlocks: block.innerBlocks.map((inner) =>
blockToJson(inner, `${path}/${inner.clientId}`)
),
});
Copy
The transformBlockToFiles function recursively processes blocks and their
children, storing each as a file:
const transformBlockToFiles = (block, files, parentPath = "blocks") => {
const basePath = `${parentPath}/${block.clientId}`;
files[`${basePath}/block.json`] = JSON.stringify(
blockToJson(block, basePath),
null,
2,
);
block.innerBlocks.forEach((innerBlock) => {
transformBlockToFiles(innerBlock, files, basePath);
});
};
Copy
This creates a file structure like:
blocks/abc123/block.json # Parent block
blocks/abc123/def456/block.json # Child block
blocks/abc123/def456/ghi789/block.json # Grandchild
Copy
Step 4: The File System Interface
The file system has four parts:
getInitialFiles
Returns all blocks as virtual files when the mode loads:
const getAllBlocksAsFiles = () => {
const files = {};
const blockEditor = getWpBlockEditor();
if (!blockEditor?.getBlocks) return files;
const blocks = blockEditor.getBlocks();
blocks.forEach((block) => {
transformBlockToFiles(block, files);
});
return files;
};
Copy
onBeforeRead
Called when the AI reads a file. Returns fresh data from WordPress instead of cached content, ensuring the AI always sees the current state:
const parseBlockPath = (path) => {
const normalizedPath = path.replace(/^\//, "");
if (!normalizedPath.startsWith("blocks/")) return null;
const parts = normalizedPath.split("/");
const fileName = parts[parts.length - 1];
const clientId = parts[parts.length - 2];
if (!fileName.endsWith(".json")) return null;
return { clientId, fileName };
};
const onBeforeRead = (path) => {
const blockEditor = getWpBlockEditor();
if (!blockEditor) return null;
const parsed = parseBlockPath(path);
if (!parsed) return null;
const block = blockEditor.getBlock(parsed.clientId);
if (!block) return null;
if (parsed.fileName === "block.json") {
const basePath = path.replace(/\/block\.json$/, "");
return JSON.stringify(blockToJson(block, basePath), null, 2);
}
return null;
};
Copy
The parseBlockPath helper extracts the clientId from a path like
blocks/abc123/block.json. The onBeforeRead function uses this to fetch the
current block data from WordPress.
onFileChange
Called when the AI writes to a file. Applies changes to the actual WordPress block:
const onFileChange = (path, content) => {
const blockEditor = getWpBlockEditor();
const dispatch = getWpDispatch();
if (!blockEditor || !dispatch) return;
const parsed = parseBlockPath(path);
if (!parsed) return;
const block = blockEditor.getBlock(parsed.clientId);
if (!block) return;
try {
if (parsed.fileName === "block.json") {
const data = JSON.parse(content);
// Update block attributes
if (data.attributes) {
dispatch.updateBlockAttributes(parsed.clientId, data.attributes);
}
// Replace block if type changed
if (data.name && data.name !== block.name) {
dispatch.replaceBlock(
parsed.clientId,
window.wp.blocks.createBlock(
data.name,
data.attributes || block.attributes,
block.innerBlocks
),
);
}
}
} catch {
// Ignore parse errors
}
};
Copy
This function does two things:
- Updates attributes: If the AI changes text or settings, they are applied via
updateBlockAttributes - Replaces blocks: If the AI changes the block type (e.g., paragraph to heading), the block is replaced entirely
subscribe
Watches for changes in WordPress and updates the virtual file system:
subscribe: (onUpdate) => {
const wp = window.wp;
if (!wp?.data?.subscribe) {
return () => {};
}
let lastBlocksJson = JSON.stringify(getAllBlocksAsFiles());
const unsubscribe = wp.data.subscribe(() => {
const newFiles = getAllBlocksAsFiles();
const newBlocksJson = JSON.stringify(newFiles);
if (newBlocksJson !== lastBlocksJson) {
lastBlocksJson = newBlocksJson;
onUpdate(newFiles);
}
});
return unsubscribe;
},
Copy
This ensures the virtual file system stays in sync when the user edits blocks directly in WordPress. The comparison prevents unnecessary updates when nothing has changed.
Step 5: Adding a Custom Tool
The get_selected_block tool tells the AI which block the user is working with:
const findBlockPath = (blocks, targetClientId, parentPath = "blocks") => {
for (const block of blocks) {
const currentPath = `${parentPath}/${block.clientId}`;
if (block.clientId === targetClientId) {
return currentPath;
}
if (block.innerBlocks.length > 0) {
const found = findBlockPath(block.innerBlocks, targetClientId, currentPath);
if (found) return found;
}
}
return null;
};
const tools = [
{
name: "get_selected_block",
description: "Get the currently selected block's client ID and file path",
schema: {
type: "object",
properties: {},
required: [],
},
execute: () => {
const blockEditor = getWpBlockEditor();
if (!blockEditor?.getSelectedBlockClientId) {
return { error: true, message: "Block editor not available" };
}
const clientId = blockEditor.getSelectedBlockClientId();
if (!clientId) {
return { error: false, clientId: null, message: "No block selected" };
}
const blocks = blockEditor.getBlocks?.() || [];
const path = findBlockPath(blocks, clientId);
return { error: false, clientId, path };
},
},
];
Copy
The findBlockPath helper is needed because nested blocks have paths like
blocks/parent/child/block.json. It recursively searches to find the full path
for any block by its clientId.
Step 6: Dynamic System Instructions
The system instruction is generated dynamically to include context about available block types:
const getBlockTypesInfo = () => {
const wp = window.wp;
if (!wp?.blocks?.getBlockTypes) return "";
const blockTypes = wp.blocks.getBlockTypes();
return blockTypes
.filter((block) => block.name.startsWith("core/"))
.slice(0, 30)
.map((block) => {
const attrs = block.attributes || {};
const textAttrs = Object.entries(attrs)
.filter(([_, config]) =>
config.type === "string" || config.type === "rich-text"
)
.map(([name]) => name)
.slice(0, 3);
const textInfo = textAttrs.length > 0 ? `: ${textAttrs.join(", ")}` : "";
return `- **${block.name}**${textInfo}`;
})
.join("\n");
};
const getContextInstructions = () => {
const blockTypesInfo = getBlockTypesInfo();
return `You are assisting with the WordPress Block Editor.
## File System Structure
Each block is a JSON file at blocks/{clientId}/block.json containing:
- name: The block type (e.g., core/paragraph)
- attributes: The block's settings and content
- innerBlocks: Nested blocks for containers
## Available Block Types
${blockTypesInfo}
## Workflow
1. Use get_selected_block to find what the user is working with
2. Read the block.json to see its current state
3. Use find_and_replace or write_file to make changes
## JavaScript Execution
Use JavaScript for operations that cannot be done via file edits:
- Inserting new blocks
- Removing blocks
- Reordering blocks`;
};
Copy
The instruction includes available block types with their text attributes, helping the AI understand what it can create and edit.
Step 7: Putting It All Together
Finally, everything is registered using extendMode:
window.sleekAiAgent.extendMode("block-editor", {
agent: {
systemInstruction: getContextInstructions,
fileSystem: {
getInitialFiles: getAllBlocksAsFiles,
onBeforeRead,
onFileChange,
subscribe: (onUpdate) => {
// ... subscription logic
},
},
tools,
},
});
Copy
The Result
With this setup, the AI can:
- See all blocks as JSON files it can browse and search
- Read block content and attributes (always fresh via
onBeforeRead) - Modify blocks by writing to their virtual files (applied via
onFileChange) - Know which block is selected via the custom tool
- Execute JavaScript to insert, delete, or move blocks
- Stay in sync when the user edits blocks directly
This demonstrates how the Modes API enables you to create AI assistants that deeply integrate with any WordPress feature or custom workflow.
Registering Custom Contexts
Use Agent::registerContext() to register contexts for custom admin screens.
See the Context documentation for details.