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 CHANGED
@@ -19,10 +19,7 @@ export interface Skill {
19
19
  category: string;
20
20
  tags: string[];
21
21
  difficulty: string;
22
- install: string;
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
- const skillsRes = await api.getSkills();
57
- cachedSkills = skillsRes.skills;
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
- ctx.logger.info(`[simmer] Cycle ${cycleCount} | tier=${currentTier} | ε=${config.epsilon.toFixed(3)} | skills=${cachedSkills.length}`);
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) => `- ${s.name} (${s.id}) — ${s.category}, ${s.difficulty}`);
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.1.5",
4
- "description": "Simmer Automaton plugin for OpenClaw — adaptive trading skill orchestration",
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
- install: string;
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
- const skillsRes = await api.getSkills();
72
- cachedSkills = skillsRes.skills;
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)} | skills=${cachedSkills.length}`,
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) => `- ${s.name} (${s.id}) — ${s.category}, ${s.difficulty}`,
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
  });