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 +19 -0
- package/dist/index.js +74 -1
- package/package.json +1 -1
- package/src/index.ts +102 -1
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:**
|
|
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
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:**
|
|
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,
|