simmer-automaton 0.6.1 → 0.6.3

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
@@ -35,6 +35,37 @@ export interface Skill {
35
35
  config?: Record<string, number | string | boolean>;
36
36
  pinned?: string[];
37
37
  }
38
+ export interface BriefingPosition {
39
+ market_id: string;
40
+ question: string;
41
+ side: string;
42
+ shares: number;
43
+ avg_entry: number;
44
+ current_price: number;
45
+ pnl: number;
46
+ resolves_at: string | null;
47
+ source: string | null;
48
+ }
49
+ export interface BriefingResponse {
50
+ portfolio: {
51
+ total_pnl: number;
52
+ pnl_percent: number;
53
+ win_rate: number;
54
+ rank: number | null;
55
+ total_agents: number;
56
+ settled_pnl: number;
57
+ };
58
+ positions: BriefingPosition[];
59
+ recent_trades: Array<{
60
+ market_question: string;
61
+ action: string;
62
+ side: string;
63
+ shares: number;
64
+ cost: number;
65
+ source: string | null;
66
+ created_at: string;
67
+ }>;
68
+ }
38
69
  export interface SkillOutcome {
39
70
  skill_slug: string;
40
71
  trades: number;
@@ -90,6 +121,7 @@ export declare class SimmerApi {
90
121
  outcomes: SkillOutcome[];
91
122
  since: string;
92
123
  }>;
124
+ getBriefing(): Promise<BriefingResponse>;
93
125
  postCycle(data: {
94
126
  active_skills: string[];
95
127
  cycle_number: number;
package/dist/api.js CHANGED
@@ -70,6 +70,9 @@ export class SimmerApi {
70
70
  const params = new URLSearchParams({ since });
71
71
  return this.request(`/api/sdk/automaton/outcomes?${params}`);
72
72
  }
73
+ async getBriefing() {
74
+ return this.request("/api/sdk/briefing");
75
+ }
73
76
  async postCycle(data) {
74
77
  return this.request("/api/sdk/automaton/cycle", {
75
78
  method: "POST",
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ let banditState = [];
19
19
  let currentTier = "normal";
20
20
  let cycleCount = 0;
21
21
  let currentSelectedMeta = [];
22
+ let lastExecutionResults = [];
23
+ let cachedPortfolio = null;
22
24
  let serviceRunning = false;
23
25
  let cycleTimer = null;
24
26
  let lastCycleTimestamp = new Date().toISOString();
@@ -163,6 +165,31 @@ function buildPromptContext() {
163
165
  lines.push(`- [${h.skill}] ${h.issue}: ${h.suggestion}`);
164
166
  }
165
167
  }
168
+ // --- Last execution results ---
169
+ if (lastExecutionResults.length > 0) {
170
+ lines.push("");
171
+ lines.push("**Last execution:**");
172
+ for (const r of lastExecutionResults) {
173
+ lines.push(`- ${r.ok ? "✓" : "✗"} ${r.slug}: ${r.detail}`);
174
+ }
175
+ }
176
+ // --- Portfolio snapshot ---
177
+ if (cachedPortfolio) {
178
+ lines.push("");
179
+ lines.push(`**Portfolio:** ${cachedPortfolio.positionCount} positions | P&L: ${fmtCurrency(cachedPortfolio.totalPnl)} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
180
+ if (cachedPortfolio.positions.length > 0) {
181
+ // Show top 3 by absolute PnL
182
+ const sorted = [...cachedPortfolio.positions].sort((a, b) => Math.abs(b.pnl) - Math.abs(a.pnl));
183
+ const top = sorted.slice(0, 3);
184
+ for (const p of top) {
185
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
186
+ lines.push(` - ${p.question.slice(0, 60)} | ${p.side} ${p.shares} shares @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}`);
187
+ }
188
+ if (sorted.length > 3) {
189
+ lines.push(` - ...and ${sorted.length - 3} more (use /simmer portfolio for full list)`);
190
+ }
191
+ }
192
+ }
166
193
  // --- Tier warnings ---
167
194
  if (currentTier === "critical") {
168
195
  lines.push("");
@@ -170,7 +197,7 @@ function buildPromptContext() {
170
197
  }
171
198
  // --- Instructions for human-facing queries ---
172
199
  lines.push("");
173
- lines.push("**When your human asks about the automaton:** Report tier, budget, burn rate, which skills are running, and any tuning hints. Use `/simmer history` for recent cycle decisions. Don't dump raw data — summarize.");
200
+ lines.push("**When your human asks about the automaton:** Report tier, budget, positions, P&L, and which skills ran last cycle. Use `/simmer portfolio` for full position details. Don't dump raw data — summarize.");
174
201
  lines.push("");
175
202
  lines.push("**Currency formatting:** $SIM amounts must be written as `XXX $SIM` (e.g. `25.00 $SIM`, `100.00 $SIM`). NEVER write `$SIM25` or `$SIMxx` — the `$SIM` suffix goes AFTER the number. Real USDC uses `$` prefix (e.g. `$25.00`).");
176
203
  return lines.join("\n");
@@ -197,10 +224,10 @@ function formatStatus() {
197
224
  // =============================================================================
198
225
  async function executeSkills(selectedSlugs, workspaceDir, logger) {
199
226
  if (selectedSlugs.length === 0)
200
- return;
227
+ return [];
201
228
  if (!runtime) {
202
229
  logger.error(`[simmer] runtime not initialized — skipping skill execution`);
203
- return;
230
+ return [];
204
231
  }
205
232
  // Build execution plan from cached skills with entrypoints
206
233
  const tasks = [];
@@ -217,7 +244,7 @@ async function executeSkills(selectedSlugs, workspaceDir, logger) {
217
244
  tasks.push({ slug, entrypoint: skill.entrypoint });
218
245
  }
219
246
  if (tasks.length === 0)
220
- return;
247
+ return [];
221
248
  const cwd = workspaceDir || process.cwd();
222
249
  const env = {
223
250
  ...process.env,
@@ -248,11 +275,23 @@ async function executeSkills(selectedSlugs, workspaceDir, logger) {
248
275
  throw e;
249
276
  }
250
277
  }));
251
- const succeeded = results.filter((r) => r.status === "fulfilled").length;
252
- const failed = results.filter((r) => r.status === "rejected").length;
278
+ const execResults = [];
279
+ for (let i = 0; i < tasks.length; i++) {
280
+ const r = results[i];
281
+ if (r.status === "fulfilled") {
282
+ const v = r.value;
283
+ execResults.push({ slug: v.slug, ok: v.code === 0, detail: v.code === 0 ? "ok" : `exit ${v.code}: ${v.stderr.slice(0, 100)}` });
284
+ }
285
+ else {
286
+ execResults.push({ slug: tasks[i].slug, ok: false, detail: `spawn error: ${r.reason}` });
287
+ }
288
+ }
289
+ const succeeded = execResults.filter((r) => r.ok).length;
290
+ const failed = execResults.filter((r) => !r.ok).length;
253
291
  if (failed > 0) {
254
292
  logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
255
293
  }
294
+ return execResults;
256
295
  }
257
296
  // =============================================================================
258
297
  // Plugin registration
@@ -291,6 +330,7 @@ export default function register(pluginApi) {
291
330
  if (!serviceRunning)
292
331
  return;
293
332
  cycleCount++;
333
+ const thisCycleNumber = cycleCount; // capture before refreshState may reset on re-init
294
334
  const cycleStarted = lastCycleTimestamp;
295
335
  lastCycleTimestamp = new Date().toISOString();
296
336
  await refreshState(ctx.logger);
@@ -359,7 +399,7 @@ export default function register(pluginApi) {
359
399
  try {
360
400
  await api.postCycle({
361
401
  active_skills: selected,
362
- cycle_number: cycleCount,
402
+ cycle_number: thisCycleNumber,
363
403
  selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
364
404
  config_changes: configChangesPayload,
365
405
  });
@@ -370,12 +410,30 @@ export default function register(pluginApi) {
370
410
  // Execute selected skills deterministically
371
411
  if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
372
412
  try {
373
- await executeSkills(selected, ctx.workspaceDir, ctx.logger);
413
+ lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
374
414
  }
375
415
  catch (e) {
376
416
  ctx.logger.error(`[simmer] Skill execution error: ${e}`);
417
+ lastExecutionResults = [];
377
418
  }
378
419
  }
420
+ else {
421
+ lastExecutionResults = [];
422
+ }
423
+ // Fetch portfolio snapshot for prompt context
424
+ try {
425
+ const briefing = await api.getBriefing();
426
+ const positions = briefing.positions || [];
427
+ cachedPortfolio = {
428
+ totalPnl: briefing.portfolio?.total_pnl ?? 0,
429
+ positionCount: positions.length,
430
+ positions,
431
+ recentTradeCount: briefing.recent_trades?.length ?? 0,
432
+ };
433
+ }
434
+ catch (e) {
435
+ ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
436
+ }
379
437
  // Also record cycle history (fire-and-forget)
380
438
  const cycleData = {
381
439
  cycle_num: cycleCount,
@@ -561,8 +619,28 @@ export default function register(pluginApi) {
561
619
  return { text: `Failed to reset config: ${e}` };
562
620
  }
563
621
  }
622
+ if (subcommand === "portfolio") {
623
+ try {
624
+ const briefing = await api.getBriefing();
625
+ const positions = briefing.positions || [];
626
+ if (positions.length === 0) {
627
+ return { text: "No open positions." };
628
+ }
629
+ const lines = positions.map((p) => {
630
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
631
+ return `${p.question.slice(0, 55)} | ${p.side} ${p.shares}sh @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}${p.source ? ` [${p.source}]` : ""}`;
632
+ });
633
+ const totalPnl = positions.reduce((sum, p) => sum + p.pnl, 0);
634
+ return {
635
+ text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
636
+ };
637
+ }
638
+ catch (e) {
639
+ return { text: `Failed to fetch portfolio: ${e}` };
640
+ }
641
+ }
564
642
  return {
565
- text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
643
+ text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
566
644
  };
567
645
  },
568
646
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simmer-automaton",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",
@@ -9,9 +9,17 @@
9
9
  "dev": "tsc --watch"
10
10
  },
11
11
  "openclaw": {
12
- "extensions": ["./dist/index.js"]
12
+ "extensions": [
13
+ "./dist/index.js"
14
+ ]
13
15
  },
14
- "keywords": ["openclaw", "plugin", "simmer", "prediction-markets", "trading"],
16
+ "keywords": [
17
+ "openclaw",
18
+ "plugin",
19
+ "simmer",
20
+ "prediction-markets",
21
+ "trading"
22
+ ],
15
23
  "license": "MIT",
16
24
  "devDependencies": {
17
25
  "typescript": "^5.4.0"
package/src/api.ts CHANGED
@@ -39,6 +39,39 @@ export interface Skill {
39
39
  pinned?: string[];
40
40
  }
41
41
 
42
+ export interface BriefingPosition {
43
+ market_id: string;
44
+ question: string;
45
+ side: string;
46
+ shares: number;
47
+ avg_entry: number;
48
+ current_price: number;
49
+ pnl: number;
50
+ resolves_at: string | null;
51
+ source: string | null;
52
+ }
53
+
54
+ export interface BriefingResponse {
55
+ portfolio: {
56
+ total_pnl: number;
57
+ pnl_percent: number;
58
+ win_rate: number;
59
+ rank: number | null;
60
+ total_agents: number;
61
+ settled_pnl: number;
62
+ };
63
+ positions: BriefingPosition[];
64
+ recent_trades: Array<{
65
+ market_question: string;
66
+ action: string;
67
+ side: string;
68
+ shares: number;
69
+ cost: number;
70
+ source: string | null;
71
+ created_at: string;
72
+ }>;
73
+ }
74
+
42
75
  export interface SkillOutcome {
43
76
  skill_slug: string;
44
77
  trades: number;
@@ -139,6 +172,10 @@ export class SimmerApi {
139
172
  return this.request(`/api/sdk/automaton/outcomes?${params}`);
140
173
  }
141
174
 
175
+ async getBriefing(): Promise<BriefingResponse> {
176
+ return this.request("/api/sdk/briefing");
177
+ }
178
+
142
179
  async postCycle(data: {
143
180
  active_skills: string[];
144
181
  cycle_number: number;
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { SimmerApi } from "./api.js";
11
- import type { AutomatonState, Skill, SkillOutcome } from "./api.js";
11
+ import type { AutomatonState, Skill, SkillOutcome, BriefingPosition } from "./api.js";
12
12
  import { selectSkills, tierMaxSkills, type SkillState } from "./bandit.js";
13
13
  import { computeTier, type Tier } from "./tiers.js";
14
14
  import { generateTuningHints, computeTuningChanges, type ConfigChange } from "./tuning.js";
@@ -57,6 +57,8 @@ let banditState: SkillState[] = [];
57
57
  let currentTier: Tier = "normal";
58
58
  let cycleCount = 0;
59
59
  let currentSelectedMeta: Array<{ slug: string; reason: string; score: number | null }> = [];
60
+ let lastExecutionResults: Array<{ slug: string; ok: boolean; detail: string }> = [];
61
+ let cachedPortfolio: { totalPnl: number; positionCount: number; positions: BriefingPosition[]; recentTradeCount: number } | null = null;
60
62
  let serviceRunning = false;
61
63
  let cycleTimer: ReturnType<typeof setInterval> | null = null;
62
64
  let lastCycleTimestamp: string = new Date().toISOString();
@@ -203,6 +205,33 @@ function buildPromptContext(): string {
203
205
  }
204
206
  }
205
207
 
208
+ // --- Last execution results ---
209
+ if (lastExecutionResults.length > 0) {
210
+ lines.push("");
211
+ lines.push("**Last execution:**");
212
+ for (const r of lastExecutionResults) {
213
+ lines.push(`- ${r.ok ? "✓" : "✗"} ${r.slug}: ${r.detail}`);
214
+ }
215
+ }
216
+
217
+ // --- Portfolio snapshot ---
218
+ if (cachedPortfolio) {
219
+ lines.push("");
220
+ lines.push(`**Portfolio:** ${cachedPortfolio.positionCount} positions | P&L: ${fmtCurrency(cachedPortfolio.totalPnl)} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
221
+ if (cachedPortfolio.positions.length > 0) {
222
+ // Show top 3 by absolute PnL
223
+ const sorted = [...cachedPortfolio.positions].sort((a, b) => Math.abs(b.pnl) - Math.abs(a.pnl));
224
+ const top = sorted.slice(0, 3);
225
+ for (const p of top) {
226
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
227
+ lines.push(` - ${p.question.slice(0, 60)} | ${p.side} ${p.shares} shares @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}`);
228
+ }
229
+ if (sorted.length > 3) {
230
+ lines.push(` - ...and ${sorted.length - 3} more (use /simmer portfolio for full list)`);
231
+ }
232
+ }
233
+ }
234
+
206
235
  // --- Tier warnings ---
207
236
  if (currentTier === "critical") {
208
237
  lines.push("");
@@ -211,7 +240,7 @@ function buildPromptContext(): string {
211
240
 
212
241
  // --- Instructions for human-facing queries ---
213
242
  lines.push("");
214
- lines.push("**When your human asks about the automaton:** Report tier, budget, burn rate, which skills are running, and any tuning hints. Use `/simmer history` for recent cycle decisions. Don't dump raw data — summarize.");
243
+ lines.push("**When your human asks about the automaton:** Report tier, budget, positions, P&L, and which skills ran last cycle. Use `/simmer portfolio` for full position details. Don't dump raw data — summarize.");
215
244
  lines.push("");
216
245
  lines.push("**Currency formatting:** $SIM amounts must be written as `XXX $SIM` (e.g. `25.00 $SIM`, `100.00 $SIM`). NEVER write `$SIM25` or `$SIMxx` — the `$SIM` suffix goes AFTER the number. Real USDC uses `$` prefix (e.g. `$25.00`).");
217
246
 
@@ -245,11 +274,11 @@ async function executeSkills(
245
274
  selectedSlugs: string[],
246
275
  workspaceDir: string | undefined,
247
276
  logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void },
248
- ): Promise<void> {
249
- if (selectedSlugs.length === 0) return;
277
+ ): Promise<Array<{ slug: string; ok: boolean; detail: string }>> {
278
+ if (selectedSlugs.length === 0) return [];
250
279
  if (!runtime) {
251
280
  logger.error(`[simmer] runtime not initialized — skipping skill execution`);
252
- return;
281
+ return [];
253
282
  }
254
283
 
255
284
  // Build execution plan from cached skills with entrypoints
@@ -267,7 +296,7 @@ async function executeSkills(
267
296
  tasks.push({ slug, entrypoint: skill.entrypoint });
268
297
  }
269
298
 
270
- if (tasks.length === 0) return;
299
+ if (tasks.length === 0) return [];
271
300
 
272
301
  const cwd = workspaceDir || process.cwd();
273
302
  const env: NodeJS.ProcessEnv = {
@@ -303,11 +332,23 @@ async function executeSkills(
303
332
  }),
304
333
  );
305
334
 
306
- const succeeded = results.filter((r) => r.status === "fulfilled").length;
307
- const failed = results.filter((r) => r.status === "rejected").length;
335
+ const execResults: Array<{ slug: string; ok: boolean; detail: string }> = [];
336
+ for (let i = 0; i < tasks.length; i++) {
337
+ const r = results[i];
338
+ if (r.status === "fulfilled") {
339
+ const v = r.value;
340
+ execResults.push({ slug: v.slug, ok: v.code === 0, detail: v.code === 0 ? "ok" : `exit ${v.code}: ${v.stderr.slice(0, 100)}` });
341
+ } else {
342
+ execResults.push({ slug: tasks[i].slug, ok: false, detail: `spawn error: ${r.reason}` });
343
+ }
344
+ }
345
+
346
+ const succeeded = execResults.filter((r) => r.ok).length;
347
+ const failed = execResults.filter((r) => !r.ok).length;
308
348
  if (failed > 0) {
309
349
  logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
310
350
  }
351
+ return execResults;
311
352
  }
312
353
 
313
354
  // =============================================================================
@@ -354,6 +395,7 @@ export default function register(pluginApi: PluginApi) {
354
395
  cycleTimer = setInterval(async () => {
355
396
  if (!serviceRunning) return;
356
397
  cycleCount++;
398
+ const thisCycleNumber = cycleCount; // capture before refreshState may reset on re-init
357
399
  const cycleStarted = lastCycleTimestamp;
358
400
  lastCycleTimestamp = new Date().toISOString();
359
401
 
@@ -430,7 +472,7 @@ export default function register(pluginApi: PluginApi) {
430
472
  try {
431
473
  await api.postCycle({
432
474
  active_skills: selected,
433
- cycle_number: cycleCount,
475
+ cycle_number: thisCycleNumber,
434
476
  selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
435
477
  config_changes: configChangesPayload,
436
478
  });
@@ -441,10 +483,27 @@ export default function register(pluginApi: PluginApi) {
441
483
  // Execute selected skills deterministically
442
484
  if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
443
485
  try {
444
- await executeSkills(selected, ctx.workspaceDir, ctx.logger);
486
+ lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
445
487
  } catch (e) {
446
488
  ctx.logger.error(`[simmer] Skill execution error: ${e}`);
489
+ lastExecutionResults = [];
447
490
  }
491
+ } else {
492
+ lastExecutionResults = [];
493
+ }
494
+
495
+ // Fetch portfolio snapshot for prompt context
496
+ try {
497
+ const briefing = await api.getBriefing();
498
+ const positions = briefing.positions || [];
499
+ cachedPortfolio = {
500
+ totalPnl: briefing.portfolio?.total_pnl ?? 0,
501
+ positionCount: positions.length,
502
+ positions,
503
+ recentTradeCount: briefing.recent_trades?.length ?? 0,
504
+ };
505
+ } catch (e) {
506
+ ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
448
507
  }
449
508
 
450
509
  // Also record cycle history (fire-and-forget)
@@ -640,8 +699,28 @@ export default function register(pluginApi: PluginApi) {
640
699
  }
641
700
  }
642
701
 
702
+ if (subcommand === "portfolio") {
703
+ try {
704
+ const briefing = await api.getBriefing();
705
+ const positions = briefing.positions || [];
706
+ if (positions.length === 0) {
707
+ return { text: "No open positions." };
708
+ }
709
+ const lines = positions.map((p: BriefingPosition) => {
710
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
711
+ return `${p.question.slice(0, 55)} | ${p.side} ${p.shares}sh @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}${p.source ? ` [${p.source}]` : ""}`;
712
+ });
713
+ const totalPnl = positions.reduce((sum: number, p: BriefingPosition) => sum + p.pnl, 0);
714
+ return {
715
+ text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
716
+ };
717
+ } catch (e) {
718
+ return { text: `Failed to fetch portfolio: ${e}` };
719
+ }
720
+ }
721
+
643
722
  return {
644
- text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
723
+ text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
645
724
  };
646
725
  },
647
726
  });