Hook System Overview
In Fragola, a Hook is the primary unit of extensibility. Because the core SDK is kept deliberately lean, complex features (like routing, moderation, or remote protocol execution) are packaged as Hooks rather than bloated base classes.
What is a Hook?
A hook is simply a function that takes an Agent instance as an argument. Inside the function, you use the standard SDK primitives to mutate the agent:
- Attach
on*event listeners. - Add scoped
Storesfor feature-specific state. - Register new
Toolsto the agent.
By bundling these together, a hook provides a plug-and-play capability. You use agent.use(myHook, "hook-id") to apply the capability, and agent.removeHook("hook-id") to tear it down.
Why use Hooks?
- Reusability: Write a logging system or a safety guardrail once, and apply it to dozens of distinct agents.
- Encapsulation: Hooks keep agent configuration clean. The logic for connecting to a Model Context Protocol (MCP) server involves event rewriting, dynamic tool injection, and custom state watchers. By wrapping it in a hook, the end-user only writes one line of code.
- Dynamic Compositions: Because hooks can be added and removed at runtime, agents can gain or lose superpowers dynamically as they traverse different parts of your application architecture.
Creating Custom Hooks
Writing a custom hook is straightforward. You define a function that adheres to the FragolaHook signature.
A Basic Hook
Here is an example of a hook that injects a specific instruction into the agent and logs all tool calls.
import { type FragolaHook, type FragolaEvent } from "@fragola-ai/agent";
const toolLoggerHook: FragolaHook = (agent) => {
// 1. Add instructions dynamically
agent.context.instructions.add("logger", "Please use tools frequently.");
// 2. Attach an event listener
agent.onAfterToolCall(({ name }) => {
console.log(`[ToolLogger]: Executed ${name}`);
});
};
// Usage:
agent.use(toolLoggerHook, "tool-logger");The Teardown Function
Hooks often leave a footprint: they add event listeners, they add stores, or they mutate instructions. If a hook is removed dynamically, it needs a way to clean up after itself.
A FragolaHook can return a teardown function (synchronous or asynchronous). Fragola will call this function automatically when the hook is removed or when the agent is disposed.
const robustLoggerHook: FragolaHook = (agent) => {
// Keep track of the events we create so we can remove them later
const listeners: FragolaEvent[] = [];
listeners.push(
agent.onAfterToolCall(() => { /* ... */ })
);
// Return the cleanup function
return () => {
// Clean up events
listeners.forEach(event => event.remove());
// Clean up instructions
agent.context.removeInstructions("logger");
};
};Managing Hooks on Agents
Fragola provides a set of dedicated methods on the Agent class to safely manage the lifecycle of your hooks.
agent.use()
Applies a hook to the agent. It takes the hook function and a unique string identifier (name). Providing a name is strongly recommended so you can reference or remove the hook later.
agent.use(myCustomHook, "my-hook-id");agent.hasHook()
Checks if a hook with the given identifier is currently active on the agent.
if (!agent.hasHook("moderator")) {
agent.use(moderatorHook, "moderator");
}agent.removeHook()
Safely detaches a hook from the agent. This triggers the hook's internal cleanup/disposal function (if one was provided) and immediately removes any tracked event listeners associated with it.
agent.removeHook("my-hook-id");agent.dispose()
Tears down the agent entirely. This iterates through all registered hooks on the agent, running their cleanup functions and disconnecting them, effectively returning the agent to its base state.
await agent.dispose();agent.init()
Because agent.use() can apply hooks that do asynchronous setup (like connecting to an external database or an MCP server), you might need to ensure all hooks have finished their setup before calling the LLM.
agent.init() returns a Promise that resolves when all currently registered hooks have completed their initialization logic.
agent.use(asyncDatabaseHook, "db");
// Wait for the async hook to finish establishing connections
await agent.init();
// Safe to start talking
await agent.userMessage({ content: "Hello" });Hook Scoping & Lifecycle
When building complex agent architectures, you need guarantees about how hooks behave when agents are modified or duplicated.
Automatic Event Cleanup
Fragola is designed to prevent memory leaks when managing event-heavy hooks.
Even if you forget to return a cleanup function from your hook, Fragola tracks all agent.on* listeners that are registered while a hook's function is executing. When you call agent.removeHook("id"), Fragola automatically purges those specific event listeners from the pipeline.
Returning a custom cleanup function is only strictly necessary when you need to remove custom stores, clear scoped instructions, or close external network connections (like WebSockets).
Hook Preservation During Forking
As detailed in the Agent Forking guide, creating a branch of an agent via agent.fork() creates a deep clone of the runtime state.
Hooks follow strict inheritance rules:
- Cloned Registration: The forked agent instantly registers the exact same hooks as the parent, preserving their identifiers.
- Re-bound Listeners: The event listeners established by the hooks are deeply cloned and re-bound to the forked agent context, guaranteeing that the hook logic fires for the fork independently.
- Independent Cleanup: Calling
fork.removeHook("id")will tear down the hook on the fork without affecting the parent agent's hook instance.