@kairos-sdk/core 0.3.2 → 0.4.5

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.
package/dist/index.cjs CHANGED
@@ -559,17 +559,31 @@ var N8nValidator = class {
559
559
  this.checkRule21(workflow, issues);
560
560
  this.checkRule22(workflow, issues);
561
561
  this.checkRule23(workflow, issues);
562
+ this.checkRule24(workflow, issues);
563
+ this.checkRule25(workflow, issues);
564
+ this.checkRule26(workflow, issues);
565
+ if (Array.isArray(workflow.nodes)) {
566
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
567
+ for (const issue of issues) {
568
+ if (issue.nodeId && !issue.nodeType) {
569
+ const nt = nodeById.get(issue.nodeId);
570
+ if (nt) issue.nodeType = nt;
571
+ }
572
+ }
573
+ }
562
574
  const errors = issues.filter((i) => i.severity === "error");
563
575
  return { valid: errors.length === 0, issues };
564
576
  }
565
- err(issues, rule, message, nodeId) {
577
+ err(issues, rule, message, nodeId, nodeType) {
566
578
  const issue = { rule, severity: "error", message };
567
579
  if (nodeId !== void 0) issue.nodeId = nodeId;
580
+ if (nodeType !== void 0) issue.nodeType = nodeType;
568
581
  issues.push(issue);
569
582
  }
570
- warn(issues, rule, message, nodeId) {
583
+ warn(issues, rule, message, nodeId, nodeType) {
571
584
  const issue = { rule, severity: "warn", message };
572
585
  if (nodeId !== void 0) issue.nodeId = nodeId;
586
+ if (nodeType !== void 0) issue.nodeType = nodeType;
573
587
  issues.push(issue);
574
588
  }
575
589
  isTriggerNode(node) {
@@ -680,10 +694,14 @@ var N8nValidator = class {
680
694
  checkRule11(w, issues) {
681
695
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
682
696
  const reachable = /* @__PURE__ */ new Set();
683
- for (const [, outputs] of Object.entries(w.connections)) {
697
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
698
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
684
699
  if (typeof outputs !== "object" || outputs === null) continue;
685
- for (const portGroup of Object.values(outputs)) {
700
+ let hasAiPort = false;
701
+ for (const [portName, portGroup] of Object.entries(outputs)) {
686
702
  if (!Array.isArray(portGroup)) continue;
703
+ const isAiPort = portName.startsWith("ai_");
704
+ if (isAiPort) hasAiPort = true;
687
705
  for (const targets of portGroup) {
688
706
  if (!Array.isArray(targets)) continue;
689
707
  for (const target of targets) {
@@ -692,10 +710,13 @@ var N8nValidator = class {
692
710
  }
693
711
  }
694
712
  }
713
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
695
714
  }
696
715
  for (const node of w.nodes) {
697
716
  if (node.type.includes("stickyNote")) continue;
698
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
717
+ if (this.isTriggerNode(node)) continue;
718
+ if (aiSubNodeSources.has(node.name)) continue;
719
+ if (!reachable.has(node.name)) {
699
720
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
700
721
  }
701
722
  }
@@ -891,6 +912,76 @@ var N8nValidator = class {
891
912
  }
892
913
  }
893
914
  }
915
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
916
+ checkRule24(w, issues) {
917
+ if (!Array.isArray(w.nodes)) return;
918
+ const deprecated = /\$node\s*\[/;
919
+ for (const node of w.nodes) {
920
+ for (const expr of this.extractExpressions(node.parameters)) {
921
+ if (deprecated.test(expr)) {
922
+ this.warn(
923
+ issues,
924
+ 24,
925
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
926
+ node.id
927
+ );
928
+ break;
929
+ }
930
+ }
931
+ }
932
+ }
933
+ // Rule 25 (WARN): wrong item index assumptions in expressions
934
+ checkRule25(w, issues) {
935
+ if (!Array.isArray(w.nodes)) return;
936
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
937
+ for (const node of w.nodes) {
938
+ for (const expr of this.extractExpressions(node.parameters)) {
939
+ if (itemIndex.test(expr)) {
940
+ this.warn(
941
+ issues,
942
+ 25,
943
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
944
+ node.id
945
+ );
946
+ break;
947
+ }
948
+ }
949
+ }
950
+ }
951
+ // Rule 26 (WARN): missing .first() or .all() on node references
952
+ checkRule26(w, issues) {
953
+ if (!Array.isArray(w.nodes)) return;
954
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
955
+ for (const node of w.nodes) {
956
+ for (const expr of this.extractExpressions(node.parameters)) {
957
+ if (bareRef.test(expr)) {
958
+ this.warn(
959
+ issues,
960
+ 26,
961
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
962
+ node.id
963
+ );
964
+ break;
965
+ }
966
+ }
967
+ }
968
+ }
969
+ extractExpressions(params) {
970
+ const expressions = [];
971
+ const walk = (val) => {
972
+ if (typeof val === "string") {
973
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
974
+ expressions.push(val);
975
+ }
976
+ } else if (Array.isArray(val)) {
977
+ for (const item of val) walk(item);
978
+ } else if (val !== null && typeof val === "object") {
979
+ for (const v of Object.values(val)) walk(v);
980
+ }
981
+ };
982
+ walk(params);
983
+ return expressions;
984
+ }
894
985
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
895
986
  checkRule21(w, issues) {
896
987
  if (!Array.isArray(w.nodes)) return;
@@ -932,14 +1023,23 @@ var ResponseParseError = class extends KairosError {
932
1023
 
933
1024
  // src/errors/validation-error.ts
934
1025
  var ValidationError = class extends KairosError {
935
- constructor(message, issues) {
1026
+ constructor(message, issues, attemptMetadata, warnedRules) {
936
1027
  super(message);
937
1028
  this.issues = issues;
1029
+ this.attemptMetadata = attemptMetadata;
1030
+ this.warnedRules = warnedRules;
938
1031
  this.name = "ValidationError";
939
1032
  }
940
1033
  issues;
1034
+ attemptMetadata;
1035
+ warnedRules;
941
1036
  };
942
1037
 
1038
+ // src/generation/prompt-builder.ts
1039
+ var import_node_fs = require("fs");
1040
+ var import_node_path = require("path");
1041
+ var import_node_os = require("os");
1042
+
943
1043
  // src/generation/prompts/v1.ts
944
1044
  var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
945
1045
 
@@ -969,9 +1069,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
969
1069
  - Never reuse IDs, never use sequential fake IDs like "node-1"
970
1070
 
971
1071
  ### Credentials:
972
- - Only reference credentials with exact type names (see catalog below)
973
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
974
- - Never put API keys or tokens in parameters when a credential type exists
1072
+ - Each credential is keyed by its type string, with an object value containing id and name:
1073
+ "credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
1074
+ - Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
1075
+ - The credentialsNeeded field in your response declares what credentials the user must configure
1076
+ - Never put API keys or tokens directly in node parameters when a credential type exists
975
1077
 
976
1078
  ### Node names:
977
1079
  - All node names must be unique within the workflow
@@ -1018,6 +1120,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1018
1120
 
1019
1121
  ---
1020
1122
 
1123
+ ## EXPRESSION SYNTAX \u2014 how to reference upstream node data
1124
+
1125
+ ### Accessing a field from an upstream node:
1126
+ - CORRECT: $('NodeName').item.json.field
1127
+ - WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
1128
+
1129
+ ### Accessing array items from $json:
1130
+ - CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
1131
+ - WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
1132
+
1133
+ ### Calling node data \u2014 always qualify with .first() or .all():
1134
+ - CORRECT: $('NodeName').first().json.field \u2190 single item
1135
+ - CORRECT: $('NodeName').all() \u2190 array of all items
1136
+ - WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
1137
+
1138
+ ---
1139
+
1021
1140
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1022
1141
 
1023
1142
  ### Triggers (always at least one required):
@@ -1117,6 +1236,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1117
1236
  5. At least one trigger node present
1118
1237
  6. Every AI Agent has an ai_languageModel sub-node
1119
1238
  7. settings block is complete with executionOrder: "v1"
1239
+ 8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
1240
+ 9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
1241
+ 10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
1120
1242
 
1121
1243
  ---
1122
1244
 
@@ -1132,8 +1254,55 @@ function scoreToMode(score) {
1132
1254
  return "scratch";
1133
1255
  }
1134
1256
 
1135
- // src/generation/prompt-builder.ts
1136
- var RULE_REMEDIES = {
1257
+ // src/validation/rule-metadata.ts
1258
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
1259
+ var RULE_PIPELINE_STAGES = {
1260
+ 1: "node_generation",
1261
+ 2: "node_generation",
1262
+ 3: "node_generation",
1263
+ 4: "node_generation",
1264
+ 5: "node_generation",
1265
+ 6: "node_generation",
1266
+ 7: "node_generation",
1267
+ 8: "node_generation",
1268
+ 9: "connection_wiring",
1269
+ 10: "connection_wiring",
1270
+ 11: "connection_wiring",
1271
+ 12: "workflow_structure",
1272
+ 13: "node_generation",
1273
+ 14: "workflow_structure",
1274
+ 15: "node_generation",
1275
+ 16: "node_generation",
1276
+ 17: "credential_injection",
1277
+ 18: "connection_wiring",
1278
+ 19: "node_generation",
1279
+ 20: "connection_wiring",
1280
+ 21: "workflow_structure",
1281
+ 22: "workflow_structure",
1282
+ 23: "node_generation",
1283
+ 24: "expression_syntax",
1284
+ 25: "expression_syntax",
1285
+ 26: "expression_syntax"
1286
+ };
1287
+ var RULE_EXAMPLES = {
1288
+ 17: {
1289
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1290
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1291
+ },
1292
+ 24: {
1293
+ bad: '$node["Fetch Data"].json.email',
1294
+ good: "$('Fetch Data').item.json.email"
1295
+ },
1296
+ 25: {
1297
+ bad: "$json.items[0].email",
1298
+ good: "$json.email"
1299
+ },
1300
+ 26: {
1301
+ bad: "$('Fetch Data').json.email",
1302
+ good: "$('Fetch Data').first().json.email"
1303
+ }
1304
+ };
1305
+ var RULE_MITIGATIONS = {
1137
1306
  1: "Provide a non-empty workflow name string",
1138
1307
  2: "Include at least one node in the nodes array",
1139
1308
  3: "Every node must have a unique UUID v4 string as its id field",
@@ -1144,18 +1313,51 @@ var RULE_REMEDIES = {
1144
1313
  8: "Every node must have a non-empty name string",
1145
1314
  9: "connections must be a plain object (use {} if no connections)",
1146
1315
  10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1316
+ 11: "Every non-trigger node should have at least one incoming connection",
1147
1317
  12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1148
- 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1318
+ 13: "workflow.settings must be a plain object if present",
1319
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1149
1320
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1150
1321
  16: "All node names must be unique within the workflow",
1151
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1322
+ 17: 'Each credential entry must be keyed by credential type with an object value: { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Credential" } } \u2014 the key is the credential type, the value has id and name strings',
1152
1323
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1153
1324
  19: "Use known safe typeVersion values for each node type",
1154
1325
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1155
1326
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1156
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1327
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1328
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1329
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1330
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1331
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
1157
1332
  };
1333
+
1334
+ // src/generation/prompt-builder.ts
1335
+ var CRITICAL_SCORE_THRESHOLD = 0.15;
1336
+ function resolveProfile() {
1337
+ const env = process.env["KAIROS_PROMPT_PROFILE"];
1338
+ if (env === "minimal" || env === "standard" || env === "rich") return env;
1339
+ return "standard";
1340
+ }
1341
+ var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
1342
+
1343
+ Always use these patterns in expressions:
1344
+ - Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
1345
+ - Access JSON field: $json.field (not $json.items[0].field)
1346
+ - Single item: $('NodeName').first().json.field
1347
+ - All items: $('NodeName').all()`;
1158
1348
  var PromptBuilder = class {
1349
+ patternsPath;
1350
+ profile;
1351
+ _lastActivePatterns = null;
1352
+ constructor(patternsPath, profile) {
1353
+ this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
1354
+ this.profile = profile ?? resolveProfile();
1355
+ }
1356
+ resolveMaxPatterns() {
1357
+ if (this.profile === "minimal") return 3;
1358
+ if (this.profile === "rich") return 15;
1359
+ return 10;
1360
+ }
1159
1361
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1160
1362
  const mode = this.resolveMode(matches);
1161
1363
  const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
@@ -1192,69 +1394,178 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1192
1394
  cache_control: { type: "ephemeral" }
1193
1395
  }
1194
1396
  ];
1195
- if (mode === "reference" && matches.length > 0) {
1196
- const refText = matches.slice(0, 3).map((m) => {
1197
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1198
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1397
+ if (this.profile !== "minimal") {
1398
+ if (mode === "reference" && matches.length > 0) {
1399
+ const refText = matches.slice(0, 3).map((m) => {
1400
+ const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1401
+ return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1199
1402
  Nodes:
1200
1403
  ${nodes}`;
1201
- }).join("\n\n");
1202
- blocks.push({
1203
- type: "text",
1204
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1205
-
1206
- ${refText}`
1207
- });
1208
- }
1209
- if (mode === "direct" && matches[0]) {
1210
- const match = matches[0];
1211
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1212
- if (json.length > 3e4) {
1213
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1404
+ }).join("\n\n");
1214
1405
  blocks.push({
1215
1406
  type: "text",
1216
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1407
+ text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1408
+
1409
+ ${refText}`
1410
+ });
1411
+ }
1412
+ if (mode === "direct" && matches[0]) {
1413
+ const match = matches[0];
1414
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1415
+ if (json.length > 3e4) {
1416
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1417
+ blocks.push({
1418
+ type: "text",
1419
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1217
1420
  Nodes:
1218
1421
  ${nodes}`
1219
- });
1220
- } else {
1221
- blocks.push({
1222
- type: "text",
1223
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1422
+ });
1423
+ } else {
1424
+ blocks.push({
1425
+ type: "text",
1426
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1224
1427
 
1225
1428
  ${json}`
1226
- });
1429
+ });
1430
+ }
1227
1431
  }
1228
- }
1229
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1230
- const hint = matches[0];
1231
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1232
- blocks.push({
1233
- type: "text",
1234
- text: `## Weak Structural Hint
1432
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1433
+ const hint = matches[0];
1434
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1435
+ blocks.push({
1436
+ type: "text",
1437
+ text: `## Weak Structural Hint
1235
1438
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1236
- });
1439
+ });
1440
+ }
1237
1441
  }
