@ema.co/mcp-toolkit 2026.2.13 → 2026.2.23-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/dist/cli/index.js +2 -2
  4. package/dist/mcp/domain/loop-detection.js +89 -0
  5. package/dist/mcp/domain/sanitizer.js +1 -1
  6. package/dist/mcp/domain/structural-rules.js +4 -5
  7. package/dist/mcp/domain/validation-rules.js +5 -5
  8. package/dist/mcp/domain/workflow-graph.js +3 -5
  9. package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
  10. package/dist/mcp/guidance.js +62 -29
  11. package/dist/mcp/handlers/debug/adapter.js +15 -0
  12. package/dist/mcp/handlers/debug/formatters.js +282 -0
  13. package/dist/mcp/handlers/debug/index.js +133 -0
  14. package/dist/mcp/handlers/demo/adapter.js +180 -0
  15. package/dist/mcp/handlers/env/config.js +2 -2
  16. package/dist/mcp/handlers/feedback/index.js +1 -1
  17. package/dist/mcp/handlers/index.js +0 -1
  18. package/dist/mcp/handlers/persona/adapter.js +135 -0
  19. package/dist/mcp/handlers/persona/index.js +237 -8
  20. package/dist/mcp/handlers/persona/schema.js +27 -0
  21. package/dist/mcp/handlers/reference/index.js +6 -4
  22. package/dist/mcp/handlers/sync/adapter.js +200 -0
  23. package/dist/mcp/handlers/workflow/adapter.js +174 -0
  24. package/dist/mcp/handlers/workflow/fix.js +11 -12
  25. package/dist/mcp/handlers/workflow/index.js +12 -40
  26. package/dist/mcp/handlers/workflow/validation.js +1 -1
  27. package/dist/mcp/knowledge-guidance-topics.js +615 -0
  28. package/dist/mcp/knowledge-types.js +7 -0
  29. package/dist/mcp/knowledge.js +75 -1403
  30. package/dist/mcp/resources-dynamic.js +2395 -0
  31. package/dist/mcp/resources-validation.js +408 -0
  32. package/dist/mcp/resources.js +72 -2508
  33. package/dist/mcp/server.js +69 -2825
  34. package/dist/mcp/tools.js +106 -5
  35. package/dist/sdk/client-adapter.js +265 -24
  36. package/dist/sdk/ema-client.js +100 -9
  37. package/dist/sdk/generated/agent-catalog.js +615 -0
  38. package/dist/sdk/generated/api-client/client/client.gen.js +3 -3
  39. package/dist/sdk/generated/api-client/client/index.js +5 -5
  40. package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
  41. package/dist/sdk/generated/api-client/client.gen.js +1 -1
  42. package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
  43. package/dist/sdk/generated/api-client/index.js +1 -1
  44. package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
  45. package/dist/sdk/generated/well-known-types.js +99 -0
  46. package/dist/sdk/generated/widget-catalog.js +60 -0
  47. package/dist/sdk/grpc-client.js +115 -1
  48. package/dist/sync/sdk.js +2 -2
  49. package/dist/sync.js +4 -3
  50. package/docs/README.md +17 -9
  51. package/package.json +4 -3
  52. package/.context/public/guides/dashboard-operations.md +0 -349
  53. package/.context/public/guides/email-patterns.md +0 -125
  54. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  55. package/dist/mcp/domain/intent-architect.js +0 -914
  56. package/dist/mcp/domain/quality-gates.js +0 -110
  57. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  58. package/dist/mcp/domain/workflow-intent.js +0 -1806
  59. package/dist/mcp/domain/workflow-merge.js +0 -449
  60. package/dist/mcp/domain/workflow-tracer.js +0 -648
  61. package/dist/mcp/domain/workflow-transformer.js +0 -742
  62. package/dist/mcp/handlers/knowledge/index.js +0 -54
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -1,914 +0,0 @@
1
- /**
2
- * Intent Architect - LLM-driven intent decomposition
3
- *
4
- * This module provides the prompt and types for decomposing user requests
5
- * into structured IntentSpecs using the Intent Architect approach.
6
- *
7
- * KEY PRINCIPLE: Intent Architect focuses on WHY + WHAT + CONSTRAINTS
8
- * It does NOT focus on HOW (mechanisms) - that's the Solver's job.
9
- *
10
- * - WHY: Business outcome, success metrics, risks
11
- * - WHAT: Deliverables, decisions, required evidence
12
- * - CONSTRAINTS: Hard requirements that limit solution space
13
- * - HOW: Deferred to Solver (which nodes, which channels)
14
- *
15
- * ## Progressive Enhancement (Additive, not Big Bang)
16
- *
17
- * Use `selectIntentStrategy()` to determine the appropriate level:
18
- *
19
- * 1. **SIMPLE**: Basic prompt, no qualification needed
20
- * - Clear goal, known channel, no approvals, no taxonomy
21
- * - Fast, cheap, skip Intent Architect
22
- *
23
- * 2. **MODERATE**: Some qualification needed
24
- * - Has approvals OR has taxonomy OR multiple intents
25
- * - Use Intent Architect to ask Gate 1-2 questions
26
- *
27
- * 3. **COMPLEX**: Full Intent Architect
28
- * - Custom approvals, taxonomy, multi-step decisions
29
- * - All 5 gates, full IntentSpec
30
- *
31
- * This allows gradual adoption - start simple, escalate as needed.
32
- */
33
- import * as fs from "fs";
34
- import * as path from "path";
35
- import { fileURLToPath } from "node:url";
36
- import { dirname } from "node:path";
37
- import { getResourcePath } from "../../sdk/paths.js";
38
- // ESM-compatible __dirname
39
- const __filename = fileURLToPath(import.meta.url);
40
- const __dirname = dirname(__filename);
41
- /** Cached gate definitions loaded from config */
42
- let cachedGateConfig = null;
43
- /**
44
- * Load gate definitions from config file.
45
- *
46
- * Config is loaded from resources/config/gates.json and cached.
47
- * Falls back to embedded defaults if config file is not found.
48
- *
49
- * @param forceReload - If true, reloads even if cached
50
- */
51
- export function loadGateConfig(forceReload = false) {
52
- if (cachedGateConfig && !forceReload) {
53
- return cachedGateConfig;
54
- }
55
- // Try multiple paths (works in both src and dist)
56
- const possiblePaths = [
57
- path.resolve(__dirname, "../../../resources/config/gates.json"),
58
- path.resolve(__dirname, "../../../../resources/config/gates.json"),
59
- getResourcePath("resources/config/gates.json"),
60
- ];
61
- for (const configPath of possiblePaths) {
62
- if (fs.existsSync(configPath)) {
63
- try {
64
- const content = fs.readFileSync(configPath, "utf-8");
65
- const config = JSON.parse(content);
66
- // Basic validation
67
- if (!config.version || !Array.isArray(config.gates) || config.gates.length === 0) {
68
- throw new Error("Invalid gate config: missing version or gates array");
69
- }
70
- // Validate each gate has required fields
71
- for (const gate of config.gates) {
72
- if (!gate.id || !gate.name || !gate.type || !gate.enforcement) {
73
- throw new Error(`Invalid gate ${gate.id}: missing required fields`);
74
- }
75
- }
76
- cachedGateConfig = config;
77
- return config;
78
- }
79
- catch (err) {
80
- throw new Error(`Failed to load gate config from ${configPath}: ${err}`);
81
- }
82
- }
83
- }
84
- // Fallback to embedded defaults (emergency only)
85
- console.warn("Gate config not found, using embedded defaults");
86
- cachedGateConfig = getDefaultGateConfig();
87
- return cachedGateConfig;
88
- }
89
- /** Embedded defaults - only used if config file not found */
90
- function getDefaultGateConfig() {
91
- return {
92
- version: "1.0.0-fallback",
93
- owner: "embedded",
94
- gates: [
95
- { id: 1, name: "Modality + Trigger + Consent", type: "blocking", enforcement: "required", base_weight: 0.6, asks: ["voice vs email vs chat", "inbound/outbound", "consent requirements"], blockers: ["channel_ambiguity", "consent_unknown"], triggers: ["voice", "call", "phone", "outbound", "inbound"], default_value: null },
96
- { id: 2, name: "Outcome + Qualification Definition", type: "blocking", enforcement: "required", base_weight: 0.6, asks: ["what 'qualified' means", "disqualifiers"], blockers: ["qualification_definition_missing", "outcome_unclear"], triggers: ["qualify", "qualified", "fit", "assess"], default_value: null },
97
- { id: 3, name: "Use-case Taxonomy + Content Scoping", type: "discovery", enforcement: "recommended", base_weight: 0.4, asks: ["taxonomy source", "KB sources"], blockers: ["taxonomy_unknown"], triggers: ["use-case", "category", "classify"], default_value: "general_category" },
98
- { id: 4, name: "Governance / HITL Semantics", type: "blocking", enforcement: "required", base_weight: 0.6, asks: ["who approves", "SLA", "fallback"], blockers: ["approver_unknown", "approval_semantics_unclear"], triggers: ["approval", "review", "hitl", "human"], default_value: null },
99
- { id: 5, name: "Integrations + Systems of Record", type: "informational", enforcement: "optional", base_weight: 0.2, asks: ["CRM", "email", "telephony"], blockers: ["integration_requirements_unknown"], triggers: ["salesforce", "crm", "hubspot"], default_value: "none" },
100
- ],
101
- };
102
- }
103
- /**
104
- * Get gate definitions as a record (for backward compatibility).
105
- * Loads from config file.
106
- */
107
- export function getGateDefinitions() {
108
- const config = loadGateConfig();
109
- const result = {};
110
- for (const gate of config.gates) {
111
- result[gate.id] = {
112
- id: gate.id,
113
- name: gate.name,
114
- type: gate.type,
115
- enforcement: gate.enforcement,
116
- asks: gate.asks,
117
- blockers: gate.blockers,
118
- triggers: gate.triggers,
119
- default_value: gate.default_value ?? undefined,
120
- };
121
- }
122
- return result;
123
- }
124
- // ─────────────────────────────────────────────────────────────────────────────
125
- // Prompt Loading
126
- // ─────────────────────────────────────────────────────────────────────────────
127
- let _cachedPrompt = null;
128
- /**
129
- * Load the Intent Architect prompt from the markdown file
130
- */
131
- export function getIntentArchitectPrompt() {
132
- if (_cachedPrompt)
133
- return _cachedPrompt;
134
- const promptPath = path.join(__dirname, "../../prompts/intent-architect.md");
135
- try {
136
- _cachedPrompt = fs.readFileSync(promptPath, "utf-8");
137
- return _cachedPrompt;
138
- }
139
- catch (e) {
140
- // Fallback: return embedded prompt summary
141
- console.warn("[IntentArchitect] Could not load prompt file, using embedded fallback");
142
- return EMBEDDED_PROMPT_SUMMARY;
143
- }
144
- }
145
- // Embedded fallback in case file isn't available (e.g., in bundled contexts)
146
- const EMBEDDED_PROMPT_SUMMARY = `You are "Intent Architect", an expert at turning vague user requests into a precise, testable IntentSpec.
147
-
148
- Your job is NOT to immediately generate a workflow. Your job is to:
149
- 1) Extract and refine intent (WHY → WHAT → HOW)
150
- 2) Identify missing qualification details
151
- 3) Ask minimal high-information questions
152
- 4) Produce a structured IntentSpec
153
- 5) Map to candidate graph skeleton only after critical unknowns resolved
154
-
155
- Key principles:
156
- - WHY = business outcome + success metrics + risks
157
- - WHAT = deliverables + decisions + required evidence
158
- - HOW = channel + policies + constraints + integrations + approvals
159
- - Never use platform jargon without translating
160
- - Make "use-case identification" explicit and concrete
161
-
162
- Output format: A) Intent Hypothesis, B) IntentSpec (YAML), C) Qualification Questions, D) Candidate Graph, E) Convergence Plan`;
163
- /**
164
- * Generate the user prompt for a specific request
165
- */
166
- export function generateIntentArchitectUserPrompt(userRequest, context) {
167
- let prompt = `## User Request\n\n"${userRequest}"\n\n`;
168
- if (context) {
169
- prompt += `## Known Context\n\n`;
170
- if (context.persona_type) {
171
- prompt += `- Interaction type: ${context.persona_type.toUpperCase()} (${context.persona_type === "voice" ? "phone/call" :
172
- context.persona_type === "chat" ? "text-based chat" : "dashboard/async"})\n`;
173
- }
174
- if (context.available_integrations?.length) {
175
- prompt += `- Available integrations: ${context.available_integrations.join(", ")}\n`;
176
- }
177
- if (context.existing_answers && Object.keys(context.existing_answers).length > 0) {
178
- prompt += `\n### Previously Answered Questions\n`;
179
- for (const [q, a] of Object.entries(context.existing_answers)) {
180
- prompt += `- ${q}: ${a}\n`;
181
- }
182
- }
183
- prompt += "\n";
184
- }
185
- if (context?.action_catalog) {
186
- prompt += `## Available Actions (for graph generation)\n\n${context.action_catalog}\n\n`;
187
- }
188
- prompt += `## Task
189
-
190
- Analyze this request and produce sections A–E as specified in your instructions.
191
-
192
- Focus on:
193
- 1. Understanding the real business goal (WHY), not just the stated mechanism
194
- 2. Identifying ALL decisions that need to be made and their required evidence
195
- 3. Making "use-case identification" concrete if mentioned
196
- 4. Specifying exactly who approves what, where, and with what SLA
197
- 5. Asking minimal, high-information questions tied to specific IntentSpec fields
198
-
199
- Return your response in the structured format (A through E).`;
200
- return prompt;
201
- }
202
- // ─────────────────────────────────────────────────────────────────────────────
203
- // Response Parsing
204
- // ─────────────────────────────────────────────────────────────────────────────
205
- /**
206
- * Parse the LLM response to extract structured data
207
- * Note: This is best-effort - the LLM output is semi-structured
208
- */
209
- export function parseIntentArchitectResponse(response) {
210
- const result = {};
211
- // Try to extract YAML from section B
212
- const yamlMatch = response.match(/```yaml\s*([\s\S]*?)\s*```/);
213
- if (yamlMatch) {
214
- try {
215
- // Note: In production, use a proper YAML parser
216
- // For now, just extract it for reference
217
- result.intent_spec = { _raw_yaml: yamlMatch[1] };
218
- }
219
- catch (e) {
220
- console.warn("[IntentArchitect] Could not parse YAML:", e);
221
- }
222
- }
223
- // Extract qualification questions
224
- const questions = [];
225
- const questionPattern = /\*\*Q(\d+)\*\*[:\s]*\*\*([^*]+)\*\*|Q(\d+)[:\s]+([^\n]+)/g;
226
- let match;
227
- while ((match = questionPattern.exec(response)) !== null) {
228
- const id = match[1] || match[3];
229
- const question = (match[2] || match[4])?.trim();
230
- if (id && question) {
231
- questions.push({
232
- id: `Q${id}`,
233
- question,
234
- tied_to_field: "", // Would need more sophisticated parsing
235
- category: "WHAT", // Default
236
- });
237
- }
238
- }
239
- if (questions.length > 0) {
240
- result.qualification_questions = questions;
241
- }
242
- // Check for deferred graph
243
- if (response.toLowerCase().includes("deferred") || response.toLowerCase().includes("blocking")) {
244
- result.candidate_graph = {
245
- status: "deferred",
246
- deferred_reason: "Critical unknowns must be resolved first",
247
- };
248
- }
249
- return result;
250
- }
251
- /**
252
- * Get the complete prompt package for Intent Architect
253
- */
254
- export function getIntentArchitectPromptPackage(userRequest, context) {
255
- return {
256
- system: getIntentArchitectPrompt(),
257
- user: generateIntentArchitectUserPrompt(userRequest, context),
258
- };
259
- }
260
- /**
261
- * Convert ComplexityAssessment to legacy IntentComplexity enum.
262
- */
263
- export function complexityAssessmentToEnum(assessment) {
264
- if (assessment.level < 0.34)
265
- return "simple";
266
- if (assessment.level < 0.67)
267
- return "moderate";
268
- return "complex";
269
- }
270
- /**
271
- * Convert legacy IntentComplexity enum to a ComplexityAssessment.
272
- */
273
- export function enumToComplexityAssessment(complexity, rationale = "Converted from legacy enum") {
274
- const levelMap = {
275
- simple: 0.2,
276
- moderate: 0.5,
277
- complex: 0.8,
278
- };
279
- return {
280
- level: levelMap[complexity],
281
- confidence: {
282
- kind: "confidence",
283
- value: 0.7, // Medium confidence for enum-based detection
284
- scale: "0_1",
285
- rationale: "Converted from legacy enum",
286
- },
287
- signals: [],
288
- blockers: [],
289
- rationale,
290
- };
291
- }
292
- /**
293
- * Gate definitions loaded from config.
294
- *
295
- * @deprecated Use getGateDefinitions() instead - this is kept for backward compatibility.
296
- * Gate definitions are now loaded from resources/config/gates.json
297
- */
298
- export const GATE_DEFINITIONS = (() => {
299
- // Lazy initialization from config
300
- try {
301
- return getGateDefinitions();
302
- }
303
- catch {
304
- // Return minimal fallback if config loading fails at module load time
305
- return {
306
- 1: { id: 1, name: "Modality", type: "blocking", enforcement: "required", asks: [], blockers: [], triggers: [] },
307
- 2: { id: 2, name: "Qualification", type: "blocking", enforcement: "required", asks: [], blockers: [], triggers: [] },
308
- 3: { id: 3, name: "Taxonomy", type: "discovery", enforcement: "recommended", asks: [], blockers: [], triggers: [] },
309
- 4: { id: 4, name: "Governance", type: "blocking", enforcement: "required", asks: [], blockers: [], triggers: [] },
310
- 5: { id: 5, name: "Integrations", type: "informational", enforcement: "optional", asks: [], blockers: [], triggers: [] },
311
- };
312
- }
313
- })();
314
- /**
315
- * Default base weights by enforcement level.
316
- * These are used if a gate doesn't specify a base_weight in config.
317
- */
318
- const DEFAULT_BASE_WEIGHTS = {
319
- required: 0.6,
320
- recommended: 0.4,
321
- optional: 0.2,
322
- };
323
- /**
324
- * Compute weights for a gate based on input and context.
325
- *
326
- * Uses gate config from resources/config/gates.json for:
327
- * - base_weight (or defaults based on enforcement level)
328
- * - triggers (signals that boost weight)
329
- */
330
- export function computeGateWeights(gateId, input, context) {
331
- const config = loadGateConfig();
332
- const gateConfig = config.gates.find(g => g.id === gateId);
333
- const gate = getGateDefinitions()[gateId];
334
- if (!gate)
335
- return { base: 0, signal_boost: 0, context_multiplier: 1, final_score: 0 };
336
- const inputLower = input.toLowerCase();
337
- // Base weight from config (or default based on enforcement)
338
- const base = gateConfig?.base_weight ?? DEFAULT_BASE_WEIGHTS[gate.enforcement];
339
- // Signal boost: count triggers found, normalize to 0-1
340
- const triggersFound = gate.triggers.filter(t => inputLower.includes(t.toLowerCase()));
341
- const signal_boost = Math.min(1.0, triggersFound.length * 0.2);
342
- // Context multiplier
343
- let context_multiplier = 1.0;
344
- if (context) {
345
- // Voice personas boost Gate 1 (modality)
346
- if (context.persona_type === "voice" && gateId === 1) {
347
- context_multiplier *= 1.5;
348
- }
349
- // Compliance mode boosts Gate 4 (governance)
350
- if (context.compliance_mode && gateId === 4) {
351
- context_multiplier *= 2.0;
352
- }
353
- // Outbound mode boosts Gate 1 (consent)
354
- if (context.outbound_mode && gateId === 1) {
355
- context_multiplier *= 1.5;
356
- }
357
- // Custom multipliers
358
- if (context.custom_multipliers?.[gateId]) {
359
- context_multiplier *= context.custom_multipliers[gateId];
360
- }
361
- }
362
- // Final score
363
- const final_score = base * (1 + signal_boost) * context_multiplier;
364
- return { base, signal_boost, context_multiplier, final_score };
365
- }
366
- /**
367
- * Get gates with computed weights, sorted by final score.
368
- * Loads gate definitions from config.
369
- */
370
- export function getGatesWithWeights(input, context) {
371
- const gates = getGateDefinitions();
372
- return Object.values(gates)
373
- .map(gate => ({
374
- ...gate,
375
- weights: computeGateWeights(gate.id, input, context),
376
- }))
377
- .sort((a, b) => b.weights.final_score - a.weights.final_score);
378
- }
379
- /**
380
- * Get gates that should be asked (score > threshold)
381
- */
382
- export function getRequiredGates(input, context, threshold = 0.5) {
383
- return getGatesWithWeights(input, context)
384
- .filter(gate => gate.weights.final_score >= threshold || gate.type === "blocking");
385
- }
386
- /**
387
- * Get discovery questions - gates where score is moderate (uncertain)
388
- * These trigger more exploration rather than blocking
389
- */
390
- export function getDiscoveryGates(input, context, uncertaintyRange = { min: 0.3, max: 0.7 }) {
391
- return getGatesWithWeights(input, context)
392
- .filter(gate => gate.type === "discovery" &&
393
- gate.weights.final_score >= uncertaintyRange.min &&
394
- gate.weights.final_score <= uncertaintyRange.max);
395
- }
396
- // ─────────────────────────────────────────────────────────────────────────────
397
- // LLM-DRIVEN DETECTION (Primary)
398
- // ─────────────────────────────────────────────────────────────────────────────
399
- /**
400
- * Prompt for LLM-based intent signal detection.
401
- *
402
- * The caller should send this to their LLM and parse the response
403
- * using parseComplexitySignalsFromLLM().
404
- */
405
- export function getComplexityDetectionPrompt(input) {
406
- return {
407
- system: `You are an intent classifier. Analyze the user request and detect INTENT signals (WHY/WHAT), NOT mechanisms (HOW).
408
-
409
- INTENT signals to detect:
410
- - governance: needs human approval, review, oversight, sign-off
411
- - decision: needs classification, categorization, routing, triage
412
- - qualification: needs evaluation, assessment, scoring, validation
413
- - multi_step: multiple sequential actions, outcomes
414
- - conditional: branching logic, different paths based on conditions
415
- - extraction: capture structured data, collect information
416
-
417
- DO NOT detect mechanisms (these are HOW, not WHY/WHAT):
418
- - "email" → mechanism, not intent
419
- - "Salesforce" → mechanism, not intent
420
- - "Slack" → mechanism, not intent
421
-
422
- Return JSON only:
423
- {
424
- "governance": true/false,
425
- "decision": true/false,
426
- "qualification": true/false,
427
- "multi_step": true/false,
428
- "conditional": true/false,
429
- "extraction": true/false,
430
- "reasoning": "brief explanation"
431
- }`,
432
- user: `Analyze this request for INTENT signals:\n\n"${input}"`,
433
- };
434
- }
435
- /**
436
- * Parse LLM response for complexity signals.
437
- */
438
- export function parseComplexitySignalsFromLLM(llmResponse) {
439
- try {
440
- // Try to extract JSON from response
441
- const jsonMatch = llmResponse.match(/\{[\s\S]*\}/);
442
- if (!jsonMatch)
443
- return null;
444
- const parsed = JSON.parse(jsonMatch[0]);
445
- const signals = {
446
- has_governance_requirement: parsed.governance === true,
447
- has_decision_requirement: parsed.decision === true,
448
- has_qualification_requirement: parsed.qualification === true,
449
- has_multi_step: parsed.multi_step === true,
450
- has_conditional_logic: parsed.conditional === true,
451
- has_extraction_requirement: parsed.extraction === true,
452
- complexity: "simple",
453
- reason: parsed.reasoning || "",
454
- required_gates: [],
455
- blockers: [],
456
- safe_defaults: {},
457
- unsafe_defaults: [],
458
- detection_method: "llm",
459
- };
460
- // Compute complexity from signals
461
- computeComplexityFromSignals(signals);
462
- return signals;
463
- }
464
- catch {
465
- return null;
466
- }
467
- }
468
- /**
469
- * Compute complexity level from detected signals.
470
- * Populates: complexity, reason, required_gates, blockers, safe_defaults, unsafe_defaults
471
- */
472
- function computeComplexityFromSignals(signals) {
473
- // Compute individual signal weights (using TypedScore)
474
- // Weights are calibrated so that:
475
- // - Any single high-priority signal (governance, decision, qualification) → moderate (0.4+)
476
- // - Two or more high-priority signals → complex (0.7+)
477
- const signalScores = [];
478
- if (signals.has_governance_requirement) {
479
- signalScores.push({ kind: "weight", value: 0.4, scale: "0_1", rationale: "governance requirement detected" });
480
- }
481
- if (signals.has_decision_requirement) {
482
- signalScores.push({ kind: "weight", value: 0.4, scale: "0_1", rationale: "decision/classification requirement detected" });
483
- }
484
- if (signals.has_qualification_requirement) {
485
- signalScores.push({ kind: "weight", value: 0.35, scale: "0_1", rationale: "qualification requirement detected" });
486
- }
487
- if (signals.has_multi_step) {
488
- signalScores.push({ kind: "weight", value: 0.2, scale: "0_1", rationale: "multi-step flow detected" });
489
- }
490
- if (signals.has_conditional_logic) {
491
- signalScores.push({ kind: "weight", value: 0.2, scale: "0_1", rationale: "conditional logic detected" });
492
- }
493
- if (signals.has_extraction_requirement) {
494
- signalScores.push({ kind: "weight", value: 0.15, scale: "0_1", rationale: "data extraction requirement detected" });
495
- }
496
- // Compute continuous complexity level (0-1)
497
- const totalWeight = signalScores.reduce((sum, s) => sum + s.value, 0);
498
- const complexityLevel = Math.min(1.0, totalWeight); // Cap at 1.0
499
- // Initialize arrays
500
- signals.blockers = [];
501
- signals.safe_defaults = {};
502
- signals.unsafe_defaults = [];
503
- signals.required_gates = [];
504
- // Determine blockers based on detected signals
505
- if (signals.has_governance_requirement) {
506
- signals.blockers.push("approver_unknown", "approval_semantics_unclear");
507
- signals.unsafe_defaults.push("who approves", "approval channel", "SLA");
508
- signals.required_gates.push(4);
509
- }
510
- if (signals.has_decision_requirement) {
511
- signals.blockers.push("taxonomy_unknown", "classification_criteria_unclear");
512
- signals.unsafe_defaults.push("use-case categories", "taxonomy source/owner");
513
- signals.required_gates.push(3);
514
- }
515
- if (signals.has_qualification_requirement) {
516
- signals.blockers.push("qualification_definition_missing");
517
- signals.unsafe_defaults.push("qualification rubric", "what 'fit' means");
518
- signals.required_gates.push(2);
519
- }
520
- if (signals.has_multi_step || signals.has_conditional_logic) {
521
- // Multi-step usually needs outcome definition
522
- if (!signals.required_gates.includes(2))
523
- signals.required_gates.push(2);
524
- }
525
- // Safe defaults (can be assumed without asking)
526
- signals.safe_defaults = {
527
- "tone": "professional",
528
- "retry_policy": "none",
529
- "page_size": "10",
530
- };
531
- // Determine complexity level (legacy enum + new continuous score)
532
- // Thresholds: simple < 0.34, moderate 0.34-0.66, complex >= 0.67
533
- if (complexityLevel >= 0.67 ||
534
- (signals.has_governance_requirement && signals.has_decision_requirement) ||
535
- (signals.has_governance_requirement && signals.has_qualification_requirement)) {
536
- signals.complexity = "complex";
537
- if (!signals.reason) {
538
- signals.reason = `High uncertainty: ${signals.blockers.length} blockers require resolution`;
539
- }
540
- // Complex = all gates
541
- signals.required_gates = [1, 2, 3, 4, 5];
542
- }
543
- else if (complexityLevel >= 0.34) {
544
- signals.complexity = "moderate";
545
- if (!signals.reason) {
546
- signals.reason = `Moderate uncertainty: ${signals.blockers.length} blockers on specific requirements`;
547
- }
548
- // Add Gate 1 (modality) if not already included
549
- if (!signals.required_gates.includes(1))
550
- signals.required_gates.unshift(1);
551
- // Sort gates
552
- signals.required_gates.sort((a, b) => a - b);
553
- }
554
- else {
555
- signals.complexity = "simple";
556
- if (!signals.reason)
557
- signals.reason = "Low uncertainty - basic workflow generation sufficient";
558
- signals.required_gates = [];
559
- signals.blockers = [];
560
- signals.unsafe_defaults = [];
561
- }
562
- // Backward compatibility
563
- signals.recommended_gates = signals.required_gates;
564
- // Populate typed assessment (new preferred structure)
565
- const confidenceValue = signals.detection_method === "llm" ? 0.85 : 0.6;
566
- signals.assessment = {
567
- level: complexityLevel,
568
- confidence: {
569
- kind: "confidence",
570
- value: confidenceValue,
571
- scale: "0_1",
572
- rationale: signals.detection_method === "llm"
573
- ? "LLM-based detection with semantic understanding"
574
- : "Regex-based fallback with limited semantic understanding",
575
- },
576
- signals: signalScores,
577
- blockers: signals.blockers,
578
- rationale: signals.reason,
579
- };
580
- }
581
- // ─────────────────────────────────────────────────────────────────────────────
582
- // REGEX FALLBACK (Offline / Emergency)
583
- // ─────────────────────────────────────────────────────────────────────────────
584
- /**
585
- * Regex-based fallback for intent signal detection.
586
- *
587
- * ⚠️ This is a FALLBACK for when LLM is unavailable.
588
- * Prefer using getComplexityDetectionPrompt() + parseComplexitySignalsFromLLM().
589
- *
590
- * Limitations:
591
- * - Doesn't understand context or semantics
592
- * - Can't handle synonyms or variations
593
- * - May miss subtle intent signals
594
- */
595
- export function analyzeComplexityFallback(input) {
596
- const signals = {
597
- has_governance_requirement: /\b(approval|approve|review|hitl|human\s+(review|approval|oversight)|sign.?off|authoriz|permission|consent|oversight)\b/i.test(input),
598
- has_decision_requirement: /\b(use.?case|category|categories|categorize|classify|classification|route|routing|determine|decide|triage|segment|bucket)\b/i.test(input),
599
- has_multi_step: /\b(and then|after that|next step|follow.?up|subsequently|finally)\b/i.test(input) ||
600
- (input.split(/\band\b/i).length > 2 && input.length > 60),
601
- has_conditional_logic: /\b(if|when|depending|based on|conditional|branch|otherwise|either.*or)\b/i.test(input),
602
- has_extraction_requirement: /\b(capture|extract|collect|gather|record|log|document|note)\b/i.test(input),
603
- has_qualification_requirement: /\b(qualify|qualifies|qualifying|qualification|evaluate|evaluates|assess|assessment|score|scoring|validate|verify|check if|determine if|eligible)\b/i.test(input),
604
- complexity: "simple",
605
- reason: "",
606
- required_gates: [],
607
- blockers: [],
608
- safe_defaults: {},
609
- unsafe_defaults: [],
610
- detection_method: "regex_fallback",
611
- };
612
- computeComplexityFromSignals(signals);
613
- return signals;
614
- }
615
- /**
616
- * Analyze complexity - uses regex fallback by default.
617
- *
618
- * For LLM-driven detection (recommended), use:
619
- * 1. getComplexityDetectionPrompt(input) → send to LLM
620
- * 2. parseComplexitySignalsFromLLM(response) → get signals
621
- *
622
- * This function is for backward compatibility and offline scenarios.
623
- */
624
- export function analyzeComplexity(input) {
625
- return analyzeComplexityFallback(input);
626
- }
627
- /**
628
- * Select the appropriate intent strategy based on complexity.
629
- *
630
- * Supports iterative refinement:
631
- * - Pass 1: Detect complexity, ask blocker questions only
632
- * - Pass 2+: Re-evaluate with previous answers, ask remaining questions
633
- *
634
- * Recommended flow for LLM-driven detection:
635
- * ```typescript
636
- * // 1. Get LLM prompt and send to your LLM
637
- * const prompt = getComplexityDetectionPrompt(input);
638
- * const llmResponse = await yourLLM.complete(prompt);
639
- *
640
- * // 2. Parse response
641
- * const signals = parseComplexitySignalsFromLLM(llmResponse);
642
- *
643
- * // 3. Pass to strategy selector
644
- * const strategy = selectIntentStrategy(input, context, { precomputed_signals: signals });
645
- *
646
- * // 4. After user answers, re-evaluate with answers
647
- * const strategy2 = selectIntentStrategy(input, context, {
648
- * precomputed_signals: signals,
649
- * iteration: 2,
650
- * previous_answers: { taxonomy: "provided", approver: "manager" }
651
- * });
652
- * ```
653
- *
654
- * If no precomputed_signals provided, falls back to regex detection.
655
- */
656
- export function selectIntentStrategy(input, context, options) {
657
- // Use precomputed signals (from LLM) or fall back to regex
658
- let signals = options?.precomputed_signals ?? analyzeComplexityFallback(input);
659
- // Apply overrides
660
- if (options?.force_complexity) {
661
- signals = { ...signals, complexity: options.force_complexity };
662
- }
663
- if (options?.max_complexity) {
664
- const levels = ["simple", "moderate", "complex"];
665
- const maxIdx = levels.indexOf(options.max_complexity);
666
- const currentIdx = levels.indexOf(signals.complexity);
667
- if (currentIdx > maxIdx) {
668
- signals = { ...signals, complexity: options.max_complexity };
669
- }
670
- }
671
- const iteration = options?.iteration ?? 1;
672
- const previousAnswers = options?.previous_answers ?? {};
673
- // ═══════════════════════════════════════════════════════════════════════════
674
- // BLOCKER-FIRST ITERATION
675
- // - If blockers exist → ask only blockers (high-information questions)
676
- // - If blockers resolved → proceed to compile
677
- // ═══════════════════════════════════════════════════════════════════════════
678
- // Filter out blockers that have been answered
679
- const unresolvedBlockers = signals.blockers.filter(blocker => {
680
- // Check if this blocker has been resolved by previous answers
681
- const resolved = Object.keys(previousAnswers).some(key => blocker.toLowerCase().includes(key.toLowerCase()) ||
682
- key.toLowerCase().includes(blocker.replace(/_/g, " ").split(" ")[0]));
683
- return !resolved;
684
- });
685
- // Determine which gates are needed based on unresolved blockers
686
- const gatesToAsk = determineGatesForBlockers(unresolvedBlockers, signals.required_gates);
687
- // Determine approach based on complexity and iteration
688
- const approach = determineApproach(signals.complexity, unresolvedBlockers.length, iteration);
689
- switch (approach) {
690
- case "template_fill":
691
- return {
692
- complexity: signals.complexity,
693
- approach: "template_fill",
694
- gates_to_ask: [],
695
- blockers: [],
696
- safe_defaults: signals.safe_defaults,
697
- reason: signals.reason,
698
- signals,
699
- iteration,
700
- previous_answers: previousAnswers,
701
- };
702
- case "guided_architect":
703
- return {
704
- complexity: signals.complexity,
705
- approach: "guided_architect",
706
- gates_to_ask: gatesToAsk.slice(0, 3), // Max 3 gates for guided
707
- blockers: unresolvedBlockers.slice(0, 3), // Prioritize top 3 blockers
708
- safe_defaults: signals.safe_defaults,
709
- prompt_package: getIntentArchitectPromptPackage(input, context),
710
- reason: signals.reason,
711
- signals,
712
- iteration,
713
- previous_answers: previousAnswers,
714
- };
715
- case "full_architect":
716
- return {
717
- complexity: signals.complexity,
718
- approach: "full_architect",
719
- gates_to_ask: gatesToAsk,
720
- blockers: unresolvedBlockers,
721
- safe_defaults: signals.safe_defaults,
722
- prompt_package: getIntentArchitectPromptPackage(input, context),
723
- reason: signals.reason,
724
- signals,
725
- iteration,
726
- previous_answers: previousAnswers,
727
- };
728
- }
729
- }
730
- /**
731
- * Determine which gates to ask based on unresolved blockers.
732
- * Uses blocker-to-gate mapping from gate config.
733
- */
734
- function determineGatesForBlockers(blockers, requiredGates) {
735
- if (blockers.length === 0)
736
- return [];
737
- const neededGates = new Set();
738
- const gates = getGateDefinitions();
739
- for (const blocker of blockers) {
740
- // Check which gate this blocker belongs to
741
- for (const [gateNum, gateDef] of Object.entries(gates)) {
742
- if (gateDef.blockers.some(b => blocker.includes(b.split("_")[0]) || b.includes(blocker.split("_")[0]))) {
743
- neededGates.add(parseInt(gateNum));
744
- }
745
- }
746
- }
747
- // If we couldn't map blockers to gates, use required_gates
748
- if (neededGates.size === 0) {
749
- return requiredGates;
750
- }
751
- return Array.from(neededGates).sort((a, b) => a - b);
752
- }
753
- /**
754
- * Determine the approach based on complexity, blockers, and iteration.
755
- *
756
- * PRINCIPLE: Always use LLM for first iteration. Keyword-based complexity
757
- * assessment is a heuristic hint, not a decision maker. The LLM prompt
758
- * (Intent Architect) does the real semantic analysis.
759
- */
760
- function determineApproach(complexity, unresolvedBlockerCount, iteration) {
761
- // FIRST ITERATION: Always use LLM analysis
762
- // The LLM (Intent Architect prompt) does semantic reasoning about:
763
- // - Side effects (email, API calls, external communication)
764
- // - HITL requirements
765
- // - Governance needs
766
- // Keyword matching can't reliably detect these.
767
- if (iteration === 1) {
768
- // Even "simple" requests get LLM analysis on first pass
769
- // LLM may confirm it's simple, or may surface hidden complexity
770
- return complexity === "simple" ? "guided_architect" : "full_architect";
771
- }
772
- // SUBSEQUENT ITERATIONS: After LLM has analyzed, we can trust its assessment
773
- if (unresolvedBlockerCount === 0) {
774
- return "template_fill";
775
- }
776
- switch (complexity) {
777
- case "simple":
778
- // LLM already analyzed, few blockers remain
779
- return unresolvedBlockerCount <= 2 ? "guided_architect" : "full_architect";
780
- case "moderate":
781
- return unresolvedBlockerCount <= 3 ? "guided_architect" : "full_architect";
782
- case "complex":
783
- return unresolvedBlockerCount <= 3 ? "guided_architect" : "full_architect";
784
- }
785
- }
786
- /**
787
- * Single canonical entrypoint for intent processing.
788
- *
789
- * Handler should call ONLY this function. It encapsulates:
790
- * 1. Complexity detection (with typed scores)
791
- * 2. Strategy selection (which approach to take)
792
- * 3. Gate qualification (which questions to ask)
793
- * 4. Prompt generation (if full architect needed)
794
- *
795
- * @example
796
- * ```typescript
797
- * // Basic usage
798
- * const result = runIntentArchitect("Voice AI SDR that qualifies leads", {
799
- * persona_type: "voice"
800
- * });
801
- *
802
- * if (result.strategy.can_proceed) {
803
- * // Simple enough - proceed to generation
804
- * } else if (result.questions) {
805
- * // Ask qualification questions
806
- * const answers = await askUser(result.questions);
807
- * // Re-run with answers
808
- * const result2 = runIntentArchitect(input, context, {
809
- * previous_answers: answers,
810
- * iteration: 2
811
- * });
812
- * } else if (result.prompt_package) {
813
- * // Full architect - send prompt to LLM
814
- * const intentSpec = await llm.complete(result.prompt_package);
815
- * }
816
- * ```
817
- */
818
- export function runIntentArchitect(input, context, options) {
819
- // 1. Get complexity signals (from LLM or fallback)
820
- const signals = options?.precomputed_signals ?? analyzeComplexityFallback(input);
821
- // 2. Get strategy using existing selectIntentStrategy
822
- const strategy = selectIntentStrategy(input, context, {
823
- precomputed_signals: signals,
824
- max_complexity: options?.max_complexity,
825
- previous_answers: options?.previous_answers,
826
- iteration: options?.iteration,
827
- });
828
- // 3. Build assessment from signals
829
- const assessment = signals.assessment ?? {
830
- level: signals.complexity === "simple" ? 0.2 : signals.complexity === "moderate" ? 0.5 : 0.8,
831
- confidence: {
832
- kind: "confidence",
833
- value: signals.detection_method === "llm" ? 0.85 : 0.6,
834
- scale: "0_1",
835
- rationale: signals.detection_method === "llm"
836
- ? "LLM-based detection"
837
- : "Regex-based fallback",
838
- },
839
- signals: [],
840
- blockers: signals.blockers,
841
- rationale: signals.reason,
842
- };
843
- const iteration = options?.iteration ?? 1;
844
- // FIRST ITERATION: Always use LLM analysis
845
- // Regex-based signals are hints only - the LLM does real semantic reasoning
846
- // about side effects, governance needs, HITL requirements, etc.
847
- //
848
- // EXCEPTION: If max_complexity is explicitly set to "simple", the caller
849
- // is signaling they've already done analysis or want to bypass LLM.
850
- const explicitlySimple = options?.max_complexity === "simple";
851
- const forceUllmAnalysis = iteration === 1 && !explicitlySimple;
852
- // 4. Build strategy decision
853
- const strategyDecision = {
854
- approach: forceUllmAnalysis && strategy.approach === "template_fill"
855
- ? "guided_architect" // Upgrade to LLM analysis on first iteration
856
- : strategy.approach,
857
- gates_to_ask: strategy.gates_to_ask,
858
- blockers: signals.blockers.filter(b => !Object.keys(options?.previous_answers ?? {}).some(a => b.includes(a))),
859
- // First iteration: NEVER proceed without LLM analysis
860
- can_proceed: !forceUllmAnalysis && (strategy.approach === "template_fill" || strategy.gates_to_ask.length === 0),
861
- next_step: forceUllmAnalysis
862
- ? "Send prompt to LLM for intent analysis (first iteration always uses LLM)"
863
- : strategy.approach === "template_fill"
864
- ? "Proceed to workflow generation"
865
- : strategy.approach === "guided_architect"
866
- ? `Ask qualification questions for gates: ${strategy.gates_to_ask.join(", ")}`
867
- : "Send prompt to LLM for full intent decomposition",
868
- };
869
- // 5. Build qualification questions from gates
870
- let questions;
871
- if (strategy.gates_to_ask.length > 0 && strategy.approach !== "full_architect" && !forceUllmAnalysis) {
872
- questions = buildQualificationQuestions(strategy.gates_to_ask, signals);
873
- }
874
- // 6. Build result
875
- const result = {
876
- assessment,
877
- strategy: strategyDecision,
878
- questions,
879
- legacy: {
880
- complexity: strategy.complexity,
881
- signals,
882
- },
883
- };
884
- // Include prompt package for LLM analysis (first iteration or full architect)
885
- if (forceUllmAnalysis || (strategy.approach === "full_architect" && strategy.prompt_package)) {
886
- result.prompt_package = strategy.prompt_package ?? getIntentArchitectPromptPackage(input, context);
887
- }
888
- return result;
889
- }
890
- /**
891
- * Build qualification questions from gate IDs
892
- */
893
- function buildQualificationQuestions(gateIds, signals) {
894
- const questions = [];
895
- const gates = getGateDefinitions();
896
- for (const gateId of gateIds) {
897
- const gate = gates[gateId];
898
- if (!gate)
899
- continue;
900
- // Create a question for each "ask" in the gate
901
- for (let i = 0; i < gate.asks.length; i++) {
902
- const ask = gate.asks[i];
903
- questions.push({
904
- id: `gate${gateId}_q${i + 1}`,
905
- gate: gateId,
906
- question: ask,
907
- category: gateId <= 2 ? "WHAT" : gateId <= 4 ? "HOW" : "HOW",
908
- answer_type: "text",
909
- blocking: gate.type === "blocking",
910
- });
911
- }
912
- }
913
- return questions;
914
- }