Overriding engine behavior
Replacing what is under the hood
The container is the registry that holds every part of a Parlant server, and it lets you replace almost any part with your own. You might route logs into your own stack, keep sessions in your database, or change the engine's cost and latency trade-offs. The recipe is always the same: your coding agent writes a class, binds it in one place, and the whole server picks it up. Nothing is forked.
- Why one binding reaches the whole server
- The two startup callbacks, and which one to use for replacing or tuning
- The three forms a binding can take
- The parts that deployments commonly replace
This works because Parlant's components depend on ports, abstract types such as p.Logger, p.SessionStore, or p.CompassOptimizationPolicy, and never on concrete implementations.
A binding is one entry in the container: one port and the implementation behind it. Parlant fills the container for you at startup. When you rebind a port, nothing else in the stack ever knew which implementation it was talking to.
Some frequent configurations have dedicated p.Server parameters, such as models=, auth=, and session_store=. Those are shortcuts that produce container bindings underneath. The container hooks described on this page are the general mechanism behind all of them.
Two moments to touch the container
Before you touch the container, decide whether you need a different implementation or only different settings. For example, storing sessions in your database replaces a component; changing the number of example responses tunes the existing one. p.Server gives you a startup callback for each job.
The first snippet below uses the MyOptimizationPolicy subclass defined further down this page. Define that class before starting the server.
Replacing implementations with configure_container
configure_container changes which implementation serves a port. Parlant runs it during startup, before the application is assembled, and it returns the container after you have rebound what you need. Anything constructed later resolves your bindings:
import parlant.sdk as p
async def configure_container(container: p.Container) -> p.Container:
container[p.CompassOptimizationPolicy] = MyOptimizationPolicy()
return container
async with p.Server(configure_container=configure_container) as server:
...
Tuning the finished container with initialize_container
initialize_container acts on components that already exist. Parlant runs it after every binding is in place and every singleton can resolve. It does not change which implementations are bound. It registers listeners, warms caches, and tunes settings.
The instance you resolve here is the same singleton the engine will use, so a setting you change sticks.
The example below tunes the responder (the component that writes your agent's replies). It caps how many canned responses (the approved example messages the responder is shown while writing) it may see on one turn. It also sets the last-resort text the responder emits when moderation blocks the agent's final message through every retry:
from parlant.core.engines.compass.responder.responder import Responder
async def initialize_container(container: p.Container) -> None:
container[Responder].max_canned_responses = 8 # the same instance the engine uses
# The last-resort text shown if an agent message stays blocked by moderation
# through every retry (see Moderating messages).
container[Responder].moderation_fallback = (
"Sorry, I can't respond to that right now. How else can I help?"
)
async with p.Server(initialize_container=initialize_container) as server:
...
Moderating messages explains when the fallback text is used.
Note that rebinding a type in initialize_container is a mistake: components that already resolved their dependencies keep what they got. Change which implementation in configure_container, and change the settings of a constructed one in initialize_container.
Inside the async with block, server.container[SomeType] resolves any bound type. Use it to wire your own application to running services.
Binding mechanics
A binding can take three forms. The form decides two things: when the implementation is built, and whether every resolution shares one object:
# An instance: every resolution gets this exact object.
container[p.CompassOptimizationPolicy] = MyOptimizationPolicy()
# A class as a lazily constructed singleton; its __init__ dependencies resolve from the container.
container[p.RateLimiter] = p.Singleton(MyRateLimiter)
# A factory, run per resolution.
container[p.Logger] = lambda c: build_logger()
There are two rules of thumb here.
First, bind the port, never your subclass: container[p.CompassOptimizationPolicy] = ..., not container[MyOptimizationPolicy] = .... Resolution is by exact type.
Second, bind early. Singletons capture their dependencies when they are first constructed.
Note that binding an instance of the wrong type raises TypeError at startup, rather than failing at some distant resolution site. So you find out right away.
What deployments commonly rebind
The optimization policy
p.CompassOptimizationPolicy collects the engine's cost and latency settings: request-hedging timeouts per route, the consecutive-message processing delay, and the review retry budget. Subclass it, override what you need, and rebind it.
Most of its methods take the turn's effort, the level from MIN to MAX that decides how much model work a turn gets, so a setting can differ by effort. The example below raises the review retry budget (the number of times the reviewer may send a draft back for regeneration) on careful turns only:
class MyOptimizationPolicy(p.CompassOptimizationPolicy):
def get_max_review_restarts(self, effort: p.Effort) -> int:
if effort >= p.Effort.HIGH:
return 8 # more retries on HIGH and MAX turns
return super().get_max_review_restarts(effort) # the built-in default elsewhere
Stores
session_store=, customer_store=, and variable_store= on p.Server accept "transient", "local", a MongoDB connection string, or an instance of your own implementation. Passing an instance binds it underneath, so a custom store needs no container code.
Guests and customers shows a CustomerStore over an existing users table.
Logging
Everything in Parlant logs through the p.Logger port: the console, the JSON log file, and diagnostic archives. Rebind it to route logs into your own stack.
Note that implementations usually wrap the existing logger rather than replace it outright, so the built-in sinks keep working.
Secrets and rate limits
p.SecretLoader resolves the p.SecretRef locators in your model routes against your secret system. p.RateLimiter governs model request admission (whether a model request is allowed to go out). You bind both in configure_container.
Model routing shows each.
Authorization
p.Server(auth=...) accepts an AuthConfig or a full p.AuthorizationPolicy, again a shortcut over a binding.
Authorization covers custom policies.
What isn't container work
Three surfaces look like dependency injection, but each has its own, better-suited parameter.
- Model selection is
models={...}on the server or agent, covered in Model routing. - Prompt wording is
prompts={...}, covered in Customizing prompts. - Reacting to engine events is hooks. You register them through
container[p.CompassHooks], but you append to it rather than rebind it. Hooking into the engine covers this.
Stability
Types exported from parlant.sdk, everything reachable as p.X, are the stable container keys, and rebinding them is supported.
Anything imported from parlant.core.* or parlant.adapters.* is internal. It works as a key today, as the Responder example above does, but its location may move between releases. Where an exported port exists, prefer it.