1238
1442
  const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1239
1443
  if (warnings) {
1240
1444
  blocks.push({ type: "text", text: warnings });
1241
1445
  }
1446
+ if (this.profile === "rich") {
1447
+ const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
1448
+ const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
1449
+ if (!expressionAlreadyCovered) {
1450
+ blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
1451
+ }
1452
+ }
1242
1453
  return blocks;
1243
1454
  }
1455
+ loadPatterns() {
1456
+ try {
1457
+ const raw = (0, import_node_fs.readFileSync)(this.patternsPath, "utf-8");
1458
+ const analysis = JSON.parse(raw);
1459
+ const patterns = analysis.topFailureRules ?? [];
1460
+ return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
1461
+ } catch {
1462
+ return [];
1463
+ }
1464
+ }
1465
+ getWarnedRules() {
1466
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1467
+ return patterns.map((p) => p.rule);
1468
+ }
1469
+ getActivePatterns(maxCount = 10) {
1470
+ const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1471
+ const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1472
+ const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1473
+ const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1474
+ return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
1475
+ }
1244
1476
  buildFailureWarnings(matches, globalFailureRates) {
1477
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
1478
+ this._lastActivePatterns = richPatterns;
1479
+ if (richPatterns.length > 0) {
1480
+ return this.buildStageGroupedWarnings(richPatterns, matches);
1481
+ }
1482
+ return this.buildLegacyWarnings(matches, globalFailureRates);
1483
+ }
1484
+ buildStageGroupedWarnings(patterns, matches) {
1485
+ const stageLabels = {
1486
+ credential_injection: "CREDENTIAL FORMATTING",
1487
+ connection_wiring: "CONNECTION WIRING",
1488
+ node_generation: "NODE GENERATION",
1489
+ workflow_structure: "WORKFLOW STRUCTURE",
1490
+ expression_syntax: "EXPRESSION SYNTAX"
1491
+ };
1492
+ const byStage = /* @__PURE__ */ new Map();
1493
+ for (const p of patterns) {
1494
+ const list = byStage.get(p.pipelineStage) ?? [];
1495
+ list.push(p);
1496
+ byStage.set(p.pipelineStage, list);
1497
+ }
1498
+ const sections = [];
1499
+ for (const [stage, stagePatterns] of byStage) {
1500
+ const label = stageLabels[stage] ?? stage;
1501
+ const byMitigation = /* @__PURE__ */ new Map();
1502
+ for (const p of stagePatterns) {
1503
+ const key = p.mitigation ?? `rule_${p.rule}`;
1504
+ const list = byMitigation.get(key) ?? [];
1505
+ list.push(p);
1506
+ byMitigation.set(key, list);
1507
+ }
1508
+ const lines = [];
1509
+ for (const group of byMitigation.values()) {
1510
+ if (group.length === 1) {
1511
+ const p = group[0];
1512
+ const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
1513
+ const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
1514
+ const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
1515
+ const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1516
+ const remedyStr = remedy ? `
1517
+ Fix: ${remedy}` : "";
1518
+ const ex = RULE_EXAMPLES[p.rule];
1519
+ const exampleStr = ex ? `
1520
+ Bad: ${ex.bad}
1521
+ Good: ${ex.good}` : "";
1522
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
1523
+ } else {
1524
+ const ruleNums = group.map((p) => p.rule).join(", ");
1525
+ const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
1526
+ const hasConfirmed = group.some((p) => p.state === "confirmed");
1527
+ const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
1528
+ const remedy = group[0].mitigation;
1529
+ const remedyStr = remedy ? `
1530
+ Fix: ${remedy}` : "";
1531
+ lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
1532
+ }
1533
+ }
1534
+ sections.push(`### ${label}
1535
+ ${lines.join("\n")}`);
1536
+ }
1537
+ for (const match of matches) {
1538
+ const fps = match.workflow.failurePatterns;
1539
+ if (!fps?.length) continue;
1540
+ const coveredRules = new Set(patterns.map((p) => p.rule));
1541
+ const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
1542
+ for (const fp of extra) {
1543
+ const remedy = RULE_MITIGATIONS[fp.rule];
1544
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1545
+ sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
1546
+ }
1547
+ }
1548
+ if (sections.length === 0) return null;
1549
+ return `## Known Failure Patterns \u2014 AVOID THESE
1550
+
1551
+ Grouped by generation stage. Fix these BEFORE outputting your response:
1552
+
1553
+ ${sections.join("\n\n")}`;
1554
+ }
1555
+ buildLegacyWarnings(matches, globalFailureRates) {
1245
1556
  const lines = [];
1246
1557
  for (const match of matches) {
1247
1558
  const patterns = match.workflow.failurePatterns;
1248
1559
  if (!patterns?.length) continue;
1249
1560
  for (const fp of patterns) {
1250
- const remedy = RULE_REMEDIES[fp.rule];
1561
+ const remedy = RULE_MITIGATIONS[fp.rule];
1251
1562
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1252
1563
  lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1253
1564
  }
1254
1565
  }
1255
1566
  const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1256
1567
  for (const rule of highFreqRules) {
1257
- const remedy = RULE_REMEDIES[rule.rule];
1568
+ const remedy = RULE_MITIGATIONS[rule.rule];
1258
1569
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1259
1570
  lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1260
1571
  }
@@ -1315,12 +1626,12 @@ var GENERATE_WORKFLOW_TOOL = {
1315
1626
  }
1316
1627
  };
