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 +32 -0
- package/dist/api.js +3 -0
- package/dist/index.js +87 -9
- package/package.json +11 -3
- package/src/api.ts +37 -0
- package/src/index.ts +90 -11
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,
|
|
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
|
|
252
|
-
|
|
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:
|
|
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.
|
|
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": [
|
|
12
|
+
"extensions": [
|
|
13
|
+
"./dist/index.js"
|
|
14
|
+
]
|
|
13
15
|
},
|
|
14
|
-
"keywords": [
|
|
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,
|
|
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<
|
|
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
|
|
307
|
-
|
|
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:
|
|
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
|
});
|