Plugin System
Custom tool+skill bundles for optional capabilities beyond the built-in toolset
Plugin System
A3S Code's plugin system lets you mount optional capabilities onto a session. Each plugin is a self-contained bundle that registers:
- One or more tools into
ToolRegistry— the LLM can call them during agent turns - Companion skills into
SkillRegistry— automatically injected into the system prompt so the LLM knows when and how to use the tools
Plugins are opt-in. Use them for custom tools, MCP-backed capabilities, or extra skill bundles on top of the built-in core toolset.
What Plugins Are For
- Add custom tools not shipped by core
- Register extra skills and usage guidance
- Compose optional capabilities per session
- Integrate external tool surfaces through MCP or custom Rust plugins
Mounting Plugins
import { Agent, SkillPlugin } from '@a3s-lab/code';
const agent = await Agent.create('agent.hcl');
const session = agent.session('.', {
plugins: [
new SkillPlugin('my-plugin', [skillMarkdown]),
],
});from a3s_code import Agent, SessionOptions, SkillPlugin
agent = Agent.create("agent.hcl")
opts = SessionOptions()
opts.plugins = [SkillPlugin("my-plugin", [skill_md])]
session = agent.session(".", opts)use a3s_code_core::{Agent, SessionOptions, SkillPlugin};
let session = agent
.session(workspace, SessionOptions::new()
.with_plugin(SkillPlugin::new("my-plugin", vec![skill_markdown])))
.await?;How Plugin Loading Works
When build_session() is called, plugins are loaded before the system prompt is assembled:
build_session(opts)
│
├─ 1. Build SkillRegistry (builtins + session skill_dirs)
│
├─ 2. Load each plugin in order
│ plugin.load(tool_registry, ctx) → registers tool
│ plugin.skills() → returns companion skills
│ skill_registry.register(skill) → added before system prompt
│
├─ 3. skill_registry.to_system_prompt() → includes plugin skills
│
└─ 4. Session starts — LLM sees plugin tools + skills from turn 1This means plugin companion skills appear in the first turn's system prompt — the LLM knows about them before any user message is processed.
Failure Isolation
If a plugin's load() call fails, only that plugin is skipped. Other plugins continue loading normally. Failed plugins do not register their companion skills.
Supported File Formats
Built-in tools work with plain-text formats out of the box:
| Category | Formats |
|---|---|
| Source code | Rust, Python, TypeScript, JavaScript, Go, Java, C/C++, and more |
| Config / data | JSON, TOML, YAML, HCL, CSV, TSV |
| Documents | Markdown, plain text |
Building a Custom Plugin
There are two levels of customization depending on your needs:
| Approach | Languages | Injects skills | Registers tools |
|---|---|---|---|
SkillPlugin | TypeScript, Python, Rust | ✅ | ❌ |
Plugin trait | Rust only | ✅ | ✅ |
For custom tools, use MCP — it works with any language and is automatically available to the LLM.
SkillPlugin — TypeScript / Python / Rust
SkillPlugin injects custom skills (LLM guidance, tool access rules, instructions) into the session without any Rust required. This covers the most common use case: shaping how the LLM behaves.
A skill is a YAML frontmatter + markdown body:
---
name: my-skill
description: Brief description used for skill selection
allowed-tools: "bash(*), read(*)"
kind: instruction
---
# My Skill
Guidance text the LLM receives when this skill is active...import { Agent, SkillPlugin } from '@a3s-lab/code';
const skillMd = `---
name: my-skill
description: Always explain commands before running them
allowed-tools: "bash(*)"
kind: instruction
---
Before executing any shell command, explain what it does and why.
`;
const session = agent.session('.', {
plugins: [
new SkillPlugin('my-plugin', [skillMd]),
],
});from a3s_code import Agent, SkillPlugin, SessionOptions
skill_md = """---
name: my-skill
description: Always explain commands before running them
allowed-tools: "bash(*)"
kind: instruction
---
Before executing any shell command, explain what it does and why.
"""
opts = SessionOptions()
opts.plugins = [SkillPlugin("my-plugin", [skill_md])]
session = agent.session(".", opts)use a3s_code_core::{SkillPlugin, SessionOptions};
let skill_md = r#"---
name: my-skill
description: Always explain commands before running them
allowed-tools: "bash(*)"
kind: instruction
---
Before executing any shell command, explain what it does and why.
"#;
let session = agent
.session(workspace, SessionOptions::new()
.with_plugin(SkillPlugin::new("my-plugin").with_skill(skill_md)))
.await?;Plugin trait (Rust) — full control
Implement the Plugin trait in Rust when you need to register custom tools alongside skills:
use a3s_code_core::plugin::{Plugin, PluginContext};
use a3s_code_core::skills::Skill;
use a3s_code_core::tools::ToolRegistry;
use anyhow::Result;
use std::sync::Arc;
pub struct MyPlugin;
impl Plugin for MyPlugin {
fn name(&self) -> &str { "my-plugin" }
fn version(&self) -> &str { "1.0.0" }
fn tool_names(&self) -> &[&str] { &["my_tool"] }
fn load(&self, registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
registry.register(Arc::new(MyTool::new()));
Ok(())
}
fn skills(&self) -> Vec<Arc<Skill>> {
let content = r#"---
name: my-skill
description: Guides the LLM on how to use my_tool
allowed-tools: "my_tool(*)"
kind: instruction
---
# My Skill
Use `my_tool` when the user asks to...
"#;
Skill::parse(content)
.map(|s| vec![Arc::new(s)])
.unwrap_or_default()
}
}
// Register:
let session = agent
.session(workspace, SessionOptions::new()
.with_plugin(MyPlugin))
.await?;Plugin trait reference
Prop
Type
PluginContext fields
Prop
Type
PluginManager (Rust API)
For advanced use cases, use PluginManager to manage plugin lifecycle independently:
use a3s_code_core::plugin::{PluginManager, PluginContext};
let mut manager = PluginManager::new();
manager.register(MyPlugin::default());
let ctx = PluginContext::new()
.with_llm(llm_client)
.with_skill_registry(skill_registry);
manager.load_all(&tool_registry, &ctx);
// Later: unload a single plugin
manager.unload("my-plugin", &tool_registry);
// Or unload all
manager.unload_all(&tool_registry);
// Query
manager.is_loaded("my-plugin"); // → bool
manager.plugin_names(); // → Vec<&str>