Tutorial 31: Plugins — Composable Agent Extensions¶
This tutorial covers:
- Plugin base class: bundle hooks + tools
- @hook decorator: auto-discovery of hook methods
- Callback handler: plain function receives events
- Cancel signal: stop agent from external thread
Prerequisites:
- Configure model via environment variables
Difficulty: Intermediate
Source¶
# Copyright (c) 2025, 2026 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v1.0 as shown at
# https://oss.oracle.com/licenses/upl/
"""
Tutorial 31: Plugins — Composable Agent Extensions
This tutorial covers:
- Plugin base class: bundle hooks + tools
- @hook decorator: auto-discovery of hook methods
- Callback handler: plain function receives events
- Cancel signal: stop agent from external thread
Prerequisites:
- Configure model via environment variables
Difficulty: Intermediate
"""
import threading
import time
from config import get_model
from locus.agent import Agent, AgentConfig
from locus.hooks.plugin import Plugin, hook
from locus.tools.decorator import tool
# =============================================================================
# Part 1: Create a Plugin
# =============================================================================
def example_plugin():
"""Bundle hooks into a reusable plugin."""
print("=== Part 1: Plugin System ===\n")
model = get_model()
class AuditPlugin(Plugin):
"""Tracks all model and tool calls."""
name = "audit"
def __init__(self):
self.log = []
@hook
async def on_before_model_call(self, event):
self.log.append(f"model: {len(event.messages)} msgs")
@hook
async def on_before_tool_call(self, event):
self.log.append(f"tool: {event.tool_name}")
@tool
def search(query: str) -> str:
"""Search for information."""
return f"Results for: {query}"
plugin = AuditPlugin()
agent = Agent(
config=AgentConfig(
system_prompt="Use the search tool to answer questions.",
max_iterations=5,
model=model,
tools=[search],
plugins=[plugin],
)
)
result = agent.run_sync("Search for Python best practices")
print(f"Response: {result.message[:100]}...")
print(f"Audit log: {plugin.log}")
# =============================================================================
# Part 2: Callback Handler
# =============================================================================
def example_callback():
"""Receive events with a plain function."""
print("\n=== Part 2: Callback Handler ===\n")
model = get_model()
events = []
agent = Agent(
config=AgentConfig(
system_prompt="Answer concisely.",
max_iterations=3,
model=model,
callback_handler=lambda e: events.append(e.event_type),
)
)
agent.run_sync("What is 2+2?")
print(f"Events received: {events}")
# =============================================================================
# Part 3: Cancel Signal
# =============================================================================
def example_cancel():
"""Stop an agent from another thread, after running one normal call first."""
print("\n=== Part 3: Cancel Signal ===\n")
model = get_model(max_tokens=80)
# 3a — A real call first, so this Part also exercises the provider.
live_agent = Agent(
config=AgentConfig(
system_prompt="Answer in one sentence.",
max_iterations=3,
model=model,
)
)
t0 = time.perf_counter()
live_result = live_agent.run_sync("In one sentence, why does an agent need a cancel signal?")
dt = time.perf_counter() - t0
print(
f" [model call: {dt:.2f}s · "
f"{live_result.metrics.prompt_tokens}→{live_result.metrics.completion_tokens} tokens]"
)
print(f" AI rationale: {live_result.message.strip()}")
# 3b — Now cancel a fresh agent before it runs.
agent = Agent(
config=AgentConfig(
system_prompt="Answer concisely.",
max_iterations=3,
model=model,
)
)
agent.cancel()
result = agent.run_sync("This should be cancelled")
print(f"Stop reason: {result.stop_reason}") # "cancelled"
if __name__ == "__main__":
example_plugin()
example_callback()
example_cancel()