A3S Docs
A3S Code

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:

  1. One or more tools into ToolRegistry — the LLM can call them during agent turns
  2. 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 1

This 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:

CategoryFormats
Source codeRust, Python, TypeScript, JavaScript, Go, Java, C/C++, and more
Config / dataJSON, TOML, YAML, HCL, CSV, TSV
DocumentsMarkdown, plain text

Building a Custom Plugin

There are two levels of customization depending on your needs:

ApproachLanguagesInjects skillsRegisters tools
SkillPluginTypeScript, Python, Rust
Plugin traitRust 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>

On this page