Skip to main content

Architecture

End-to-end flow (the big picture)


Request enters ConversationController

HTTP request received

REST API receives {conversationId, text}, audits USER_INPUT, and forwards control to DefaultConversationalEngine.

EngineSession created + pipeline execution

Engine initializes execution context

EngineSession is a working POJO holding intent, state, context_json, payload, and history. The engine dynamically builds the pipeline from Spring-managed steps.

Conversation bootstrap + load

Conversation row ensured and restored

ce_conversation is created (if missing) and loaded. Existing state_code, intent_code, and context_json are restored into the session.

Intent resolution

Intent classified using DB + LLM

Intent resolution is driven by ce_intent, ce_intent_classifier, and ce_prompt_template. AGENT classifiers invoke the LLM under strict JSON contracts.

MCP tool execution

Planner selects and executes tools

MCP context is cleared first. Planner selects tools from ce_mcp_tool + ce_mcp_db_tool, executes SQL safely, and stores observations and finalAnswer under context_json.mcp.

Rules engine evaluation

DB-driven state machine applied

Rules from ce_rule run in priority order, mutating state, intent, or short-circuiting execution using resolver factories.

Response resolution

Final payload generated

ce_response selects EXACT vs DERIVED output. DERIVED responses may invoke LLM; EXACT responses are deterministic and bypass generation.

Conversation persisted + response returned

Conversation saved and returned to client

Updated intent, state, context_json, and payload are persisted. The full execution trace is visible via ce_audit.


1) Request entry point - controller

File: api/controller/ConversationController.java

What happens here

  • Accepts ConversationMessageRequest with conversationId + text
  • Writes a USER_INPUT audit so UI can replay the chain
  • Delegates to ConversationalEngine.process(...)
  • Returns OutputPayload (TextPayload or JsonPayload)
🧩 ConversationController.message(...)
@PostMapping("/message")
public OutputPayload message(@RequestBody ConversationMessageRequest request) {
UUID conversationId = request.getConversationId();

audit.audit(
"USER_INPUT",
conversationId,
"{\"text\":\"" + JsonUtil.escape(request.getText()) + "\"}"
);

EngineSession session = engine.process(conversationId, request.getText());

audit.audit(
"ENGINE_OUTPUT",
conversationId,
"{\"payload\":\"" + JsonUtil.escape(String.valueOf(session.getPayload())) + "\"}"
);

return session.getPayload();
}

2) ConversationalEngine - session + pipeline

File: engine/provider/DefaultConversationalEngine.java

This is the single orchestration point that:

  • creates a new EngineSession
  • creates the pipeline (dynamic steps)
  • executes it
  • returns the session to the controller
🧩 DefaultConversationalEngine.process(...)

EngineSession is intentionally a POJO working state.

It should not “reach into DB”.

DB reads/writes are done inside steps/services (like ConversationService, AuditHistoryProvider).


3) EnginePipeline - the core loop (StepResult)

File: engine/pipeline/EnginePipeline.java

Every step returns a StepResult:

  • Continue → next step
  • Stop → pipeline terminates early and returns final OutputPayload
🧩 EnginePipeline.execute(...)
public OutputPayload execute(EngineSession session) {

for (EngineStep step : steps) {

StepResult r = step.execute(session);

if (r instanceof StepResult.Stop stop) {
return stop.result();
}
}

return session.getPayload();
}
It's how the engine can hard-stop when required preconditions fail (missing payload, invalid state, no response configured).

It keeps “stop logic” centralized and predictable.

4) Pipeline composition - how steps are discovered and ordered

File: engine/factory/EnginePipelineFactory.java

Steps are:

  • discovered from Spring as beans of EngineStep
  • filtered (conversation persisted requirement, etc.)
  • ordered using annotations:
    • @MustRunAfter
    • @MustRunBefore
    • @RequiresConversationPersisted
🧩 EnginePipelineFactory ordering strategy (concept)
Tip: adding a new step
  1. Create a Spring component that implements EngineStep
  2. Annotate with MustRunAfter / MustRunBefore relative to existing steps
  3. If it requires a ce_conversation row, add @RequiresConversationPersisted
  4. Add audits inside the step so UI shows it

