simmer-automaton 0.1.5 → 0.2.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.
- package/dist/api.d.ts +32 -4
- package/dist/api.js +18 -0
- package/dist/index.js +80 -5
- package/package.json +2 -2
- package/src/api.ts +34 -4
- package/src/index.ts +81 -5
package/dist/api.d.ts
CHANGED
|
@@ -19,10 +19,7 @@ export interface Skill {
|
|
|
19
19
|
category: string;
|
|
20
20
|
tags: string[];
|
|
21
21
|
difficulty: string;
|
|
22
|
-
|
|
23
|
-
clawhub_url: string;
|
|
24
|
-
requires: string[];
|
|
25
|
-
best_when: string | null;
|
|
22
|
+
enabled: boolean;
|
|
26
23
|
}
|
|
27
24
|
export declare class SimmerApi {
|
|
28
25
|
private baseUrl;
|
|
@@ -37,4 +34,35 @@ export declare class SimmerApi {
|
|
|
37
34
|
skills: Skill[];
|
|
38
35
|
total: number;
|
|
39
36
|
}>;
|
|
37
|
+
getAutomatonSkills(): Promise<{
|
|
38
|
+
skills: Skill[];
|
|
39
|
+
total: number;
|
|
40
|
+
}>;
|
|
41
|
+
disableSkill(slug: string): Promise<unknown>;
|
|
42
|
+
enableSkill(slug: string): Promise<unknown>;
|
|
43
|
+
recordCycle(data: {
|
|
44
|
+
cycle_num: number;
|
|
45
|
+
tier: string;
|
|
46
|
+
epsilon: number;
|
|
47
|
+
selected_skills: Array<{
|
|
48
|
+
slug: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
score: number | null;
|
|
51
|
+
}>;
|
|
52
|
+
tuning_hints: Array<{
|
|
53
|
+
skill: string;
|
|
54
|
+
issue: string;
|
|
55
|
+
suggestion: string;
|
|
56
|
+
}>;
|
|
57
|
+
budget_usd?: number;
|
|
58
|
+
spent_usd?: number;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
id: number;
|
|
61
|
+
cycle_num: number;
|
|
62
|
+
created_at: string;
|
|
63
|
+
}>;
|
|
64
|
+
getCycles(limit?: number, since?: string): Promise<{
|
|
65
|
+
cycles: Array<Record<string, unknown>>;
|
|
66
|
+
total: number;
|
|
67
|
+
}>;
|
|
40
68
|
}
|
package/dist/api.js
CHANGED
|
@@ -42,4 +42,22 @@ export class SimmerApi {
|
|
|
42
42
|
async getSkills() {
|
|
43
43
|
return this.request("/api/sdk/skills");
|
|
44
44
|
}
|
|
45
|
+
async getAutomatonSkills() {
|
|
46
|
+
return this.request("/api/sdk/automaton/skills");
|
|
47
|
+
}
|
|
48
|
+
async disableSkill(slug) {
|
|
49
|
+
return this.request(`/api/sdk/automaton/skills/${encodeURIComponent(slug)}/disable`, { method: "POST" });
|
|
50
|
+
}
|
|
51
|
+
async enableSkill(slug) {
|
|
52
|
+
return this.request(`/api/sdk/automaton/skills/${encodeURIComponent(slug)}/enable`, { method: "POST" });
|
|
53
|
+
}
|
|
54
|
+
async recordCycle(data) {
|
|
55
|
+
return this.request("/api/sdk/automaton/cycles", { method: "POST", body: JSON.stringify(data) });
|
|
56
|
+
}
|
|
57
|
+
async getCycles(limit = 20, since) {
|
|
58
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
59
|
+
if (since)
|
|
60
|
+
params.set("since", since);
|
|
61
|
+
return this.request(`/api/sdk/automaton/cycles?${params}`);
|
|
62
|
+
}
|
|
45
63
|
}
|
package/dist/index.js
CHANGED
|
@@ -53,8 +53,16 @@ function loadConfig(pluginConfig) {
|
|
|
53
53
|
async function refreshState(logger) {
|
|
54
54
|
try {
|
|
55
55
|
cachedState = await api.getAutomatonState();
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Use automaton skills endpoint (respects per-user enable/disable prefs)
|
|
57
|
+
try {
|
|
58
|
+
const res = await api.getAutomatonSkills();
|
|
59
|
+
cachedSkills = res.skills;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Fall back to generic skills endpoint if automaton endpoint not available
|
|
63
|
+
const res = await api.getSkills();
|
|
64
|
+
cachedSkills = res.skills;
|
|
65
|
+
}
|
|
58
66
|
if (cachedState.initialized) {
|
|
59
67
|
// Compute tier (totalPnl = 0 for now, will be enriched when P&L tracking is added)
|
|
60
68
|
currentTier = computeTier(cachedState, 0);
|
|
@@ -166,7 +174,21 @@ export default function register(pluginApi) {
|
|
|
166
174
|
await refreshState(ctx.logger);
|
|
167
175
|
// Decay epsilon
|
|
168
176
|
config.epsilon = Math.max(config.minEpsilon, config.epsilon * config.epsilonDecay);
|
|
169
|
-
|
|
177
|
+
// Select skills and generate hints for this cycle
|
|
178
|
+
const n = tierMaxSkills(currentTier, config.maxConcurrent);
|
|
179
|
+
const { selected, meta } = selectSkills(banditState, n, currentTier, config.epsilon);
|
|
180
|
+
const hints = generateTuningHints(banditState, cachedState?.budget_usd ?? 0);
|
|
181
|
+
// Record cycle to API (fire-and-forget — don't block the loop)
|
|
182
|
+
api.recordCycle({
|
|
183
|
+
cycle_num: cycleCount,
|
|
184
|
+
tier: currentTier,
|
|
185
|
+
epsilon: parseFloat(config.epsilon.toFixed(4)),
|
|
186
|
+
selected_skills: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
187
|
+
tuning_hints: hints,
|
|
188
|
+
budget_usd: cachedState?.budget_usd,
|
|
189
|
+
spent_usd: cachedState?.spent_usd,
|
|
190
|
+
}).catch((e) => ctx.logger.error(`[simmer] Failed to record cycle: ${e}`));
|
|
191
|
+
ctx.logger.info(`[simmer] Cycle ${cycleCount} | tier=${currentTier} | ε=${config.epsilon.toFixed(3)} | selected=${selected.length} skills`);
|
|
170
192
|
}, config.cycleIntervalMs);
|
|
171
193
|
},
|
|
172
194
|
stop: async (ctx) => {
|
|
@@ -214,11 +236,64 @@ export default function register(pluginApi) {
|
|
|
214
236
|
if (cachedSkills.length === 0) {
|
|
215
237
|
return { text: "No skills in registry." };
|
|
216
238
|
}
|
|
217
|
-
const lines = cachedSkills.map((s) =>
|
|
239
|
+
const lines = cachedSkills.map((s) => {
|
|
240
|
+
const status = s.enabled === false ? " [DISABLED]" : "";
|
|
241
|
+
return `- ${s.name} (${s.id}) — ${s.category}, ${s.difficulty}${status}`;
|
|
242
|
+
});
|
|
218
243
|
return { text: `Skills (${cachedSkills.length}):\n${lines.join("\n")}` };
|
|
219
244
|
}
|
|
245
|
+
if (subcommand === "disable") {
|
|
246
|
+
const slug = ctx.args?.trim().split(/\s+/)[1];
|
|
247
|
+
if (!slug) {
|
|
248
|
+
return { text: "Usage: /simmer disable <skill-slug>" };
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
await api.disableSkill(slug);
|
|
252
|
+
await refreshState(logger);
|
|
253
|
+
return { text: `Disabled skill: ${slug}. It won't be selected by the automaton.` };
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return { text: `Failed to disable: ${e}` };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (subcommand === "enable") {
|
|
260
|
+
const slug = ctx.args?.trim().split(/\s+/)[1];
|
|
261
|
+
if (!slug) {
|
|
262
|
+
return { text: "Usage: /simmer enable <skill-slug>" };
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await api.enableSkill(slug);
|
|
266
|
+
await refreshState(logger);
|
|
267
|
+
return { text: `Enabled skill: ${slug}. It will be included in the automaton's skill pool.` };
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return { text: `Failed to enable: ${e}` };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (subcommand === "history") {
|
|
274
|
+
const limitArg = ctx.args?.trim().split(/\s+/)[1];
|
|
275
|
+
const limit = limitArg ? Math.min(parseInt(limitArg, 10) || 10, 50) : 10;
|
|
276
|
+
try {
|
|
277
|
+
const res = await api.getCycles(limit);
|
|
278
|
+
if (res.cycles.length === 0) {
|
|
279
|
+
return { text: "No cycle history yet. Cycles are recorded once the automaton starts running." };
|
|
280
|
+
}
|
|
281
|
+
const lines = res.cycles.map((c) => {
|
|
282
|
+
const skills = c.selected_skills || [];
|
|
283
|
+
const skillStr = skills.length > 0
|
|
284
|
+
? skills.map((s) => `${s.slug} (${s.reason})`).join(", ")
|
|
285
|
+
: "none";
|
|
286
|
+
const eps = typeof c.epsilon === "number" ? c.epsilon.toFixed(3) : "?";
|
|
287
|
+
return `#${c.cycle_num} | ${c.tier} | ε=${eps} | skills=[${skillStr}] | budget=${c.budget_usd ?? "?"}/${c.spent_usd ?? "?"} | ${c.created_at}`;
|
|
288
|
+
});
|
|
289
|
+
return { text: `Last ${res.cycles.length} cycles:\n${lines.join("\n")}` };
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return { text: `Failed to fetch history: ${e}` };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
220
295
|
return {
|
|
221
|
-
text: "Usage: /simmer [status|halt|resume|skills]",
|
|
296
|
+
text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>]",
|
|
222
297
|
};
|
|
223
298
|
},
|
|
224
299
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simmer-automaton",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Simmer Automaton plugin for OpenClaw —
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Simmer Automaton plugin for OpenClaw — autonomous trading skill orchestration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
package/src/api.ts
CHANGED
|
@@ -21,10 +21,7 @@ export interface Skill {
|
|
|
21
21
|
category: string;
|
|
22
22
|
tags: string[];
|
|
23
23
|
difficulty: string;
|
|
24
|
-
|
|
25
|
-
clawhub_url: string;
|
|
26
|
-
requires: string[];
|
|
27
|
-
best_when: string | null;
|
|
24
|
+
enabled: boolean;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export class SimmerApi {
|
|
@@ -74,4 +71,37 @@ export class SimmerApi {
|
|
|
74
71
|
async getSkills(): Promise<{ skills: Skill[]; total: number }> {
|
|
75
72
|
return this.request("/api/sdk/skills");
|
|
76
73
|
}
|
|
74
|
+
|
|
75
|
+
async getAutomatonSkills(): Promise<{ skills: Skill[]; total: number }> {
|
|
76
|
+
return this.request("/api/sdk/automaton/skills");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async disableSkill(slug: string) {
|
|
80
|
+
return this.request(`/api/sdk/automaton/skills/${encodeURIComponent(slug)}/disable`, { method: "POST" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async enableSkill(slug: string) {
|
|
84
|
+
return this.request(`/api/sdk/automaton/skills/${encodeURIComponent(slug)}/enable`, { method: "POST" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async recordCycle(data: {
|
|
88
|
+
cycle_num: number;
|
|
89
|
+
tier: string;
|
|
90
|
+
epsilon: number;
|
|
91
|
+
selected_skills: Array<{ slug: string; reason: string; score: number | null }>;
|
|
92
|
+
tuning_hints: Array<{ skill: string; issue: string; suggestion: string }>;
|
|
93
|
+
budget_usd?: number;
|
|
94
|
+
spent_usd?: number;
|
|
95
|
+
}) {
|
|
96
|
+
return this.request<{ id: number; cycle_num: number; created_at: string }>(
|
|
97
|
+
"/api/sdk/automaton/cycles",
|
|
98
|
+
{ method: "POST", body: JSON.stringify(data) },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getCycles(limit = 20, since?: string): Promise<{ cycles: Array<Record<string, unknown>>; total: number }> {
|
|
103
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
104
|
+
if (since) params.set("since", since);
|
|
105
|
+
return this.request(`/api/sdk/automaton/cycles?${params}`);
|
|
106
|
+
}
|
|
77
107
|
}
|
package/src/index.ts
CHANGED
|
@@ -68,8 +68,15 @@ function loadConfig(pluginConfig?: Record<string, unknown>) {
|
|
|
68
68
|
async function refreshState(logger: { info: (m: string) => void; error: (m: string) => void }) {
|
|
69
69
|
try {
|
|
70
70
|
cachedState = await api.getAutomatonState();
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Use automaton skills endpoint (respects per-user enable/disable prefs)
|
|
72
|
+
try {
|
|
73
|
+
const res = await api.getAutomatonSkills();
|
|
74
|
+
cachedSkills = res.skills;
|
|
75
|
+
} catch {
|
|
76
|
+
// Fall back to generic skills endpoint if automaton endpoint not available
|
|
77
|
+
const res = await api.getSkills();
|
|
78
|
+
cachedSkills = res.skills;
|
|
79
|
+
}
|
|
73
80
|
|
|
74
81
|
if (cachedState.initialized) {
|
|
75
82
|
// Compute tier (totalPnl = 0 for now, will be enriched when P&L tracking is added)
|
|
@@ -202,8 +209,24 @@ export default function register(pluginApi: PluginApi) {
|
|
|
202
209
|
config.epsilon * config.epsilonDecay,
|
|
203
210
|
);
|
|
204
211
|
|
|
212
|
+
// Select skills and generate hints for this cycle
|
|
213
|
+
const n = tierMaxSkills(currentTier, config.maxConcurrent);
|
|
214
|
+
const { selected, meta } = selectSkills(banditState, n, currentTier, config.epsilon);
|
|
215
|
+
const hints = generateTuningHints(banditState, cachedState?.budget_usd ?? 0);
|
|
216
|
+
|
|
217
|
+
// Record cycle to API (fire-and-forget — don't block the loop)
|
|
218
|
+
api.recordCycle({
|
|
219
|
+
cycle_num: cycleCount,
|
|
220
|
+
tier: currentTier,
|
|
221
|
+
epsilon: parseFloat(config.epsilon.toFixed(4)),
|
|
222
|
+
selected_skills: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
223
|
+
tuning_hints: hints,
|
|
224
|
+
budget_usd: cachedState?.budget_usd,
|
|
225
|
+
spent_usd: cachedState?.spent_usd,
|
|
226
|
+
}).catch((e) => ctx.logger.error(`[simmer] Failed to record cycle: ${e}`));
|
|
227
|
+
|
|
205
228
|
ctx.logger.info(
|
|
206
|
-
`[simmer] Cycle ${cycleCount} | tier=${currentTier} | ε=${config.epsilon.toFixed(3)} |
|
|
229
|
+
`[simmer] Cycle ${cycleCount} | tier=${currentTier} | ε=${config.epsilon.toFixed(3)} | selected=${selected.length} skills`,
|
|
207
230
|
);
|
|
208
231
|
}, config.cycleIntervalMs);
|
|
209
232
|
},
|
|
@@ -257,13 +280,66 @@ export default function register(pluginApi: PluginApi) {
|
|
|
257
280
|
return { text: "No skills in registry." };
|
|
258
281
|
}
|
|
259
282
|
const lines = cachedSkills.map(
|
|
260
|
-
(s) =>
|
|
283
|
+
(s) => {
|
|
284
|
+
const status = (s as any).enabled === false ? " [DISABLED]" : "";
|
|
285
|
+
return `- ${s.name} (${s.id}) — ${s.category}, ${s.difficulty}${status}`;
|
|
286
|
+
},
|
|
261
287
|
);
|
|
262
288
|
return { text: `Skills (${cachedSkills.length}):\n${lines.join("\n")}` };
|
|
263
289
|
}
|
|
264
290
|
|
|
291
|
+
if (subcommand === "disable") {
|
|
292
|
+
const slug = ctx.args?.trim().split(/\s+/)[1];
|
|
293
|
+
if (!slug) {
|
|
294
|
+
return { text: "Usage: /simmer disable <skill-slug>" };
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
await api.disableSkill(slug);
|
|
298
|
+
await refreshState(logger);
|
|
299
|
+
return { text: `Disabled skill: ${slug}. It won't be selected by the automaton.` };
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return { text: `Failed to disable: ${e}` };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (subcommand === "enable") {
|
|
306
|
+
const slug = ctx.args?.trim().split(/\s+/)[1];
|
|
307
|
+
if (!slug) {
|
|
308
|
+
return { text: "Usage: /simmer enable <skill-slug>" };
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
await api.enableSkill(slug);
|
|
312
|
+
await refreshState(logger);
|
|
313
|
+
return { text: `Enabled skill: ${slug}. It will be included in the automaton's skill pool.` };
|
|
314
|
+
} catch (e) {
|
|
315
|
+
return { text: `Failed to enable: ${e}` };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (subcommand === "history") {
|
|
320
|
+
const limitArg = ctx.args?.trim().split(/\s+/)[1];
|
|
321
|
+
const limit = limitArg ? Math.min(parseInt(limitArg, 10) || 10, 50) : 10;
|
|
322
|
+
try {
|
|
323
|
+
const res = await api.getCycles(limit);
|
|
324
|
+
if (res.cycles.length === 0) {
|
|
325
|
+
return { text: "No cycle history yet. Cycles are recorded once the automaton starts running." };
|
|
326
|
+
}
|
|
327
|
+
const lines = res.cycles.map((c: Record<string, unknown>) => {
|
|
328
|
+
const skills = (c.selected_skills as Array<{ slug: string; reason: string }>) || [];
|
|
329
|
+
const skillStr = skills.length > 0
|
|
330
|
+
? skills.map((s) => `${s.slug} (${s.reason})`).join(", ")
|
|
331
|
+
: "none";
|
|
332
|
+
const eps = typeof c.epsilon === "number" ? c.epsilon.toFixed(3) : "?";
|
|
333
|
+
return `#${c.cycle_num} | ${c.tier} | ε=${eps} | skills=[${skillStr}] | budget=${c.budget_usd ?? "?"}/${c.spent_usd ?? "?"} | ${c.created_at}`;
|
|
334
|
+
});
|
|
335
|
+
return { text: `Last ${res.cycles.length} cycles:\n${lines.join("\n")}` };
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return { text: `Failed to fetch history: ${e}` };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
265
341
|
return {
|
|
266
|
-
text: "Usage: /simmer [status|halt|resume|skills]",
|
|
342
|
+
text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>]",
|
|
267
343
|
};
|
|
268
344
|
},
|
|
269
345
|
});
|