simmer-automaton 0.6.1 → 0.6.2

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
@@ -370,12 +409,30 @@ export default function register(pluginApi) {
370
409
  // Execute selected skills deterministically
371
410
  if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
372
411
  try {
373
- await executeSkills(selected, ctx.workspaceDir, ctx.logger);
412
+ lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
374
413
  }
375
414
  catch (e) {
376
415
  ctx.logger.error(`[simmer] Skill execution error: ${e}`);
416
+ lastExecutionResults = [];
377
417
  }
378
418
  }
419
+ else {
420
+ lastExecutionResults = [];
421
+ }
422
+ // Fetch portfolio snapshot for prompt context
423
+ try {
424
+ const briefing = await api.getBriefing();
425
+ const positions = briefing.positions || [];
426
+ cachedPortfolio = {
427
+ totalPnl: briefing.portfolio?.total_pnl ?? 0,
428
+ positionCount: positions.length,
429
+ positions,
430
+ recentTradeCount: briefing.recent_trades?.length ?? 0,
431
+ };
432
+ }
433
+ catch (e) {
434
+ ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
435
+ }
379
436
  // Also record cycle history (fire-and-forget)
380
437
  const cycleData = {
381
438
  cycle_num: cycleCount,
@@ -561,8 +618,28 @@ export default function register(pluginApi) {
561
618
  return { text: `Failed to reset config: ${e}` };
562
619
  }
563
620
  }
621
+ if (subcommand === "portfolio") {
622
+ try {
623
+ const briefing = await api.getBriefing();
624
+ const positions = briefing.positions || [];
625
+ if (positions.length === 0) {
626
+ return { text: "No open positions." };
627
+ }
628
+ const lines = positions.map((p) => {
629
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
630
+ 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}]` : ""}`;
631
+ });
632
+ const totalPnl = positions.reduce((sum, p) => sum + p.pnl, 0);
633
+ return {
634
+ text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
635
+ };
636
+ }
637
+ catch (e) {
638
+ return { text: `Failed to fetch portfolio: ${e}` };
639
+ }
640
+ }
564
641
  return {
565
- text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
642
+ text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
566
643
  };
567
644
  },
568
645
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simmer-automaton",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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/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
  // =============================================================================
@@ -441,10 +482,27 @@ export default function register(pluginApi: PluginApi) {
441
482
  // Execute selected skills deterministically
442
483
  if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
443
484
  try {
444
- await executeSkills(selected, ctx.workspaceDir, ctx.logger);
485
+ lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
445
486
  } catch (e) {
446
487
  ctx.logger.error(`[simmer] Skill execution error: ${e}`);
488
+ lastExecutionResults = [];
447
489
  }
490
+ } else {
491
+ lastExecutionResults = [];
492
+ }
493
+
494
+ // Fetch portfolio snapshot for prompt context
495
+ try {
496
+ const briefing = await api.getBriefing();
497
+ const positions = briefing.positions || [];
498
+ cachedPortfolio = {
499
+ totalPnl: briefing.portfolio?.total_pnl ?? 0,
500
+ positionCount: positions.length,
501
+ positions,
502
+ recentTradeCount: briefing.recent_trades?.length ?? 0,
503
+ };
504
+ } catch (e) {
505
+ ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
448
506
  }
449
507
 
450
508
  // Also record cycle history (fire-and-forget)
@@ -640,8 +698,28 @@ export default function register(pluginApi: PluginApi) {
640
698
  }
641
699
  }
642
700
 
701
+ if (subcommand === "portfolio") {
702
+ try {
703
+ const briefing = await api.getBriefing();
704
+ const positions = briefing.positions || [];
705
+ if (positions.length === 0) {
706
+ return { text: "No open positions." };
707
+ }
708
+ const lines = positions.map((p: BriefingPosition) => {
709
+ const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
710
+ 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}]` : ""}`;
711
+ });
712
+ const totalPnl = positions.reduce((sum: number, p: BriefingPosition) => sum + p.pnl, 0);
713
+ return {
714
+ text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
715
+ };
716
+ } catch (e) {
717
+ return { text: `Failed to fetch portfolio: ${e}` };
718
+ }
719
+ }
720
+
643
721
  return {
644
- text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
722
+ text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
645
723
  };
646
724
  },
647
725
  });