@coinrithm/mcp-trading 0.7.1 → 0.7.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +48 -3
  2. package/README.md +240 -238
  3. package/dist/agent/act.d.ts +2 -2
  4. package/dist/agent/act.js +24 -3
  5. package/dist/agent/cli.js +59 -18
  6. package/dist/agent/client.d.ts +33 -0
  7. package/dist/agent/client.js +34 -7
  8. package/dist/agent/decision.d.ts +3 -0
  9. package/dist/agent/decision.js +26 -3
  10. package/dist/agent/decisionValidator.js +2 -1
  11. package/dist/agent/deploymentOverlay.js +25 -5
  12. package/dist/agent/engine.d.ts +2 -1
  13. package/dist/agent/engine.js +4 -1
  14. package/dist/agent/extract.js +3 -1
  15. package/dist/agent/gate.js +25 -5
  16. package/dist/agent/index.js +0 -1
  17. package/dist/agent/indicators.js +4 -2
  18. package/dist/agent/manifest.js +1 -1
  19. package/dist/agent/mechanical.d.ts +36 -0
  20. package/dist/agent/mechanical.js +286 -0
  21. package/dist/agent/observe.js +120 -52
  22. package/dist/agent/prompt.d.ts +3 -1
  23. package/dist/agent/prompt.js +17 -6
  24. package/dist/agent/providers.js +39 -4
  25. package/dist/agent/resolve.js +23 -6
  26. package/dist/agent/resolvePm.js +14 -3
  27. package/dist/agent/runEvidence.js +6 -2
  28. package/dist/agent/runner.d.ts +8 -2
  29. package/dist/agent/runner.js +370 -60
  30. package/dist/agent/scorecard.js +12 -4
  31. package/dist/agent/setups.js +57 -9
  32. package/dist/agent/skill.js +1 -1
  33. package/dist/agent/state.js +9 -4
  34. package/dist/agent/types.d.ts +17 -2
  35. package/dist/agent/types.js +2 -1
  36. package/dist/agent/util.js +11 -4
  37. package/dist/agent/version.d.ts +1 -1
  38. package/dist/agent/version.js +1 -1
  39. package/dist/client.d.ts +33 -0
  40. package/dist/client.js +12 -3
  41. package/dist/executionPolicy.d.ts +2 -0
  42. package/dist/executionPolicy.js +21 -0
  43. package/dist/http.js +10 -2
  44. package/dist/tools.d.ts +1 -0
  45. package/dist/tools.js +220 -32
  46. package/package.json +9 -1
package/dist/agent/cli.js CHANGED
@@ -7,14 +7,14 @@
7
7
  import { mkdirSync, writeFileSync, existsSync, statSync, readFileSync, openSync, closeSync, unlinkSync, } from "node:fs";
8
8
  import { resolve as resolvePath, dirname, join, basename } from "node:path";
9
9
  import { parse as parseYaml } from "yaml";
10
- import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource } from "./resolve.js";
10
+ import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, } from "./resolve.js";
11
11
  import { buildSpec, loadAgent } from "./skill.js";
12
12
  import { validateSkill } from "./skillValidator.js";
13
13
  import { strictLint } from "./strictLint.js";
14
14
  import { checkCapabilityDrift } from "./capabilityGuard.js";
15
15
  import { buildManifest, writeManifest } from "./manifest.js";
16
16
  import { parseFrontmatter } from "./frontmatter.js";
17
- import { renderFolderOfOne, ejectFiles, PRESET_NAMES } from "./templates.js";
17
+ import { renderFolderOfOne, ejectFiles, PRESET_NAMES, } from "./templates.js";
18
18
  import { COINRITHM_API } from "./version.js";
19
19
  import { stableStringify, envFlag } from "./util.js";
20
20
  import { CoinRithmClient } from "./client.js";
@@ -62,7 +62,9 @@ export function cmdNew(targetPath, opts = {}) {
62
62
  }
63
63
  const preset = (opts.preset ?? "conservative");
64
64
  if (!PRESET_NAMES.includes(preset)) {
65
- return fail([`unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`]);
65
+ return fail([
66
+ `unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`,
67
+ ]);
66
68
  }
67
69
  const dir = resolvePath(targetPath);
68
70
  if (!dir || dir === resolvePath("."))
