Engine Framework
Status: ✅ Production (February 23, 2026)
Engine Framework
Table of Contents
- Overview
- Architecture
- Core Concepts
- Framework Components
- Engine Types
- Configuration System
- Handoff Protocol
- Phase State Machine
- Extraction Pipeline
- Completeness Engine
- Background Task Manager
- Context Assembly
- Sidebar Builder
- Technique Library
- Strategy System
- Database Schema
- Testing
- Usage Examples
Overview
Status: ✅ Production (February 23, 2026)
The Engine Framework is Praetor's generic, configuration-driven conversational engine system that powers all discovery and exploration conversations through a single unified architecture. It replaces engine-specific implementations with a framework that can run ANY conversational engine through configuration alone.
Key Innovation
One runner, infinite engines. The same EngineRunner handles:
- Extract-dominant engines (Project Discovery: 70% user input, 30% system output)
- Inform-dominant engines (Exploration: 30% user input, 70% system output)
- Business Profile engines (Extract-dominant with dynamic forms)
- Ideation engines (Balanced exploration and validation)
All through configuration — zero engine-specific logic in the framework.
Key Features
✅ Configuration-driven — Engines defined entirely through EngineConfiguration<TExtraction>
✅ Generic 19-step loop — Same processMessage() handles all engine types
✅ Extraction pipeline — Per-turn structured data extraction with provenance tracking
✅ Phase state machine — Deterministic phase transitions with checkpoints
✅ Completeness engine — Pluggable models (obligation, decision-readiness, maturity, coverage)
✅ Handoff protocol — Inter-engine handoffs with I-PASS verification
✅ Mode blend system — 7 conversation modes (Extract, Explore, Inform, Validate, Refine, Persuade, Deliberate)
✅ Technique library — 16+ conversation techniques from HUMINT, Motivational Interviewing, Cognitive Interview
✅ Background task orchestration — Async research with failure strategies
✅ Context assembly — Token budget management with handoff positioning (Lost in the Middle)
✅ Sidebar builder — Dynamic UI state generation
✅ Strategy system — Pluggable strategies for mode classification, technique scheduling, task failure, compression
Comparison with Old Discovery System
| Aspect | Old Discovery | Engine Framework |
|---|---|---|
| Architecture | Context-specific implementations | Generic configuration-driven runner |
| Code duplication | 3 separate pipelines (business_profile, idea_discovery, project_discovery) | 1 unified EngineRunner |
| Adding new engine | ~2,000 lines of engine-specific code | ~400 lines of configuration |
| Type safety | Loosely typed state | Strongly typed TExtraction with ExtractedField<T> wrappers |
| Extraction tracking | Implicit | Explicit provenance with confidence scores |
| Inter-engine transitions | Manual context passing | Structured EngineHandoff with I-PASS verification |
| Mode enforcement | None | 7-mode blend system with technique activation |
| Completeness models | Fixed obligation-based | 7 pluggable models |
| Testing | Integration tests only | Unit tests for all framework components |
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ ENGINE FRAMEWORK ARCHITECTURE │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ ENGINE REGISTRY │ │
│ │ │ │
│ │ engineId → EngineConfiguration<TExtraction> │ │
│ │ │ │
│ │ - project-discovery → PROJECT_DISCOVERY_CONFIG │ │
│ │ - exploration → EXPLORATION_CONFIG │ │
│ │ - business-profile → BUSINESS_PROFILE_CONFIG (stub) │ │
│ │ - ideation → IDEATION_CONFIG (stub) │ │
│ └──────────────────────────┬───────────────────────────────────────────┘ │
│ │ loads config │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ ENGINE RUNNER │ │
│ │ (19-Step Generic Loop) │ │
│ │ │ │
│ │ Input: EngineConfiguration<TExtraction> + userMessage │ │
│ │ Output: EngineResponse + updatedState │ │
│ │ │ │
│ │ Steps: │ │
│ │ 1. Pre-processing 11. Compression check │ │
│ │ 2. Extraction 12. Response generation │ │
│ │ 3. Saturation 13. QA check │ │
│ │ 4. Socio-emotional 14. Mode classification │ │
│ │ 5. Completeness 15. Telemetry │ │
│ │ 6. Phase transition eval 16. Sidebar │ │
│ │ 7. Phase transition exec 17. Exit transitions │ │
│ │ 8. Background tasks 18. Persist state │ │
│ │ 9. Technique selection 19. Return │ │
│ │ 10. Context assembly │ │
│ └──────────┬─────────────────────────────────────────────────────────┬─┘ │
│ │ delegates to delegates to │ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ ┌──────────────────────┐ │
│ │ FRAMEWORK COMPONENTS │ │ STRATEGY SYSTEM │ │
│ │ │ │ │ │
│ │ - PhaseStateMachine │ │ - ModeClassifier │ │
│ │ - ExtractionPipeline │ │ - TechniqueScheduler │ │
│ │ - CompletenessEngine │ │ - TaskFailureStrategy│ │
│ │ - ContextAssembler │ │ - JourneyCompressor │ │
│ │ - BackgroundTaskManager │ │ │ │
│ │ - SidebarBuilder │ │ │ │
│ │ - TechniqueLibrary │ │ │ │
│ └─────────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ ENGINE WORKING MEMORY │ │
│ │ EngineWorkingMemory<TExtraction> │ │
│ │ │ │
│ │ - extraction: TExtraction (structured, typed) │ │
│ │ - phase: { current, turnsInPhase, history } │ │
│ │ - obligations: { items[], summary } │ │
│ │ - completeness: { overall, byCategory } │ │
│ │ - techniques: { active[], cooldowns } │ │
│ │ - backgroundTasks: { pending[], completed[] } │ │
│ │ - context: { tokens, compressionEvents } │ │
│ │ - inboundHandoff: EngineHandoff | null │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ DATABASE PERSISTENCE │ │
│ │ │ │
│ │ elicitation_sessions.state_json: │ │
│ │ - engine_id, engine_version │ │
│ │ - extraction_state (TExtraction) │ │
│ │ - inbound_handoff_id, outbound_handoff_id │ │
│ │ │ │
│ │ eng_handoffs: │ │
│ │ - source_engine_id, target_engine_id │ │
│ │ - handoff_payload (JSONB) │ │
│ │ - verified, verification_result │ │
│ │ │ │
│ │ eng_extractions_log: │ │
│ │ - field_path, extracted_value │ │
│ │ - confidence, extraction_type (new/update/supersede) │ │
│ │ │ │
│ │ eng_state_checkpoints: │ │
│ │ - at_turn, state_snapshot (JSONB) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘Core Concepts
1. Engine Configuration
An engine is defined by an EngineConfiguration<TExtraction> object with 13 configurable surfaces:
interface EngineConfiguration<TExtraction extends Record<string, unknown>> {
// Identity
engineId: string; // 'project-discovery', 'exploration'
name: string; // Display name
description: string; // Purpose
version: string; // Semver
engineType: 'extract-dominant' | 'inform-dominant';
// Phase Management
phases: PhaseDefinition[]; // Ordered phases
initialPhase: string; // Starting phase ID
// Obligation System
obligations: ObligationDefinition[];
// Completeness Model
completenessModel: CompletenessModelConfig;
// Background Tasks
backgroundTasks: BackgroundTaskDefinition[];
// Context Configuration
contextConfig: ContextAssemblyConfig;
// Information Ratio (user:system balance)
informationRatio: { user: number; system: number }; // Must sum to 100
// Sidebar Configuration
sidebarConfig: SidebarConfig;
// Technique Activation
availableTechniques: string[]; // IDs from TechniqueLibrary
// Exit Transitions
exitTransitions: ExitTransitionDefinition[];
// System Prompt Template
systemPromptTemplate: string;
// Extraction Schema
extractionSchema: ExtractionSchemaConfig<TExtraction>;
}2. Extraction with Provenance
All extracted data uses ExtractedField<T> wrappers that track:
interface ExtractedField<T> {
value: T;
confidence: number; // 0.0 - 1.0
extractedFromTurns: number[]; // Which turns contributed
lastUpdatedAtTurn: number; // Most recent update
supersededPrevious?: { // What was replaced
value: T;
atTurn: number;
};
}Example extraction state:
interface ProjectDiscoveryExtraction {
purposeAndProblem: {
coreProblem: ExtractedField<string>;
triggerEvent: ExtractedField<string>;
currentSolution: ExtractedField<string | null>;
};
usersAndPersonas: {
primaryUser: ExtractedField<string>;
jobsToBeDone: ExtractedField<string[]>;
};
// ... more fields
}3. Mode Blend System
Every phase defines a mode blend — the percentage distribution of 7 conversation modes:
type ModeBlend = {
extract: number; // Elicit information from user
explore: number; // Jointly discover possibilities
inform: number; // Teach user new information
validate: number; // Confirm understanding
refine: number; // Polish existing extraction
persuade: number; // Guide toward decisions
deliberate: number; // Weigh tradeoffs
};Extract-dominant (Project Discovery):
- Extract: 70%, Validate: 20%, Explore: 10%
Inform-dominant (Exploration):
- Inform: 50%, Deliberate: 30%, Validate: 15%, Explore: 5%
4. Phase State Machine
Phases define:
- Entry conditions — Must be met to enter phase
- Transition triggers — Conditions that trigger automatic phase change
- Mandatory checkpoints — Must be satisfied before allowing transition
- Mode blend — Conversation style for this phase
- Technique activation — Which techniques are available
- Agent instructions — Phase-specific prompt additions
Example phase definition:
{
id: 'VALIDATION',
displayName: 'Validation & Confirmation',
order: 4,
entryConditions: {
type: 'completeness_score',
minCompleteness: 0.80,
},
transitionTriggers: [
{
type: 'obligation_satisfaction',
category: 'validation',
minSatisfactionRate: 0.90,
},
],
mandatoryCheckpoints: [
{ obligationId: 'confirm_project_scope', required: true },
],
modeBlend: {
extract: 5,
validate: 70,
refine: 20,
inform: 5,
},
activeTechniques: ['summary_reflection', 'confidence_scaling', 'teach_back'],
agentInstructions: 'Confirm all extracted information with the user...',
}5. Handoff Protocol
When one engine completes and hands off to another (e.g., Exploration → Project Discovery), it produces an EngineHandoff:
interface EngineHandoff {
metadata: {
sourceEngineId: string;
targetEngineId: string;
handoffDate: string;
sessionId: string;
projectId: string;
turnCount: number;
completenessScore: number;
transitionReason: string;
};
phaseResults: {
phaseName: string;
completionPercentage: number;
keyOutputs: string;
remainingGaps: string[];
}[];
actionList: {
decisionsReached: { decision: string; confidence: number; reasoning: string }[];
optionsEliminated: { option: string; reasons: string[] }[];
constraintsIdentified: { constraint: string; implication: string }[];
assumptionsCarriedForward: { assumption: string; warrantReview: boolean }[];
};
situationAwareness: {
userConfidenceLevel: 'low' | 'medium' | 'high';
knowledgeGapsRevealed: string[];
userGoals: string[];
keyPainPoints: string[];
};
accumulatedContext: {
conceptsTaught: { concept: string; understandingLevel: 'basic' | 'solid' | 'expert' }[];
priorExperience: string[];
technicalLevel: 'beginner' | 'intermediate' | 'advanced' | 'expert';
};
relationalState: {
trustLevel: 'low' | 'medium' | 'high';
rapport: string;
openQuestions: string[];
sensitivities: string[];
};
}I-PASS Verification ensures the receiving engine acknowledges the handoff:
- Identity: Correct source/target engines
- Patient/Project: Correct project
- Assessment: Key decisions and context acknowledged
- Situation: Gaps, constraints, and goals understood
- Safety: No dropped information, sensitivities noted
Framework Components
1. EngineRunner
Location: src/engine/framework/engine-runner.ts
The heart of the framework — a generic 19-step processMessage() loop that works for ALL engines.
Signature:
class EngineRunner {
async processMessage(input: ProcessMessageInput): Promise<EngineResponse> {
// 1. Pre-processing: Load state, validate input
// 2. Extraction: Run per-turn extraction LLM
// 3. Saturation: Detect information saturation
// 4. Socio-emotional: Track rapport, trust, friction
// 5. Completeness: Evaluate overall completeness
// 6. Phase transition eval: Check triggers
// 7. Phase transition exec: Execute if triggered
// 8. Background tasks: Evaluate and dispatch
// 9. Technique selection: Choose active techniques
// 10. Context assembly: Build context window
// 11. Compression check: Detect context overflow
// 12. Response generation: Run main agent
// 13. QA check: Verify response quality
// 14. Mode classification: Classify response mode
// 15. Telemetry: Log metrics
// 16. Sidebar: Build UI state
// 17. Exit transitions: Check for handoffs
// 18. Persist state: Save to DB
// 19. Return: Return EngineResponse
}
}Critical Property: Contains zero engine-specific logic. All behavior comes from configuration.
2. PhaseStateMachine
Location: src/engine/framework/phase-state-machine.ts
Manages phase transitions deterministically.
API:
class PhaseStateMachine {
constructor(
phases: PhaseDefinition[],
initialPhase: string,
);
evaluateTransition(
memory: EngineWorkingMemory<any>,
): { shouldTransition: boolean; targetPhase: string | null };
executeTransition(
currentPhase: string,
targetPhase: string,
memory: EngineWorkingMemory<any>,
): void;
canTransition(
fromPhase: string,
toPhase: string,
memory: EngineWorkingMemory<any>,
): boolean;
}Transition trigger types:
obligation_satisfaction— X% of obligations in category satisfiedturn_count— At least N turns in phasecompleteness_score— Overall completeness ≥ thresholdcustom_evaluator— Custom function returns true
Checkpoint enforcement:
Before allowing a transition, all mandatoryCheckpoints in the current phase must be satisfied.
3. ExtractionPipeline
Location: src/engine/framework/extraction-pipeline.ts
Runs per-turn extraction on every user message.
API:
class ExtractionPipeline<TExtraction> {
async extract(
userMessage: string,
conversationContext: string,
currentExtraction: TExtraction,
turnNumber: number,
metadata: { sessionId, projectId, messageId, engineId },
): Promise<ExtractionResult<TExtraction>> {
// Returns:
// - updatedExtraction: TExtraction (with new ExtractedField wrappers)
// - fieldsUpdated: string[] (field paths)
// - newConceptCount: number
// - sensitiveInfoShared: boolean
// - supersededFields: { fieldPath, oldValue, newValue, reason }[]
}
}Extraction process:
- Call extraction LLM with field definitions and current state
- For each extracted value:
- Check confidence threshold (from field definition)
- Build
ExtractedField<T>wrapper with provenance - Detect supersession (value changed from previous turn)
- Update extraction state
- Log to
eng_extractions_logaudit trail
Sensitive info detection: Regex patterns detect emails, phone numbers, SSNs, credit cards, passwords, API keys.
4. CompletenessEngine
Location: src/engine/framework/completeness-engine.ts
Generic completeness evaluation that delegates to model-specific evaluators.
Supported models:
obligation— % of obligations satisfied (used by Project Discovery)decision-readiness— Decision made with sufficient understanding (used by Exploration)maturity— Solution maturity levelestimability— Sufficient detail for cost/time estimationcoverage— % of domain coveredprofile— Profile completenesssaturation— Information saturation point
API:
class CompletenessEngine {
async evaluate<TExtraction>(
model: CompletenessModelConfig,
memory: EngineWorkingMemory<TExtraction>,
): Promise<{
overall: number; // 0.0 - 1.0
byCategory: Record<string, number>;
isReady: boolean; // overall >= readyThreshold
gaps: string[];
}>;
}Example model config (decision-readiness):
{
type: 'decision-readiness',
readyThreshold: 0.60, // 60% = ready
heuristics: [
{
id: 'knowledge_gaps_identified',
weight: 0.20,
evaluator: 'knowledgeGapsIdentified', // Custom function
},
{
id: 'solution_landscape_coverage',
weight: 0.25,
evaluator: 'solutionLandscapeCoverage',
},
{
id: 'decision_made',
weight: 0.30,
evaluator: 'decisionMade',
},
// ...
],
}5. ContextAssembler
Location: src/engine/framework/context-assembler.ts
Assembles context for each turn with token budget management.
Key features:
- Handoff positioning — Inbound handoff context goes at TOP of context window (Lost in the Middle research)
- Budget allocation — Percentages from config: obligations (40%), conversation history (30%), background intelligence (20%), extraction summary (10%)
- Compression strategies —
summarize_old(default),sliding_window,extractive - PCS integration — Delegates to Project Context Service for artifact resolution
API:
class ContextAssembler {
async assemble<TExtraction>(
config: ContextAssemblyConfig,
memory: EngineWorkingMemory<TExtraction>,
): Promise<{
assembledContext: string;
tokensUsed: number;
compressionApplied: boolean;
}>;
}6. BackgroundTaskManager
Location: src/engine/framework/background-task-manager.ts
Orchestrates async background tasks (research, analysis, generation).
Task definition:
interface BackgroundTaskDefinition {
taskId: string;
displayName: string;
description: string;
triggerCondition: {
type: 'low_confidence' | 'obligation_status' | 'custom';
threshold?: number;
obligationIds?: string[];
};
handler: string; // Inngest function name
estimatedDuration: number; // seconds
failureStrategy: {
type: 'retry' | 'degrade' | 'stall' | 'proceed-with-gap';
maxRetries?: number;
retryDelay?: number;
degradedBehavior?: string;
};
blockPhaseTransition: boolean; // If true, phase can't advance until task completes
}Failure strategies:
retry— Retry up to maxRetries times with exponential backoffdegrade— Proceed with degraded functionality (e.g., skip enrichment)stall— Block progress until task succeedsproceed-with-gap— Continue but flag the gap
7. SidebarBuilder
Location: src/engine/framework/sidebar-builder.ts
Maps working memory state → sidebar UI state.
API:
class SidebarBuilder {
build<TExtraction>(
memory: EngineWorkingMemory<TExtraction>,
currentPhase: PhaseDefinition,
): SidebarState {
// Returns:
// - sections: { id, title, visible, collapsed, items[] }[]
// - phaseIndicator: { currentPhase, label } | null
// - progressBar: { percentage, visible, showPercentage } | null
}
}Section types:
- Static sections — Always present (e.g., "Core Requirements")
- Dynamic sections — Conditional on phase (e.g., "Validation Checklist" only in VALIDATION phase)
Item mapping:
{
obligationId: string;
label: string; // Obligation description
status: 'pending' | 'partial' | 'satisfied' | 'deferred';
confidence: number; // 0.0 - 1.0
}8. TechniqueLibrary
Location: src/engine/framework/technique-library.ts
16+ conversation techniques from HUMINT, Motivational Interviewing, and Cognitive Interview.
Categories:
-
Cognitive Interview (8 techniques)
- Context reinstatement, Reverse order recall, Change perspective, Report everything, Open-ended questions, Avoid interruptions, Sketch/visualize, Multiple retrieval attempts
-
Motivational Interviewing (3 techniques)
- Reflective listening, Elicit change talk, Roll with resistance
-
HUMINT/Rapport Building (2 techniques)
- Active listening, Mirroring
-
Inform Delivery (1 technique)
- Chunking with check-ins
-
Deliberation Support (1 technique)
- Pros/cons listing
-
Persuasion (1 technique)
- Social proof
Technique definition:
interface TechniqueDefinition {
id: string;
name: string;
description: string;
category: string;
applicableModes: ModeType[]; // Which modes can use this
agentInstruction: string; // Prompt addition
effectivenessSignal: string; // How to detect success
cooldownTurns: number; // Turns before re-use
}Example (Context Reinstatement):
{
id: 'context_reinstatement',
name: 'Context Reinstatement',
description: 'Ask the user to recall the environment, emotions, and thoughts surrounding the event',
category: 'cognitive-interview',
applicableModes: ['extract', 'explore'],
agentInstruction: 'Help the user mentally return to the situation by asking about the setting, who was present, what they were feeling, and what led up to the moment.',
effectivenessSignal: 'User provides richer, more detailed responses',
cooldownTurns: 5,
}Engine Types
1. Project Discovery (Extract-Dominant)
Config: src/engine/engines/project-discovery/config.ts
Purpose: Extract detailed project requirements from the user.
Characteristics:
- 5 phases: OPENING, CORE_CAPTURE, USERS_AND_GOALS, CONSTRAINTS, VALIDATION
- 25 obligations across 5 categories (purpose, users, success, constraints, screening)
- Extract-dominant mode blend: 70% extract, 20% validate, 10% explore
- Completeness model: Obligation-based (threshold: 0.70)
- Extraction schema:
ProjectDiscoveryExtraction(purposeAndProblem, usersAndPersonas, successCriteria, constraints, nonFunctionalScreening, edgeCases)
Sample phase (CORE_CAPTURE):
{
id: 'CORE_CAPTURE',
displayName: 'Core Problem Discovery',
order: 2,
modeBlend: {
extract: 75,
validate: 15,
explore: 10,
},
activeTechniques: [
'context_reinstatement',
'open_ended_questions',
'active_listening',
],
transitionTriggers: [
{
type: 'obligation_satisfaction',
category: 'purpose',
minSatisfactionRate: 0.80,
},
],
}Exit transition: Targets exploration engine when completeness ≥ 0.90 and user explicitly requests exploration.
2. Exploration (Inform-Dominant)
Config: src/engine/engines/exploration/config.ts
Purpose: Educate user on solution options and guide them to a decision.
Characteristics:
- 3 phases: ORIENTATION, EDUCATION, DELIBERATION
- 18 obligations across 3 categories (understanding, landscape, decision)
- Inform-dominant mode blend: 50% inform, 30% deliberate, 15% validate, 5% explore
- Completeness model: Decision-readiness (threshold: 0.60)
- Extraction schema:
ExplorationExtraction(initialState, solutionLandscape, understanding, decision)
Sample phase (EDUCATION):
{
id: 'EDUCATION',
displayName: 'Solution Education',
order: 2,
modeBlend: {
inform: 60,
explore: 20,
validate: 15,
deliberate: 5,
},
activeTechniques: [
'chunking_with_checkin',
'examples_and_analogies',
'incremental_teach',
],
transitionTriggers: [
{
type: 'custom_evaluator',
evaluatorName: 'evaluateSolutionSpaceAwareness',
threshold: 0.70,
},
],
mandatoryCheckpoints: [
{ obligationId: 'teach_back_passed', required: true },
],
}Custom evaluators (13 total):
knowledgeGapsIdentified,backgroundResearchAvailable,userDemonstratesUnderstanding,solutionLandscapeCoverage,decisionMade,atLeastOneTeachBackPassed,noUncorrectedMisconceptions,decisionCapturedWithReasoning,evaluateSolutionSpaceAwareness,evaluateTradeoffUnderstanding,evaluateConstraintAwareness,evaluatePrecedentExposure,evaluateRiskIdentification
Decision-readiness model (5 heuristics):
{
type: 'decision-readiness',
readyThreshold: 0.60,
heuristics: [
{ id: 'knowledge_gaps_identified', weight: 0.20, evaluator: 'knowledgeGapsIdentified' },
{ id: 'solution_landscape_coverage', weight: 0.25, evaluator: 'solutionLandscapeCoverage' },
{ id: 'decision_made', weight: 0.30, evaluator: 'decisionMade' },
{ id: 'teach_back_passed', weight: 0.15, evaluator: 'atLeastOneTeachBackPassed' },
{ id: 'no_misconceptions', weight: 0.10, evaluator: 'noUncorrectedMisconceptions' },
],
}Exit transition: Targets project-discovery when decision made with readiness ≥ 0.60.
3. Business Profile (Stub)
Config: src/engine/engines/business-profile/config.ts (STUB — identity fields only)
Extraction schema: src/engine/engines/business-profile/extraction-schema.ts (FULL)
14 dimensions: industry, businessModel, targetMarket, productOffering, teamComposition, operations, customerExperience, technologyStack, dataAndAnalytics, financialModel, brandAndMarketing, partnershipsAndIntegrations, regulatoryAndCompliance, growthAndScaling
4. Ideation (Stub)
Config: src/engine/engines/ideation/config.ts (STUB — identity fields only)
Extraction schema: src/engine/engines/ideation/extraction-schema.ts (FULL)
5 dimensions: problemSpace (9 fields), ideaGeneration (7 fields), evaluation (5 fields), refinement (5 fields), selection (6 fields)
Configuration System
EngineConfiguration<TExtraction>
13 configurable surfaces:
- Identity —
engineId,name,description,version,engineType - Phase Management —
phases[],initialPhase - Obligations —
obligations[] - Completeness Model —
completenessModel - Background Tasks —
backgroundTasks[] - Context Configuration —
contextConfig - Information Ratio —
informationRatio: { user, system } - Sidebar Configuration —
sidebarConfig - Technique Activation —
availableTechniques[] - Exit Transitions —
exitTransitions[] - System Prompt Template —
systemPromptTemplate - Extraction Schema —
extractionSchema - Custom Evaluators — (optional, provided via deps)
Validation
Tool: validateEngineConfig() in src/engine/framework/config-validator.ts
Checks:
- Required identity fields present
- Valid phase graph (all phases reachable, no orphans)
- Phase order is sequential
- Initial phase exists
- All obligation references valid
- Completeness model well-formed
- Heuristic weights sum to 1.0
- Context allocation sums to ~100%
- Information ratio sums to 100
- Exit transitions reference valid target engines
- All technique IDs valid (exist in TechniqueLibrary)
- All background task handlers exist
Returns:
{
valid: boolean;
errors: string[]; // Blocking issues
warnings: string[]; // Non-blocking issues
}Handoff Protocol
Producing a Handoff
When an engine completes, it produces an EngineHandoff:
Tool: mapToHandoff() in engine-specific context-mapper.ts
Example (Exploration → Project Discovery):
// src/engine/engines/exploration/context-mapper.ts
export function mapExplorationToProjectDiscovery(
memory: EngineWorkingMemory<ExplorationExtraction>,
sessionId: string,
projectId: string,
): EngineHandoff {
return {
metadata: {
sourceEngineId: 'exploration',
targetEngineId: 'project-discovery',
handoffDate: new Date().toISOString(),
sessionId,
projectId,
turnCount: memory.turnNumber,
completenessScore: memory.completeness.overall,
transitionReason: 'User reached decision and ready to define requirements',
},
phaseResults: memory.phase.history.map(p => ({
phaseName: p.phaseId,
completionPercentage: Math.round(p.completeness * 100),
keyOutputs: `Phase ${p.phaseId} key outputs...`,
remainingGaps: [],
})),
actionList: {
decisionsReached: memory.extraction.decision.decisionMade
? [{
decision: memory.extraction.decision.decisionMade.value.option,
confidence: memory.extraction.decision.decisionMade.confidence,
reasoning: memory.extraction.decision.decisionMade.value.reasoning,
}]
: [],
optionsEliminated: memory.extraction.solutionLandscape.options
.filter(opt => opt.value.eliminated)
.map(opt => ({
option: opt.value.name,
reasons: opt.value.reactions?.map(r => r.concern) || [],
})),
constraintsIdentified: memory.extraction.decision.constraints?.value || [],
assumptionsCarriedForward: [],
},
situationAwareness: {
userConfidenceLevel: memory.extraction.decision.decisionMade?.confidence > 0.8 ? 'high' : 'medium',
knowledgeGapsRevealed: memory.extraction.understanding.questions?.value || [],
userGoals: [],
keyPainPoints: [],
},
accumulatedContext: {
conceptsTaught: memory.extraction.understanding.concepts?.value.map(c => ({
concept: c.name,
understandingLevel: c.understood ? 'solid' : 'basic',
})) || [],
priorExperience: [],
technicalLevel: 'intermediate',
},
relationalState: {
trustLevel: memory.socioEmotional.trust > 0.7 ? 'high' : 'medium',
rapport: 'Collaborative and engaged',
openQuestions: [],
sensitivities: [],
},
};
}Validating a Handoff
Tool: validateHandoff() in src/engine/framework/handoff.ts
Validation rules:
- Required fields present in all 5 sections
reasoningfield >20 characters (not placeholder)confidencein range [0, 1]- No self-transitions (source !== target)
- At least one decision or constraint or gap identified
Returns:
{
valid: boolean;
errors: string[];
}Formatting for Context
Tool: formatHandoffForContext() in src/engine/framework/handoff.ts
Produces an executive summary formatted for the TOP of the context window (Lost in the Middle):
═══════════════════════════════════════════════════════════
HANDOFF CONTEXT — READ THIS FIRST
═══════════════════════════════════════════════════════════
Source: Exploration Engine → Target: Project Requirements Discovery
Session: sess_abc123 | Project: proj_xyz789 | Turns: 15 | Completeness: 0.75
Transition Reason: User reached decision and ready to define requirements
─── DECISIONS REACHED ───
✓ Chose "Custom Web App" (confidence: 0.85)
Reasoning: Full control over the product, better long-term TCO at scale...
─── OPTIONS ELIMINATED ───
✗ WordPress — Not flexible enough, vendor lock-in concerns
✗ No-Code Platform — Scalability limitations
─── CONSTRAINTS IDENTIFIED ───
• Budget: $50k-$75k initial build
• Timeline: 6 months to MVP
• Must integrate with Stripe and QuickBooks
─── KNOWLEDGE GAPS ───
• User unclear on hosting requirements
• Unsure about authentication approach (OAuth vs custom)
─── CONCEPTS TAUGHT ───
• API Design (solid understanding)
• Database Normalization (basic understanding)
• Microservices vs Monolith (solid understanding)
─── USER PROFILE ───
Technical Level: Intermediate
Confidence: High
Trust: High
Rapport: Collaborative and engaged
═══════════════════════════════════════════════════════════I-PASS Verification
After the receiving engine starts with a handoff, it generates its first response. The system then verifies the response acknowledges the handoff using fuzzy keyword matching:
Tool: verifyHandoff() in src/engine/framework/handoff.ts
Verification logic:
function verifyHandoff(
handoff: EngineHandoff,
firstResponseText: string,
): {
verified: boolean;
score: number; // 0.0 - 1.0
details: {
identityMatch: boolean;
decisionsAcknowledged: number; // % of decisions mentioned
constraintsAcknowledged: number;
gapsAcknowledged: number;
overallAcknowledgment: number; // % of critical elements
};
} {
// Extract keywords from decisions, constraints, gaps
// Fuzzy match against response text (Jaccard similarity)
// Verified if overallAcknowledgment > 0.5 (>50% of critical elements)
}Keyword extraction:
function extractKeywords(text: string): string[] {
return text
.toLowerCase()
.split(/[\s,.:;!?()[\]{}"'`]+/) // Split on punctuation + quotes
.filter(w => w.length > 3 && !STOPWORDS.has(w));
}Fuzzy matching:
function jaccardSimilarity(set1: Set<string>, set2: Set<string>): number {
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}If verification fails (verified: false), the system logs a warning but does NOT block the conversation.
Phase State Machine
Implementation: src/engine/framework/phase-state-machine.ts
Transition Evaluation
After every turn, the PhaseStateMachine checks if any transition triggers are satisfied:
evaluateTransition(memory: EngineWorkingMemory): {
shouldTransition: boolean;
targetPhase: string | null;
}Trigger types:
-
obligation_satisfaction
{ type: 'obligation_satisfaction', category: 'purpose', // Obligation category minSatisfactionRate: 0.80, // 80% of obligations in category } -
turn_count
{ type: 'turn_count', minTurns: 5, // At least 5 turns in current phase } -
completeness_score
{ type: 'completeness_score', minCompleteness: 0.70, // Overall completeness ≥ 70% } -
custom_evaluator
{ type: 'custom_evaluator', evaluatorName: 'decisionMade', threshold: 0.80, }
Checkpoint Enforcement
Before executing a transition, the system checks mandatoryCheckpoints:
canTransition(
fromPhase: string,
toPhase: string,
memory: EngineWorkingMemory,
): boolean {
const phase = this.phases.find(p => p.id === fromPhase);
if (!phase) return false;
for (const checkpoint of phase.mandatoryCheckpoints) {
const obligation = memory.obligations.items.find(o => o.id === checkpoint.obligationId);
if (!obligation || obligation.status !== 'satisfied') {
return false; // Checkpoint not met
}
}
return true;
}If a checkpoint is not met, the transition is blocked even if the trigger fires.
Transition Execution
When a transition is allowed:
executeTransition(
currentPhase: string,
targetPhase: string,
memory: EngineWorkingMemory,
): void {
// 1. Add current phase to history
memory.phase.history.push({
phaseId: currentPhase,
enteredAtTurn: memory.phase.enteredAtTurn,
exitedAtTurn: memory.turnNumber,
turnsInPhase: memory.phase.turnsInPhase,
completeness: memory.completeness.overall,
});
// 2. Update current phase
memory.phase.current = targetPhase;
memory.phase.turnsInPhase = 0;
memory.phase.enteredAtTurn = memory.turnNumber;
}Extraction Pipeline
Implementation: src/engine/framework/extraction-pipeline.ts
Per-Turn Extraction
On every user message, the ExtractionPipeline:
- Calls extraction LLM with field definitions and current extraction state
- Processes results for each field:
- Check confidence threshold (from field definition)
- Build
ExtractedField<T>wrapper - Detect supersession (value changed)
- Update extraction state
- Log to
eng_extractions_log
Extraction LLM Interface
interface ExtractionDeps {
runExtractionLLM(
userMessage: string,
conversationContext: string,
fieldDefinitions: ExtractionFieldDefinition[],
currentExtraction: unknown,
): Promise<Record<string, {
value: unknown;
confidence: number;
reasoning?: string;
} | null>>;
logExtraction(entry: ExtractionLogEntry): Promise<void>;
}Field definition:
interface ExtractionFieldDefinition {
path: string; // 'purposeAndProblem.coreProblem'
displayName: string;
description: string;
valueType: 'string' | 'number' | 'boolean' | 'array' | 'object';
required: boolean;
confidenceThreshold: number; // 0.0 - 1.0 (default: 0.70)
}Extraction Result
interface ExtractionResult<TExtraction> {
updatedExtraction: TExtraction;
fieldsUpdated: string[];
newConceptCount: number;
sensitiveInfoShared: boolean;
supersededFields: {
fieldPath: string;
oldValue: unknown;
newValue: unknown;
reason: string;
}[];
}Audit Trail
Every extraction is logged to eng_extractions_log:
interface ExtractionLogEntry {
sessionId: string;
projectId: string;
messageId: string;
engineId: string;
fieldPath: string; // 'purposeAndProblem.coreProblem'
extractedValue: unknown; // JSONB
confidence: number;
extractionType: 'new' | 'update' | 'supersede';
reasoning: string | null;
supersededBy: string | null; // messageId if superseded later
turnNumber: number;
}Completeness Engine
Implementation: src/engine/framework/completeness-engine.ts
Model Types
1. Obligation Model (used by Project Discovery)
Completeness = % of obligations satisfied
{
type: 'obligation',
readyThreshold: 0.70,
heuristics: [
{ id: 'purpose_obligations', weight: 0.25, category: 'purpose' },
{ id: 'users_obligations', weight: 0.20, category: 'users' },
{ id: 'success_obligations', weight: 0.20, category: 'success' },
{ id: 'constraints_obligations', weight: 0.20, category: 'constraints' },
{ id: 'screening_obligations', weight: 0.15, category: 'screening' },
],
}Calculation:
- For each category:
satisfied / total - Overall: weighted sum of categories
2. Decision-Readiness Model (used by Exploration)
Completeness = readiness to make an informed decision
{
type: 'decision-readiness',
readyThreshold: 0.60,
heuristics: [
{ id: 'knowledge_gaps_identified', weight: 0.20, evaluator: 'knowledgeGapsIdentified' },
{ id: 'solution_landscape_coverage', weight: 0.25, evaluator: 'solutionLandscapeCoverage' },
{ id: 'decision_made', weight: 0.30, evaluator: 'decisionMade' },
{ id: 'teach_back_passed', weight: 0.15, evaluator: 'atLeastOneTeachBackPassed' },
{ id: 'no_misconceptions', weight: 0.10, evaluator: 'noUncorrectedMisconceptions' },
],
}Custom evaluator example:
function decisionMade(memory: EngineWorkingMemory<ExplorationExtraction>): number {
return memory.extraction.decision.decisionMade?.value ? 1.0 : 0.0;
}
function solutionLandscapeCoverage(memory: EngineWorkingMemory<ExplorationExtraction>): number {
const options = memory.extraction.solutionLandscape.options;
if (!options || options.length === 0) return 0.0;
const coveredAspects = options.filter(opt =>
opt.value.prosExplored && opt.value.consExplored && opt.value.reactions
).length;
return coveredAspects / Math.max(options.length, 3); // Expect at least 3 options
}3-7. Other Models
maturity— Solution maturity level (concept → prototype → production-ready)estimability— Sufficient detail for cost/time estimationcoverage— % of domain/scope coveredprofile— Profile completeness (for business profile engine)saturation— Information saturation point reached
Evaluation API
async evaluate<TExtraction>(
model: CompletenessModelConfig,
memory: EngineWorkingMemory<TExtraction>,
): Promise<{
overall: number;
byCategory: Record<string, number>;
isReady: boolean;
gaps: string[];
}>Background Task Manager
Implementation: src/engine/framework/background-task-manager.ts
Task Lifecycle
- Trigger evaluation — After each turn, check if task should be dispatched
- Task dispatch — Send to Inngest handler
- Polling — Check task status
- Result consumption — Merge results into working memory
- Failure handling — Execute failure strategy
Trigger Conditions
Low confidence:
{
type: 'low_confidence',
threshold: 0.60, // Trigger if any obligation confidence < 0.60
}Obligation status:
{
type: 'obligation_status',
obligationIds: ['tech_feasibility_assessed'],
status: 'pending', // Trigger if still pending
}Custom:
{
type: 'custom',
evaluator: (memory) => {
// Custom logic
return memory.extraction.decision.options.length < 3;
},
}Failure Strategies
1. Retry
{
type: 'retry',
maxRetries: 3,
retryDelay: 5000, // ms
}Exponential backoff: 5s, 10s, 20s
2. Degrade
{
type: 'degrade',
degradedBehavior: 'Skip background research and proceed with user-provided info only',
}Continue with reduced functionality.
3. Stall
{
type: 'stall',
}Block progress until task succeeds. User sees "Waiting for background research..." message.
4. Proceed with Gap
{
type: 'proceed-with-gap',
}Continue but flag the gap in working memory. Later phases can attempt to fill it.
Block Phase Transition
If blockPhaseTransition: true, the phase cannot advance until the task completes:
{
taskId: 'competitive_analysis',
blockPhaseTransition: true,
failureStrategy: { type: 'stall' },
}Context Assembly
Implementation: src/engine/framework/context-assembler.ts
Budget Allocation
Context is assembled from 4 sources with percentage allocation:
{
maxTokens: 128000, // Claude 3.5 Sonnet context window
allocation: {
obligations: 0.40, // 40% = 51,200 tokens
conversationHistory: 0.30, // 30% = 38,400 tokens
backgroundIntelligence: 0.20, // 20% = 25,600 tokens
extractionSummary: 0.10, // 10% = 12,800 tokens
},
compressionStrategy: 'summarize_old',
}Handoff Positioning
Inbound handoff context is placed at the TOP of the context window (position 1) based on "Lost in the Middle" research:
┌─────────────────────────────────────┐
│ 1. HANDOFF CONTEXT (if present) │ ← TOP (highest recall)
├─────────────────────────────────────┤
│ 2. Current Phase Instructions │
├─────────────────────────────────────┤
│ 3. Obligations Summary │
├─────────────────────────────────────┤
│ 4. Background Intelligence │
├─────────────────────────────────────┤
│ 5. Conversation History │ ← MIDDLE (lowest recall)
├─────────────────────────────────────┤
│ 6. Extraction Summary │
├─────────────────────────────────────┤
│ 7. Current User Message │ ← BOTTOM (high recall)
└─────────────────────────────────────┘Compression Strategies
1. summarize_old (default)
When history exceeds budget:
- Keep most recent N turns intact
- Summarize older turns using LLM
2. sliding_window
Keep only the most recent N turns, discard older.
3. extractive
Extract key facts from all history, discard the rest.
PCS Integration
For obligation and background intelligence sections, the ContextAssembler delegates to the Project Context Service (PCS):
const obligationContext = await pcs.assembleContext({
projectId: memory.projectId,
artifactTypes: ['obligation'],
budgetTokens: allocations.obligations,
});
const backgroundContext = await pcs.assembleContext({
projectId: memory.projectId,
artifactTypes: ['research', 'pattern'],
budgetTokens: allocations.backgroundIntelligence,
});Sidebar Builder
Implementation: src/engine/framework/sidebar-builder.ts
Section Types
Static sections — Always present:
{
staticSections: [
{
id: 'core_requirements',
title: 'Core Requirements',
obligationCategories: ['purpose', 'users', 'success'],
defaultCollapsed: false,
},
],
}Dynamic sections — Conditional on phase:
{
dynamicSections: [
{
section: {
id: 'validation_checklist',
title: 'Validation Checklist',
obligationCategories: ['validation'],
defaultCollapsed: false,
},
appearsWhen: {
type: 'turn_count',
minTurns: 10,
},
},
],
}Visibility Conditions
Turn count:
{ type: 'turn_count', minTurns: 5 }Completeness score:
{ type: 'completeness_score', minCompleteness: 0.70 }Obligation satisfaction:
{
type: 'obligation_satisfaction',
obligationCategory: 'purpose',
minSatisfactionRate: 0.80,
}Phase Indicator
{
phaseIndicator: {
show: true,
labels: {
'OPENING': 'Getting Started',
'CORE_CAPTURE': 'Understanding Your Needs',
'VALIDATION': 'Confirming Details',
},
},
}Progress Bar
{
progressBar: {
show: true,
showPercentage: true,
},
}Percentage = Math.round(memory.completeness.overall * 100)
Technique Library
Implementation: src/engine/framework/technique-library.ts
All 16 Techniques
Cognitive Interview (8):
- context_reinstatement — Mental return to the situation
- reverse_order_recall — Recount events backward
- change_perspective — View from different angles
- report_everything — No detail too small
- open_ended_questions — Avoid yes/no questions
- avoid_interruptions — Let user finish thoughts
- sketch_or_visualize — Draw diagrams or flows
- multiple_retrieval_attempts — Re-ask in different ways
Motivational Interviewing (3): 9. reflective_listening — Mirror user's concerns 10. elicit_change_talk — Surface user's own motivations 11. roll_with_resistance — Accept pushback gracefully
HUMINT/Rapport (2): 12. active_listening — Demonstrate understanding 13. mirroring — Match user's language and tone
Inform Delivery (1): 14. chunking_with_checkin — Break info into digestible chunks
Deliberation Support (1): 15. pros_cons_listing — Structured tradeoff analysis
Persuasion (1): 16. social_proof — Reference common patterns
Technique Activation
Techniques are activated per-phase via activeTechniques array:
{
id: 'CORE_CAPTURE',
activeTechniques: [
'context_reinstatement',
'open_ended_questions',
'active_listening',
],
}The agent receives these as prompt additions:
Active Techniques (use these in your response):
- Context Reinstatement: Help the user mentally return to the situation...
- Open-Ended Questions: Ask questions that encourage detailed responses...
- Active Listening: Demonstrate that you understand the user's perspective...Cooldown
After a technique is used, it goes on cooldown for N turns:
{
id: 'context_reinstatement',
cooldownTurns: 5, // Can't use again for 5 turns
}Prevents overuse of the same technique.
Strategy System
Location: src/engine/strategies/
The framework uses pluggable strategies for 4 cross-cutting concerns:
1. ModeClassifier
Interface:
interface ModeClassifier {
classify(responseText: string): Promise<ModeType[]>;
}v1 Implementation: NoOpModeClassifier — No enforcement, returns empty array.
v2 TODO: LLMDrivenModeClassifier — Use LLM to classify response mode.
2. TechniqueScheduler
Interface:
interface TechniqueScheduler {
selectTechniques(
availableTechniques: TechniqueDefinition[],
memory: EngineWorkingMemory<any>,
): Promise<string[]>; // Returns technique IDs
}v1 Implementation: LLMDrivenScheduler — LLM decides which techniques to use via prompt.
v2 TODO: RuleBasedScheduler — Heuristic rules for technique selection.
3. TaskFailureStrategy
Interface:
interface TaskFailureStrategy {
handleFailure(
task: BackgroundTaskDefinition,
error: Error,
attemptNumber: number,
): Promise<'retry' | 'degrade' | 'stall' | 'proceed-with-gap'>;
}v1 Implementation: ConfigDrivenFailureStrategy — Reads failureStrategy from task definition.
4. JourneyCompressor
Interface:
interface JourneyCompressor {
compress(
conversationHistory: ConversationMessage[],
targetTokens: number,
): Promise<string>;
}v1 Implementation: Stub only (not yet implemented).
v2 TODO: SummarizeOldCompressor — Summarize old turns, keep recent intact.
Database Schema
New Tables (Migrations 208-210)
1. eng_handoffs (Migration 208)
CREATE TABLE eng_handoffs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
source_engine_id VARCHAR(100) NOT NULL,
source_session_id UUID NOT NULL,
target_engine_id VARCHAR(100) NOT NULL,
target_session_id UUID,
handoff_payload JSONB NOT NULL,
verified BOOLEAN DEFAULT false,
verification_result JSONB,
transition_reason TEXT,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_eng_handoffs_project ON eng_handoffs(project_id);
CREATE INDEX idx_eng_handoffs_source_session ON eng_handoffs(source_session_id);
CREATE INDEX idx_eng_handoffs_target_session ON eng_handoffs(target_session_id);Purpose: Track inter-engine handoffs with verification results.
2. eng_extractions_log (Migration 209)
CREATE TABLE eng_extractions_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
message_id UUID NOT NULL,
engine_id VARCHAR(100) NOT NULL,
field_path VARCHAR(200) NOT NULL,
extracted_value JSONB,
confidence NUMERIC(5,4),
extraction_type VARCHAR(20) CHECK (extraction_type IN ('new', 'update', 'supersede')),
reasoning TEXT,
superseded_by UUID REFERENCES eng_extractions_log(id),
turn_number INT,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_eng_extractions_session ON eng_extractions_log(session_id);
CREATE INDEX idx_eng_extractions_project ON eng_extractions_log(project_id);
CREATE INDEX idx_eng_extractions_field ON eng_extractions_log(field_path);Purpose: Audit trail of all extractions with provenance.
3. eng_state_checkpoints (Migration 210)
CREATE TABLE eng_state_checkpoints (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
at_turn INT NOT NULL,
state_snapshot JSONB NOT NULL,
context_tokens_at_checkpoint INT,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_eng_checkpoints_session ON eng_state_checkpoints(session_id);
CREATE INDEX idx_eng_checkpoints_turn ON eng_state_checkpoints(at_turn);Purpose: Periodic state snapshots for recovery after compression.
Extended Tables (Migration 211)
elicitation_sessions.state_json — Extended with engine framework fields:
{
engine_id: string; // 'project-discovery', 'exploration'
engine_version: string; // Semver
extraction_state: TExtraction; // Strongly-typed extraction state
inbound_handoff_id: string; // UUID of eng_handoffs record
outbound_handoff_id: string; // UUID of eng_handoffs record
// ... existing fields
}No schema change (JSONB allows arbitrary fields), but documents the expected structure.
Testing
Test Coverage
460 tests across 19 test files — All passing, 0 failures.
Unit Tests by Component
Framework Core:
config-validator.test.ts(41 tests) — Config validation rulesphase-state-machine.test.ts(46 tests) — Phase transitions, checkpointsextraction-pipeline.test.ts(32 tests) — Extraction logic, supersessioncompleteness-engine.test.ts(35 tests) — Completeness modelscontext-assembler.test.ts(28 tests) — Budget allocation, handoff positioningbackground-task-manager.test.ts(24 tests) — Task dispatch, failure strategiessidebar-builder.test.ts(22 tests) — Section visibility, dynamic sectionsengine-runner.test.ts(30 tests) — Full 19-step loophandoff.test.ts(36 tests) — Validation, formatting, I-PASS verificationtechnique-library.test.ts(18 tests) — Technique definitions
Project Discovery Engine:
config.test.ts(29 tests) — Config validationrunner-integration.test.ts(24 tests) — Full loop with PROJECT_DISCOVERY_CONFIG
Exploration Engine:
evaluators.test.ts(13 tests) — Custom evaluatorscontent-tracker.test.ts(12 tests) — Source confidence trackingconfig.test.ts(45 tests) — Config validationrunner-integration.test.ts(28 tests) — Full loop with EXPLORATION_CONFIGmappers.test.ts(14 tests) — Output mapper, context mapper
Integration Tests:
handoff-flow.test.ts(30 tests) — Complete Exploration → Project Discovery handoff flowframework-agnostic.test.ts(16 tests) — Same runner handles both engine types
Key Test Scenarios
1. Config Validation
it('rejects invalid phase graph (orphaned phase)', () => {
const invalidConfig = {
...validConfig,
phases: [
{ id: 'PHASE_A', order: 1, nextPhase: 'PHASE_B' },
{ id: 'PHASE_C', order: 2 }, // Orphan: no way to reach it
],
};
const result = validateEngineConfig(invalidConfig);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Orphaned phase: PHASE_C');
});2. Phase Transition with Checkpoint
it('blocks transition if mandatory checkpoint not met', () => {
const memory = makeMemory({
phase: { current: 'VALIDATION' },
obligations: [
{ id: 'confirm_scope', status: 'pending' }, // Not satisfied
],
});
const canTransition = phaseMachine.canTransition('VALIDATION', 'CLOSING', memory);
expect(canTransition).toBe(false);
});3. Extraction with Supersession
it('detects supersession when value changes', async () => {
const currentExtraction = {
coreProblem: {
value: 'Need a website',
confidence: 0.70,
extractedFromTurns: [1],
},
};
const result = await pipeline.extract(
'Actually, I need a mobile app, not a website',
'context...',
currentExtraction,
2,
metadata,
);
expect(result.supersededFields).toHaveLength(1);
expect(result.supersededFields[0].fieldPath).toBe('coreProblem');
expect(result.supersededFields[0].oldValue).toBe('Need a website');
expect(result.supersededFields[0].newValue).toContain('mobile app');
});4. Handoff I-PASS Verification
it('verifies handoff when response acknowledges key decisions', () => {
const handoff = makeHandoff({
decisionsReached: [
{ decision: 'Custom Web App', confidence: 0.85, reasoning: '...' },
],
});
const response = 'Great! You decided on a Custom Web App. Let's define the requirements...';
const verification = verifyHandoff(handoff, response);
expect(verification.verified).toBe(true);
expect(verification.details.decisionsAcknowledged).toBeGreaterThan(0.5);
});5. Framework Agnosticism
it('handles extract-dominant and inform-dominant with same runner', async () => {
const runner = new EngineRunner(deps);
// Run Project Discovery (extract-dominant)
const discoveryResult = await runner.processMessage({
config: PROJECT_DISCOVERY_CONFIG,
userMessage: 'I need a project management tool',
currentState: emptyState,
});
// Run Exploration (inform-dominant)
const explorationResult = await runner.processMessage({
config: EXPLORATION_CONFIG,
userMessage: 'Tell me about project management approaches',
currentState: emptyState,
});
expect(discoveryResult.updatedState.engineType).toBe('extract-dominant');
expect(explorationResult.updatedState.engineType).toBe('inform-dominant');
// Same runner, different behavior via config!
});Usage Examples
1. Starting a Discovery Session
import { EngineRunner } from './src/engine/framework/engine-runner';
import { PROJECT_DISCOVERY_CONFIG } from './src/engine/engines/project-discovery/config';
import { createEmptyWorkingMemory } from './src/engine/framework/types';
const runner = new EngineRunner({
runExtractionLLM: async (msg, ctx, fields, current) => {
// Call LLM for extraction
},
runMainAgent: async (prompt) => {
// Call main agent for response generation
},
logExtraction: async (entry) => {
// Log to eng_extractions_log
},
// ... other deps
});
const initialState = createEmptyWorkingMemory(
PROJECT_DISCOVERY_CONFIG,
'sess_abc123',
'proj_xyz789',
);
const result = await runner.processMessage({
config: PROJECT_DISCOVERY_CONFIG,
userMessage: 'I need help building a project management tool for my team.',
currentState: initialState,
metadata: {
sessionId: 'sess_abc123',
projectId: 'proj_xyz789',
messageId: 'msg_001',
userId: 'user_123',
},
});
console.log('Response:', result.responseText);
console.log('Sidebar:', result.sidebarState);
console.log('Completeness:', result.updatedState.completeness.overall);2. Continuing a Session
const secondTurn = await runner.processMessage({
config: PROJECT_DISCOVERY_CONFIG,
userMessage: 'We have about 20 people on the team and need task tracking, deadlines, and notifications.',
currentState: result.updatedState, // Use previous state
metadata: {
sessionId: 'sess_abc123',
projectId: 'proj_xyz789',
messageId: 'msg_002',
userId: 'user_123',
},
});3. Producing a Handoff
import { mapProjectDiscoveryToExploration } from './src/engine/engines/project-discovery/context-mapper';
import { validateHandoff, formatHandoffForContext } from './src/engine/framework/handoff';
// When discovery is complete (completeness ≥ 0.90)
if (result.updatedState.completeness.overall >= 0.90) {
const handoff = mapProjectDiscoveryToExploration(
result.updatedState,
'sess_abc123',
'proj_xyz789',
);
// Validate handoff
const validation = validateHandoff(handoff);
if (!validation.valid) {
throw new Error(`Invalid handoff: ${validation.errors.join(', ')}`);
}
// Save to database
await db.insert('eng_handoffs', {
project_id: 'proj_xyz789',
source_engine_id: 'project-discovery',
source_session_id: 'sess_abc123',
target_engine_id: 'exploration',
handoff_payload: handoff,
transition_reason: handoff.metadata.transitionReason,
});
// Format for context (will be used by Exploration engine)
const contextSummary = formatHandoffForContext(handoff);
console.log(contextSummary);
}4. Starting Exploration with Handoff
import { EXPLORATION_CONFIG } from './src/engine/engines/exploration/config';
// Load handoff from database
const handoff = await db.query('eng_handoffs').where({ target_session_id: 'sess_exp_1' }).first();
// Create initial state with inbound handoff
const explorationState = createEmptyWorkingMemory(
EXPLORATION_CONFIG,
'sess_exp_1',
'proj_xyz789',
);
explorationState.inboundHandoff = handoff.handoff_payload;
// First turn with handoff
const explorationResult = await runner.processMessage({
config: EXPLORATION_CONFIG,
userMessage: 'Yes, let's explore the options.',
currentState: explorationState,
metadata: { /* ... */ },
});
// Verify handoff was acknowledged (I-PASS)
const verification = verifyHandoff(
handoff.handoff_payload,
explorationResult.responseText,
);
console.log('Handoff verified:', verification.verified);
// Save verification result
await db.update('eng_handoffs', handoff.id, {
verified: verification.verified,
verification_result: verification,
});5. Adding a New Engine
To add a new engine (e.g., "Feature Prioritization"):
Step 1: Define extraction schema
// src/engine/engines/feature-prioritization/extraction-schema.ts
export interface FeaturePrioritizationExtraction {
features: {
identified: ExtractedField<{ name: string; description: string }[]>;
userValue: ExtractedField<Record<string, number>>; // feature_name → score
businessValue: ExtractedField<Record<string, number>>;
effort: ExtractedField<Record<string, number>>;
dependencies: ExtractedField<Record<string, string[]>>;
};
criteria: {
prioritizationApproach: ExtractedField<'value' | 'effort' | 'impact' | 'custom'>;
customWeights: ExtractedField<Record<string, number>>;
};
prioritization: {
ranked: ExtractedField<{ name: string; score: number; rank: number }[]>;
mustHaves: ExtractedField<string[]>;
niceToHaves: ExtractedField<string[]>;
};
}Step 2: Create engine config
// src/engine/engines/feature-prioritization/config.ts
export const FEATURE_PRIORITIZATION_CONFIG: EngineConfiguration<FeaturePrioritizationExtraction> = {
engineId: 'feature-prioritization',
name: 'Feature Prioritization',
description: 'Collaboratively prioritize features based on value and effort',
version: '1.0.0',
engineType: 'extract-dominant',
phases: [
{
id: 'FEATURE_DISCOVERY',
displayName: 'Discover Features',
order: 1,
modeBlend: { extract: 60, explore: 30, validate: 10 },
activeTechniques: ['report_everything', 'open_ended_questions'],
// ...
},
{
id: 'VALUE_ASSESSMENT',
displayName: 'Assess Value',
order: 2,
modeBlend: { extract: 50, validate: 30, inform: 20 },
activeTechniques: ['reflective_listening', 'pros_cons_listing'],
// ...
},
// ...
],
initialPhase: 'FEATURE_DISCOVERY',
obligations: [
{ id: 'features_identified', category: 'features', description: 'List of features identified', required: true },
{ id: 'user_value_assessed', category: 'value', description: 'User value scores for each feature', required: true },
// ...
],
completenessModel: {
type: 'obligation',
readyThreshold: 0.75,
heuristics: [
{ id: 'features', weight: 0.30, category: 'features' },
{ id: 'value', weight: 0.40, category: 'value' },
{ id: 'prioritization', weight: 0.30, category: 'prioritization' },
],
},
// ... rest of config
};Step 3: Register in engine registry
// src/engine/registry.ts
import { FEATURE_PRIORITIZATION_CONFIG } from './engines/feature-prioritization/config';
export const ENGINE_REGISTRY: Record<string, EngineConfiguration<any>> = {
'project-discovery': PROJECT_DISCOVERY_CONFIG,
'exploration': EXPLORATION_CONFIG,
'feature-prioritization': FEATURE_PRIORITIZATION_CONFIG, // NEW
};Step 4: Validate config
import { validateEngineConfig } from './src/engine/framework/config-validator';
const validation = validateEngineConfig(FEATURE_PRIORITIZATION_CONFIG);
if (!validation.valid) {
throw new Error(`Invalid config: ${validation.errors.join(', ')}`);
}Step 5: Use it
const state = createEmptyWorkingMemory(
FEATURE_PRIORITIZATION_CONFIG,
'sess_fp_1',
'proj_xyz',
);
const result = await runner.processMessage({
config: FEATURE_PRIORITIZATION_CONFIG,
userMessage: 'We need to prioritize features for our Q1 roadmap.',
currentState: state,
metadata: { /* ... */ },
});Summary
The Engine Framework is Praetor's unified conversational engine architecture that:
- Eliminates code duplication — One
EngineRunnerreplaces multiple engine-specific implementations - Enables rapid engine creation — ~400 lines of config vs ~2,000 lines of custom code
- Enforces type safety — Strongly-typed
TExtractionwithExtractedField<T>provenance - Supports both paradigms — Extract-dominant AND inform-dominant through same runner
- Provides observability — Complete audit trail in
eng_extractions_log - Enables inter-engine transitions — Structured handoffs with I-PASS verification
- Is production-ready — 460 tests, 0 failures, comprehensive validation
Test Coverage: 19 test files, 460 tests passing, 0 failures Implementation: 64 tasks completed (Sections A-H), February 23, 2026 Status: ✅ Production-ready
Last Updated: February 23, 2026 Engine Framework Version: 1.0.0