5) Database “brain” - all configuration tables (don't skip any)

Below is an overview of every core table that drives behavior.

🗄️ Configuration tables

TableWhat it controlsUsed by (code)Notes / key columns
ce_intentAllowed intents, priority ordering, description + llmHintAllowedIntentService, AgentIntentResolverintent_code, enabled, priority, llm_hint
ce_intent_classifierHow intents are classified (AGENT/REGEX/EXACT etc) and which resolver to useIntentResolutionStepclassifier_type, enabled, priority
ce_prompt_templatePrompt templates by purpose + optional intent scopingAgentIntentResolver, MCP planner, Response resolverspurpose, intent_code, system_prompt, user_prompt, enabled
ce_output_schemaStrict JSON schema control for LLM calls (by intent/state)AgentIntentResolver, response format resolversintent_code, state_code, json_schema, enabled
ce_responseWhich response to use for (state,intent) and how to generate itResponseResolutionStep + ResponseTypeResolverFactorystate_code, intent_code, response_type, output_format, priority
ce_ruleDB-driven state machine + short-circuit logicRulesStep + RuleTypeResolverFactory + RuleActionResolverFactoryintent_code, rule_type, match_pattern, action, action_value, priority
ce_mcp_toolTool registry (code/group/enabled/description)McpToolStep planner & tool loadertool_code, tool_group, enabled
ce_mcp_db_toolDB tool configuration (dialect, sql template, param schema, safe mode, max rows)McpDbToolExecutortool_id (FK), sql_template, param_schema (jsonb), safe_mode, max_rows

🗄️ Runtime tables

TableWhat it storesWritten/Read byKey columns
ce_conversationOne row per conversation with current state/intent + context_json + last response payloadPersistConversationBootstrapStep, LoadOrCreateConversationStep, PersistConversationStepconversation_id, intent_code, state_code, context_json, last_assistant_json
ce_auditAppend-only timeline of everything that happened during processingAuditService implementations; UI reads this for the chainconversation_id, stage, payload_json (json), created_at

6) Step-by-step pipeline walk-through (actual steps)

Below is the practical flow you'll see in your engine.
(Exact ordering can vary slightly based on annotations, but this is the intended lifecycle.)

Step 1 - PersistConversationBootstrapStep

File: engine/steps/PersistConversationBootstrapStep.java

Purpose:

  • Ensures the conversation exists in DB when needed (bootstrap)
  • Audits the bootstrap action so your UI can see it

Step 2 - LoadOrCreateConversationStep

File: engine/steps/LoadOrCreateConversationStep.java

Purpose:

  • Loads existing conversation row OR creates a new one
  • Syncs conversation's intent_code, state_code, context_json into EngineSession
This is the boundary between “stateless HTTP call” and “stateful conversation engine”.

If this step is wrong, you'll see bugs like “old move id answer repeats” because context wasn't updated/cleared correctly.

Step 3 - IntentResolutionStep

File: engine/steps/IntentResolutionStep.java

Purpose:

  • Picks which classifier to run (from ce_intent_classifier)
  • Classifiers can be AGENT / REGEX / EXACT
  • Sets session.intent and audits
🧩 IntentResolutionStep (key idea)

Step 4 - (Validation / schema / extraction steps)

You have these steps in your engine to enrich session before MCP/Response:

  • ValidationStep
  • SchemaExtractionStep
  • AutoAdvanceStep (Exact behavior depends on your table config and your program features.)

Step 5 - McpToolStep (MCP loop)

File: engine/steps/McpToolStep.java

Purpose:

  • Clears old context_json.mcp (audit: MCP_CONTEXT_CLEARED)
  • Planner calls LLM to decide tool usage
  • Executes tools from DB tables:
    • ce_mcp_tool
    • ce_mcp_db_tool
  • Writes:
    • mcp.observations[]
    • mcp.finalAnswer
📐 context_json.mcp shape
{
"mcp": {
"observations": [
{
"tool": "postgres.schema",
"result": { "tables": ["..."] }
},
{
"tool": "postgres.move_status",
"result": { "rows": [ { "connection_id": "...", "status": "MOVED" } ] }
}
],
"finalAnswer": {
"answer": "The status of your move for connection ... is MOVED."
}
}
}
Tools come from ce_mcp_tool and ce_mcp_db_tool, not from if/else in Java.

SQL templates and param_schema are stored in DB and enforced via safe_mode + max_rows.

The planner's choice is audited so you can see exactly why it picked a tool.

Step 6 - RulesStep (DB-driven state machine + shortcuts)

File: engine/steps/RulesStep.java

Purpose:

  • Iterates enabled rules ordered by priority
  • Uses factory pattern:
    • RuleTypeResolverFactory → resolves whether rule matches (JSON_PATH / REGEX / …)
    • RuleActionResolverFactory → applies mutation (SET_STATE / SET_INTENT / SHORT_CIRCUIT)
  • Audits RULE_APPLIED / RULE_NOT_APPLIED etc

This is where you build your state machine
Example: if MCP produced mcp.finalAnswer, you can SHORT_CIRCUIT deterministically (no LLM formatting required).


Step 7 - ResponseResolutionStep (ce_response driven)

File: engine/steps/ResponseResolutionStep.java

