simmer-automaton 0.5.0 → 0.6.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/index.d.ts CHANGED
@@ -6,6 +6,23 @@
6
6
  * LLM decides market-level trades within selected skills.
7
7
  * API enforces hard spending limits on every trade.
8
8
  */
9
+ interface SpawnResult {
10
+ stdout: string;
11
+ stderr: string;
12
+ code: number | null;
13
+ signal: NodeJS.Signals | null;
14
+ killed: boolean;
15
+ termination: "exit" | "timeout" | "no-output-timeout" | "signal";
16
+ }
17
+ interface PluginRuntime {
18
+ system: {
19
+ runCommandWithTimeout: (argv: string[], opts: {
20
+ timeoutMs: number;
21
+ cwd?: string;
22
+ env?: NodeJS.ProcessEnv;
23
+ }) => Promise<SpawnResult>;
24
+ };
25
+ }
9
26
  interface PluginApi {
10
27
  pluginConfig?: Record<string, unknown>;
11
28
  logger: {
@@ -13,6 +30,7 @@ interface PluginApi {
13
30
  warn: (msg: string) => void;
14
31
  error: (msg: string) => void;
15
32
  };
33
+ runtime: PluginRuntime;
16
34
  on: (hook: string, handler: (...args: unknown[]) => unknown) => void;
17
35
  registerService: (service: {
18
36
  id: string;
@@ -30,6 +48,7 @@ interface PluginApi {
30
48
  }
31
49
  interface ServiceCtx {
32
50
  stateDir: string;
51
+ workspaceDir?: string;
33
52
  logger: {
34
53
  info: (msg: string) => void;
35
54
  warn: (msg: string) => void;
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { selectSkills, tierMaxSkills } from "./bandit.js";
11
11
  import { computeTier } from "./tiers.js";
12
12
  import { generateTuningHints, computeTuningChanges } from "./tuning.js";
13
13
  // Plugin-local state (in-memory, refreshed from API each cycle)
14
+ let runtime;
14
15
  let api;
15
16
  let cachedState = null;
16
17
  let cachedSkills = [];
@@ -148,7 +149,7 @@ function buildPromptContext() {
148
149
  else {
149
150
  lines.push("**Active skills:** none selected yet");
150
151
  }
151
- lines.push("**Status:** Skills run independently. The automaton selects which ones can trade.");
152
+ lines.push("**Status:** The automaton executes selected skills directly no cron setup needed.");
152
153
  // --- Status ---
153
154
  lines.push("");
154
155
  lines.push(`**Tier:** ${currentTier} | **Venue:** ${config.venue}`);
@@ -192,6 +193,68 @@ function formatStatus() {
192
193
  return lines.join("\n");
193
194
  }
194
195
  // =============================================================================
196
+ // Skill execution — deterministic, no LLM in the loop
197
+ // =============================================================================
198
+ async function executeSkills(selectedSlugs, workspaceDir, logger) {
199
+ if (selectedSlugs.length === 0)
200
+ return;
201
+ if (!runtime) {
202
+ logger.error(`[simmer] runtime not initialized — skipping skill execution`);
203
+ return;
204
+ }
205
+ // Build execution plan from cached skills with entrypoints
206
+ const tasks = [];
207
+ for (const slug of selectedSlugs) {
208
+ const skill = cachedSkills.find((s) => s.id === slug);
209
+ if (!skill?.entrypoint) {
210
+ logger.warn(`[simmer] Skill ${slug} has no entrypoint — skipping execution`);
211
+ continue;
212
+ }
213
+ if (slug.includes('..') || slug.includes('/') || skill.entrypoint.includes('..') || skill.entrypoint.startsWith('/')) {
214
+ logger.warn(`[simmer] Skill ${slug} has suspicious path components — skipping`);
215
+ continue;
216
+ }
217
+ tasks.push({ slug, entrypoint: skill.entrypoint });
218
+ }
219
+ if (tasks.length === 0)
220
+ return;
221
+ const cwd = workspaceDir || process.cwd();
222
+ const env = {
223
+ ...process.env,
224
+ TRADING_VENUE: config.venue,
225
+ SIMMER_API_KEY: config.apiKey,
226
+ };
227
+ const SKILL_TIMEOUT_MS = 90_000;
228
+ logger.info(`[simmer] Executing ${tasks.length} skills: ${tasks.map((t) => t.slug).join(", ")}`);
229
+ const results = await Promise.allSettled(tasks.map(async (task) => {
230
+ const skillDir = `${cwd}/skills/${task.slug}`;
231
+ const argv = ["python3", `${skillDir}/${task.entrypoint}`, "--live", "--quiet"];
232
+ try {
233
+ const result = await runtime.system.runCommandWithTimeout(argv, {
234
+ timeoutMs: SKILL_TIMEOUT_MS,
235
+ cwd: skillDir,
236
+ env,
237
+ });
238
+ if (result.code === 0) {
239
+ logger.info(`[simmer] ✓ ${task.slug} completed (exit 0)`);
240
+ }
241
+ else {
242
+ logger.warn(`[simmer] ✗ ${task.slug} exited ${result.code} | ${result.termination} | stderr: ${result.stderr.slice(0, 200)}`);
243
+ }
244
+ return { slug: task.slug, ...result };
245
+ }
246
+ catch (e) {
247
+ logger.error(`[simmer] ✗ ${task.slug} spawn error: ${e}`);
248
+ throw e;
249
+ }
250
+ }));
251
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
252
+ const failed = results.filter((r) => r.status === "rejected").length;
253
+ if (failed > 0) {
254
+ logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
255
+ }
256
+ }
257
+ // =============================================================================
195
258
  // Plugin registration
196
259
  // =============================================================================
197
260
  export default function register(pluginApi) {
@@ -205,6 +268,7 @@ export default function register(pluginApi) {
205
268
  return;
206
269
  }
207
270
  api = new SimmerApi(config.apiKey, config.apiUrl);
271
+ runtime = pluginApi.runtime;
208
272
  const logger = pluginApi.logger;
209
273
  // --- Hook: before_prompt_build ---
210
274
  // Inject survival context into every LLM prompt
@@ -303,6 +367,15 @@ export default function register(pluginApi) {
303
367
  catch (e) {
304
368
  ctx.logger.warn(`[simmer] Failed to post cycle (active_skills may be stale): ${e}`);
305
369
  }
370
+ // Execute selected skills deterministically
371
+ if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
372
+ try {
373
+ await executeSkills(selected, ctx.workspaceDir, ctx.logger);
374
+ }
375
+ catch (e) {
376
+ ctx.logger.error(`[simmer] Skill execution error: ${e}`);
377
+ }
378
+ }
306
379
  // Also record cycle history (fire-and-forget)
307
380
  const cycleData = {
308
381
  cycle_num: cycleCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simmer-automaton",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Simmer Automaton plugin for OpenClaw — autonomous trading skill orchestration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -14,9 +14,25 @@ import { computeTier, type Tier } from "./tiers.js";
14
14
  import { generateTuningHints, computeTuningChanges, type ConfigChange } from "./tuning.js";
15
15
 
16
16
  // OpenClaw types — we declare minimal interfaces to avoid requiring the SDK as a dependency
17
+ interface SpawnResult {
18
+ stdout: string;
19
+ stderr: string;
20
+ code: number | null;
21
+ signal: NodeJS.Signals | null;
22
+ killed: boolean;
23
+ termination: "exit" | "timeout" | "no-output-timeout" | "signal";
24
+ }
25
+
26
+ interface PluginRuntime {
27
+ system: {
28
+ runCommandWithTimeout: (argv: string[], opts: { timeoutMs: number; cwd?: string; env?: NodeJS.ProcessEnv }) => Promise<SpawnResult>;
29
+ };
30
+ }
31
+
17
32
  interface PluginApi {
18
33
  pluginConfig?: Record<string, unknown>;
19
34
  logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
35
+ runtime: PluginRuntime;
20
36
  on: (hook: string, handler: (...args: unknown[]) => unknown) => void;
21
37
  registerService: (service: { id: string; start: (ctx: ServiceCtx) => Promise<void>; stop?: (ctx: ServiceCtx) => Promise<void> }) => void;
22
38
  registerCommand: (cmd: { name: string; description: string; acceptsArgs?: boolean; handler: (ctx: CommandCtx) => Promise<{ text: string }> }) => void;
@@ -24,6 +40,7 @@ interface PluginApi {
24
40
 
25
41
  interface ServiceCtx {
26
42
  stateDir: string;
43
+ workspaceDir?: string;
27
44
  logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
28
45
  }
29
46
 
@@ -32,6 +49,7 @@ interface CommandCtx {
32
49
  }
33
50
 
34
51
  // Plugin-local state (in-memory, refreshed from API each cycle)
52
+ let runtime: PluginRuntime;
35
53
  let api: SimmerApi;
36
54
  let cachedState: AutomatonState | null = null;
37
55
  let cachedSkills: Skill[] = [];
@@ -168,7 +186,7 @@ function buildPromptContext(): string {
168
186
  } else {
169
187
  lines.push("**Active skills:** none selected yet");
170
188
  }
171
- lines.push("**Status:** Skills run independently. The automaton selects which ones can trade.");
189
+ lines.push("**Status:** The automaton executes selected skills directly no cron setup needed.");
172
190
 
173
191
  // --- Status ---
174
192
  lines.push("");
@@ -219,6 +237,79 @@ function formatStatus(): string {
219
237
  return lines.join("\n");
220
238
  }
221
239
 
240
+ // =============================================================================
241
+ // Skill execution — deterministic, no LLM in the loop
242
+ // =============================================================================
243
+
244
+ async function executeSkills(
245
+ selectedSlugs: string[],
246
+ workspaceDir: string | undefined,
247
+ logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void },
248
+ ): Promise<void> {
249
+ if (selectedSlugs.length === 0) return;
250
+ if (!runtime) {
251
+ logger.error(`[simmer] runtime not initialized — skipping skill execution`);
252
+ return;
253
+ }
254
+
255
+ // Build execution plan from cached skills with entrypoints
256
+ const tasks: Array<{ slug: string; entrypoint: string }> = [];
257
+ for (const slug of selectedSlugs) {
258
+ const skill = cachedSkills.find((s) => s.id === slug);
259
+ if (!skill?.entrypoint) {
260
+ logger.warn(`[simmer] Skill ${slug} has no entrypoint — skipping execution`);
261
+ continue;
262
+ }
263
+ if (slug.includes('..') || slug.includes('/') || skill.entrypoint.includes('..') || skill.entrypoint.startsWith('/')) {
264
+ logger.warn(`[simmer] Skill ${slug} has suspicious path components — skipping`);
265
+ continue;
266
+ }
267
+ tasks.push({ slug, entrypoint: skill.entrypoint });
268
+ }
269
+
270
+ if (tasks.length === 0) return;
271
+
272
+ const cwd = workspaceDir || process.cwd();
273
+ const env: NodeJS.ProcessEnv = {
274
+ ...process.env,
275
+ TRADING_VENUE: config.venue,
276
+ SIMMER_API_KEY: config.apiKey,
277
+ };
278
+
279
+ const SKILL_TIMEOUT_MS = 90_000;
280
+
281
+ logger.info(`[simmer] Executing ${tasks.length} skills: ${tasks.map((t) => t.slug).join(", ")}`);
282
+
283
+ const results = await Promise.allSettled(
284
+ tasks.map(async (task) => {
285
+ const skillDir = `${cwd}/skills/${task.slug}`;
286
+ const argv = ["python3", `${skillDir}/${task.entrypoint}`, "--live", "--quiet"];
287
+ try {
288
+ const result = await runtime.system.runCommandWithTimeout(argv, {
289
+ timeoutMs: SKILL_TIMEOUT_MS,
290
+ cwd: skillDir,
291
+ env,
292
+ });
293
+ if (result.code === 0) {
294
+ logger.info(`[simmer] ✓ ${task.slug} completed (exit 0)`);
295
+ } else {
296
+ logger.warn(`[simmer] ✗ ${task.slug} exited ${result.code} | ${result.termination} | stderr: ${result.stderr.slice(0, 200)}`);
297
+ }
298
+ return { slug: task.slug, ...result };
299
+ } catch (e) {
300
+ logger.error(`[simmer] ✗ ${task.slug} spawn error: ${e}`);
301
+ throw e;
302
+ }
303
+ }),
304
+ );
305
+
306
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
307
+ const failed = results.filter((r) => r.status === "rejected").length;
308
+ if (failed > 0) {
309
+ logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
310
+ }
311
+ }
312
+
222
313
  // =============================================================================
223
314
  // Plugin registration
224
315
  // =============================================================================
@@ -237,6 +328,7 @@ export default function register(pluginApi: PluginApi) {
237
328
  }
238
329
 
239
330
  api = new SimmerApi(config.apiKey, config.apiUrl);
331
+ runtime = pluginApi.runtime;
240
332
  const logger = pluginApi.logger;
241
333
 
242
334
  // --- Hook: before_prompt_build ---
@@ -346,6 +438,15 @@ export default function register(pluginApi: PluginApi) {
346
438
  ctx.logger.warn(`[simmer] Failed to post cycle (active_skills may be stale): ${e}`);
347
439
  }
348
440
 
441
+ // Execute selected skills deterministically
442
+ if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
443
+ try {
444
+ await executeSkills(selected, ctx.workspaceDir, ctx.logger);
445
+ } catch (e) {
446
+ ctx.logger.error(`[simmer] Skill execution error: ${e}`);
447
+ }
448
+ }
449
+
349
450
  // Also record cycle history (fire-and-forget)
350
451
  const cycleData: Record<string, unknown> = {
351
452
  cycle_num: cycleCount,