@@ -125,17 +127,25 @@ export function cmdLock(path) {
125
127
  ...drift.map((i) => ` [${i.code}] ${i.path ? `${i.path}: ` : ""}${i.message}`),
126
128
  ]
127
129
  : [];
128
- return { ok: true, code: 0, lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn] };
130
+ return {
131
+ ok: true,
132
+ code: 0,
133
+ lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn],
134
+ };
129
135
  }
130
136
  export function cmdEject(path) {
131
137
  const agentDir = agentDirOf(path);
132
138
  const abs = resolvePath(path);
133
- const keystone = existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "agent.md") : abs;
139
+ const keystone = existsSync(abs) && statSync(abs).isDirectory()
140
+ ? join(abs, "agent.md")
141
+ : abs;
134
142
  if (!existsSync(keystone))
135
143
  return fail([`no agent.md at ${keystone}`]);
136
144
  const { data: fm, body } = parseFrontmatter(readFileSync(keystone, "utf8"));
137
145
  if (Array.isArray(fm.extends)) {
138
- return fail(["agent already uses `extends` (already ejected?) — nothing to do"]);
146
+ return fail([
147
+ "agent already uses `extends` (already ejected?) — nothing to do",
148
+ ]);
139
149
  }
140
150
  const before = buildSpec(fm);
141
151
  const { files } = ejectFiles(fm, body);
@@ -149,13 +159,17 @@ export function cmdEject(path) {
149
159
  after = buildSpec(resolveAgent(agentDir).rawFrontmatter);
150
160
  }
151
161
  catch (e) {
152
- return fail([`ejected folder failed to re-resolve: ${e.message}`]);
162
+ return fail([
163
+ `ejected folder failed to re-resolve: ${e.message}`,
164
+ ]);
153
165
  }
154
166
  const same = stableStringify(before) === stableStringify(after);
155
167
  const lines = [
156
168
  `ejected into ${agentDir}`,
157
169
  ...Object.keys(files).map((f) => ` + ${f}`),
158
- same ? "✓ resolved spec unchanged" : "✗ WARNING: resolved spec CHANGED after eject",
170
+ same
171
+ ? "✓ resolved spec unchanged"
172
+ : "✗ WARNING: resolved spec CHANGED after eject",
159
173
  ];
160
174
  return { ok: same, code: same ? 0 : 1, lines };
161
175
  }
@@ -170,7 +184,10 @@ export function cmdInspect(path, json = false) {
170
184
  throw e;
171
185
  }
172
186
  const spec = buildSpec(resolved.rawFrontmatter);
173
- const lint = [...strictLint(resolved.rawFrontmatter), ...checkCapabilityDrift(resolved, spec)];
187
+ const lint = [
188
+ ...strictLint(resolved.rawFrontmatter),
189
+ ...checkCapabilityDrift(resolved, spec),
190
+ ];
174
191
  const v = validateSkill({ spec, body: resolved.mergedProse, raw: resolved.rawFrontmatter }, "self-host");
