openclaw-smartmeter 0.1.0

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.
@@ -0,0 +1,163 @@
1
+ import { createAgents } from "./agent-creator.js";
2
+ import { deepMerge } from "./merger.js";
3
+ import { validate } from "./validator.js";
4
+
5
+ /**
6
+ * Generate an optimized openclaw.json config from analysis data.
7
+ *
8
+ * @param {object} analysis - Enriched analysis from recommend()
9
+ * @param {object} currentConfig - Existing openclaw.json (or {} for fresh)
10
+ * @returns {{ config: object, validation: object, backup: object }}
11
+ */
12
+ export function generateConfig(analysis, currentConfig = {}) {
13
+ const backup = structuredClone(currentConfig);
14
+ const comments = {};
15
+
16
+ // Start from a scaffold, then merge with existing config
17
+ let config = deepMerge(scaffold(), currentConfig);
18
+
19
+ // 1. Primary model optimization
20
+ const { primary, oldPrimary } = optimizePrimary(analysis, config);
21
+ config.agents.defaults.model.primary = primary;
22
+ if (oldPrimary && oldPrimary !== primary) {
23
+ comments["agents.defaults.model.primary"] =
24
+ `SMARTMETER: Changed from ${oldPrimary}. ` +
25
+ `Saves ~$${analysis.summary.potentialSavings}/month`;
26
+ }
27
+
28
+ // 2. Specialized agents
29
+ const agents = createAgents(analysis);
30
+ for (const [name, agentConfig] of Object.entries(agents)) {
31
+ config.agents[name] = agentConfig;
32
+ comments[`agents.${name}`] =
33
+ `SMARTMETER: Auto-created for ${name} workload`;
34
+ }
35
+
36
+ // 3. Skill optimization (no-op while skills are stubbed)
37
+ const usedSkillNames = Object.keys(analysis.skills.used || {});
38
+ if (usedSkillNames.length > 0 && usedSkillNames.length < 20) {
39
+ config.skills = {
40
+ allowBundled: false,
41
+ allow: usedSkillNames,
42
+ };
43
+ const unusedCount = (analysis.skills.unused || []).length;
44
+ comments["skills"] =
45
+ `SMARTMETER: Disabled ${unusedCount} unused skills. ` +
46
+ `Saves ~${unusedCount * 200} tokens/request`;
47
+ }
48
+
49
+ // 4. Caching configuration
50
+ const patterns = analysis.temporal?.patterns || {};
51
+ if (patterns.burstUsage) {
52
+ const primaryModel = config.agents.defaults.model.primary;
53
+ config.models = config.models || {};
54
+ config.models[primaryModel] = deepMerge(
55
+ config.models[primaryModel] || {},
56
+ { params: { cacheRetention: "long" } },
57
+ );
58
+
59
+ if (patterns.quietHours?.length > 0) {
60
+ const quietRange = patterns.quietHours[0]; // e.g. "00-06"
61
+ config.heartbeat = {
62
+ every: "55m",
63
+ schedule: quietRange,
64
+ };
65
+ }
66
+
67
+ comments["models"] = "SMARTMETER: Long cache retention for burst usage";
68
+ }
69
+
70
+ // 5. Budget controls
71
+ const dailyAvg = (analysis.summary.currentMonthlyCost || 0) / 30;
72
+ const dailyBudget = Math.ceil(dailyAvg * 1.2 * 100) / 100;
73
+ const weeklyBudget = Math.ceil(dailyBudget * 7 * 100) / 100;
74
+
75
+ config.agents.defaults.budget = deepMerge(
76
+ config.agents.defaults.budget || {},
77
+ {
78
+ daily: dailyBudget,
79
+ weekly: weeklyBudget,
80
+ alert: { telegram: true, threshold: 0.75 },
81
+ },
82
+ );
83
+
84
+ // 6. Fallback chain
85
+ const fallback = buildFallbackChain(
86
+ analysis.models,
87
+ config.agents.defaults.model.primary,
88
+ );
89
+ if (fallback.length > 0) {
90
+ config.agents.defaults.model.fallback = fallback;
91
+ }
92
+
93
+ // Metadata
94
+ config._smartmeter = {
95
+ generatedAt: new Date().toISOString(),
96
+ analysisperiod: analysis.period,
97
+ comments,
98
+ };
99
+
100
+ const validation = validate(config);
101
+
102
+ return { config, validation, backup };
103
+ }
104
+
105
+ // --- Internals ---
106
+
107
+ function scaffold() {
108
+ return {
109
+ agents: {
110
+ defaults: {
111
+ model: { primary: "unknown" },
112
+ budget: {},
113
+ },
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Determine the optimal primary model.
120
+ * Strategy: find the dominant category (most tasks), use its recommendation.
121
+ */
122
+ function optimizePrimary(analysis, config) {
123
+ const oldPrimary = config.agents?.defaults?.model?.primary;
124
+
125
+ // Find the category with the most tasks that has a recommendation
126
+ let bestCategory = null;
127
+ let bestCount = 0;
128
+ for (const [name, cat] of Object.entries(analysis.categories || {})) {
129
+ if (cat.recommendation && cat.count > bestCount) {
130
+ bestCount = cat.count;
131
+ bestCategory = cat;
132
+ }
133
+ }
134
+
135
+ if (bestCategory?.recommendation?.confidence >= 0.7) {
136
+ return {
137
+ primary: bestCategory.recommendation.optimalModel,
138
+ oldPrimary,
139
+ };
140
+ }
141
+
142
+ // No strong recommendation — keep existing or pick the most-used model
143
+ if (oldPrimary && oldPrimary !== "unknown") {
144
+ return { primary: oldPrimary, oldPrimary: null };
145
+ }
146
+
147
+ const models = Object.entries(analysis.models || {});
148
+ if (models.length === 0) return { primary: "unknown", oldPrimary: null };
149
+
150
+ models.sort((a, b) => b[1].count - a[1].count);
151
+ return { primary: models[0][0], oldPrimary: null };
152
+ }
153
+
154
+ /**
155
+ * Build fallback chain: all models sorted by avgCostPerTask ascending,
156
+ * excluding the primary.
157
+ */
158
+ function buildFallbackChain(models, primary) {
159
+ return Object.entries(models || {})
160
+ .filter(([name]) => name !== primary)
161
+ .sort((a, b) => a[1].avgCostPerTask - b[1].avgCostPerTask)
162
+ .map(([name]) => name);
163
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Deep merge source into target. Returns a new object.
3
+ * - Objects are recursively merged
4
+ * - Arrays from source replace target arrays
5
+ * - Primitives from source overwrite target
6
+ * - Neither input is mutated
7
+ */
8
+ export function deepMerge(target, source) {
9
+ const result = { ...target };
10
+
11
+ for (const key of Object.keys(source)) {
12
+ const srcVal = source[key];
13
+ const tgtVal = result[key];
14
+
15
+ if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
16
+ result[key] = deepMerge(tgtVal, srcVal);
17
+ } else {
18
+ result[key] = structuredClone(srcVal);
19
+ }
20
+ }
21
+
22
+ return result;
23
+ }
24
+
25
+ function isPlainObject(val) {
26
+ return val !== null && typeof val === "object" && !Array.isArray(val);
27
+ }
@@ -0,0 +1,54 @@
1
+ const ALLOWED_TOP_KEYS = new Set([
2
+ "agents",
3
+ "skills",
4
+ "models",
5
+ "heartbeat",
6
+ "_smartmeter",
7
+ ]);
8
+
9
+ /**
10
+ * Validate an openclaw config object.
11
+ * Returns { valid: boolean, errors: string[] }.
12
+ */
13
+ export function validate(config) {
14
+ const errors = [];
15
+
16
+ if (config == null || typeof config !== "object" || Array.isArray(config)) {
17
+ return { valid: false, errors: ["Config must be a non-null object"] };
18
+ }
19
+
20
+ // Check top-level keys
21
+ for (const key of Object.keys(config)) {
22
+ if (!ALLOWED_TOP_KEYS.has(key)) {
23
+ errors.push(`Unknown top-level key: "${key}"`);
24
+ }
25
+ }
26
+
27
+ // agents.defaults.model.primary must exist and be a string
28
+ const primary = config.agents?.defaults?.model?.primary;
29
+ if (primary === undefined) {
30
+ errors.push("Missing agents.defaults.model.primary");
31
+ } else if (typeof primary !== "string") {
32
+ errors.push("agents.defaults.model.primary must be a string");
33
+ }
34
+
35
+ // Budget validation (if present)
36
+ const budget = config.agents?.defaults?.budget;
37
+ if (budget) {
38
+ if (budget.daily !== undefined && (typeof budget.daily !== "number" || budget.daily <= 0)) {
39
+ errors.push("agents.defaults.budget.daily must be a positive number");
40
+ }
41
+ if (budget.weekly !== undefined && (typeof budget.weekly !== "number" || budget.weekly <= 0)) {
42
+ errors.push("agents.defaults.budget.weekly must be a positive number");
43
+ }
44
+ if (
45
+ typeof budget.daily === "number" &&
46
+ typeof budget.weekly === "number" &&
47
+ budget.weekly < budget.daily
48
+ ) {
49
+ errors.push("agents.defaults.budget.weekly must be >= daily");
50
+ }
51
+ }
52
+
53
+ return { valid: errors.length === 0, errors };
54
+ }