A product adapter is the only thing that sits between the behavioral AI platform's engine outputs and a real product. This article explains why that boundary matters, how to design an adapter correctly, and what happens when domain logic leaks across it.
The Problem: Domain Logic Creep
Without an explicit adapter layer, product-specific logic accumulates in the engines themselves: "add 0.1 to the score if the domain is legal", "skip the emotional engine for B2B contexts." These changes make engines domain-specific, break their reuse across products, and destroy the clean separation that makes the governance wrapper effective.
An adapter isolates all of this. The engines stay generic. The adapter translates engine outputs into domain scores.
The Adapter Pattern
// adapters/the chatbot platform.adapter.js
export class the chatbot platformAdapter {
// The engines this adapter consumes
static requiredEngines = [
'bayesian-confidence',
'behavioral-entropy',
'motivation-hierarchy',
'negotiation-batna',
'emotional-regulation',
'state-machine-runtime',
];
async computeLeadScore(actorId, context) {
// 1. Run only the required engines through the control plane
const engineOutputs = await controlPlane.runForEngines(
actorId, context, the chatbot platformAdapter.requiredEngines
);
// 2. Translate engine outputs to domain scores
return {
intentScore: this._computeIntent(engineOutputs),
engagementScore: this._computeEngagement(engineOutputs),
objectionType: this._classifyObjection(engineOutputs),
followUpTone: this._recommendTone(engineOutputs),
confidence: this._aggregateConfidence(engineOutputs),
};
}
_computeIntent(outputs) {
return (
outputs['motivation-hierarchy'].score * 0.35 +
outputs['negotiation-batna'].score * 0.30 +
outputs['state-machine-runtime'].score * 0.35
);
}
}
API Route Design for Adapters
// routes/the chatbot platform.routes.js
router.post('/api/v1/lead/:actorId/score', authenticate, async (req, res) => {
const adapter = new the chatbot platformAdapter();
const score = await adapter.computeLeadScore(
req.params.actorId,
{ teamId: req.auth.teamId, ...req.body }
);
// Governance wrapper has already run inside controlPlane.runForEngines()
res.json({ actorId: req.params.actorId, ...score, scoredAt: new Date() });
});
What to Watch For
- Engine creep — if an adapter requests more than 8 engines, it is probably doing things that should be split across multiple domain scores.
- Governance bypass — adapters must never access raw engine outputs before the governance wrapper has run. Always use
controlPlane.runForEngines(), neverengine.score()directly. - Weight drift — adapter weights (the
0.35,0.30etc.) are policy decisions. Store them in configuration, not in code, and treat changes as deployment events.