nexus-agents 2.28.0 → 2.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/dist/adaptive-memory-RST6DZYR.js +15 -0
- package/dist/chunk-2UR7YN6T.js +700 -0
- package/dist/chunk-2UR7YN6T.js.map +1 -0
- package/dist/chunk-2UUUKVNR.js +61 -0
- package/dist/chunk-2UUUKVNR.js.map +1 -0
- package/dist/chunk-3EVVQ32X.js +838 -0
- package/dist/chunk-3EVVQ32X.js.map +1 -0
- package/dist/{chunk-UGNLR4NZ.js → chunk-3GXDN4AX.js} +2 -2
- package/dist/{chunk-LKSTILEE.js → chunk-4AGPR6XZ.js} +194 -1461
- package/dist/chunk-4AGPR6XZ.js.map +1 -0
- package/dist/chunk-4HA5PAL7.js +246 -0
- package/dist/chunk-4HA5PAL7.js.map +1 -0
- package/dist/chunk-633WH2ML.js +127 -0
- package/dist/chunk-633WH2ML.js.map +1 -0
- package/dist/chunk-7SKAKA4I.js +90 -0
- package/dist/chunk-7SKAKA4I.js.map +1 -0
- package/dist/chunk-AP2FD37C.js +127 -0
- package/dist/chunk-AP2FD37C.js.map +1 -0
- package/dist/chunk-BC3M4VLP.js +359 -0
- package/dist/chunk-BC3M4VLP.js.map +1 -0
- package/dist/chunk-BQ4YXGGQ.js +127 -0
- package/dist/chunk-BQ4YXGGQ.js.map +1 -0
- package/dist/chunk-C2C5ONFR.js +195 -0
- package/dist/chunk-C2C5ONFR.js.map +1 -0
- package/dist/chunk-CGWRJ4EM.js +12661 -0
- package/dist/chunk-CGWRJ4EM.js.map +1 -0
- package/dist/chunk-CLYZ7FWP.js +30 -0
- package/dist/chunk-CLYZ7FWP.js.map +1 -0
- package/dist/chunk-ED6VQWNG.js +63 -0
- package/dist/chunk-ED6VQWNG.js.map +1 -0
- package/dist/{chunk-L2SHSW4T.js → chunk-ELIFTCYM.js} +2705 -2473
- package/dist/chunk-ELIFTCYM.js.map +1 -0
- package/dist/chunk-FYJVXQHX.js +944 -0
- package/dist/chunk-FYJVXQHX.js.map +1 -0
- package/dist/{chunk-QZEAD6AG.js → chunk-FZFZ77UJ.js} +19434 -38182
- package/dist/chunk-FZFZ77UJ.js.map +1 -0
- package/dist/chunk-IECE6DBS.js +1585 -0
- package/dist/chunk-IECE6DBS.js.map +1 -0
- package/dist/chunk-LLGUX44Z.js +356 -0
- package/dist/chunk-LLGUX44Z.js.map +1 -0
- package/dist/chunk-MRU6L7YJ.js +931 -0
- package/dist/chunk-MRU6L7YJ.js.map +1 -0
- package/dist/chunk-N5SY7V45.js +44 -0
- package/dist/chunk-N5SY7V45.js.map +1 -0
- package/dist/chunk-OOIPRRPX.js +3790 -0
- package/dist/chunk-OOIPRRPX.js.map +1 -0
- package/dist/chunk-SRECH7OQ.js +354 -0
- package/dist/chunk-SRECH7OQ.js.map +1 -0
- package/dist/{chunk-YSDUVCCZ.js → chunk-VKRMXD62.js} +6 -6
- package/dist/{chunk-E7EX2KQJ.js → chunk-XU3NADFE.js} +2 -2
- package/dist/chunk-YSTJEMQX.js +122 -0
- package/dist/chunk-YSTJEMQX.js.map +1 -0
- package/dist/cli-circuit-breaker-5FAODXVY.js +13 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +1485 -683
- package/dist/cli.js.map +1 -1
- package/dist/codebase-search-CZUA37RU.js +9 -0
- package/dist/composite-router-FNW7ZWL7.js +17 -0
- package/dist/composite-router-FNW7ZWL7.js.map +1 -0
- package/dist/consensus-vote-757YULIP.js +25 -0
- package/dist/consensus-vote-757YULIP.js.map +1 -0
- package/dist/{dist-H5XNXVAV.js → dist-CV74KUT7.js} +1521 -972
- package/dist/dist-CV74KUT7.js.map +1 -0
- package/dist/{doctor-deep-BDE2PHVX.js → doctor-deep-LMCEAFU4.js} +4 -3
- package/dist/doctor-deep-LMCEAFU4.js.map +1 -0
- package/dist/expert-bridge-L2D4OXOR.js +10 -0
- package/dist/expert-bridge-L2D4OXOR.js.map +1 -0
- package/dist/expert-config-A5CHKUGI.js +22 -0
- package/dist/expert-config-A5CHKUGI.js.map +1 -0
- package/dist/factory-ELEDP2WD.js +17 -0
- package/dist/factory-ELEDP2WD.js.map +1 -0
- package/dist/factory-IDTIBX6B.js +14 -0
- package/dist/factory-IDTIBX6B.js.map +1 -0
- package/dist/index.d.ts +1814 -777
- package/dist/index.js +785 -355
- package/dist/index.js.map +1 -1
- package/dist/issue-triage-SJPKJLXH.js +15 -0
- package/dist/issue-triage-SJPKJLXH.js.map +1 -0
- package/dist/learning-persistence-WMWZJZ35.js +16 -0
- package/dist/learning-persistence-WMWZJZ35.js.map +1 -0
- package/dist/mcp-config-2OXIOMJ6.js +12 -0
- package/dist/mcp-config-2OXIOMJ6.js.map +1 -0
- package/dist/mobimem-5S3VLNSU.js +13 -0
- package/dist/mobimem-5S3VLNSU.js.map +1 -0
- package/dist/{model-capabilities-types-CSWO27YN.d.ts → model-capabilities-types-B57GZryc.d.ts} +1 -1
- package/dist/repo-analyze-HWMXSK5C.js +24 -0
- package/dist/repo-analyze-HWMXSK5C.js.map +1 -0
- package/dist/repo-security-plan-MUFDGWSQ.js +17 -0
- package/dist/repo-security-plan-MUFDGWSQ.js.map +1 -0
- package/dist/research-helpers-synthesize-OBQJ5BGX.js +10 -0
- package/dist/research-helpers-synthesize-OBQJ5BGX.js.map +1 -0
- package/dist/routing-memory-3QBQTS4A.js +13 -0
- package/dist/routing-memory-3QBQTS4A.js.map +1 -0
- package/dist/session-memory-VXWLOFRC.js +22 -0
- package/dist/session-memory-VXWLOFRC.js.map +1 -0
- package/dist/{setup-command-SS7LMN7Y.js → setup-command-E6MXO5RZ.js} +9 -5
- package/dist/setup-command-E6MXO5RZ.js.map +1 -0
- package/dist/setup-config-O5F3AZBL.js +10 -0
- package/dist/setup-config-O5F3AZBL.js.map +1 -0
- package/dist/shared-memory-AEO2HJLC.js +8 -0
- package/dist/shared-memory-AEO2HJLC.js.map +1 -0
- package/dist/symbol-extractor-UEBANFSN.js +10 -0
- package/dist/symbol-extractor-UEBANFSN.js.map +1 -0
- package/dist/weather-report-MUGSIOU5.js +15 -0
- package/dist/weather-report-MUGSIOU5.js.map +1 -0
- package/package.json +18 -19
- package/dist/chunk-L2SHSW4T.js.map +0 -1
- package/dist/chunk-LKSTILEE.js.map +0 -1
- package/dist/chunk-QZEAD6AG.js.map +0 -1
- package/dist/dist-H5XNXVAV.js.map +0 -1
- package/dist/setup-config-DSMOOLVW.js +0 -9
- /package/dist/{doctor-deep-BDE2PHVX.js.map → adaptive-memory-RST6DZYR.js.map} +0 -0
- /package/dist/{chunk-UGNLR4NZ.js.map → chunk-3GXDN4AX.js.map} +0 -0
- /package/dist/{chunk-YSDUVCCZ.js.map → chunk-VKRMXD62.js.map} +0 -0
- /package/dist/{chunk-E7EX2KQJ.js.map → chunk-XU3NADFE.js.map} +0 -0
- /package/dist/{setup-command-SS7LMN7Y.js.map → cli-circuit-breaker-5FAODXVY.js.map} +0 -0
- /package/dist/{setup-config-DSMOOLVW.js.map → codebase-search-CZUA37RU.js.map} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/indexer/symbol-extractor.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { extname } from "path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
function getKind(node) {
|
|
6
|
+
if (ts.isFunctionDeclaration(node)) return "function";
|
|
7
|
+
if (ts.isClassDeclaration(node)) return "class";
|
|
8
|
+
if (ts.isInterfaceDeclaration(node)) return "interface";
|
|
9
|
+
if (ts.isTypeAliasDeclaration(node)) return "type";
|
|
10
|
+
if (ts.isEnumDeclaration(node)) return "enum";
|
|
11
|
+
if (ts.isMethodDeclaration(node)) return "method";
|
|
12
|
+
if (ts.isVariableStatement(node)) return "variable";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function getName(node) {
|
|
16
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
17
|
+
const nameNode = node.name;
|
|
18
|
+
return nameNode ? nameNode.getText() : "<anonymous>";
|
|
19
|
+
}
|
|
20
|
+
if (ts.isVariableStatement(node)) {
|
|
21
|
+
const decls = node.declarationList.declarations;
|
|
22
|
+
const firstDecl = decls[0];
|
|
23
|
+
if (firstDecl !== void 0) {
|
|
24
|
+
return firstDecl.name.getText();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return "<anonymous>";
|
|
28
|
+
}
|
|
29
|
+
function isExported(node) {
|
|
30
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
31
|
+
if (modifiers) {
|
|
32
|
+
return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function visitNode(node, sourceFile, symbols) {
|
|
37
|
+
const kind = getKind(node);
|
|
38
|
+
if (kind !== null) {
|
|
39
|
+
const name = getName(node);
|
|
40
|
+
if (name !== "<anonymous>") {
|
|
41
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
42
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
43
|
+
symbols.push({
|
|
44
|
+
name,
|
|
45
|
+
kind,
|
|
46
|
+
startLine: start.line + 1,
|
|
47
|
+
endLine: end.line + 1,
|
|
48
|
+
text: node.getText(sourceFile),
|
|
49
|
+
exported: isExported(node)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (ts.isClassDeclaration(node)) {
|
|
54
|
+
visitClassMembers(node, sourceFile, symbols);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
ts.forEachChild(node, (child) => {
|
|
58
|
+
visitNode(child, sourceFile, symbols);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function visitClassMembers(node, sourceFile, symbols) {
|
|
62
|
+
for (const member of node.members) {
|
|
63
|
+
if (ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member)) {
|
|
64
|
+
const memberName = member.name.getText();
|
|
65
|
+
if (memberName !== "<anonymous>") {
|
|
66
|
+
const start = sourceFile.getLineAndCharacterOfPosition(member.getStart());
|
|
67
|
+
const end = sourceFile.getLineAndCharacterOfPosition(member.getEnd());
|
|
68
|
+
symbols.push({
|
|
69
|
+
name: memberName,
|
|
70
|
+
kind: "method",
|
|
71
|
+
startLine: start.line + 1,
|
|
72
|
+
endLine: end.line + 1,
|
|
73
|
+
text: member.getText(sourceFile),
|
|
74
|
+
exported: false
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function computeSavings(totalChars, symbolChars) {
|
|
81
|
+
return totalChars > 0 ? Math.round(100 * (1 - symbolChars / totalChars) * 10) / 10 : 0;
|
|
82
|
+
}
|
|
83
|
+
async function extractSymbols(filePath) {
|
|
84
|
+
const ext = extname(filePath).toLowerCase();
|
|
85
|
+
if (![".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
86
|
+
return {
|
|
87
|
+
filePath,
|
|
88
|
+
symbols: [],
|
|
89
|
+
totalLines: 0,
|
|
90
|
+
totalChars: 0,
|
|
91
|
+
symbolChars: 0,
|
|
92
|
+
savingsPercent: 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const source = await readFile(filePath, "utf-8");
|
|
96
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
97
|
+
const symbols = [];
|
|
98
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
99
|
+
visitNode(node, sourceFile, symbols);
|
|
100
|
+
});
|
|
101
|
+
const totalChars = source.length;
|
|
102
|
+
const symbolChars = symbols.reduce((sum, s) => sum + s.text.length, 0);
|
|
103
|
+
return {
|
|
104
|
+
filePath,
|
|
105
|
+
symbols,
|
|
106
|
+
totalLines: source.split("\n").length,
|
|
107
|
+
totalChars,
|
|
108
|
+
symbolChars,
|
|
109
|
+
savingsPercent: computeSavings(totalChars, symbolChars)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function extractSymbolIndex(filePath) {
|
|
113
|
+
const result = await extractSymbols(filePath);
|
|
114
|
+
if (result.symbols.length === 0) return "";
|
|
115
|
+
const lines = result.symbols.map((s) => {
|
|
116
|
+
const exp = s.exported ? "export " : "";
|
|
117
|
+
return `${exp}${s.kind} ${s.name} (L${String(s.startLine)}-${String(s.endLine)})`;
|
|
118
|
+
});
|
|
119
|
+
return `// ${filePath} \u2014 ${String(result.symbols.length)} symbols
|
|
120
|
+
${lines.join("\n")}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
extractSymbols,
|
|
125
|
+
extractSymbolIndex
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=chunk-BQ4YXGGQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexer/symbol-extractor.ts"],"sourcesContent":["/**\n * AST symbol extraction for token-efficient code retrieval.\n *\n * Uses TypeScript's compiler API to extract function, class, method,\n * interface, and type definitions from source files.\n *\n * Token savings: ~80-99% vs reading full files.\n * No additional dependencies — uses TypeScript (already a project dep).\n *\n * @module indexer/symbol-extractor\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport ts from 'typescript';\n\n/** A symbol extracted from source code. */\nexport interface CodeSymbol {\n /** Symbol name */\n name: string;\n /** Symbol kind */\n kind: 'function' | 'class' | 'method' | 'interface' | 'type' | 'variable' | 'enum';\n /** Start line (1-based) */\n startLine: number;\n /** End line (1-based) */\n endLine: number;\n /** Full source text of the symbol */\n text: string;\n /** Whether the symbol is exported */\n exported: boolean;\n}\n\n/** Result of extracting symbols from a file. */\nexport interface SymbolExtractionResult {\n filePath: string;\n symbols: CodeSymbol[];\n totalLines: number;\n totalChars: number;\n symbolChars: number;\n savingsPercent: number;\n}\n\nfunction getKind(node: ts.Node): CodeSymbol['kind'] | null {\n if (ts.isFunctionDeclaration(node)) return 'function';\n if (ts.isClassDeclaration(node)) return 'class';\n if (ts.isInterfaceDeclaration(node)) return 'interface';\n if (ts.isTypeAliasDeclaration(node)) return 'type';\n if (ts.isEnumDeclaration(node)) return 'enum';\n if (ts.isMethodDeclaration(node)) return 'method';\n if (ts.isVariableStatement(node)) return 'variable';\n return null;\n}\n\nfunction getName(node: ts.Node): string {\n if (\n ts.isFunctionDeclaration(node) ||\n ts.isClassDeclaration(node) ||\n ts.isInterfaceDeclaration(node) ||\n ts.isTypeAliasDeclaration(node) ||\n ts.isEnumDeclaration(node) ||\n ts.isMethodDeclaration(node)\n ) {\n const nameNode = (node as ts.NamedDeclaration).name;\n return nameNode ? nameNode.getText() : '<anonymous>';\n }\n if (ts.isVariableStatement(node)) {\n const decls = node.declarationList.declarations;\n const firstDecl = decls[0];\n if (firstDecl !== undefined) {\n return firstDecl.name.getText();\n }\n }\n return '<anonymous>';\n}\n\nfunction isExported(node: ts.Node): boolean {\n const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;\n if (modifiers) {\n return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);\n }\n return false;\n}\n\nfunction visitNode(node: ts.Node, sourceFile: ts.SourceFile, symbols: CodeSymbol[]): void {\n const kind = getKind(node);\n if (kind !== null) {\n const name = getName(node);\n if (name !== '<anonymous>') {\n const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());\n symbols.push({\n name,\n kind,\n startLine: start.line + 1,\n endLine: end.line + 1,\n text: node.getText(sourceFile),\n exported: isExported(node),\n });\n }\n }\n if (ts.isClassDeclaration(node)) {\n visitClassMembers(node, sourceFile, symbols);\n return;\n }\n ts.forEachChild(node, (child) => {\n visitNode(child, sourceFile, symbols);\n });\n}\n\nfunction visitClassMembers(\n node: ts.ClassDeclaration,\n sourceFile: ts.SourceFile,\n symbols: CodeSymbol[]\n): void {\n for (const member of node.members) {\n if (ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member)) {\n const memberName = member.name.getText();\n if (memberName !== '<anonymous>') {\n const start = sourceFile.getLineAndCharacterOfPosition(member.getStart());\n const end = sourceFile.getLineAndCharacterOfPosition(member.getEnd());\n symbols.push({\n name: memberName,\n kind: 'method',\n startLine: start.line + 1,\n endLine: end.line + 1,\n text: member.getText(sourceFile),\n exported: false,\n });\n }\n }\n }\n}\n\nfunction computeSavings(totalChars: number, symbolChars: number): number {\n return totalChars > 0 ? Math.round(100 * (1 - symbolChars / totalChars) * 10) / 10 : 0;\n}\n\n/**\n * Extract symbols from a TypeScript/JavaScript file.\n */\nexport async function extractSymbols(filePath: string): Promise<SymbolExtractionResult> {\n const ext = extname(filePath).toLowerCase();\n if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {\n return {\n filePath,\n symbols: [],\n totalLines: 0,\n totalChars: 0,\n symbolChars: 0,\n savingsPercent: 0,\n };\n }\n\n const source = await readFile(filePath, 'utf-8');\n const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);\n const symbols: CodeSymbol[] = [];\n\n ts.forEachChild(sourceFile, (node) => {\n visitNode(node, sourceFile, symbols);\n });\n\n const totalChars = source.length;\n const symbolChars = symbols.reduce((sum, s) => sum + s.text.length, 0);\n\n return {\n filePath,\n symbols,\n totalLines: source.split('\\n').length,\n totalChars,\n symbolChars,\n savingsPercent: computeSavings(totalChars, symbolChars),\n };\n}\n\n/**\n * Extract a compact symbol index (names + locations only, no source text).\n * This is the minimal representation for LLM context — ~95%+ token savings.\n */\nexport async function extractSymbolIndex(filePath: string): Promise<string> {\n const result = await extractSymbols(filePath);\n if (result.symbols.length === 0) return '';\n\n const lines = result.symbols.map((s) => {\n const exp = s.exported ? 'export ' : '';\n return `${exp}${s.kind} ${s.name} (L${String(s.startLine)}-${String(s.endLine)})`;\n });\n\n return `// ${filePath} — ${String(result.symbols.length)} symbols\\n${lines.join('\\n')}`;\n}\n"],"mappings":";AAYA,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,OAAO,QAAQ;AA4Bf,SAAS,QAAQ,MAA0C;AACzD,MAAI,GAAG,sBAAsB,IAAI,EAAG,QAAO;AAC3C,MAAI,GAAG,mBAAmB,IAAI,EAAG,QAAO;AACxC,MAAI,GAAG,uBAAuB,IAAI,EAAG,QAAO;AAC5C,MAAI,GAAG,uBAAuB,IAAI,EAAG,QAAO;AAC5C,MAAI,GAAG,kBAAkB,IAAI,EAAG,QAAO;AACvC,MAAI,GAAG,oBAAoB,IAAI,EAAG,QAAO;AACzC,MAAI,GAAG,oBAAoB,IAAI,EAAG,QAAO;AACzC,SAAO;AACT;AAEA,SAAS,QAAQ,MAAuB;AACtC,MACE,GAAG,sBAAsB,IAAI,KAC7B,GAAG,mBAAmB,IAAI,KAC1B,GAAG,uBAAuB,IAAI,KAC9B,GAAG,uBAAuB,IAAI,KAC9B,GAAG,kBAAkB,IAAI,KACzB,GAAG,oBAAoB,IAAI,GAC3B;AACA,UAAM,WAAY,KAA6B;AAC/C,WAAO,WAAW,SAAS,QAAQ,IAAI;AAAA,EACzC;AACA,MAAI,GAAG,oBAAoB,IAAI,GAAG;AAChC,UAAM,QAAQ,KAAK,gBAAgB;AACnC,UAAM,YAAY,MAAM,CAAC;AACzB,QAAI,cAAc,QAAW;AAC3B,aAAO,UAAU,KAAK,QAAQ;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,MAAwB;AAC1C,QAAM,YAAY,GAAG,iBAAiB,IAAI,IAAI,GAAG,aAAa,IAAI,IAAI;AACtE,MAAI,WAAW;AACb,WAAO,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,aAAa;AAAA,EACrE;AACA,SAAO;AACT;AAEA,SAAS,UAAU,MAAe,YAA2B,SAA6B;AACxF,QAAM,OAAO,QAAQ,IAAI;AACzB,MAAI,SAAS,MAAM;AACjB,UAAM,OAAO,QAAQ,IAAI;AACzB,QAAI,SAAS,eAAe;AAC1B,YAAM,QAAQ,WAAW,8BAA8B,KAAK,SAAS,CAAC;AACtE,YAAM,MAAM,WAAW,8BAA8B,KAAK,OAAO,CAAC;AAClE,cAAQ,KAAK;AAAA,QACX;AAAA,QACA;AAAA,QACA,WAAW,MAAM,OAAO;AAAA,QACxB,SAAS,IAAI,OAAO;AAAA,QACpB,MAAM,KAAK,QAAQ,UAAU;AAAA,QAC7B,UAAU,WAAW,IAAI;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,GAAG,mBAAmB,IAAI,GAAG;AAC/B,sBAAkB,MAAM,YAAY,OAAO;AAC3C;AAAA,EACF;AACA,KAAG,aAAa,MAAM,CAAC,UAAU;AAC/B,cAAU,OAAO,YAAY,OAAO;AAAA,EACtC,CAAC;AACH;AAEA,SAAS,kBACP,MACA,YACA,SACM;AACN,aAAW,UAAU,KAAK,SAAS;AACjC,QAAI,GAAG,oBAAoB,MAAM,KAAK,GAAG,sBAAsB,MAAM,GAAG;AACtE,YAAM,aAAa,OAAO,KAAK,QAAQ;AACvC,UAAI,eAAe,eAAe;AAChC,cAAM,QAAQ,WAAW,8BAA8B,OAAO,SAAS,CAAC;AACxE,cAAM,MAAM,WAAW,8BAA8B,OAAO,OAAO,CAAC;AACpE,gBAAQ,KAAK;AAAA,UACX,MAAM;AAAA,UACN,MAAM;AAAA,UACN,WAAW,MAAM,OAAO;AAAA,UACxB,SAAS,IAAI,OAAO;AAAA,UACpB,MAAM,OAAO,QAAQ,UAAU;AAAA,UAC/B,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,eAAe,YAAoB,aAA6B;AACvE,SAAO,aAAa,IAAI,KAAK,MAAM,OAAO,IAAI,cAAc,cAAc,EAAE,IAAI,KAAK;AACvF;AAKA,eAAsB,eAAe,UAAmD;AACtF,QAAM,MAAM,QAAQ,QAAQ,EAAE,YAAY;AAC1C,MAAI,CAAC,CAAC,OAAO,QAAQ,OAAO,MAAM,EAAE,SAAS,GAAG,GAAG;AACjD,WAAO;AAAA,MACL;AAAA,MACA,SAAS,CAAC;AAAA,MACV,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,SAAS,UAAU,OAAO;AAC/C,QAAM,aAAa,GAAG,iBAAiB,UAAU,QAAQ,GAAG,aAAa,QAAQ,IAAI;AACrF,QAAM,UAAwB,CAAC;AAE/B,KAAG,aAAa,YAAY,CAAC,SAAS;AACpC,cAAU,MAAM,YAAY,OAAO;AAAA,EACrC,CAAC;AAED,QAAM,aAAa,OAAO;AAC1B,QAAM,cAAc,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,QAAQ,CAAC;AAErE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAY,OAAO,MAAM,IAAI,EAAE;AAAA,IAC/B;AAAA,IACA;AAAA,IACA,gBAAgB,eAAe,YAAY,WAAW;AAAA,EACxD;AACF;AAMA,eAAsB,mBAAmB,UAAmC;AAC1E,QAAM,SAAS,MAAM,eAAe,QAAQ;AAC5C,MAAI,OAAO,QAAQ,WAAW,EAAG,QAAO;AAExC,QAAM,QAAQ,OAAO,QAAQ,IAAI,CAAC,MAAM;AACtC,UAAM,MAAM,EAAE,WAAW,YAAY;AACrC,WAAO,GAAG,GAAG,GAAG,EAAE,IAAI,IAAI,EAAE,IAAI,MAAM,OAAO,EAAE,SAAS,CAAC,IAAI,OAAO,EAAE,OAAO,CAAC;AAAA,EAChF,CAAC;AAED,SAAO,MAAM,QAAQ,WAAM,OAAO,OAAO,QAAQ,MAAM,CAAC;AAAA,EAAa,MAAM,KAAK,IAAI,CAAC;AACvF;","names":[]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircuitBreakerRegistry,
|
|
3
|
+
CircuitError,
|
|
4
|
+
mapCliErrorToCategory
|
|
5
|
+
} from "./chunk-LLGUX44Z.js";
|
|
6
|
+
import {
|
|
7
|
+
createLogger,
|
|
8
|
+
err,
|
|
9
|
+
getTimeProvider,
|
|
10
|
+
ok
|
|
11
|
+
} from "./chunk-ELIFTCYM.js";
|
|
12
|
+
|
|
13
|
+
// src/cli-adapters/fallback-chains.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var FallbackChainSchema = z.array(z.enum(["claude", "gemini", "codex", "opencode"])).min(1).readonly();
|
|
16
|
+
var FallbackChainRegistrySchema = z.object({
|
|
17
|
+
code: FallbackChainSchema,
|
|
18
|
+
research: FallbackChainSchema,
|
|
19
|
+
documentation: FallbackChainSchema,
|
|
20
|
+
analysis: FallbackChainSchema,
|
|
21
|
+
general: FallbackChainSchema
|
|
22
|
+
});
|
|
23
|
+
var DEFAULT_FALLBACK_CHAINS = {
|
|
24
|
+
// code_generation/code_review/testing: codex primary, claude secondary
|
|
25
|
+
code: ["codex", "claude", "gemini", "opencode"],
|
|
26
|
+
// research/exploration: gemini primary, claude secondary
|
|
27
|
+
research: ["gemini", "claude", "codex", "opencode"],
|
|
28
|
+
// documentation: gemini primary, claude secondary
|
|
29
|
+
documentation: ["gemini", "claude", "codex", "opencode"],
|
|
30
|
+
// architecture/security/planning: claude primary, gemini secondary
|
|
31
|
+
// Weather data: gemini arch 66.7% (n=24) > codex 33.3% (n=3)
|
|
32
|
+
analysis: ["claude", "gemini", "codex", "opencode"],
|
|
33
|
+
// general: balanced order
|
|
34
|
+
general: ["claude", "gemini", "codex", "opencode"]
|
|
35
|
+
};
|
|
36
|
+
var CATEGORY_CHAIN_OVERRIDES = {
|
|
37
|
+
architecture: ["gemini", "claude", "codex", "opencode"],
|
|
38
|
+
security_review: ["codex", "gemini", "claude", "opencode"],
|
|
39
|
+
code_review: ["claude", "codex", "gemini", "opencode"],
|
|
40
|
+
exploration: ["gemini", "codex", "claude", "opencode"],
|
|
41
|
+
devops: ["claude", "gemini", "codex", "opencode"],
|
|
42
|
+
// codex has 15% research success (n=20) — push to last position (#1536)
|
|
43
|
+
research: ["gemini", "claude", "opencode", "codex"],
|
|
44
|
+
// codex has 33.3% docs success (n=6) — push to last position (#1536)
|
|
45
|
+
documentation: ["gemini", "claude", "opencode", "codex"]
|
|
46
|
+
};
|
|
47
|
+
function getFallbackChainForCategory(category, bucketType, registry = DEFAULT_FALLBACK_CHAINS) {
|
|
48
|
+
const override = CATEGORY_CHAIN_OVERRIDES[category];
|
|
49
|
+
if (override !== void 0) return override;
|
|
50
|
+
return registry[bucketType];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/cli-adapters/cli-circuit-breaker.ts
|
|
54
|
+
var CATEGORY_TO_FALLBACK = {
|
|
55
|
+
code_generation: "code",
|
|
56
|
+
code_review: "code",
|
|
57
|
+
testing: "code",
|
|
58
|
+
research: "research",
|
|
59
|
+
exploration: "research",
|
|
60
|
+
documentation: "documentation",
|
|
61
|
+
architecture: "analysis",
|
|
62
|
+
security_review: "analysis",
|
|
63
|
+
planning: "analysis",
|
|
64
|
+
devops: "general"
|
|
65
|
+
};
|
|
66
|
+
var DEFAULT_FALLBACK_CHAIN = ["claude", "gemini", "codex", "opencode"];
|
|
67
|
+
var DEFAULT_CONFIG = {
|
|
68
|
+
perCliConfig: {},
|
|
69
|
+
fallbackChain: DEFAULT_FALLBACK_CHAIN,
|
|
70
|
+
enableFallback: true,
|
|
71
|
+
maxFallbackAttempts: 2
|
|
72
|
+
};
|
|
73
|
+
var CliCircuitBreakerIntegration = class {
|
|
74
|
+
registry;
|
|
75
|
+
adapters = /* @__PURE__ */ new Map();
|
|
76
|
+
config;
|
|
77
|
+
logger;
|
|
78
|
+
constructor(adapters, config, logger) {
|
|
79
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
80
|
+
this.logger = logger ?? createLogger({ component: "cli-circuit-breaker-integration" });
|
|
81
|
+
this.registry = new CircuitBreakerRegistry();
|
|
82
|
+
for (const adapter of adapters) {
|
|
83
|
+
this.adapters.set(adapter.name, adapter);
|
|
84
|
+
this.registry.getBreaker(adapter.name, this.config.perCliConfig[adapter.name]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async execute(adapter, task, taskCategory) {
|
|
88
|
+
const primaryCli = adapter.name;
|
|
89
|
+
const fallbackAttempts = [];
|
|
90
|
+
let lastError;
|
|
91
|
+
const primaryResult = await this.executeWithBreaker(adapter, task);
|
|
92
|
+
if (primaryResult.ok) {
|
|
93
|
+
return ok({ response: primaryResult.value, executedBy: primaryCli, usedFallback: false });
|
|
94
|
+
}
|
|
95
|
+
lastError = primaryResult.error;
|
|
96
|
+
if (!this.config.enableFallback || !(lastError instanceof CircuitError)) {
|
|
97
|
+
return err(lastError);
|
|
98
|
+
}
|
|
99
|
+
for (const cli of this.getFallbackClis(primaryCli, taskCategory).slice(
|
|
100
|
+
0,
|
|
101
|
+
this.config.maxFallbackAttempts
|
|
102
|
+
)) {
|
|
103
|
+
const fallbackAdapter = this.adapters.get(cli);
|
|
104
|
+
if (!fallbackAdapter) continue;
|
|
105
|
+
fallbackAttempts.push(cli);
|
|
106
|
+
this.logger.info("Attempting fallback", { from: primaryCli, to: cli });
|
|
107
|
+
const result = await this.executeWithBreaker(fallbackAdapter, task);
|
|
108
|
+
if (result.ok) {
|
|
109
|
+
return ok({
|
|
110
|
+
response: result.value,
|
|
111
|
+
executedBy: cli,
|
|
112
|
+
usedFallback: true,
|
|
113
|
+
fallbackAttempts
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
lastError = result.error;
|
|
117
|
+
}
|
|
118
|
+
this.logger.warn("All fallback attempts failed", { primaryCli, fallbackAttempts });
|
|
119
|
+
return err(lastError);
|
|
120
|
+
}
|
|
121
|
+
getHealthStatus() {
|
|
122
|
+
const snapshots = this.registry.getAllSnapshots();
|
|
123
|
+
const clis = [];
|
|
124
|
+
let healthyCount = 0;
|
|
125
|
+
for (const name of this.adapters.keys()) {
|
|
126
|
+
const snapshot = snapshots.get(name);
|
|
127
|
+
if (!snapshot) continue;
|
|
128
|
+
const healthy = snapshot.state === "closed";
|
|
129
|
+
if (healthy) healthyCount++;
|
|
130
|
+
clis.push({
|
|
131
|
+
name,
|
|
132
|
+
healthy,
|
|
133
|
+
circuitState: snapshot.state,
|
|
134
|
+
failureCount: snapshot.failureCount,
|
|
135
|
+
lastFailureTime: snapshot.lastFailureTime
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
clis,
|
|
140
|
+
systemHealthy: healthyCount > 0,
|
|
141
|
+
healthyCount,
|
|
142
|
+
timestamp: getTimeProvider().now()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
getCircuitSnapshots() {
|
|
146
|
+
return this.registry.getAllSnapshots();
|
|
147
|
+
}
|
|
148
|
+
resetCircuit(cliName) {
|
|
149
|
+
this.registry.reset(cliName);
|
|
150
|
+
this.logger.info("Circuit reset", { cliName });
|
|
151
|
+
}
|
|
152
|
+
resetAllCircuits() {
|
|
153
|
+
this.registry.resetAll();
|
|
154
|
+
this.logger.info("All circuits reset");
|
|
155
|
+
}
|
|
156
|
+
addStateChangeListener(listener) {
|
|
157
|
+
this.registry.addGlobalStateChangeListener(listener);
|
|
158
|
+
}
|
|
159
|
+
async executeWithBreaker(adapter, task) {
|
|
160
|
+
const breaker = this.registry.getBreaker(adapter.name);
|
|
161
|
+
const result = await breaker.execute(async () => {
|
|
162
|
+
const execResult = await adapter.execute(task);
|
|
163
|
+
if (!execResult.ok) {
|
|
164
|
+
breaker.recordFailure(mapCliErrorToCategory(execResult.error.code));
|
|
165
|
+
const wrappedError = new Error(execResult.error.message);
|
|
166
|
+
wrappedError.cliError = execResult.error;
|
|
167
|
+
throw wrappedError;
|
|
168
|
+
}
|
|
169
|
+
return execResult.value;
|
|
170
|
+
});
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
if (result.error.circuitErrorCode === "CIRCUIT_OPEN") return err(result.error);
|
|
173
|
+
const wrapped = result.error.cause;
|
|
174
|
+
if (wrapped?.cliError) return err(wrapped.cliError);
|
|
175
|
+
return err(result.error);
|
|
176
|
+
}
|
|
177
|
+
return ok(result.value);
|
|
178
|
+
}
|
|
179
|
+
getFallbackClis(excludeCli, taskCategory) {
|
|
180
|
+
const chain = taskCategory !== void 0 ? getFallbackChainForCategory(taskCategory, CATEGORY_TO_FALLBACK[taskCategory]) : this.config.fallbackChain;
|
|
181
|
+
return [...chain].filter(
|
|
182
|
+
(cli) => cli !== excludeCli && !this.registry.isOpen(cli) && this.adapters.has(cli)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function createCliCircuitBreakerIntegration(adapters, config, logger) {
|
|
187
|
+
return new CliCircuitBreakerIntegration(adapters, config, logger);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
getFallbackChainForCategory,
|
|
192
|
+
CliCircuitBreakerIntegration,
|
|
193
|
+
createCliCircuitBreakerIntegration
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=chunk-C2C5ONFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli-adapters/fallback-chains.ts","../src/cli-adapters/cli-circuit-breaker.ts"],"sourcesContent":["/**\n * nexus-agents/cli-adapters - Fallback Chain Registry\n *\n * Configurable fallback chains per task type with metrics tracking.\n * Provides ordered CLI preferences based on task classification.\n *\n * @module cli-adapters/fallback-chains\n * (Source: Issue #362 - Task-type-aware fallback chains)\n */\n\nimport { z } from 'zod';\nimport type { CliName } from './types-core.js';\nimport type { FallbackTaskType } from './task-classifier.js';\nimport type { TaskCategory } from '../config/task-specialization-types.js';\n\n/**\n * Fallback chain: ordered list of CLIs to try for a task type.\n */\nexport type FallbackChain = readonly CliName[];\n\n/**\n * Registry mapping task types to fallback chains.\n */\nexport type FallbackChainRegistry = Readonly<Record<FallbackTaskType, FallbackChain>>;\n\n/**\n * Metrics for tracking fallback chain success rates.\n */\nexport interface FallbackChainMetrics {\n /** Total attempts for this task type */\n readonly totalAttempts: number;\n /** Successes by position in chain (0-indexed) */\n readonly successByPosition: readonly number[];\n /** Failures (exhausted all positions) */\n readonly exhaustedCount: number;\n /** Average position of successful attempt (lower is better) */\n readonly avgSuccessPosition: number;\n}\n\n/**\n * Metrics for all task types.\n */\nexport type FallbackMetricsRegistry = Readonly<Record<FallbackTaskType, FallbackChainMetrics>>;\n\n/**\n * Outcome of a fallback chain execution.\n */\nexport interface FallbackOutcome {\n /** Task type that was classified */\n readonly taskType: FallbackTaskType;\n /** Position in chain that succeeded (0-indexed), or -1 if exhausted */\n readonly successPosition: number;\n /** CLI that succeeded, or undefined if exhausted */\n readonly successfulCli?: CliName | undefined;\n /** Whether the chain was exhausted without success */\n readonly exhausted: boolean;\n}\n\n/**\n * Zod schema for fallback chain validation.\n */\nexport const FallbackChainSchema = z\n .array(z.enum(['claude', 'gemini', 'codex', 'opencode']))\n .min(1)\n .readonly();\n\n/**\n * Zod schema for fallback chain registry.\n */\nexport const FallbackChainRegistrySchema = z.object({\n code: FallbackChainSchema,\n research: FallbackChainSchema,\n documentation: FallbackChainSchema,\n analysis: FallbackChainSchema,\n general: FallbackChainSchema,\n});\n\n/**\n * Default fallback chains derived from the task specialization matrix.\n * Each chain orders CLIs by: primary → secondary → others.\n *\n * - code: Codex primary (92.4% code_gen), Claude secondary\n * - research: Gemini primary (86.8% research), Claude secondary\n * - documentation: Gemini primary (71.4% docs), Claude secondary\n * - analysis: Claude primary (architecture/security/planning), Codex secondary\n * - general: Balanced fallback order\n */\nexport const DEFAULT_FALLBACK_CHAINS: FallbackChainRegistry = {\n // code_generation/code_review/testing: codex primary, claude secondary\n code: ['codex', 'claude', 'gemini', 'opencode'],\n // research/exploration: gemini primary, claude secondary\n research: ['gemini', 'claude', 'codex', 'opencode'],\n // documentation: gemini primary, claude secondary\n documentation: ['gemini', 'claude', 'codex', 'opencode'],\n // architecture/security/planning: claude primary, gemini secondary\n // Weather data: gemini arch 66.7% (n=24) > codex 33.3% (n=3)\n analysis: ['claude', 'gemini', 'codex', 'opencode'],\n // general: balanced order\n general: ['claude', 'gemini', 'codex', 'opencode'],\n} as const;\n\n/**\n * Per-category fallback chain overrides.\n *\n * Categories in the same FallbackTaskType bucket may need different CLI ordering.\n * Entries here override the bucket-level chain from DEFAULT_FALLBACK_CHAINS.\n *\n * Weather data 2026-03-09:\n * - architecture: gemini 69.6% (n=23) > claude 40.9% (n=235) → gemini first\n * - planning: claude 92.2% → keep claude first (bucket default)\n * - security_review: codex 60% (n=5) > gemini 53.8% (n=13) > claude 54.2% (n=382) → codex first\n * - exploration: gemini 100% (n=307) > claude 83.9% (n=380) → gemini first, codex secondary (#1526)\n * - devops: claude 80% (n=5), gemini 100% (n=1) → keep claude first, gemini secondary (#1526)\n * - research: gemini 86.8% (n=38) > claude 84.1% (n=44) > codex 15% (n=20) → gemini first, codex last (#1536)\n * - documentation: gemini 71.4% (n=35) > claude 64.7% (n=17) > codex 33.3% (n=6) → gemini first, codex last (#1536)\n * - code_review: claude 91% (n=200) > codex 89.2% (n=93) > gemini 37.5% (n=8) → claude first (#1401)\n */\nexport const CATEGORY_CHAIN_OVERRIDES: Partial<Record<TaskCategory, FallbackChain>> = {\n architecture: ['gemini', 'claude', 'codex', 'opencode'],\n security_review: ['codex', 'gemini', 'claude', 'opencode'],\n code_review: ['claude', 'codex', 'gemini', 'opencode'],\n exploration: ['gemini', 'codex', 'claude', 'opencode'],\n devops: ['claude', 'gemini', 'codex', 'opencode'],\n // codex has 15% research success (n=20) — push to last position (#1536)\n research: ['gemini', 'claude', 'opencode', 'codex'],\n // codex has 33.3% docs success (n=6) — push to last position (#1536)\n documentation: ['gemini', 'claude', 'opencode', 'codex'],\n} as const;\n\n/**\n * Gets the fallback chain for a specific TaskCategory.\n * Returns a category-specific override if available, otherwise falls through\n * to the bucket-level chain via CATEGORY_TO_FALLBACK mapping.\n */\nexport function getFallbackChainForCategory(\n category: TaskCategory,\n bucketType: FallbackTaskType,\n registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS\n): FallbackChain {\n const override = CATEGORY_CHAIN_OVERRIDES[category];\n if (override !== undefined) return override;\n return registry[bucketType];\n}\n\n/**\n * Creates initial empty metrics for a task type.\n */\nfunction createEmptyMetrics(): FallbackChainMetrics {\n return {\n totalAttempts: 0,\n successByPosition: [0, 0, 0, 0],\n avgSuccessPosition: 0,\n exhaustedCount: 0,\n };\n}\n\n/**\n * Creates initial metrics registry with empty metrics for all types.\n */\nfunction createEmptyMetricsRegistry(): Record<FallbackTaskType, FallbackChainMetrics> {\n return {\n code: createEmptyMetrics(),\n research: createEmptyMetrics(),\n documentation: createEmptyMetrics(),\n analysis: createEmptyMetrics(),\n general: createEmptyMetrics(),\n };\n}\n\n/**\n * Gets the fallback chain for a task type.\n *\n * @param taskType - Task type to get chain for\n * @param registry - Optional custom registry (uses default if not provided)\n * @returns Ordered list of CLIs to try\n *\n * @example\n * ```typescript\n * const chain = getFallbackChain('code');\n * // chain === ['claude', 'codex', 'gemini']\n * ```\n */\nexport function getFallbackChain(\n taskType: FallbackTaskType,\n registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS\n): FallbackChain {\n return registry[taskType];\n}\n\n/**\n * Filters a fallback chain to only include available CLIs.\n *\n * @param chain - Original fallback chain\n * @param availableClis - Set of currently available CLI names\n * @returns Filtered chain with only available CLIs\n *\n * @example\n * ```typescript\n * const filtered = filterAvailableClis(['claude', 'codex', 'gemini'], new Set(['claude', 'gemini']));\n * // filtered === ['claude', 'gemini']\n * ```\n */\nexport function filterAvailableClis(\n chain: FallbackChain,\n availableClis: ReadonlySet<CliName>\n): CliName[] {\n return chain.filter((cli) => availableClis.has(cli));\n}\n\n/**\n * Gets the next CLI to try in a fallback chain.\n *\n * @param chain - Fallback chain\n * @param currentPosition - Current position (0-indexed)\n * @returns Next CLI to try, or undefined if chain is exhausted\n */\nexport function getNextCli(chain: FallbackChain, currentPosition: number): CliName | undefined {\n const nextPosition = currentPosition + 1;\n return chain[nextPosition];\n}\n\n/**\n * Checks if a fallback chain is exhausted at the given position.\n *\n * @param chain - Fallback chain\n * @param currentPosition - Current position (0-indexed)\n * @returns True if no more CLIs to try\n */\nexport function isChainExhausted(chain: FallbackChain, currentPosition: number): boolean {\n return currentPosition >= chain.length - 1;\n}\n\n/**\n * Fallback chain manager with metrics tracking.\n */\nexport class FallbackChainManager {\n private readonly registry: FallbackChainRegistry;\n private readonly metrics: Record<FallbackTaskType, FallbackChainMetrics>;\n\n constructor(registry: FallbackChainRegistry = DEFAULT_FALLBACK_CHAINS) {\n this.registry = registry;\n this.metrics = createEmptyMetricsRegistry();\n }\n\n /**\n * Gets the fallback chain for a task type.\n */\n getChain(taskType: FallbackTaskType): FallbackChain {\n return getFallbackChain(taskType, this.registry);\n }\n\n /**\n * Gets the chain filtered by available CLIs.\n */\n getAvailableChain(taskType: FallbackTaskType, availableClis: ReadonlySet<CliName>): CliName[] {\n const chain = this.getChain(taskType);\n return filterAvailableClis(chain, availableClis);\n }\n\n /**\n * Records an outcome and updates metrics.\n *\n * @param outcome - The outcome to record\n */\n recordOutcome(outcome: FallbackOutcome): void {\n const currentMetrics = this.metrics[outcome.taskType];\n const newSuccessByPosition = [...currentMetrics.successByPosition];\n\n // Update success counts\n if (!outcome.exhausted && outcome.successPosition >= 0) {\n const position = outcome.successPosition;\n if (position < newSuccessByPosition.length) {\n const currentCount = newSuccessByPosition[position] ?? 0;\n newSuccessByPosition[position] = currentCount + 1;\n } else {\n // Extend array if needed\n while (newSuccessByPosition.length <= position) {\n newSuccessByPosition.push(0);\n }\n newSuccessByPosition[position] = 1;\n }\n }\n\n // Calculate new average success position\n const totalSuccesses = newSuccessByPosition.reduce((sum, count) => sum + count, 0);\n const weightedSum = newSuccessByPosition.reduce((sum, count, pos) => sum + count * pos, 0);\n const avgSuccessPosition = totalSuccesses > 0 ? weightedSum / totalSuccesses : 0;\n\n // Update metrics\n this.metrics[outcome.taskType] = {\n totalAttempts: currentMetrics.totalAttempts + 1,\n successByPosition: newSuccessByPosition,\n exhaustedCount: currentMetrics.exhaustedCount + (outcome.exhausted ? 1 : 0),\n avgSuccessPosition,\n };\n }\n\n /**\n * Gets metrics for a specific task type.\n */\n getMetrics(taskType: FallbackTaskType): FallbackChainMetrics {\n return this.metrics[taskType];\n }\n\n /**\n * Gets metrics for all task types.\n */\n getAllMetrics(): FallbackMetricsRegistry {\n return { ...this.metrics };\n }\n\n /**\n * Gets the success rate at each position for a task type.\n *\n * @param taskType - Task type to get rates for\n * @returns Array of success rates (0-1) by position\n */\n getSuccessRatesByPosition(taskType: FallbackTaskType): readonly number[] {\n const metrics = this.metrics[taskType];\n if (metrics.totalAttempts === 0) {\n return [];\n }\n return metrics.successByPosition.map((count) => count / metrics.totalAttempts);\n }\n\n /**\n * Gets overall success rate for a task type.\n */\n getOverallSuccessRate(taskType: FallbackTaskType): number {\n const metrics = this.metrics[taskType];\n if (metrics.totalAttempts === 0) {\n return 0;\n }\n const totalSuccesses = metrics.successByPosition.reduce((sum, count) => sum + count, 0);\n return totalSuccesses / metrics.totalAttempts;\n }\n\n /**\n * Resets metrics for all task types.\n */\n resetMetrics(): void {\n const types: FallbackTaskType[] = ['code', 'research', 'documentation', 'analysis', 'general'];\n for (const type of types) {\n this.metrics[type] = createEmptyMetrics();\n }\n }\n}\n\n/**\n * Creates a new FallbackChainManager instance.\n *\n * @param registry - Optional custom fallback chain registry\n * @returns New FallbackChainManager instance\n */\nexport function createFallbackChainManager(registry?: FallbackChainRegistry): FallbackChainManager {\n return new FallbackChainManager(registry);\n}\n\n/**\n * Creates a custom fallback chain registry by merging with defaults.\n *\n * @param overrides - Partial registry with overrides\n * @returns Complete registry with defaults for unspecified types\n */\nexport function createFallbackChainRegistry(\n overrides: Partial<FallbackChainRegistry>\n): FallbackChainRegistry {\n return {\n ...DEFAULT_FALLBACK_CHAINS,\n ...overrides,\n };\n}\n","/**\n * nexus-agents/cli-adapters - CLI Circuit Breaker Integration\n *\n * Wraps CLI adapter calls with circuit breaker pattern for resilient\n * multi-CLI execution with automatic fallback on failures.\n *\n * (Source: Issue #359 - Integrate circuit breaker with CLI adapters)\n */\n\nimport type { Result, ILogger } from '../core/index.js';\nimport { ok, err, createLogger, getTimeProvider } from '../core/index.js';\nimport type { TaskCategory } from '../config/task-specialization-types.js';\nimport type { FallbackTaskType } from './task-classifier.js';\nimport { getFallbackChainForCategory } from './fallback-chains.js';\nimport type { ICliAdapter, CliName, CliTask, CliResponse, CliError } from './types.js';\nimport {\n CircuitBreakerRegistry,\n CircuitError,\n mapCliErrorToCategory,\n type CircuitBreakerConfig,\n type CircuitBreakerSnapshot,\n type CircuitStateChangeListener,\n} from './circuit-breaker.js';\n\n/** Maps canonical TaskCategory (10 types) to FallbackTaskType (5 types). */\nconst CATEGORY_TO_FALLBACK: Record<TaskCategory, FallbackTaskType> = {\n code_generation: 'code',\n code_review: 'code',\n testing: 'code',\n research: 'research',\n exploration: 'research',\n documentation: 'documentation',\n architecture: 'analysis',\n security_review: 'analysis',\n planning: 'analysis',\n devops: 'general',\n};\n\n/** Configuration for CLI circuit breaker integration. */\nexport interface CliCircuitBreakerConfig {\n readonly perCliConfig?: Partial<Record<CliName, Partial<CircuitBreakerConfig>>>;\n readonly fallbackChain?: ReadonlyArray<CliName>;\n readonly enableFallback?: boolean;\n readonly maxFallbackAttempts?: number;\n}\n\n/** Result of a circuit-protected execution with fallback info. */\nexport interface CircuitProtectedResult {\n readonly response: CliResponse;\n readonly executedBy: CliName;\n readonly usedFallback: boolean;\n readonly fallbackAttempts?: ReadonlyArray<CliName>;\n}\n\n/** Health status for all CLIs with circuit state. */\nexport interface CliCircuitHealthStatus {\n readonly clis: ReadonlyArray<{\n readonly name: CliName;\n readonly healthy: boolean;\n readonly circuitState: 'closed' | 'open' | 'half-open';\n readonly failureCount: number;\n readonly lastFailureTime: number | null;\n }>;\n readonly systemHealthy: boolean;\n readonly healthyCount: number;\n readonly timestamp: number;\n}\n\n/** Interface for CLI circuit breaker integration. */\nexport interface ICliCircuitBreakerIntegration {\n execute(\n adapter: ICliAdapter,\n task: CliTask,\n taskCategory?: TaskCategory\n ): Promise<Result<CircuitProtectedResult, CircuitError | CliError>>;\n getHealthStatus(): CliCircuitHealthStatus;\n getCircuitSnapshots(): Map<CliName, CircuitBreakerSnapshot>;\n resetCircuit(cliName: CliName): void;\n resetAllCircuits(): void;\n addStateChangeListener(listener: CircuitStateChangeListener): void;\n}\n\nconst DEFAULT_FALLBACK_CHAIN: ReadonlyArray<CliName> = ['claude', 'gemini', 'codex', 'opencode'];\nconst DEFAULT_CONFIG: Required<CliCircuitBreakerConfig> = {\n perCliConfig: {},\n fallbackChain: DEFAULT_FALLBACK_CHAIN,\n enableFallback: true,\n maxFallbackAttempts: 2,\n};\n\n/**\n * Integrates circuit breaker pattern with CLI adapters.\n * Provides automatic fallback when a CLI's circuit opens.\n */\nexport class CliCircuitBreakerIntegration implements ICliCircuitBreakerIntegration {\n private readonly registry: CircuitBreakerRegistry;\n private readonly adapters: Map<CliName, ICliAdapter> = new Map();\n private readonly config: Required<CliCircuitBreakerConfig>;\n private readonly logger: ILogger;\n\n constructor(\n adapters: ReadonlyArray<ICliAdapter>,\n config?: CliCircuitBreakerConfig,\n logger?: ILogger\n ) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.logger = logger ?? createLogger({ component: 'cli-circuit-breaker-integration' });\n this.registry = new CircuitBreakerRegistry();\n for (const adapter of adapters) {\n this.adapters.set(adapter.name, adapter);\n this.registry.getBreaker(adapter.name, this.config.perCliConfig[adapter.name]);\n }\n }\n\n async execute(\n adapter: ICliAdapter,\n task: CliTask,\n taskCategory?: TaskCategory\n ): Promise<Result<CircuitProtectedResult, CircuitError | CliError>> {\n const primaryCli = adapter.name;\n const fallbackAttempts: CliName[] = [];\n let lastError: CircuitError | CliError | undefined;\n\n const primaryResult = await this.executeWithBreaker(adapter, task);\n if (primaryResult.ok) {\n return ok({ response: primaryResult.value, executedBy: primaryCli, usedFallback: false });\n }\n lastError = primaryResult.error;\n\n if (!this.config.enableFallback || !(lastError instanceof CircuitError)) {\n return err(lastError);\n }\n\n for (const cli of this.getFallbackClis(primaryCli, taskCategory).slice(\n 0,\n this.config.maxFallbackAttempts\n )) {\n const fallbackAdapter = this.adapters.get(cli);\n if (!fallbackAdapter) continue;\n fallbackAttempts.push(cli);\n this.logger.info('Attempting fallback', { from: primaryCli, to: cli });\n const result = await this.executeWithBreaker(fallbackAdapter, task);\n if (result.ok) {\n return ok({\n response: result.value,\n executedBy: cli,\n usedFallback: true,\n fallbackAttempts,\n });\n }\n lastError = result.error;\n }\n\n this.logger.warn('All fallback attempts failed', { primaryCli, fallbackAttempts });\n return err(lastError);\n }\n\n getHealthStatus(): CliCircuitHealthStatus {\n const snapshots = this.registry.getAllSnapshots();\n const clis: CliCircuitHealthStatus['clis'][number][] = [];\n let healthyCount = 0;\n for (const name of this.adapters.keys()) {\n const snapshot = snapshots.get(name);\n if (!snapshot) continue;\n const healthy = snapshot.state === 'closed';\n if (healthy) healthyCount++;\n clis.push({\n name,\n healthy,\n circuitState: snapshot.state,\n failureCount: snapshot.failureCount,\n lastFailureTime: snapshot.lastFailureTime,\n });\n }\n return {\n clis,\n systemHealthy: healthyCount > 0,\n healthyCount,\n timestamp: getTimeProvider().now(),\n };\n }\n\n getCircuitSnapshots(): Map<CliName, CircuitBreakerSnapshot> {\n return this.registry.getAllSnapshots();\n }\n\n resetCircuit(cliName: CliName): void {\n this.registry.reset(cliName);\n this.logger.info('Circuit reset', { cliName });\n }\n\n resetAllCircuits(): void {\n this.registry.resetAll();\n this.logger.info('All circuits reset');\n }\n\n addStateChangeListener(listener: CircuitStateChangeListener): void {\n this.registry.addGlobalStateChangeListener(listener);\n }\n\n private async executeWithBreaker(\n adapter: ICliAdapter,\n task: CliTask\n ): Promise<Result<CliResponse, CircuitError | CliError>> {\n const breaker = this.registry.getBreaker(adapter.name);\n const result = await breaker.execute(async () => {\n const execResult = await adapter.execute(task);\n if (!execResult.ok) {\n breaker.recordFailure(mapCliErrorToCategory(execResult.error.code));\n const wrappedError = new Error(execResult.error.message);\n (wrappedError as Error & { cliError: CliError }).cliError = execResult.error;\n throw wrappedError;\n }\n return execResult.value;\n });\n\n if (!result.ok) {\n if (result.error.circuitErrorCode === 'CIRCUIT_OPEN') return err(result.error);\n const wrapped = result.error.cause as (Error & { cliError?: CliError }) | undefined;\n if (wrapped?.cliError) return err(wrapped.cliError);\n return err(result.error);\n }\n return ok(result.value);\n }\n\n private getFallbackClis(excludeCli: CliName, taskCategory?: TaskCategory): CliName[] {\n const chain =\n taskCategory !== undefined\n ? getFallbackChainForCategory(taskCategory, CATEGORY_TO_FALLBACK[taskCategory])\n : this.config.fallbackChain;\n return [...chain].filter(\n (cli) => cli !== excludeCli && !this.registry.isOpen(cli) && this.adapters.has(cli)\n );\n }\n}\n\n/** Creates a CLI circuit breaker integration with the specified adapters. */\nexport function createCliCircuitBreakerIntegration(\n adapters: ReadonlyArray<ICliAdapter>,\n config?: CliCircuitBreakerConfig,\n logger?: ILogger\n): CliCircuitBreakerIntegration {\n return new CliCircuitBreakerIntegration(adapters, config, logger);\n}\n"],"mappings":"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","names":[]}
|