Skip to main content

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.

What you'll learn
  • 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
Talk this page through with your coding agent

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.

info

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.

info

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:
...
Learn more about the fallback

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.

tip

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.

tip

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.

tip

Model routing shows each.

Authorization​

p.Server(auth=...) accepts an AuthConfig or a full p.AuthorizationPolicy, again a shortcut over a binding.

tip

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.