Tier 2: XState statechart como motor de turno (opt-in)
Reemplaza el dispatcher en turnEngineV3.js por un statechart formal en
XState v5. La machine es pura: produce un effect log (pending_actions) +
un descriptor de reply (pending_reply) que el runner traduce afuera.
API externa intacta: runTurnV3 sigue retornando { plan, decision } con
shape compatible con pipeline.js. Snapshot persiste en
context.xstate_snapshot dentro del JSONB existente.
- machine/index.js: statechart top-level (idle/cart/shipping/payment/
waiting/awaiting_human) + cart sub-statechart con todo el flujo
multi-turno (searching/resolving/askingClarification/askingQuantity/
computingFromPersonas/added/showing/pricing/researching).
- guards.js: portados de fsm.js (hasCart, wantsToAddProduct, etc).
- actions.js: assigns para mutations + reply descriptors (pending_reply
con templateKey/vars/rawText). Las async no entran en la machine.
- actors.js: fromPromise wrappers de retrieveCandidates y getProductQtyRules.
- runner.js: boot con prev_context.xstate_snapshot o migrateOldContext.
NLU → nluToEvent → send → settle (espera invokes) → realizeReply
(renderReply real con rewriter) → getPersistedSnapshot → format.
- nluToEvent.js: adapter NLU intent → evento XState (1:1).
Feature flags: USE_XSTATE=1 reemplaza el path; XSTATE_SHADOW=1 corre
ambos en paralelo, devuelve legacy y loguea diffs estructurales para
validar antes de flippar prod.
16 unit tests para la machine cubren: arranque, regla universal cart-on-add,
flow de cart con strong/multi match, checkout completo (shipping/pickup/
payment/cash) y rehidratación de snapshot. 224 tests totales pasando.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,9 +20,41 @@ import {
|
||||
} from "./stateHandlers.js";
|
||||
import { getStoreConfig } from "../0-ui/db/settingsRepo.js";
|
||||
import { pushRecent } from "./replyTemplates.js";
|
||||
import { runTurnXState } from "./machine/runner.js";
|
||||
|
||||
// Feature flag para NLU modular
|
||||
const USE_MODULAR_NLU = process.env.USE_MODULAR_NLU === "true";
|
||||
// Feature flags para XState
|
||||
function useXState() {
|
||||
const v = String(process.env.USE_XSTATE || "").toLowerCase();
|
||||
return v === "1" || v === "true" || v === "yes";
|
||||
}
|
||||
function shadowXState() {
|
||||
const v = String(process.env.XSTATE_SHADOW || "").toLowerCase();
|
||||
return v === "1" || v === "true" || v === "yes";
|
||||
}
|
||||
|
||||
/**
|
||||
* Compara plan/decision entre legacy y XState para shadow mode.
|
||||
* No hace assertions; solo loguea diferencias estructurales.
|
||||
*/
|
||||
function diffResults(legacy, xstate) {
|
||||
const diffs = [];
|
||||
if (legacy?.plan?.next_state !== xstate?.plan?.next_state) {
|
||||
diffs.push({ key: "next_state", legacy: legacy?.plan?.next_state, xstate: xstate?.plan?.next_state });
|
||||
}
|
||||
const lActions = (legacy?.decision?.actions || []).map((a) => a.type).sort().join(",");
|
||||
const xActions = (xstate?.decision?.actions || []).map((a) => a.type).sort().join(",");
|
||||
if (lActions !== xActions) {
|
||||
diffs.push({ key: "action_types", legacy: lActions, xstate: xActions });
|
||||
}
|
||||
const lCart = (legacy?.decision?.context_patch?.order?.cart || []).map((c) => `${c.woo_id}:${c.qty}${c.unit}`).sort().join(",");
|
||||
const xCart = (xstate?.decision?.context_patch?.order?.cart || []).map((c) => `${c.woo_id}:${c.qty}${c.unit}`).sort().join(",");
|
||||
if (lCart !== xCart) {
|
||||
diffs.push({ key: "cart", legacy: lCart, xstate: xCart });
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un resumen corto del historial para el NLU
|
||||
@@ -50,6 +82,11 @@ export async function runTurnV3({
|
||||
prev_context,
|
||||
conversation_history,
|
||||
}) {
|
||||
// Branch: XState completo (USE_XSTATE=1)
|
||||
if (useXState() && !shadowXState()) {
|
||||
return runTurnXState({ tenantId, chat_id, text, prev_state, prev_context, conversation_history });
|
||||
}
|
||||
|
||||
const audit = {
|
||||
trace: {
|
||||
tenantId,
|
||||
@@ -176,7 +213,21 @@ export async function runTurnV3({
|
||||
result = await handleIdleState(handlerParams);
|
||||
}
|
||||
|
||||
return formatResult(result, prev_context, recentReplies, failedSearches);
|
||||
const legacyResult = formatResult(result, prev_context, recentReplies, failedSearches);
|
||||
|
||||
// Shadow mode: corre XState en paralelo, devuelve legacy, loguea diffs.
|
||||
if (shadowXState()) {
|
||||
runTurnXState({ tenantId, chat_id, text, prev_state, prev_context, conversation_history })
|
||||
.then((xstateResult) => {
|
||||
const diffs = diffResults(legacyResult, xstateResult);
|
||||
if (diffs.length) {
|
||||
console.log("[xstate-shadow] diffs", { chat_id, diffs });
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("[xstate-shadow] error", err?.message || err));
|
||||
}
|
||||
|
||||
return legacyResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user