Skip to content

Plugins

Plugins are reusable bundles of hooks, tools, and initialisation logic that extend agent behaviour. Attach one or many at construction time — each registers its own functionality independently.

Creating a plugin

Extend Plugin and decorate methods with @hook or @tool:

python
from elsai import Agent, tool
from elsai.plugins import Plugin
from elsai.hooks import BeforeModelCallEvent, hook

class LoggingPlugin(Plugin):
    name = "logging"

    @hook
    def on_model_call(self, event: BeforeModelCallEvent) -> None:
        print(f"[{self.name}] calling model — agent: {event.agent.name}")

    @tool
    def ping(self) -> str:
        """Check if the logging plugin is active."""
        return f"{self.name} plugin is active"

agent = Agent(plugins=[LoggingPlugin()])
result = agent.tool.ping()   # → "logging plugin is active"

Plugin lifecycle

  1. The framework scans all @tool-decorated methods and registers them.
  2. init_agent() runs — use it for setup that needs access to the agent.
  3. @hook handlers bind to the event bus.

Async initialisation

python
class DataPlugin(Plugin):
    name = "data"

    async def init_agent(self, agent: "Agent") -> None:
        self.config = await fetch_remote_config()

Accessing agent state

Plugins can read and write the agent's persistent state store:

python
class CounterPlugin(Plugin):
    name = "counter"

    @hook
    def count(self, event: BeforeModelCallEvent) -> None:
        calls = event.agent.state.get("call_count", 0)
        event.agent.state.set("call_count", calls + 1)
        print(f"Total model calls: {calls + 1}")

Composing multiple plugins

Plugins stack cleanly — each operates independently:

python
agent = Agent(
    tools=[my_search_tool],
    plugins=[
        LoggingPlugin(),
        CounterPlugin(),
        SkillsPlugin(skills="./skills/"),
        ContextOffloader(storage=InMemoryStorage()),
    ],
)

Built-in plugins

PluginPurpose
AgentSkillsOn-demand modular instructions agents discover and activate at runtime
LLMSteeringHandlerContext-aware guidance injected at decision points via lifecycle hooks
ContextOffloaderOffloads oversized tool results to storage and provides an auto-generated retrieval tool

AgentSkills

Skills solve context bloat in complex agents. Instead of embedding all instructions in one monolithic system prompt, skills implement progressive disclosure: lightweight metadata stays in the prompt, and full instructions load only when the agent activates a specific skill.

How it works

  1. Discovery — On init, skill names and descriptions are injected as XML into the system prompt so the agent knows what's available without loading full instructions.
  2. Activation — When the agent determines it needs a skill, it calls the built-in skills tool. The full instructions and resource file listing are returned.
  3. Execution — The agent follows the loaded instructions and accesses resource files through provided tools.

The injected block looks like:

xml
<available_skills>
  <skill>
    <name>pdf-processing</name>
    <description>Extract text and tables from PDF documents</description>
  </skill>
</available_skills>

SKILL.md format

Skills follow the Agent Skills specification. Each skill lives in its own directory:

skills/
└── pdf-processing/
    ├── SKILL.md          ← required: frontmatter + instructions
    ├── scripts/          ← executable files the agent can run
    ├── references/       ← documentation and guides
    └── assets/           ← static templates and configs

SKILL.md frontmatter:

markdown
---
name: pdf-processing
description: Extract text and tables from PDF documents using pdfplumber
allowed-tools: file_read shell
---

# PDF Processing

Use `pdfplumber` to extract content from PDF files.

## Steps
1. Open the file with `pdfplumber.open(path)`
2. Iterate pages with `pdf.pages`
3. Extract text with `page.extract_text()`
4. Extract tables with `page.extract_tables()`
Frontmatter fieldRequiredDescription
nameYesLowercase alphanumeric + hyphens, 1–64 chars. Must match the directory name.
descriptionYesCapability summary shown in the system prompt
allowed-toolsNoSpace-delimited tool names the skill uses (informational)

Usage

python
from elsai import Agent
from elsai.plugins.skills import AgentSkills
from elsai.tools import file_read, shell

# Load skills from a directory
plugin = AgentSkills(skills="./skills/")
agent = Agent(plugins=[plugin], tools=[file_read, shell])

result = agent("Extract the tables from report.pdf")

Create skills programmatically:

python
from elsai.plugins.skills import AgentSkills, Skill

skill = Skill(
    name="code-review",
    description="Review Python code for best practices and bugs",
    instructions="Review the provided code. Check for: 1) PEP 8 compliance..."
)

plugin = AgentSkills(skills=[skill])
agent = Agent(plugins=[plugin])

Load from a SKILL.md string:

python
skill = Skill.from_content("""---
name: data-analysis
description: Analyse tabular data using pandas
---
Use pandas to load and analyse the data...""")

plugin = AgentSkills(skills=[skill])

Configuration

