Why architecture matters here
The reason a plugin system is architectural is that cross-cutting concerns are, by definition, the concerns you cannot cleanly place inside any single component — and if the framework gives you no global seam for them, they leak into every component instead. Without plugins, adding audit logging to an application means editing every agent to log its runs, editing every tool to log its calls, and remembering to do the same in every new agent anyone adds forever. The result is code duplication that is not merely ugly but dangerous: the day someone adds an agent and forgets the logging, you have an audit gap you won't discover until you need the log that isn't there. A global hook mechanism exists precisely so that 'do X on every model call' is expressed once and cannot be forgotten.
The plugin system matters because it makes these concerns consistent and centralized. A guardrail registered as a plugin runs on every tool call in the app by construction; there is no agent it can be forgotten on, because agents don't opt into it — the Runner applies it universally. This is the difference between a safety control that is a property of the application and one that is a convention developers are supposed to remember. For concerns like security, compliance, and observability, that universality is the entire value: a redaction plugin that runs everywhere is a guarantee, while redaction code that developers copy into agents is a hope.
The second load-bearing property is the short-circuit capability, which turns plugins from passive observers into active control. Because a before_model callback can return a response, a plugin can prevent the model call from happening at all — a cache plugin returns a stored answer, a guardrail returns a refusal, a rate-limit plugin returns a throttle error — and because a before_tool callback can return a result, a plugin can veto or replace a tool execution. This is what lets policy live in plugins: the decision to allow, block, or substitute an operation is made in one cross-cutting place, before the expensive or sensitive step runs, rather than being scattered through agent logic where it is hard to audit and easy to bypass.
The third reason is separation of concerns as a design discipline. The plugin system lets an agent's own code stay focused on its task — its instructions, its tools, its reasoning — while the ambient machinery of a production system (logging, metrics, safety, caching, cost tracking) lives in composable plugins layered around it. That separation is what keeps agents readable and testable as the operational surface grows: you can reason about an agent's behavior without the noise of logging and guardrail code interleaved through it, and you can add, remove, or reorder the ambient behaviors independently. An application without this seam eventually has agents whose real logic is buried under cross-cutting boilerplate; the plugin system is what keeps the two cleanly apart.
The architecture: every piece explained
Top row: registration and dispatch. The Runner — the object that executes agents against a session — owns a plugin registry: the ordered list of plugins configured for the application. A plugin manager is responsible for invoking the plugins' callbacks at each lifecycle point, walking the registry in order so behavior is deterministic. The plugins themselves are classes — a logging plugin, a guardrail plugin, a metrics plugin — each implementing whichever callbacks it cares about and ignoring the rest. Registering a plugin with the Runner is the single action that makes its behavior apply everywhere.
Middle row: the lifecycle callback points. The plugin manager fires callbacks around three nested scopes. Around an agent's execution: before_agent and after_agent, wrapping a whole agent run with access to the incoming user message and the run context. Around a tool call: before_tool and after_tool, with access to the tool's arguments and, afterward, its result. Around a model call: before_model and after_model, with access to the outgoing request and the returned response. There are also broader hooks — on user-message receipt, on run start and end, on error — but these six around agent, tool, and model are the workhorses, because they bracket exactly the operations a cross-cutting concern wants to observe or control.
Bottom rows: the two powers a callback has. Short-circuit: if a before_* callback returns a value, that value replaces the normal execution of the step — a before_model returning a response means the model is never called and that response is used; a before_tool returning a result means the tool never runs. This is how caching, guardrails, and policy enforcement work. Chaining: multiple plugins register for the same callback, and the manager runs them in a defined order, each seeing the (possibly mutated) state left by the previous — so a redaction plugin and a logging plugin compose, the redactor running before the logger so secrets never reach the log. The ops strip names what you manage: the distinction between global plugin callbacks and per-agent callbacks, the registration order that determines chaining, error isolation so one plugin's exception doesn't crash the run, and the performance cost of running callbacks on every step.
The ordering-and-short-circuit interaction is the subtle part that determines whether a plugin chain behaves as intended. Because plugins run in registration order and any one of them can short-circuit, the sequence is a pipeline where an early plugin's decision can preempt every later one: if a guardrail plugin registered first returns a refusal from before_model, the caching and logging plugins registered after it for the same hook may never see that call at all, depending on the framework's short-circuit semantics. This means the order you register plugins encodes real policy — safety checks that must always run belong early and should be careful about short-circuiting in ways that skip required auditing, while observability that must see everything may need to run before anything that can short-circuit, or be placed on the after_* side. Treating registration order as an incidental detail is how teams end up with a logging plugin that mysteriously misses exactly the requests a guardrail blocked; treating it as deliberate pipeline design is how the chain does what you meant.
End-to-end flow
Trace a single user request through an application with three plugins registered in order: a PII-redaction plugin, a guardrail plugin, and a logging-plus-metrics plugin. The user sends a message; the Runner begins an agent run and the plugin manager fires before_agent for each plugin in turn — the redactor scrubs any PII from the incoming message, the guardrail checks it against content policy, and the logger records the run start with timing.
Model call and short-circuit. The agent decides to call the model. Before the request goes out, the manager fires before_model down the chain. Suppose a caching plugin were present and had seen this exact request before: it would return the cached response from before_model, short-circuiting the call — the model is never invoked, the cached answer flows back, and the app saves a token spend and a round-trip. In our three-plugin setup, the guardrail's before_model inspects the outgoing prompt; finding it acceptable, it returns nothing and lets the call proceed. Had the prompt violated policy, the guardrail would have returned a refusal here, and the model call would never have happened — policy enforced in one place, before the expensive step.
After the model. The model responds. The manager fires after_model: the guardrail scans the output for policy violations (and could replace it with a safe refusal if needed), the redactor scrubs any PII the model emitted, and the metrics plugin records latency and token usage. Note the ordering doing real work — the redactor runs before the logger, so when the logging plugin records the response, the secrets are already gone; had they been registered the other way, the raw response would have hit the log first.
Tool call and completion. The agent then invokes a tool. before_tool fires: the guardrail can validate the tool arguments (blocking, say, a shell tool called with a dangerous command by short-circuiting with an error result), and the logger records the intended call. The tool runs; after_tool fires, the metrics plugin times it and the logger records the result. Finally the agent produces its answer and after_agent fires, letting the metrics plugin close out the run's timing and the logger write the completion record. Across the entire request, none of this cross-cutting behavior lived in the agent's own code — the agent simply reasoned and called tools, while redaction, safety, and observability were applied uniformly by plugins the Runner ran around every step. Add a second agent tomorrow and it inherits all three plugins automatically, with no change to its code, which is precisely the property that makes the plugin system worth its indirection: the ambient behavior of the whole application is defined in one composable place and cannot be accidentally omitted from any agent.