Purpose:

  • Finds the matching ce_response row
  • Delegates to ResponseTypeResolverFactory:
    • EXACT → return configured response content directly
    • DERIVED → uses templates + LLM (through OutputFormatResolverFactory)
  • Writes audits including:
    • RESOLVE_RESPONSE
    • RESOLVE_RESPONSE_LLM_INPUT
    • RESOLVE_RESPONSE_LLM_OUTPUT
Different programs can change responses without a build.

State+Intent drives the output: same intent can respond differently based on state_code.

DERIVED responses still remain controlled via templates + output schema.

Step 8 - PersistConversationStep + PipelineEndGuardStep

Purpose:

  • PersistConversationStep syncs final intent/state/context/last_assistant_json to ce_conversation
  • PipelineEndGuardStep ensures payload exists; otherwise returns STOP with error payload

7) Putting it together - example conversation + “behind the scenes”

FAQ Example - How ConvEngine Answers FAQs (No MCP)

This document explains exactly how ConvEngine processes a FAQ-style question. Unlike MOVE or other operational intents, FAQ does NOT use MCP tools.

If MCP runs for FAQ, that is a bug - not a feature.


🗣️ Example FAQ Conversation

Can I move my connections within zapper?

👤

🤖

Yes, internal account moves are supported.


🔄 High-level Flow (FAQ)

Controller → Engine → Pipeline → Intent → Response → Output

Key difference from MOVE:

  • ❌ No MCP planner
  • ❌ No MCP tools
  • ❌ No DB queries via MCP
  • ✅ Direct FAQ resolution

1️⃣ Controller receives the request

🧩 ConversationController.message(...)
So UI can replay the exact question later

So intent mistakes can be traced precisely

2️⃣ Engine creates session and pipeline

🧩 DefaultConversationalEngine.process(...)

EngineSession is a POJO.
It does NOT decide behavior - the pipeline + DB do.


3️⃣ Pipeline executes steps in order

🧩 EnginePipeline.execute(...)
🛑 INFO
  • Allows SHORT_CIRCUIT
  • Allows rules to stop flow early
  • No hardcoded branching

4️⃣ IntentResolutionStep → FAQ

Tables involved:

🗄️ ce_intent

Allowed intents

  • intent_code = FAQ
  • priority
  • enabled

🗄️ ce_prompt_template

Intent agent prompt

  • purpose = INTENT_AGENT
  • intent_code = null

The AgentIntentResolver:

  • Sees allowed intents
  • Uses LLM only to classify
  • Returns FAQ

FAQ intent is resolved BEFORE any MCP logic exists.


5️⃣ MCP is SKIPPED (IMPORTANT)

There is no MCP planning because:

  • FAQ intent has no MCP tools
  • No ce_mcp_tool applies
  • No ce_mcp_db_tool is loaded

If MCP runs for FAQ, your configuration is wrong.

You will not see:

  • MCP_PLAN_LLM_INPUT
  • MCP_TOOL_CALL
  • MCP_FINAL_ANSWER

6️⃣ RulesStep (usually no-op for FAQ)

Typical FAQ rules:

  • none
  • or cosmetic state changes

🗄️ ce_rule

Optional FAQ rules

  • intent_code = FAQ
  • rule_type = REGEX / JSON_PATH

FAQ usually has zero rules.


7️⃣ ResponseResolutionStep

🗄️ ce_response

FAQ responses

  • intent_code = FAQ
  • state_code = IDLE
  • response_type = EXACT or DERIVED

Two patterns:

Pattern A - EXACT (no LLM)

  • exact_text is returned
  • fastest path

Pattern B - DERIVED (LLM formatting)

  • derivation_hint used
  • still no MCP

FAQ responses NEVER depend on MCP context.


🔍 What you'll see in ce_audit for FAQ

Typical stages:

  • USER_INPUT
  • INTENT_AGENT_LLM_INPUT
  • INTENT_AGENT_LLM_OUTPUT
  • RESOLVE_RESPONSE
  • RESOLVE_RESPONSE_LLM_OUTPUT (only if DERIVED)

No MCP stages. Ever.


✅ Final Takeaways (FAQ)

🛑 SUCCESS
  • Intent = FAQ
  • No MCP planning
  • No DB tools
  • Response comes from ce_response
  • Fully deterministic unless DERIVED

If FAQ ever:

  • reuses previous answers
  • runs MCP
  • hits DB tools

👉 the bug is in configuration, not engine code.


MOVE_CONNECTIONS - Architecture Deep Dive

This document explains exactly how ConvEngine processes a MOVE_CONNECTIONS request - from REST controller to MCP tools to final response.

