@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/README.md +46 -11
- package/dist/{chunk-KQSNT3HZ.js → chunk-4TS6GW6O.js} +148 -368
- package/dist/chunk-4TS6GW6O.js.map +1 -0
- package/dist/chunk-6CLI43FI.js +315 -0
- package/dist/chunk-6CLI43FI.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/{chunk-RYGYNOR6.js → chunk-6IXW3WCC.js} +936 -412
- package/dist/chunk-6IXW3WCC.js.map +1 -0
- package/dist/chunk-CR2NHLOH.js +523 -0
- package/dist/chunk-CR2NHLOH.js.map +1 -0
- package/dist/cli.cjs +1402 -170
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +140 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1262 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -537
- package/dist/index.d.ts +5 -537
- package/dist/index.js +8 -4
- package/dist/mcp-server.cjs +1259 -129
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +113 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-CpUcHhKW.d.cts +566 -0
- package/dist/reader-CpUcHhKW.d.ts +566 -0
- package/dist/standalone.cjs +2460 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +105 -0
- package/dist/standalone.d.ts +105 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +6 -1
- package/dist/chunk-KQSNT3HZ.js.map +0 -1
- package/dist/chunk-RYGYNOR6.js.map +0 -1
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
-
|
|
973
|
-
|
|
974
|
-
-
|
|
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/
|
|
1136
|
-
var
|
|
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
|
-
|
|
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: '
|
|
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 (
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
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
|
-
|
|
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: `##
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1433
|
-
var
|
|
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,
|
|
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,
|
|
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
|
|
1469
|
-
var
|
|
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,
|
|
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"
|
|
1843
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
1485
1844
|
);
|
|
1486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
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
|
|
1863
|
-
var
|
|
1864
|
-
var
|
|
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
|
-
|
|
3069
|
+
meta = [];
|
|
2097
3070
|
initPromise = null;
|
|
2098
3071
|
writeQueue = Promise.resolve();
|
|
2099
3072
|
constructor(dir) {
|
|
2100
|
-
this.dir = dir ?? (0,
|
|
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,
|
|
2110
|
-
const indexPath = (0,
|
|
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
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
-
|
|
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
|
|
2127
|
-
if (
|
|
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
|
|
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 =
|
|
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,
|
|
2141
|
-
const clusters = clusterWorkflows(
|
|
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
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
|
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.
|
|
2177
|
-
if (this.
|
|
2178
|
-
this.
|
|
2179
|
-
|
|
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
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
|
2194
|
-
if (!
|
|
3263
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3264
|
+
if (!m) return;
|
|
2195
3265
|
if (outcome.mode === "direct") {
|
|
2196
|
-
|
|
3266
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2197
3267
|
} else {
|
|
2198
|
-
|
|
3268
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2199
3269
|
}
|
|
2200
|
-
const stats =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3292
|
+
let filtered = this.meta;
|
|
2219
3293
|
if (filters?.platform) {
|
|
2220
|
-
|
|
3294
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2221
3295
|
}
|
|
2222
3296
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2223
|
-
|
|
3297
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2224
3298
|
}
|
|
2225
|
-
|
|
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,
|
|
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,
|
|
2245
|
-
await (0,
|
|
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
|
}
|