175
192
  const output = {
176
193
  resolvedConfig: resolved.rawFrontmatter,
@@ -179,7 +196,12 @@ export function cmdInspect(path, json = false) {
179
196
  validation: { valid: v.valid, issues: v.issues, lint },
180
197
  };
181
198
  if (json) {
182
- return { ok: v.valid, code: 0, lines: [JSON.stringify(output, null, 2)], data: output };
199
+ return {
200
+ ok: v.valid,
201
+ code: 0,
202
+ lines: [JSON.stringify(output, null, 2)],
203
+ data: output,
204
+ };
183
205
  }
184
206
  const lines = [
185
207
  `name: ${spec.name}`,
@@ -278,7 +300,9 @@ export async function cmdRun(path, opts = {}) {
278
300
  }
279
301
  const apiKey = process.env.COINRITHM_API_KEY;
280
302
  if (!apiKey)
281
- return fail(["COINRITHM_API_KEY is not set (needed to read your paper account)"]);
303
+ return fail([
304
+ "COINRITHM_API_KEY is not set (needed to read your paper account)",
305
+ ]);
282
306
  let provider;
283
307
  try {
284
308
  provider = selectProvider(loaded.spec, process.env, fetch);
@@ -286,11 +310,16 @@ export async function cmdRun(path, opts = {}) {
286
310
  catch (e) {
287
311
  return fail([e.message]);
288
312
  }
289
- const client = new CoinRithmClient({ apiKey, baseUrl: process.env.COINRITHM_API_URL });
313
+ const client = new CoinRithmClient({
314
+ apiKey,
315
+ baseUrl: process.env.COINRITHM_API_URL,
316
+ });
290
317
  const stateFile = opts.stateFile ?? join(agentDirOf(path), ".agent.state.json");
291
318
  const release = acquireLock(stateFile);
292
319
  if (!release) {
293
- return fail([`another runner holds ${stateFile}.lock — only one runner per agent at a time`]);
320
+ return fail([
321
+ `another runner holds ${stateFile}.lock — only one runner per agent at a time`,
322
+ ]);
294
323
  }
295
324
  // A self-host `run` is a cadence-paced loop users stop with Ctrl-C. Node exits
296
325
  // on SIGINT/SIGTERM WITHOUT unwinding the finally across the awaited loop, so
@@ -315,7 +344,9 @@ export async function cmdRun(path, opts = {}) {
315
344
  return fail([e.message]);
316
345
  }
317
346
  if (state.disabled) {
318
- return fail([`agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`]);
347
+ return fail([
348
+ `agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`,
349
+ ]);
319
350
  }
320
351
  const live = !!opts.live;
321
352
  const lines = [
@@ -399,7 +430,10 @@ export async function main(argv) {
399
430
  let r;
400
431
  switch (cmd) {
401
432
  case "new":
402
- r = cmdNew(pos[0] ?? "", { template: flags.template, preset: flags.preset });
433
+ r = cmdNew(pos[0] ?? "", {
434
+ template: flags.template,
435
+ preset: flags.preset,
436
+ });
403
437
  break;
404
438
  case "validate":
405
439
  r = cmdValidate(pos[0] ?? ".", flags.hosted ? "hosted" : "self-host");
@@ -416,7 +450,11 @@ export async function main(argv) {
416
450
  case "run": {
417
451
  // dry-run is the default; only --live (or LIVE=1) AND not --dry-run trades.
418
452
  const live = (!!flags.live || process.env.LIVE === "1") && !flags.dryRun;
419
- r = await cmdRun(pos[0] ?? ".", { once: flags.once, live, stateFile: flags.state });
453
+ r = await cmdRun(pos[0] ?? ".", {
454
+ once: flags.once,
455
+ live,
456
+ stateFile: flags.state,
457
+ });
420
458
  break;
421
459
  }
422
460
  case undefined:
@@ -426,10 +464,13 @@ export async function main(argv) {
426
464
  r = { ok: true, code: 0, lines: usageLines() };
427
465
  break;
428
466
  default:
429
- r = { ok: false, code: 1, lines: [`unknown command "${cmd}"`, ...usageLines()] };
467
+ r = {
468
+ ok: false,
469
+ code: 1,
470
+ lines: [`unknown command "${cmd}"`, ...usageLines()],
471
+ };
430
472
  }
431
473
  for (const line of r.lines) {
432
- // eslint-disable-next-line no-console
433
474
  console.log(line);
434
475
  }
435
476
  return r.code;
@@ -1,5 +1,20 @@
1
1
  import { AgentTrace, ApiResult } from "./types.js";
2
2
  export declare const DEFAULT_BASE_URL = "https://api.coinrithm.com";
3
+ export type ProvenanceReport = {
4
+ runtimeKind?: "hosted_scheduler" | "self_host_runner" | "byo_api" | "mcp_tool";
5
+ packageVersion?: string;
6
+ bundleId?: string;
7
+ bundleVersion?: string;
8
+ skillVersions?: Record<string, string>;
9
+ promptHash?: string;
10
+ configHash?: string;
11
+ modelProvider?: string;
12
+ modelName?: string;
13
+ evidenceRef?: {
14
+ snapshotIds?: string[];
15
+ sourceCapturedAt?: string;
16
+ };
17
+ };
3
18
  export interface ClientConfig {
4
19
  apiKey: string;
5
20
  baseUrl?: string;
@@ -107,8 +122,26 @@ export declare class CoinRithmClient {
107
122
  outcomeExternalMarketId: string;
108
123
  stakeMusd: number;
109
124
  idempotencyKey: string;
125
+ forecastProbability?: number;
126
+ provenance?: ProvenanceReport;
110
127
  agentTrace?: AgentTrace;
111
128
  }): Promise<ApiResult>;
129
+ reportPmOpportunity(body: {
130
+ kind: "abstained" | "forecast_only" | "quote_expired";
131
+ source?: string;
132
+ slug?: string;
133
+ outcomeExternalMarketId?: string;
134
+ forecastProbability?: number;
135
+ marketProbability?: number;
136
+ reasonCode?: string;
137
+ cohort?: {
138
+ universeSize?: number;
139
+ horizon?: string;
140
+ };
141
+ decisionId?: string | null;
142
+ runId?: string | null;
143
+ provenance?: ProvenanceReport;
144
+ }, trace?: AgentTrace): Promise<ApiResult>;
112
145
  exportRunEvidence(runId: string): Promise<ApiResult>;
113
146
  }
114
147
  export declare function isFailClosed(status: number): boolean;
@@ -64,7 +64,10 @@ export class CoinRithmClient {
64
64
  return {
65
65
  ok: false,
66
66
  status: 0,
67
- data: { error: "network_error", message: err instanceof Error ? err.message : String(err) },
67
+ data: {
68
+ error: "network_error",
69
+ message: err instanceof Error ? err.message : String(err),
70
+ },
68
71
  };
69
72
  }
70
73
  const retryAfter = Number(res.headers.get("retry-after"));
@@ -88,7 +91,9 @@ export class CoinRithmClient {
88
91
  ok: res.ok,
89
92
  status: res.status,
90
93
  data,
91
- retryAfterSeconds: res.status === 429 && Number.isFinite(retryAfter) ? retryAfter : undefined,
94
+ retryAfterSeconds: res.status === 429 && Number.isFinite(retryAfter)
95
+ ? retryAfter
96
+ : undefined,
92
97
  rateLimitRemaining: Number(res.headers.get("ratelimit-remaining")) || undefined,
93
98
  ledgerEventId: res.headers.get("x-coinrithm-ledger-event-id"),
94
99
  };
@@ -120,17 +125,24 @@ export class CoinRithmClient {
120
125
  return this.request("GET", "/api/agent/trades", { query, trace });
121
126
  }
122
127
  futuresPositions(query, trace) {
123
- return this.request("GET", "/api/agent/positions/futures", { query, trace });
128
+ return this.request("GET", "/api/agent/positions/futures", {
129
+ query,
130
+ trace,
131
+ });
124
132
  }
125
133
  futuresQuote(body, trace) {
126
- return this.request("POST", "/api/agent/futures/quote", { body: { ...body, agentTrace: trace } });
134
+ return this.request("POST", "/api/agent/futures/quote", {
135
+ body: { ...body, agentTrace: trace },
136
+ });
127
137
  }
128
138
  // ── spot ─────────────────────────────────────────────────────────────────
129
139
  openOrders(query, trace) {
130
140
  return this.request("GET", "/api/agent/orders/open", { query, trace });
131
141
  }
132
142
  spotQuote(body, trace) {
133
- return this.request("POST", "/api/agent/spot/quote", { body: { ...body, agentTrace: trace } });
143
+ return this.request("POST", "/api/agent/spot/quote", {
144
+ body: { ...body, agentTrace: trace },
145
+ });
134
146
  }
135
147
  // ── prediction markets ───────────────────────────────────────────────────
136
148
  discoverPmMarkets(query, trace) {
@@ -149,7 +161,9 @@ export class CoinRithmClient {
149
161
  return this.request("GET", "/api/agent/positions/pm", { query, trace });
150
162
  }
151
163
  pmQuote(body, trace) {
152
- return this.request("POST", "/api/agent/pm/quote", { body: { ...body, agentTrace: trace } });
164
+ return this.request("POST", "/api/agent/pm/quote", {
165
+ body: { ...body, agentTrace: trace },
166
+ });
153
167
  }
154
168
  // ── writes ─────────────────────────────────────────────────────────────────
155
169
  openFutures(body) {
@@ -173,9 +187,22 @@ export class CoinRithmClient {
173
187
  openPmPosition(body) {
174
188
  return this.request("POST", "/api/agent/pm/open", { body });
175
189
  }
190
+ // Report a NON-opened PM opportunity (abstained / forecast_only / quote_expired)
191
+ // so the public evaluation captures the FULL opportunity universe, not only
192
+ // opened trades. EVIDENCE, not a trade: needs only the read scope and does not
193
+ // move a wallet/position. Best-effort at the call site — a failure never affects
194
+ // the cycle. decisionId is the per-cycle idempotency key (server dedupes on
195
+ // (apiKeyId, decisionId)).
196
+ reportPmOpportunity(body, trace) {
197
+ return this.request("POST", "/api/agent/pm/opportunity", {
198
+ body: { ...body, agentTrace: trace },
199
+ });
200
+ }
176
201
  // Run-evidence export — runId is URL-encoded into the query.
177
202
  exportRunEvidence(runId) {
178
- return this.request("GET", "/api/agent/ledger/export", { query: { runId } });
203
+ return this.request("GET", "/api/agent/ledger/export", {
204
+ query: { runId },
205
+ });
179
206
  }
180
207
  }
181
208
  // 401/403/409/422 are terminal, fail-closed outcomes for a cycle (auth/scope,
@@ -111,12 +111,14 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
111
111
  stakeMusd: z.ZodEffects<z.ZodTypeAny, any, unknown>;
112
112
  confidence: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>, any, unknown>;
113
113
  rationaleSummary: z.ZodOptional<z.ZodString>;
114
+ forecastProbability: z.ZodOptional<z.ZodEffects<z.ZodAny, number | undefined, any>>;
114
115
  }, "strict", z.ZodTypeAny, {
115
116
  type: "pm_open";
116
117
  source?: string | undefined;
117
118
  slug?: string | undefined;
118
119
  outcomeExternalMarketId?: string | undefined;
119
120
  stakeMusd?: any;
121
+ forecastProbability?: number | undefined;
120
122
  confidence?: any;
121
123
  rationaleSummary?: string | undefined;
122
124
  ref?: string | undefined;
@@ -126,6 +128,7 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
126
128
  slug?: string | undefined;
127
129
  outcomeExternalMarketId?: string | undefined;
128
130
  stakeMusd?: unknown;
131
+ forecastProbability?: any;
129
132
  confidence?: unknown;
130
133
  rationaleSummary?: string | undefined;
131
134
  ref?: string | undefined;
@@ -8,12 +8,29 @@ import { z } from "zod";
8
8
  // validating; leave anything else untouched so genuine garbage ("abc", "pos#5")
9
9
  // still fails closed. This is why actions were being rejected with
10
10
  // "actions.0.positionId: Expected number, received string".
11
- const num = (inner) => z.preprocess((v) => (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : v), inner);
11
+ const num = (inner) => z.preprocess((v) => typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))
12
+ ? Number(v)
13
+ : v, inner);
12
14
  // Optional 0..1 confidence, tolerant of stringified/null input.
13
15
  const confidence = num(z.number().min(0).max(1))
14
16
  .nullable()
15
17
  .optional()
16
18
  .transform((v) => v ?? undefined);
19
+ // The model's OWN 0-100 forecast for the backed PM side (its independent
20
+ // probability the side wins, decided from the question — NOT the market price).
21
+ // Deliberately TOLERANT: a missing / null / non-numeric value becomes undefined
22
+ // (the runner then omits the field) rather than failing the whole action. A bad
23
+ // forecast must NEVER block an otherwise-valid trade. Out-of-range values are NOT
24
+ // rejected here — the runner clamps them to the backend's [1,99] rail. Kept as a
25
+ // permissive `any→number|undefined` so it can never throw inside the strict
26
+ // discriminated union.
27
+ const forecastProbability = z
28
+ .any()
29
+ .transform((v) => {
30
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
31
+ return typeof n === "number" && Number.isFinite(n) ? n : undefined;
32
+ })
33
+ .optional();
17
34
  const futuresOpen = z
18
35
  .object({
19
36
  type: z.literal("futures_open"),
@@ -79,6 +96,7 @@ const pmOpen = z
79
96
  stakeMusd: num(z.number().positive()),
80
97
  confidence,
81
98
  rationaleSummary: z.string().optional(),
99
+ forecastProbability,
82
100
  })
83
101
  .strict();
84
102
  export const actionSchema = z.discriminatedUnion("type", [
@@ -164,13 +182,18 @@ export function parseDecision(text) {
164
182
  obj = normalizeDecisionVerb(coerceJson(text));
165
183
  }
166
184
  catch (err) {
167
- return { ok: false, error: `model output is not valid JSON: ${err instanceof Error ? err.message : String(err)}` };
185
+ return {
186
+ ok: false,
187
+ error: `model output is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
188
+ };
168
189
  }
169
190
  const res = decisionSchema.safeParse(obj);
170
191
  if (!res.success) {
171
192
  return {
172
193
  ok: false,
173
- error: res.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
194
+ error: res.error.issues
195
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
196
+ .join("; "),
174
197
  };
175
198
  }
176
199
  const d = res.data;
@@ -21,7 +21,8 @@ export function validateAction(action, ctx) {
21
21
  // throttled (we want an active Arena), and hosted agents only when the customer sets a
22
22
  // positive cap. The risk caps below (daily loss, open margin, leverage, stops) are the
23
23
  // real guardrails and always apply regardless of the trade-count cap.
24
- if (spec.limits.maxTradesPerDay > 0 && ctx.writesToday >= spec.limits.maxTradesPerDay) {
24
+ if (spec.limits.maxTradesPerDay > 0 &&
25
+ ctx.writesToday >= spec.limits.maxTradesPerDay) {
25
26
  return fail("daily_trade_cap", `maxTradesPerDay ${spec.limits.maxTradesPerDay} reached`);
26
27
  }
27
28
  // Deny-list: an open on a blocked symbol is rejected up front (deny wins over
@@ -8,14 +8,34 @@
8
8
  // agent runs on a default tier and this still enforces it.)
9
9
  // Effective caps per tier. Tightenable later from config; these are the defaults.
10
10
  export const TIER_LIMITS = {
11
- free_demo: { maxLlmCallsPerHour: 4, minCadenceSeconds: 3600, maxConcurrentAgents: 1 },
12
- builder: { maxLlmCallsPerHour: 20, minCadenceSeconds: 900, maxConcurrentAgents: 3 },
13
- pro: { maxLlmCallsPerHour: 120, minCadenceSeconds: 60, maxConcurrentAgents: 10 },
11
+ free_demo: {
12
+ maxLlmCallsPerHour: 4,
13
+ minCadenceSeconds: 3600,
14
+ maxConcurrentAgents: 1,
15
+ },
16
+ builder: {
17
+ maxLlmCallsPerHour: 20,
18
+ minCadenceSeconds: 900,
19
+ maxConcurrentAgents: 3,
20
+ },
21
+ pro: {
22
+ maxLlmCallsPerHour: 120,
23
+ minCadenceSeconds: 60,
24
+ maxConcurrentAgents: 10,
25
+ },
14
26
  // BYO key = the user's own model quota, so we don't cap their calls; we still
15
27
  // host + meter + verify (that's what they pay the infra fee for).
16
- byok: { maxLlmCallsPerHour: 0, minCadenceSeconds: 60, maxConcurrentAgents: 5 },
28
+ byok: {
29
+ maxLlmCallsPerHour: 0,
30
+ minCadenceSeconds: 60,
31
+ maxConcurrentAgents: 5,
32
+ },
17
33
  // The house showcase fleet — uncapped, runs on our pooled keys.
18
- house: { maxLlmCallsPerHour: 0, minCadenceSeconds: 60, maxConcurrentAgents: 0 },
34
+ house: {
35
+ maxLlmCallsPerHour: 0,
36
+ minCadenceSeconds: 60,
37
+ maxConcurrentAgents: 0,
38
+ },
19
39
  };
20
40
  // Tighten one numeric cap: 0 means "unlimited" on either side. The tier always wins
21
41
  // where it imposes a finite cap; it can never raise a request.
@@ -1,5 +1,5 @@
1
1
  export { runCycle, type RunnerDeps } from "./runner.js";
2
- export { selectProvider, type ProviderEnv, type Provider } from "./providers.js";
2
+ export { selectProvider, type ProviderEnv, type Provider, } from "./providers.js";
3
3
  export { CoinRithmClient } from "./client.js";
4
4
  export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
5
5
  export { resolveAgent } from "./resolve.js";
@@ -7,4 +7,5 @@ export { validateSkill, type SkillValidationMode } from "./skillValidator.js";
7
7
  export { newState, rollDay } from "./state.js";
8
8
  export { makeRunId } from "./runEvidence.js";
9
9
  export { parseCadenceMs } from "./util.js";
10
+ export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, type BenchmarkStrategy, type BenchmarkAgentDefinition, } from "./mechanical.js";
10
11
  export type { AgentSpec, RunState, CycleResult, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.js";
@@ -6,7 +6,7 @@
6
6
  // This barrel is the ONE import a host scheduler needs; it re-exports only the
7
7
  // stable engine pieces, never the CLI.
8
8
  export { runCycle } from "./runner.js";
9
- export { selectProvider } from "./providers.js";
9
+ export { selectProvider, } from "./providers.js";
10
10
  export { CoinRithmClient } from "./client.js";
11
11
  export { loadAgent, buildSpec } from "./skill.js";
12
12
  export { resolveAgent } from "./resolve.js";
@@ -14,3 +14,6 @@ export { validateSkill } from "./skillValidator.js";
14
14
  export { newState, rollDay } from "./state.js";
15
15
  export { makeRunId } from "./runEvidence.js";
16
16
  export { parseCadenceMs } from "./util.js";
17
+ // Mechanical BENCHMARK baseline agents (sol #7): the deterministic, non-LLM
18
+ // reference line the scheduler seeds into agent_runtime and the runner executes.
19
+ export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, } from "./mechanical.js";
@@ -1,5 +1,7 @@
1
1
  // Defensive extractors for untyped API JSON.
2
- export const asObj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2
+ export const asObj = (v) => v && typeof v === "object" && !Array.isArray(v)
3
+ ? v
4
+ : {};
3
5
  export const asArr = (v) => (Array.isArray(v) ? v : []);
4
6
  export const asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
5
7
  export const asStr = (v) => typeof v === "string" ? v : undefined;
@@ -64,9 +64,17 @@ export function evaluateGate(observation, state, policy, nowMs) {
64
64
  if (pmAvailable &&
65
65
  policy.pmEvalCooldownMinutes > 0 &&
66
66
  sinceLastCall >= policy.pmEvalCooldownMinutes * 60_000) {
67
- return { fire: true, codes: ["PM_PERIODIC"], reason: "PM periodic eval (quiet price tape)" };
67
+ return {
68
+ fire: true,
69
+ codes: ["PM_PERIODIC"],
70
+ reason: "PM periodic eval (quiet price tape)",
71
+ };
68
72
  }
69
- return { fire: false, codes: [], reason: "no trigger (flat tape, no open position)" };
73
+ return {
74
+ fire: false,
75
+ codes: [],
76
+ reason: "no trigger (flat tape, no open position)",
77
+ };
70
78
  }
71
79
  // A real trigger exists. Open positions are NEVER starved by budget/debounce
72
80
  // (managing a live position is always allowed); the caps below only throttle
@@ -75,7 +83,11 @@ export function evaluateGate(observation, state, policy, nowMs) {
75
83
  if (policy.maxLlmCallsPerHour > 0) {
76
84
  const recent = (state.llmCallTimestamps ?? []).filter((t) => nowMs - t < 3_600_000);
77
85
  if (recent.length >= policy.maxLlmCallsPerHour) {
78
- return { fire: false, codes: codeList, reason: `hourly LLM budget ${policy.maxLlmCallsPerHour} reached` };
86
+ return {
87
+ fire: false,
88
+ codes: codeList,
89
+ reason: `hourly LLM budget ${policy.maxLlmCallsPerHour} reached`,
90
+ };
79
91
  }
80
92
  }
81
93
  if (policy.debounceMinutes > 0) {
@@ -83,11 +95,19 @@ export function evaluateGate(observation, state, policy, nowMs) {
83
95
  if (state.lastTriggerFingerprint === fp &&
84
96
  state.lastLlmCallAt != null &&
85
97
  nowMs - state.lastLlmCallAt < policy.debounceMinutes * 60_000) {
86
- return { fire: false, codes: codeList, reason: `debounced (same triggers within ${policy.debounceMinutes}m)` };
98
+ return {
99
+ fire: false,
100
+ codes: codeList,
101
+ reason: `debounced (same triggers within ${policy.debounceMinutes}m)`,
102
+ };
87
103
  }
88
104
  }
89
105
  }
90
- return { fire: true, codes: codeList, reason: `triggers: ${codeList.join(",")}` };
106
+ return {
107
+ fire: true,
108
+ codes: codeList,
109
+ reason: `triggers: ${codeList.join(",")}`,
110
+ };
91
111
  }
92
112
  // Record that this cycle spent an LLM call — feeds the budget + debounce next
93
113
  // cycle. Mutates state; the caller persists it.
@@ -4,7 +4,6 @@ import { main } from "./cli.js";
4
4
  main(process.argv.slice(2))
5
5
  .then((code) => process.exit(code))
6
6
  .catch((err) => {
7
- // eslint-disable-next-line no-console
8
7
  console.error(err instanceof Error ? err.message : String(err));
9
8
  process.exit(1);
10
9
  });
@@ -133,14 +133,16 @@ export function computeIndicators(candles, opts = {}) {
133
133
  const f = Math.pow(10, figs - Math.ceil(Math.log10(Math.abs(n))));
134
134
  return Math.round(n * f) / f;
135
135
  };
136
- const sigN = (n, figs = 6) => (n == null ? null : sig(n, figs));
136
+ const sigN = (n, figs = 6) => n == null ? null : sig(n, figs);
137
137
  return {
138
138
  asOfClose: sig(close),
139
139
  rsi14: rsi14 == null ? null : Math.round(rsi14 * 10) / 10,
140
140
  ema20: sigN(ema20),
141
141
  ema50: sigN(ema50),
142
142
  atr14: sigN(atr14),
143
- bollinger: bb == null ? null : { upper: sig(bb.upper), mid: sig(bb.mid), lower: sig(bb.lower) },
143
+ bollinger: bb == null
144
+ ? null
145
+ : { upper: sig(bb.upper), mid: sig(bb.mid), lower: sig(bb.lower) },
144
146
  recent20: r20 == null ? null : { high: sig(r20.high), low: sig(r20.low) },
145
147
  aboveEma20: ema20 == null ? null : close > ema20,
146
148
  ema20AboveEma50: ema20 == null || ema50 == null ? null : ema20 > ema50,
@@ -5,7 +5,7 @@
5
5
  import { writeFileSync, mkdirSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { sha256, stableStringify, toPosix } from "./util.js";
8
- import { RESOLVER_VERSION, RUNNER_VERSION, MANIFEST_SCHEMA } from "./version.js";
8
+ import { RESOLVER_VERSION, RUNNER_VERSION, MANIFEST_SCHEMA, } from "./version.js";
9
9
  export function buildManifest(resolved, spec) {
10
10
  // configHash binds the RESOLVED spec to the resolver + schema version, so a
11
11
  // run reproduces only against the same compile (not just the same files).
@@ -0,0 +1,36 @@
1
+ import { AgentSpec, Decision, Observation, PmMarket } from "./types.js";
2
+ export declare const BENCHMARK_STRATEGIES: readonly ["market-implied", "base-rate", "random"];
3
+ export type BenchmarkStrategy = (typeof BENCHMARK_STRATEGIES)[number];
4
+ export declare function isBenchmarkStrategy(s: string): s is BenchmarkStrategy;
5
+ export declare const BASE_RATE_UNINFORMATIVE = 50;
6
+ export declare const BENCHMARK_STAKE_MUSD = 10;
7
+ export declare const RANDOM_FORECAST_MIN = 20;
8
+ export declare const RANDOM_FORECAST_MAX = 80;
9
+ export declare function marketKey(m: {
10
+ source: string;
11
+ slug: string;
12
+ outcomeExternalMarketId: string;
13
+ }): string;
14
+ export declare function seededRandomForecast(seed: string): number;
15
+ export declare function pickBenchmarkMarket(markets: PmMarket[], heldKeys?: Set<string>): PmMarket | undefined;
16
+ export declare function benchmarkForecast(strategy: BenchmarkStrategy, market: PmMarket, dateKey: string): number | undefined;
17
+ export interface MechanicalDecideInput {
18
+ strategy: string;
19
+ observation: Observation;
20
+ dateKey?: string;
21
+ stakeMusd?: number;
22
+ }
23
+ export interface MechanicalDecideResult {
24
+ decision: Decision;
25
+ log: string[];
26
+ }
27
+ export declare function decideMechanical(input: MechanicalDecideInput): MechanicalDecideResult;
28
+ export interface BenchmarkAgentDefinition {
29
+ handle: string;
30
+ displayName: string;
31
+ strategy: BenchmarkStrategy;
32
+ cadenceSeconds: number;
33
+ spec: AgentSpec;
34
+ prose: string;
35
+ }
36
+ export declare const BENCHMARK_AGENTS: BenchmarkAgentDefinition[];