ParameterDefaultDescription
skillsrequiredPath string, list of paths, or Skill instances
state_key"agent_skills"Agent state key for persisting activated skills
max_resource_files20Maximum resource files listed per activation
strictFalseIf True, raises errors on validation failures instead of warning

Runtime management

python
# View available skills
plugin.get_available_skills()

# Add or replace skills at runtime
plugin.set_available_skills([new_skill])

# See which skills the agent has activated this session
plugin.get_activated_skills()

Activated skills persist in agent state across sessions when a SessionManager is configured.

When to use skills vs. multi-agent

Skills are ideal when a single agent handles multiple specialised domains and you want to avoid context bloat without the overhead of separate agents. For truly independent workloads, prefer multi-agent patterns.


LLMSteeringHandler

Steering provides just-in-time guidance at decision points inside the agent loop. Instead of front-loading 30+ instructions into a system prompt (where models tend to ignore them), steering intercepts tool calls and model responses, evaluates them, and injects corrective feedback only when needed.

How it works

Two steering points:

HookEvaluatesActions
steer_before_tool()Incoming tool call (BeforeToolCallEvent)Proceed — run the tool; Guide — cancel and inject feedback; Interrupt — pause for human
steer_after_model()Model response (AfterModelCallEvent)Proceed — accept response; Guide — discard and retry with guidance injected

Usage

python
from elsai import Agent
from elsai.plugins.steering import LLMSteeringHandler

handler = LLMSteeringHandler(
    system_prompt="""You are a safety reviewer for an agent that manages files.

Rules:
- Never allow deletion of files outside /tmp
- Always confirm before overwriting existing files
- Reject shell commands that contain 'rm -rf'
"""
)

agent = Agent(tools=[file_read, file_write, shell], plugins=[handler])
result = agent("Clean up the project directory")

Context providers

The LedgerProvider (built-in) tracks tool call history and makes it available to the steering model:

python
from elsai.plugins.steering import LLMSteeringHandler, LedgerProvider

handler = LLMSteeringHandler(
    system_prompt="Review tool calls for safety...",
    context_providers=[LedgerProvider()],
)

The ledger captures per-tool-call data: inputs, outputs, timing, and success/failure status — all in steering_context["ledger"].

Steering actions

python
from elsai.plugins.steering import LLMSteeringHandler, ToolSteeringAction

class MySteeringHandler(LLMSteeringHandler):
    def steer_before_tool(self, event):
        tool_name = event.tool_use["name"]
        if tool_name == "shell":
            cmd = event.tool_use["input"].get("command", "")
            if "rm -rf" in cmd:
                return ToolSteeringAction.guide("Destructive commands are not allowed.")
        return ToolSteeringAction.proceed()

Python only

LLMSteeringHandler is currently available in Python only.


ContextOffloader

The ContextOffloader plugin prevents large tool results from exhausting the context window. It intercepts results after tool execution, stores oversized ones in a backend, and replaces them with a short preview plus a reference — keeping the context window lean.

How it works

After offloading, the agent can call the auto-registered retrieve_offloaded_content tool to fetch specific sections on demand.

Storage backends

BackendPersistenceBest for
InMemoryStorageProcess lifetimeDevelopment and testing
FileStorageLocal diskDebugging, artifact inspection
S3StorageAmazon S3Production deployments

Usage

python
from elsai import Agent
from elsai.plugins.context_offloader import ContextOffloader, InMemoryStorage, FileStorage, S3Storage

# Development
agent = Agent(plugins=[
    ContextOffloader(storage=InMemoryStorage())
])

# Local disk
agent = Agent(plugins=[
    ContextOffloader(
        storage=FileStorage("./offloaded-results/"),
        max_result_tokens=5_000,
        preview_tokens=2_000,
    )
])

# Production (S3)
agent = Agent(plugins=[
    ContextOffloader(
        storage=S3Storage(
            bucket="my-agent-artifacts",
            prefix="tool-results/",
        ),
        max_result_tokens=5_000,
        preview_tokens=2_000,
    )
])

Configuration

ParameterDefaultDescription
storagerequiredStorage backend instance
max_result_tokens2500Results exceeding this are offloaded
preview_tokens1000How many tokens of preview to keep in context
include_retrieval_toolTrueWhether to register retrieve_offloaded_content tool

Retrieving offloaded content

The auto-registered tool lets the agent fetch stored results selectively:

python
# The agent calls this automatically — shown here for illustration
agent.tool.retrieve_offloaded_content(
    reference="offload://abc123",
    pattern="error",          # optional: keyword/regex search
    line_range=[100, 150],    # optional: specific lines
)

Token estimation

Token counts are estimated using the model's native tokeniser when available, or a character-based heuristic (chars ÷ 4 for text, chars ÷ 2 for JSON). Adjust max_result_tokens based on your model's context window and workload.

Copyright © 2026 Elsai Foundry.