If you read this once, you should be able to:

  • Debug any MOVE flow issue
  • Add new MCP tools safely
  • Understand where stale answers come from
  • Explain the engine to another engineer confidently

🗣️ Example Conversation

What is the status of my move for connection USPSC003BA100SA277CON1388

👤

🤖

The status of your move for connection USPSC003BA100SA277CON1388 is MOVED.


What is the status of my move for connection USPSC003BA100SA277CON1128

👤

🤖

The status of your move for connection USPSC003BA100SA277CON1128 is IN_PROGRESS.


🔁 High-level Flow (One Turn)

Controller → Engine → Pipeline → Intent → MCP → Rules → Response

Every User → Assistant turn follows this exact lifecycle.


1️⃣ Request enters via REST controller

File: ConversationController.java

🧩 ConversationController.message(...)
🛑 INFO
  • UI replay: ce_audit becomes a full timeline
  • Debug truth: broken JSON or stale state is visible immediately
  • No hidden behavior: every request is auditable

2️⃣ EngineSession + Pipeline orchestration

File: DefaultConversationalEngine.java

🧩 DefaultConversationalEngine.process(...)

EngineSession is a POJO.
All intelligence lives in pipeline steps + DB configuration.


3️⃣ Pipeline execution model

File: EnginePipeline.java

🧩 EnginePipeline.execute(...)
🛑 INFO
  • Allows SHORT_CIRCUIT responses
  • Allows rules to stop execution
  • Keeps pipeline generic & extensible

4️⃣ Core pipeline steps (order matters)

Selects resolver (AGENT / REGEX / EXACT) from DB.


5️⃣ Intent resolution for MOVE_CONNECTIONS

Tables involved

🗄️ ce_intent

Allowed intents

  • intent_code
  • priority
  • llm_hint
  • enabled

🗄️ ce_prompt_template

Intent prompts

  • purpose = INTENT_AGENT
  • system_prompt
  • user_prompt

🗄️ ce_output_schema

Strict JSON control

  • intent_code
  • state_code
  • json_schema

The LLM returns structured JSON like:

📐 Intent agent output
{
"intent": "MOVE_CONNECTIONS",
"confidence": 0.92,
"needsClarification": false,
"clarificationResolved": false,
"clarificationQuestion": ""
}

6️⃣ MCP Tool Step (MOVE_CONNECTIONS only)

The FIRST thing MCP does is clear old context.
If this does not happen, stale answers WILL appear.

MCP execution loop

  1. MCP_CONTEXT_CLEARED
  2. MCP_PLAN_LLM_INPUT
  3. MCP_PLAN_LLM_OUTPUT
  4. MCP_TOOL_CALL
  5. MCP_TOOL_RESULT
  6. Repeat until MCP_FINAL_ANSWER

7️⃣ MCP database tools

🗄️ ce_mcp_tool

Tool registry

  • tool_code
  • tool_group
  • enabled

🗄️ ce_mcp_db_tool

SQL-backed tools

  • tool_id (FK)
  • sql_template
  • param_schema
  • safe_mode
  • max_rows

Example SQL template:

select status
from move_request
where connection_id = :connection_id


8️⃣ MCP writes final answer to context

📐 context_json.mcp
{
"mcp": {
"observations": [
{
"tool": "postgres.move_status",
"rowsJson": "[{ status: 'MOVED' }]"
}
],
"finalAnswer": {
"answer": "The status of your move is MOVED."
}
}
}

9️⃣ RulesStep - optional short-circuit

Rules can:

  • SET_STATE
  • SET_INTENT
  • SHORT_CIRCUIT

Typical MOVE rule:

  • rule_type = JSON_PATH
  • match_pattern = $.mcp.finalAnswer
  • action = SHORT_CIRCUIT
Customer-specific behavior without code changes

Configurable state machine

Deterministic overrides

🔟 ResponseResolutionStep

Uses ce_response:

🗄️ ce_response

Final output configuration

  • state_code
  • intent_code
  • response_type (EXACT / DERIVED)
  • output_format (TEXT / JSON)

Two valid patterns

🛑 SUCCESS
  • Rules SHORT_CIRCUIT
  • No LLM call
  • Fast & safe
🛑 INFO
  • derivation_hint checks mcp.finalAnswer
  • LLM formats response

🐞 Debugging: stale move ID bug

If you see:

Second request returns previous move result

The cause is almost always one of:

🛑 DANGER
  • MCP context not cleared early enough
  • derivation_hint blindly reusing mcp.finalAnswer
  • Tool param extraction failed and reused old params

✅ Final takeaway

ConvEngine MOVE flow is:

  • Deterministic
  • Auditable
  • DB-driven
  • MCP-powered
  • Safe when configured correctly

If you understand this file, you understand the MOVE engine end-to-end.