1317
1628
  var WorkflowDesigner = class {
1318
- constructor(anthropic, model, logger) {
1629
+ constructor(anthropic, model, logger, patternsPath) {
1319
1630
  this.anthropic = anthropic;
1320
1631
  this.model = model;
1321
1632
  this.logger = logger;
1322
1633
  this.validator = new N8nValidator();
1323
- this.promptBuilder = new PromptBuilder();
1634
+ this.promptBuilder = new PromptBuilder(patternsPath);
1324
1635
  }
1325
1636
  anthropic;
1326
1637
  model;
@@ -1365,7 +1676,7 @@ var WorkflowDesigner = class {
1365
1676
  issues: validation.issues
1366
1677
  });
1367
1678
  if (validation.valid) {
1368
- return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
1679
+ return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata, warnedRules: this.promptBuilder.getWarnedRules() };
1369
1680
  }
1370
1681
  lastErrors = errors;
1371
1682
  this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
@@ -1375,7 +1686,9 @@ var WorkflowDesigner = class {
1375
1686
  const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
1376
1687
  throw new ValidationError(
1377
1688
  `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
1378
- finalIssues
1689
+ finalIssues,
1690
+ attemptMetadata,
1691
+ this.promptBuilder.getWarnedRules()
1379
1692
  );
1380
1693
  }
1381
1694
  async callClaude(system, userMessage, temperature) {
@@ -1402,6 +1715,11 @@ var WorkflowDesigner = class {
1402
1715
  }
1403
1716
  }
1404
1717
  extractToolUse(message) {
1718
+ if (message.stop_reason === "max_tokens") {
1719
+ throw new GenerationError(
1720
+ "Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
1721
+ );
1722
+ }
1405
1723
  const toolUseBlock = message.content.find(
1406
1724
  (block) => block.type === "tool_use"
1407
1725
  );
@@ -1429,8 +1747,8 @@ var WorkflowDesigner = class {
1429
1747
 
1430
1748
  // src/telemetry/collector.ts
1431
1749
  var import_promises = require("fs/promises");
1432
- var import_node_path = require("path");
1433
- var import_node_os = require("os");
1750
+ var import_node_path2 = require("path");
1751
+ var import_node_os2 = require("os");
1434
1752
 
1435
1753
  // src/telemetry/types.ts
1436
1754
  var TELEMETRY_SCHEMA_VERSION = 2;
@@ -1441,14 +1759,15 @@ var TelemetryCollector = class {
1441
1759
  sessionId;
1442
1760
  dirReady = null;
1443
1761
  constructor(dir) {
1444
- this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "telemetry");
1762
+ this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1445
1763
  this.sessionId = generateUUID();
1446
1764
  }
1447
- async emit(eventType, data) {
1765
+ async emit(eventType, data, runId) {
1448
1766
  const event = {
1449
1767
  schemaVersion: TELEMETRY_SCHEMA_VERSION,
1450
1768
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1451
1769
  sessionId: this.sessionId,
1770
+ ...runId ? { runId } : {},
1452
1771
  eventType,
1453
1772
  data
1454
1773
  };
@@ -1458,21 +1777,61 @@ var TelemetryCollector = class {
1458
1777
  }
1459
1778
  await this.dirReady;
1460
1779
  const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
1461
- const filepath = (0, import_node_path.join)(this.dir, filename);
1780
+ const filepath = (0, import_node_path2.join)(this.dir, filename);
1462
1781
  await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
1463
1782
  }
1464
1783
  };
1465
1784
 
1466
1785
  // src/telemetry/reader.ts
1786
+ var import_node_os3 = require("os");
1787
+ var import_node_path4 = require("path");
1788
+
1789
+ // src/telemetry/event-reader.ts
1467
1790
  var import_promises2 = require("fs/promises");
1468
- var import_node_path2 = require("path");
1469
- var import_node_os2 = require("os");
1791
+ var import_node_fs2 = require("fs");
1792
+ var import_node_path3 = require("path");
1793
+ var import_node_readline = require("readline");
1794
+ async function readTelemetryEvents(dir, days) {
1795
+ let files;
1796
+ try {
1797
+ files = await (0, import_promises2.readdir)(dir);
1798
+ } catch {
1799
+ return [];
1800
+ }
1801
+ const cutoff = /* @__PURE__ */ new Date();
1802
+ cutoff.setDate(cutoff.getDate() - days);
1803
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1804
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1805
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1806
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
1807
+ const events = [];
1808
+ for (const file of recentFiles) {
1809
+ const fileDate = file.replace(".jsonl", "");
1810
+ try {
1811
+ const rl = (0, import_node_readline.createInterface)({
1812
+ input: (0, import_node_fs2.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
1813
+ crlfDelay: Infinity
1814
+ });
1815
+ for await (const line of rl) {
1816
+ if (!line.trim()) continue;
1817
+ try {
1818
+ events.push({ ...JSON.parse(line), fileDate });
1819
+ } catch {
1820
+ }
1821
+ }
1822
+ } catch {
1823
+ }
1824
+ }
1825
+ return events;
1826
+ }
1827
+
1828
+ // src/telemetry/reader.ts
1470
1829
  var TelemetryReader = class {
1471
1830
  dir;
1472
1831
  cache = null;
1473
1832
  cacheTime = 0;
1474
1833
  constructor(dir) {
1475
- this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1834
+ this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
1476
1835
  }
1477
1836
  async getFailureRates(days = 30) {
1478
1837
  const now = Date.now();
@@ -1481,9 +1840,10 @@ var TelemetryReader = class {
1481
1840
  }
1482
1841
  const events = await this.readRecentEvents(days);
1483
1842
  const buildSessions = new Set(
1484
- events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1843
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
1485
1844
  );
1486
- if (buildSessions.size === 0) return [];
1845
+ const MIN_BUILDS_FOR_RATES = 3;
1846
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1487
1847
  const ruleSessions = /* @__PURE__ */ new Map();
1488
1848
  for (const event of events) {
1489
1849
  if (event.eventType !== "generation_attempt") continue;
@@ -1521,32 +1881,487 @@ var TelemetryReader = class {
1521
1881
  return rates;
1522
1882
  }
1523
1883
  async readRecentEvents(days) {
1524
- let files;
1884
+ return readTelemetryEvents(this.dir, days);
1885
+ }
1886
+ };
1887
+
1888
+ // src/telemetry/pattern-analyzer.ts
1889
+ var import_promises3 = require("fs/promises");
1890
+ var import_node_path5 = require("path");
1891
+ var import_node_os4 = require("os");
1892
+ var PATTERN_SCHEMA_VERSION = 2;
1893
+ var PatternAnalyzer = class _PatternAnalyzer {
1894
+ telemetryDir;
1895
+ outputDir;
1896
+ _cachedEvents = null;
1897
+ constructor(telemetryDir) {
1898
+ const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1899
+ this.telemetryDir = telemetryDir ?? defaultDir;
1900
+ this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
1901
+ }
1902
+ async loadPreviousPatterns() {
1525
1903
  try {
1526
- files = await (0, import_promises2.readdir)(this.dir);
1904
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
1905
+ const prev = JSON.parse(raw);
1906
+ const version = prev.schemaVersion ?? 0;
1907
+ const patterns = prev.topFailureRules ?? [];
1908
+ if (version === PATTERN_SCHEMA_VERSION) return patterns;
1909
+ return this.migratePatterns(patterns, version);
1527
1910
  } catch {
1528
1911
  return [];
1529
1912
  }
1530
- const cutoff = /* @__PURE__ */ new Date();
1531
- cutoff.setDate(cutoff.getDate() - days);
1532
- const cutoffStr = cutoff.toISOString().slice(0, 10);
1533
- const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1534
- const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1535
- const events = [];
1536
- for (const file of recentFiles) {
1537
- try {
1538
- const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
1539
- for (const line of content.split("\n")) {
1540
- if (!line.trim()) continue;
1541
- try {
1542
- events.push(JSON.parse(line));
1543
- } catch {
1913
+ }
1914
+ migratePatterns(patterns, fromVersion) {
1915
+ let migrated = patterns;
1916
+ if (fromVersion < 1) {
1917
+ migrated = migrated.map((p) => ({
1918
+ ...p,
1919
+ compositeScore: p.compositeScore ?? 0,
1920
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1921
+ pipelineStage: p.pipelineStage ?? "node_generation"
1922
+ }));
1923
+ }
1924
+ if (fromVersion < 2) {
1925
+ migrated = migrated.map((p) => {
1926
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
1927
+ return {
1928
+ ...p,
1929
+ scoringFactors: {
1930
+ ...sf,
1931
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
1544
1932
  }
1933
+ };
1934
+ });
1935
+ }
1936
+ return migrated;
1937
+ }
1938
+ async analyze(days = 30) {
1939
+ const previousPatterns = await this.loadPreviousPatterns();
1940
+ const events = await this.readAllEvents(days);
1941
+ this._cachedEvents = events;
1942
+ const starts = events.filter((e) => e.eventType === "build_start");
1943
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
1944
+ const passed = attempts.filter(
1945
+ (a) => a.data.validationPassed === true
1946
+ );
1947
+ const failed = attempts.filter(
1948
+ (a) => a.data.validationPassed === false
1949
+ );
1950
+ const ruleFailures = /* @__PURE__ */ new Map();
1951
+ const credentialFailures = /* @__PURE__ */ new Map();
1952
+ for (const a of failed) {
1953
+ const weight = this.recencyWeight(a.fileDate);
1954
+ const buildId = a.runId ?? a.sessionId;
1955
+ const data = a.data;
1956
+ for (const issue of data.issues ?? []) {
1957
+ if (issue.severity === "warn") continue;
1958
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
1959
+ entry.count++;
1960
+ entry.sessions.add(buildId);
1961
+ entry.recencyWeights.push(weight);
1962
+ entry.allMessages.push(issue.message);
1963
+ if (data.workflowType) {
1964
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
1965
+ }
1966
+ ruleFailures.set(issue.rule, entry);
1967
+ if (issue.rule === 17) {
1968
+ const credPatterns = [
1969
+ /credential\s+"([^"]+)"/,
1970
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
1971
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
1972
+ ];
1973
+ let credType = "unknown";
1974
+ for (const re of credPatterns) {
1975
+ const m = issue.message.match(re);
1976
+ if (m?.[1]) {
1977
+ credType = m[1];
1978
+ break;
1979
+ }
1980
+ }
1981
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
1982
+ }
1983
+ }
1984
+ }
1985
+ const failedByDate = /* @__PURE__ */ new Map();
1986
+ for (const a of failed) {
1987
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
1988
+ }
1989
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1990
+ const hasTrendData = sortedFailDates.length >= 3;
1991
+ let midDate = "";
1992
+ if (hasTrendData) {
1993
+ const halfTotal = failed.length / 2;
1994
+ let cumulative = 0;
1995
+ for (const [date, count] of sortedFailDates) {
1996
+ cumulative += count;
1997
+ if (cumulative >= halfTotal) {
1998
+ midDate = date;
1999
+ break;
1545
2000
  }
1546
- } catch {
1547
2001
  }
1548
2002
  }
1549
- return events;
2003
+ const ruleTrends = /* @__PURE__ */ new Map();
2004
+ if (hasTrendData) {
2005
+ for (const a of failed) {
2006
+ const data = a.data;
2007
+ const isNewer = a.fileDate > midDate;
2008
+ for (const issue of data.issues ?? []) {
2009
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
2010
+ if (isNewer) entry.newer++;
2011
+ else entry.older++;
2012
+ ruleTrends.set(issue.rule, entry);
2013
+ }
2014
+ }
2015
+ }
2016
+ const sessions = /* @__PURE__ */ new Map();
2017
+ for (const a of attempts) {
2018
+ const buildId = a.runId ?? a.sessionId;
2019
+ const list = sessions.get(buildId) ?? [];
2020
+ list.push(a);
2021
+ sessions.set(buildId, list);
2022
+ }
2023
+ let firstTryPass = 0;
2024
+ let correctionNeeded = 0;
2025
+ let singleAttemptFail = 0;
2026
+ for (const sessionAttempts of sessions.values()) {
2027
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
2028
+ const lastPassed = lastAttempt.data.validationPassed === true;
2029
+ if (sessionAttempts.length === 1 && lastPassed) {
2030
+ firstTryPass++;
2031
+ } else if (sessionAttempts.length > 1 && lastPassed) {
2032
+ correctionNeeded++;
2033
+ } else {
2034
+ singleAttemptFail++;
2035
+ }
2036
+ }
2037
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
2038
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
2039
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
2040
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
2041
+ const totalSessions = Math.max(sessions.size, 1);
2042
+ const stickinessCount = /* @__PURE__ */ new Map();
2043
+ for (const sessionAttempts of sessions.values()) {
2044
+ if (sessionAttempts.length < 2) continue;
2045
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
2046
+ const curr = sessionAttempts[i].data;
2047
+ const next = sessionAttempts[i + 1].data;
2048
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
2049
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
2050
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
2051
+ for (const rule of currRules) {
2052
+ if (nextRules.has(rule)) {
2053
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
2054
+ }
2055
+ }
2056
+ }
2057
+ }
2058
+ const CONFIRMED_THRESHOLD = 3;
2059
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
2060
+ const RESOLVED_TTL_DAYS = 90;
2061
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
2062
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
2063
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
2064
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
2065
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
2066
+ const stickiness = stickinessCount.get(rule) ?? 0;
2067
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
2068
+ const pattern = {
2069
+ rule,
2070
+ failureCount: entry.count,
2071
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
2072
+ compositeScore,
2073
+ scoringFactors: factors,
2074
+ state,
2075
+ trend: this.classifyTrend(t.older, t.newer),
2076
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
2077
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
2078
+ mitigation: RULE_MITIGATIONS[rule] ?? null
2079
+ };
2080
+ if (entry.workflowTypes.size > 0) {
2081
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2082
+ }
2083
+ return pattern;
2084
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
2085
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
2086
+ for (const p of activePatterns) {
2087
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
2088
+ if (prev?.state === "resolved") {
2089
+ p.trend = "worsening";
2090
+ p.regressed = true;
2091
+ }
2092
+ }
2093
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
2094
+ for (const a of failed) {
2095
+ const data = a.data;
2096
+ for (const issue of data.issues ?? []) {
2097
+ const existing = ruleLastFailureDate.get(issue.rule);
2098
+ if (!existing || a.fileDate > existing) {
2099
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
2100
+ }
2101
+ }
2102
+ }
2103
+ const newlyResolved = previousPatterns.filter((p) => {
2104
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
2105
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
2106
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
2107
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
2108
+ }).map((p) => ({
2109
+ ...p,
2110
+ state: "resolved",
2111
+ trend: "improving",
2112
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
2113
+ confidence: 0,
2114
+ compositeScore: 0,
2115
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
2116
+ failureCount: 0,
2117
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
2118
+ }));
2119
+ const ttlCutoff = /* @__PURE__ */ new Date();
2120
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
2121
+ const ttlCutoffStr = ttlCutoff.toISOString();
2122
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
2123
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
2124
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
2125
+ const deduped = [
2126
+ ...newlyResolved,
2127
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
2128
+ ...pendingResolution
2129
+ ];
2130
+ const patterns = [...activePatterns, ...deduped];
2131
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
2132
+ const drift = this.detectDrift(patterns);
2133
+ const warnEffMap = /* @__PURE__ */ new Map();
2134
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2135
+ for (const bc of buildCompletes) {
2136
+ const bcData = bc.data;
2137
+ const warned = bcData.warnedRules ?? [];
2138
+ if (warned.length === 0) continue;
2139
+ const sessionFailedRules = /* @__PURE__ */ new Set();
2140
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
2141
+ for (const a of sessionAttempts) {
2142
+ const ad = a.data;
2143
+ if (ad.validationPassed === false) {
2144
+ for (const issue of ad.issues ?? []) {
2145
+ sessionFailedRules.add(issue.rule);
2146
+ }
2147
+ }
2148
+ }
2149
+ for (const rule of warned) {
2150
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
2151
+ entry.warned++;
2152
+ if (sessionFailedRules.has(rule)) entry.failed++;
2153
+ else entry.passed++;
2154
+ warnEffMap.set(rule, entry);
2155
+ }
2156
+ }
2157
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
2158
+ rule,
2159
+ timesWarned: e.warned,
2160
+ timesWarnedAndPassed: e.passed,
2161
+ timesWarnedAndFailed: e.failed,
2162
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
2163
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
2164
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
2165
+ for (const a of failed) {
2166
+ const data = a.data;
2167
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
2168
+ for (let i = 0; i < rules.length; i++) {
2169
+ for (let j = i + 1; j < rules.length; j++) {
2170
+ const key = `${rules[i]},${rules[j]}`;
2171
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
2172
+ }
2173
+ }
2174
+ }
2175
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
2176
+ const [a, b] = key.split(",").map(Number);
2177
+ return { rules: [a, b], count };
2178
+ }).sort((a, b) => b.count - a.count);
2179
+ const attemptDistribution = {};
2180
+ for (const sessionAttempts of sessions.values()) {
2181
+ const depth = sessionAttempts.length;
2182
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
2183
+ }
2184
+ return {
2185
+ schemaVersion: PATTERN_SCHEMA_VERSION,
2186
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2187
+ summary: {
2188
+ totalBuilds: starts.length,
2189
+ totalAttempts: attempts.length,
2190
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
2191
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
2192
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
2193
+ avgDurationMs: Math.round(avgDuration),
2194
+ totalTokensInput: totalInput,
2195
+ totalTokensOutput: totalOutput,
2196
+ attemptDistribution
2197
+ },
2198
+ topFailureRules: patterns,
2199
+ failingCredentialTypes: credTypes,
2200
+ drift,
2201
+ warningEffectiveness,
2202
+ ruleCoOccurrence
2203
+ };
2204
+ }
2205
+ async analyzeAndSave(days = 30) {
2206
+ const analysis = await this.analyze(days);
2207
+ await (0, import_promises3.mkdir)(this.outputDir, { recursive: true });
2208
+ const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
2209
+ const tmpPath = `${outputPath}.tmp`;
2210
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
2211
+ await (0, import_promises3.rename)(tmpPath, outputPath);
2212
+ const historySummary = {
2213
+ timestamp: analysis.generatedAt,
2214
+ totalBuilds: analysis.summary.totalBuilds,
2215
+ firstTryPassRate: analysis.summary.firstTryPassRate,
2216
+ correctionRate: analysis.summary.correctionRate,
2217
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
2218
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
2219
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
2220
+ };
2221
+ const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2222
+ await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2223
+ const sessions = await this.buildSessionSummaries(days);
2224
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2225
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2226
+ await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2227
+ await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
2228
+ return analysis;
2229
+ }
2230
+ async getSessions(limit = 20) {
2231
+ try {
2232
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2233
+ const all = JSON.parse(raw);
2234
+ return all.slice(-limit);
2235
+ } catch {
2236
+ return [];
2237
+ }
2238
+ }
2239
+ async buildSessionSummaries(days = 30) {
2240
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2241
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2242
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2243
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2244
+ const buildId = e.runId ?? e.sessionId;
2245
+ const list = attemptsByBuild.get(buildId) ?? [];
2246
+ list.push(e);
2247
+ attemptsByBuild.set(buildId, list);
2248
+ }
2249
+ const summaries = buildCompletes.map((bc) => {
2250
+ const data = bc.data;
2251
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2252
+ const failedRules = Array.from(new Set(
2253
+ sessionAttempts.flatMap((a) => {
2254
+ const ad = a.data;
2255
+ if (ad.validationPassed !== false) return [];
2256
+ return (ad.issues ?? []).map((i) => i.rule);
2257
+ })
2258
+ ));
2259
+ return {
2260
+ sessionId: bc.sessionId,
2261
+ date: bc.fileDate,
2262
+ description: data.description ?? "",
2263
+ workflowType: data.workflowType ?? null,
2264
+ attempts: data.totalAttempts ?? 1,
2265
+ success: data.success ?? false,
2266
+ failedRules,
2267
+ workflowName: data.workflowName ?? null
2268
+ };
2269
+ });
2270
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2271
+ }
2272
+ async getHistory(limit = 20) {
2273
+ try {
2274
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
2275
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
2276
+ } catch {
2277
+ return [];
2278
+ }
2279
+ }
2280
+ static fromEnv() {
2281
+ const dir = process.env["KAIROS_TELEMETRY"];
2282
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
2283
+ }
2284
+ detectDrift(patterns) {
2285
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
2286
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
2287
+ const alerts = [];
2288
+ for (const p of patterns) {
2289
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
2290
+ alerts.push({
2291
+ type: "stale_pattern",
2292
+ rule: p.rule,
2293
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
2294
+ });
2295
+ }
2296
+ }
2297
+ for (const rule of VALIDATOR_RULES) {
2298
+ if (!(rule in RULE_MITIGATIONS)) {
2299
+ alerts.push({
2300
+ type: "missing_mitigation",
2301
+ rule,
2302
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
2303
+ });
2304
+ }
2305
+ if (!(rule in RULE_PIPELINE_STAGES)) {
2306
+ alerts.push({
2307
+ type: "missing_stage_mapping",
2308
+ rule,
2309
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
2310
+ });
2311
+ }
2312
+ }
2313
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
2314
+ return {
2315
+ healthy: alerts.length === 0,
2316
+ alerts,
2317
+ coveredRules,
2318
+ totalRules: VALIDATOR_RULES.length
2319
+ };
2320
+ }
2321
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
2322
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
2323
+ const stateWeight = stateWeights[state];
2324
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
2325
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
2326
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
2327
+ return {
2328
+ compositeScore,
2329
+ factors: {
2330
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
2331
+ impact: Math.round(impact * 1e3) / 1e3,
2332
+ recency: Math.round(avgRecency * 1e3) / 1e3,
2333
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
2334
+ }
2335
+ };
2336
+ }
2337
+ classifyTrend(older, newer) {
2338
+ const total = older + newer;
2339
+ if (total === 0) return "stable";
2340
+ if (older === 0) return "new";
2341
+ const newerRatio = newer / total;
2342
+ if (newerRatio >= 0.65) return "worsening";
2343
+ if (newerRatio <= 0.35) return "improving";
2344
+ return "stable";
2345
+ }
2346
+ deduplicateMessages(messages, maxCount = 3) {
2347
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
2348
+ const seen = /* @__PURE__ */ new Set();
2349
+ const unique = [];
2350
+ for (const msg of messages) {
2351
+ const key = normalize(msg);
2352
+ if (!seen.has(key) && unique.length < maxCount) {
2353
+ seen.add(key);
2354
+ unique.push(msg);
2355
+ }
2356
+ }
2357
+ return unique;
2358
+ }
2359
+ recencyWeight(fileDate, halfLifeDays = 30) {
2360
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
2361
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
2362
+ }
2363
+ async readAllEvents(days) {
2364
+ return readTelemetryEvents(this.telemetryDir, days);
1550
2365
  }
1551
2366
  };
1552
2367
 
@@ -1562,7 +2377,59 @@ var nullLogger = {
1562
2377
  }
1563
2378
  };
1564
2379
 
2380
+ // src/utils/workflow-type.ts
2381
+ var TYPE_KEYWORDS = [
2382
+ ["gmail", "email"],
2383
+ ["imap", "email"],
2384
+ ["smtp", "email"],
2385
+ [" email", "email"],
2386
+ ["slack", "slack"],
2387
+ ["telegram", "messaging"],
2388
+ ["discord", "messaging"],
2389
+ [" sms", "messaging"],
2390
+ ["twilio", "messaging"],
2391
+ ["webhook", "webhook"],
2392
+ ["google sheets", "data"],
2393
+ ["spreadsheet", "data"],
2394
+ ["airtable", "data"],
2395
+ ["notion", "data"],
2396
+ ["github", "devops"],
2397
+ ["gitlab", "devops"],
2398
+ ["schedule", "schedule"],
2399
+ [" cron", "schedule"],
2400
+ ["daily", "schedule"],
2401
+ ["weekly", "schedule"],
2402
+ ["hourly", "schedule"],
2403
+ ["every day", "schedule"],
2404
+ ["every hour", "schedule"],
2405
+ ["every morning", "schedule"],
2406
+ ["postgres", "database"],
2407
+ ["mysql", "database"],
2408
+ ["supabase", "database"],
2409
+ ["redis", "database"],
2410
+ [" database", "database"],
2411
+ [" llm", "ai"],
2412
+ [" gpt", "ai"],
2413
+ ["claude", "ai"],
2414
+ [" agent", "ai"],
2415
+ ["langchain", "ai"],
2416
+ [" ai ", "ai"],
2417
+ [" ai", "ai"],
2418
+ ["http request", "api"],
2419
+ ["rest api", "api"],
2420
+ [" api", "api"]
2421
+ ];
2422
+ function inferWorkflowType(description) {
2423
+ const lower = " " + description.toLowerCase();
2424
+ for (const [keyword, type] of TYPE_KEYWORDS) {
2425
+ if (lower.includes(keyword)) return type;
2426
+ }
2427
+ return null;
2428
+ }
2429
+
1565
2430
  // src/client.ts
2431
+ var import_node_os5 = require("os");
2432
+ var import_node_path6 = require("path");
1566
2433
  var DEFAULT_MODEL = "claude-sonnet-4-6";
1567
2434
  var Kairos = class {
1568
2435
  provider;
@@ -1572,6 +2439,7 @@ var Kairos = class {
1572
2439
  logger;
1573
2440
  telemetry;
1574
2441
  telemetryReader;
2442
+ patternAnalyzer;
1575
2443
  model;
1576
2444
  saveQueue = Promise.resolve(null);
1577
2445
  constructor(options) {
@@ -1590,19 +2458,23 @@ var Kairos = class {
1590
2458
  this.provider = null;
1591
2459
  }
1592
2460
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
1593
- this.designer = new WorkflowDesigner(anthropic, this.model, logger);
2461
+ const patternsPath = typeof options.telemetry === "string" ? (0, import_node_path6.join)(options.telemetry, "..", "patterns.json") : (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "patterns.json");
2462
+ this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
1594
2463
  this.validator = new N8nValidator();
1595
2464
  this.library = options.library ?? new NullLibrary();
1596
2465
  this.logger = logger;
1597
2466
  if (options.telemetry === true) {
1598
2467
  this.telemetry = new TelemetryCollector();
1599
2468
  this.telemetryReader = new TelemetryReader();
2469
+ this.patternAnalyzer = new PatternAnalyzer();
1600
2470
  } else if (typeof options.telemetry === "string") {
1601
2471
  this.telemetry = new TelemetryCollector(options.telemetry);
1602
2472
  this.telemetryReader = new TelemetryReader(options.telemetry);
2473
+ this.patternAnalyzer = new PatternAnalyzer(options.telemetry);
1603
2474
  } else {
1604
2475
  this.telemetry = null;
1605
2476
  this.telemetryReader = null;
2477
+ this.patternAnalyzer = null;
1606
2478
  }
1607
2479
  }
1608
2480
  requireProvider() {
@@ -1620,11 +2492,13 @@ var Kairos = class {
1620
2492
  this.validateDescription(description);
1621
2493
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
1622
2494
  const buildStart = Date.now();
2495
+ const runId = generateUUID();
2496
+ const workflowType = inferWorkflowType(description);
1623
2497
  await this.telemetry?.emit("build_start", {
1624
2498
  description,
1625
2499
  model: this.model,
1626
2500
  dryRun: options?.dryRun ?? false
1627
- });
2501
+ }, runId);
1628
2502
  await this.library.initialize();
1629
2503
  const matches = await this.library.search(description);
1630
2504
  if (matches.length > 0) {
@@ -1640,12 +2514,48 @@ var Kairos = class {
1640
2514
  this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
1641
2515
  }
1642
2516
  }
1643
- const designResult = await this.designer.design(
1644
- { description, ...options?.name ? { name: options.name } : {} },
1645
- matches,
1646
- globalFailureRates
1647
- );
1648
- await this.emitAttemptTelemetry(description, designResult);
2517
+ let designResult;
2518
+ try {
2519
+ designResult = await this.designer.design(
2520
+ { description, ...options?.name ? { name: options.name } : {} },
2521
+ matches,
2522
+ globalFailureRates
2523
+ );
2524
+ } catch (err) {
2525
+ if (err instanceof ValidationError && err.attemptMetadata) {
2526
+ for (const meta of err.attemptMetadata) {
2527
+ await this.telemetry?.emit("generation_attempt", {
2528
+ description,
2529
+ attempt: meta.attempt,
2530
+ temperature: meta.temperature,
2531
+ durationMs: meta.durationMs,
2532
+ tokensInput: meta.tokensInput,
2533
+ tokensOutput: meta.tokensOutput,
2534
+ validationPassed: meta.validationPassed,
2535
+ issueCount: meta.issues.length,
2536
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2537
+ workflowType
2538
+ }, runId);
2539
+ }
2540
+ await this.telemetry?.emit("build_complete", {
2541
+ description,
2542
+ success: false,
2543
+ totalAttempts: err.attemptMetadata.length,
2544
+ totalDurationMs: Date.now() - buildStart,
2545
+ totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
2546
+ totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
2547
+ workflowName: null,
2548
+ workflowId: null,
2549
+ dryRun: options?.dryRun ?? false,
2550
+ credentialsNeeded: 0,
2551
+ warnedRules: err.warnedRules ?? [],
2552
+ workflowType
2553
+ }, runId);
2554
+ this.updatePatterns();
2555
+ }
2556
+ throw err;
2557
+ }
2558
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
1649
2559
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
1650
2560
  this.saveToLibrary(workflow, description, designResult, matches);
1651
2561
  if (options?.dryRun) {
@@ -1661,8 +2571,11 @@ var Kairos = class {
1661
2571
  workflowName: workflow.name,
1662
2572
  workflowId: null,
1663
2573
  dryRun: true,
1664
- credentialsNeeded: designResult.credentialsNeeded.length
1665
- });
2574
+ credentialsNeeded: designResult.credentialsNeeded.length,
2575
+ warnedRules: designResult.warnedRules,
2576
+ workflowType
2577
+ }, runId);
2578
+ this.updatePatterns();
1666
2579
  return {
1667
2580
  workflowId: null,
1668
2581
  name: workflow.name,
@@ -1691,8 +2604,11 @@ var Kairos = class {
1691
2604
  workflowName: deployed.name,
1692
2605
  workflowId: deployed.workflowId,
1693
2606
  dryRun: false,
1694
- credentialsNeeded: designResult.credentialsNeeded.length
1695
- });
2607
+ credentialsNeeded: designResult.credentialsNeeded.length,
2608
+ warnedRules: designResult.warnedRules,
2609
+ workflowType
2610
+ }, runId);
2611
+ this.updatePatterns();
1696
2612
  return {
1697
2613
  workflowId: deployed.workflowId,
1698
2614
  name: deployed.name,
@@ -1707,16 +2623,54 @@ var Kairos = class {
1707
2623
  this.validateDescription(description);
1708
2624
  this.logger.info("Kairos.update", { id, description });
1709
2625
  const buildStart = Date.now();
2626
+ const runId = generateUUID();
2627
+ const workflowType = inferWorkflowType(description);
1710
2628
  await this.telemetry?.emit("build_start", {
1711
2629
  description,
1712
2630
  model: this.model,
1713
2631
  dryRun: false
1714
- });
2632
+ }, runId);
1715
2633
  await this.library.initialize();
1716
2634
  const matches = await this.library.search(description);
1717
2635
  const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1718
- const designResult = await this.designer.design({ description }, matches, globalFailureRates);
1719
- await this.emitAttemptTelemetry(description, designResult);
2636
+ let designResult;
2637
+ try {
2638
+ designResult = await this.designer.design({ description }, matches, globalFailureRates);
2639
+ } catch (err) {
2640
+ if (err instanceof ValidationError && err.attemptMetadata) {
2641
+ for (const meta of err.attemptMetadata) {
2642
+ await this.telemetry?.emit("generation_attempt", {
2643
+ description,
2644
+ attempt: meta.attempt,
2645
+ temperature: meta.temperature,
2646
+ durationMs: meta.durationMs,
2647
+ tokensInput: meta.tokensInput,
2648
+ tokensOutput: meta.tokensOutput,
2649
+ validationPassed: meta.validationPassed,
2650
+ issueCount: meta.issues.length,
2651
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2652
+ workflowType
2653
+ }, runId);
2654
+ }
2655
+ await this.telemetry?.emit("build_complete", {
2656
+ description,
2657
+ success: false,
2658
+ totalAttempts: err.attemptMetadata.length,
2659
+ totalDurationMs: Date.now() - buildStart,
2660
+ totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
2661
+ totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
2662
+ workflowName: null,
2663
+ workflowId: null,
2664
+ dryRun: false,
2665
+ credentialsNeeded: 0,
2666
+ warnedRules: err.warnedRules ?? [],
2667
+ workflowType
2668
+ }, runId);
2669
+ this.updatePatterns();
2670
+ }
2671
+ throw err;
2672
+ }
2673
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
1720
2674
  const provider = this.requireProvider();
1721
2675
  const deployed = await provider.update(id, designResult.workflow);
1722
2676
  this.saveToLibrary(designResult.workflow, description, designResult, matches);
@@ -1733,8 +2687,11 @@ var Kairos = class {
1733
2687
  workflowName: deployed.name,
1734
2688
  workflowId: deployed.workflowId,
1735
2689
  dryRun: false,
1736
- credentialsNeeded: designResult.credentialsNeeded.length
1737
- });
2690
+ credentialsNeeded: designResult.credentialsNeeded.length,
2691
+ warnedRules: designResult.warnedRules,
2692
+ workflowType
2693
+ }, runId);
2694
+ this.updatePatterns();
1738
2695
  return {
1739
2696
  workflowId: deployed.workflowId,
1740
2697
  name: deployed.name,
@@ -1749,7 +2706,14 @@ var Kairos = class {
1749
2706
  await this.saveQueue.catch(() => {
1750
2707
  });
1751
2708
  }
1752
- async emitAttemptTelemetry(description, designResult) {
2709
+ updatePatterns() {
2710
+ if (!this.patternAnalyzer) return;
2711
+ this.saveQueue = this.saveQueue.then(() => this.patternAnalyzer.analyzeAndSave()).then(() => null).catch((err) => {
2712
+ this.logger.warn("Pattern analysis failed (non-fatal)", { err: String(err) });
2713
+ return null;
2714
+ });
2715
+ }
2716
+ async emitAttemptTelemetry(description, designResult, workflowType, runId) {
1753
2717
  for (const meta of designResult.attemptMetadata) {
1754
2718
  await this.telemetry?.emit("generation_attempt", {
1755
2719
  description,
@@ -1760,8 +2724,9 @@ var Kairos = class {
1760
2724
  tokensOutput: meta.tokensOutput,
1761
2725
  validationPassed: meta.validationPassed,
1762
2726
  issueCount: meta.issues.length,
1763
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1764
- });
2727
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2728
+ workflowType
2729
+ }, runId);
1765
2730
  }
1766
2731
  }
1767
2732
  recordDeploy() {
@@ -1859,9 +2824,9 @@ var Kairos = class {
1859
2824
  };
1860
2825
 
1861
2826
  // src/library/file-library.ts
1862
- var import_promises3 = require("fs/promises");
1863
- var import_node_path3 = require("path");
1864
- var import_node_os3 = require("os");
2827
+ var import_promises4 = require("fs/promises");
2828
+ var import_node_path7 = require("path");
2829
+ var import_node_os6 = require("os");
1865
2830
 
1866
2831
  // src/library/scorer.ts
1867
2832
  var WEIGHTS = {
@@ -2091,13 +3056,27 @@ function buildSearchCorpus(w) {
2091
3056
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2092
3057
  }
2093
3058
  var MAX_LIBRARY_SIZE = 500;
3059
+ function isValidMeta(item) {
3060
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
3061
+ }
3062
+ function isValidOldEntry(item) {
3063
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
3064
+ item.workflow.nodes
3065
+ );
3066
+ }
2094
3067
  var FileLibrary = class {
2095
3068
  dir;
2096
- workflows = [];
3069
+ meta = [];
2097
3070
  initPromise = null;
2098
3071
  writeQueue = Promise.resolve();
2099
3072
  constructor(dir) {
2100
- this.dir = dir ?? (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".kairos", "library");
3073
+ this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
3074
+ }
3075
+ get workflowsDir() {
3076
+ return (0, import_node_path7.join)(this.dir, "workflows");
3077
+ }
3078
+ workflowFilePath(id) {
3079
+ return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
2101
3080
  }
2102
3081
  async initialize() {
2103
3082
  if (!this.initPromise) {
@@ -2106,62 +3085,149 @@ var FileLibrary = class {
2106
3085
  return this.initPromise;
2107
3086
  }
2108
3087
  async doInitialize() {
2109
- await (0, import_promises3.mkdir)(this.dir, { recursive: true });
2110
- const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
3088
+ await (0, import_promises4.mkdir)(this.dir, { recursive: true });
3089
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3090
+ let workflowsDirExists = false;
2111
3091
  try {
2112
- const raw = await (0, import_promises3.readFile)(indexPath, "utf-8");
2113
- const parsed = JSON.parse(raw);
2114
- if (!Array.isArray(parsed)) {
2115
- this.workflows = [];
2116
- } else {
2117
- this.workflows = parsed.filter(
2118
- (item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(item.workflow.nodes)
2119
- );
3092
+ await (0, import_promises4.stat)(this.workflowsDir);
3093
+ workflowsDirExists = true;
3094
+ } catch {
3095
+ }
3096
+ if (workflowsDirExists) {
3097
+ try {
3098
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3099
+ const parsed = JSON.parse(raw);
3100
+ if (Array.isArray(parsed)) {
3101
+ this.meta = parsed.filter(isValidMeta);
3102
+ }
3103
+ } catch {
3104
+ this.meta = [];
2120
3105
  }
3106
+ } else {
3107
+ try {
3108
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3109
+ const parsed = JSON.parse(raw);
3110
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
3111
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
3112
+ return;
3113
+ }
3114
+ } catch {
3115
+ }
3116
+ this.meta = [];
3117
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3118
+ }
3119
+ }
3120
+ /**
3121
+ * One-time transparent migration from v0.4.x monolithic index.json.
3122
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
3123
+ * meta entry. Rewrites index.json in the new format.
3124
+ */
3125
+ async migrateFromMonolithic(oldEntries) {
3126
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3127
+ const newMeta = [];
3128
+ for (const entry of oldEntries) {
3129
+ const wfPath = this.workflowFilePath(entry.id);
3130
+ const tmpPath = `${wfPath}.tmp`;
3131
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
3132
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3133
+ const { workflow, ...metaFields } = entry;
3134
+ newMeta.push({
3135
+ ...metaFields,
3136
+ workflowName: workflow.name,
3137
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
3138
+ });
3139
+ }
3140
+ this.meta = newMeta;
3141
+ await this.persistNow();
3142
+ }
3143
+ async loadWorkflowFile(id) {
3144
+ try {
3145
+ const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
3146
+ return JSON.parse(raw);
2121
3147
  } catch {
2122
- this.workflows = [];
3148
+ return null;
2123
3149
  }
2124
3150
  }
3151
+ async writeWorkflowFile(id, workflow) {
3152
+ const wfPath = this.workflowFilePath(id);
3153
+ const tmpPath = `${wfPath}.tmp`;
3154
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
3155
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3156
+ }
3157
+ /**
3158
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
3159
+ * scoring / clustering. Only node.type is populated in each node — no other
3160
+ * node fields are used by hybridScore or clusterWorkflows.
3161
+ */
3162
+ makeSearchShell(m) {
3163
+ return {
3164
+ ...m,
3165
+ workflow: {
3166
+ name: m.workflowName,
3167
+ nodes: m.cachedNodeTypes.map((type) => ({
3168
+ id: "",
3169
+ name: "",
3170
+ type,
3171
+ typeVersion: 1,
3172
+ position: [0, 0],
3173
+ parameters: {}
3174
+ })),
3175
+ connections: {}
3176
+ }
3177
+ };
3178
+ }
2125
3179
  async search(description, options) {
2126
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2127
- if (searchable.length === 0) return [];
3180
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
3181
+ if (filteredMeta.length === 0) return [];
2128
3182
  const limit = options?.limit ?? 3;
2129
3183
  const queryTokens = tokenize(description);
2130
3184
  if (queryTokens.length === 0) return [];
2131
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
3185
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
3186
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
2132
3187
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2133
- const docCount = searchable.length;
3188
+ const docCount = shells.length;
2134
3189
  const idf = /* @__PURE__ */ new Map();
2135
3190
  const allTokens = new Set(queryTokens);
2136
3191
  for (const token of allTokens) {
2137
3192
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2138
3193
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2139
3194
  }
2140
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2141
- const clusters = clusterWorkflows(searchable);
3195
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
3196
+ const clusters = clusterWorkflows(shells);
2142
3197
  const reranked = rerank(scored, clusters).slice(0, limit);
2143
- const results = reranked.map((m) => {
2144
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2145
- });
2146
- if (results.length > 0) {
2147
- for (const r of results) {
2148
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2149
- }
2150
- this.persist();
2151
- }
2152
- return results;
3198
+ if (reranked.length === 0) return [];
3199
+ for (const r of reranked) {
3200
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
3201
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
3202
+ }
3203
+ this.persist();
3204
+ const results = await Promise.all(
3205
+ reranked.map(async (r) => {
3206
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
3207
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
3208
+ if (!workflow) return null;
3209
+ return {
3210
+ workflow: { ...m, workflow },
3211
+ score: r.score,
3212
+ mode: scoreToMode(r.score)
3213
+ };
3214
+ })
3215
+ );
3216
+ return results.filter((r) => r !== null);
2153
3217
  }
2154
3218
  async save(workflow, metadata) {
2155
3219
  const id = generateUUID();
3220
+ await this.writeWorkflowFile(id, workflow);
2156
3221
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2157
- const stored = {
3222
+ const meta = {
2158
3223
  id,
2159
- workflow,
2160
3224
  description: metadata.description,
2161
3225
  tags: metadata.tags ?? [],
2162
3226
  platform: metadata.platform ?? "n8n",
2163
3227
  deployCount: 0,
2164
3228
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3229
+ workflowName: workflow.name,
3230
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
2165
3231
  ...failurePatterns?.length ? { failurePatterns } : {},
2166
3232
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2167
3233
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -2173,31 +3239,35 @@ var FileLibrary = class {
2173
3239
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2174
3240
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2175
3241
  };
2176
- this.workflows.push(stored);
2177
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
2178
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2179
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
3242
+ this.meta.push(meta);
3243
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
3244
+ this.meta.sort((a, b) => {
3245
+ if (a.id === id) return -1;
3246
+ if (b.id === id) return 1;
3247
+ return (b.deployCount ?? 0) - (a.deployCount ?? 0);
3248
+ });
3249
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
2180
3250
  }
2181
3251
  await this.persist();
2182
3252
  return id;
2183
3253
  }
2184
3254
  async recordDeployment(id) {
2185
- const w = this.workflows.find((w2) => w2.id === id);
2186
- if (w) {
2187
- w.deployCount++;
2188
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
3255
+ const m = this.meta.find((m2) => m2.id === id);
3256
+ if (m) {
3257
+ m.deployCount++;
3258
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2189
3259
  await this.persist();
2190
3260
  }
2191
3261
  }
2192
3262
  async recordOutcome(id, outcome) {
2193
- const w = this.workflows.find((w2) => w2.id === id);
2194
- if (!w) return;
3263
+ const m = this.meta.find((m2) => m2.id === id);
3264
+ if (!m) return;
2195
3265
  if (outcome.mode === "direct") {
2196
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
3266
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
2197
3267
  } else {
2198
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
3268
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
2199
3269
  }
2200
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
3270
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2201
3271
  stats.totalUses++;
2202
3272
  stats.totalAttempts += outcome.attempts;
2203
3273
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -2205,24 +3275,35 @@ var FileLibrary = class {
2205
3275
  const key = String(rule);
2206
3276
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2207
3277
  }
2208
- w.outcomeStats = stats;
3278
+ m.outcomeStats = stats;
2209
3279
  await this.persist();
2210
3280
  }
2211
3281
  async drain() {
2212
3282
  await this.writeQueue;
2213
3283
  }
2214
3284
  async get(id) {
2215
- return this.workflows.find((w) => w.id === id) ?? null;
3285
+ const m = this.meta.find((m2) => m2.id === id);
3286
+ if (!m) return null;
3287
+ const workflow = await this.loadWorkflowFile(id);
3288
+ if (!workflow) return null;
3289
+ return { ...m, workflow };
2216
3290
  }
2217
3291
  async list(filters) {
2218
- let result = this.workflows;
3292
+ let filtered = this.meta;
2219
3293
  if (filters?.platform) {
2220
- result = result.filter((w) => w.platform === filters.platform);
3294
+ filtered = filtered.filter((m) => m.platform === filters.platform);
2221
3295
  }
2222
3296
  if (filters?.tags && filters.tags.length > 0) {
2223
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
3297
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
2224
3298
  }
2225
- return result;
3299
+ const results = await Promise.all(
3300
+ filtered.map(async (m) => {
3301
+ const workflow = await this.loadWorkflowFile(m.id);
3302
+ if (!workflow) return null;
3303
+ return { ...m, workflow };
3304
+ })
3305
+ );
3306
+ return results.filter((r) => r !== null);
2226
3307
  }
2227
3308
  deduplicateFailurePatterns(patterns) {
2228
3309
  if (!patterns?.length) return void 0;
@@ -2237,12 +3318,37 @@ var FileLibrary = class {
2237
3318
  }
2238
3319
  return [...map.values()];
2239
3320
  }
3321
+ /**
3322
+ * Direct write used only during migration (before writeQueue is needed).
3323
+ */
3324
+ async persistNow() {
3325
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3326
+ const tmpPath = `${indexPath}.tmp`;
3327
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
3328
+ await (0, import_promises4.rename)(tmpPath, indexPath);
3329
+ }
2240
3330
  persist() {
2241
3331
  this.writeQueue = this.writeQueue.then(async () => {
2242
- const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
3332
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3333
+ let onDisk = [];
3334
+ try {
3335
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3336
+ const parsed = JSON.parse(raw);
3337
+ if (Array.isArray(parsed)) {
3338
+ onDisk = parsed.filter(isValidMeta);
3339
+ }
3340
+ } catch {
3341
+ }
3342
+ const ourIds = new Set(this.meta.map((m) => m.id));
3343
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
3344
+ let merged = [...this.meta, ...external];
3345
+ if (merged.length > MAX_LIBRARY_SIZE) {
3346
+ merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
3347
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
3348
+ }
2243
3349
  const tmpPath = `${indexPath}.tmp`;
2244
- await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2245
- await (0, import_promises3.rename)(tmpPath, indexPath);
3350
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
3351
+ await (0, import_promises4.rename)(tmpPath, indexPath);
2246
3352
  });
2247
3353
  return this.writeQueue;
2248
3354
  }