@coinrithm/mcp-trading 0.7.2 → 0.7.4

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 (47) hide show
  1. package/CHANGELOG.md +171 -113
  2. package/README.md +277 -238
  3. package/dist/agent/act.d.ts +2 -2
  4. package/dist/agent/act.js +24 -3
  5. package/dist/agent/cli.js +68 -23
  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.d.ts +4 -0
  22. package/dist/agent/observe.js +140 -53
  23. package/dist/agent/prompt.d.ts +3 -1
  24. package/dist/agent/prompt.js +17 -6
  25. package/dist/agent/providers.js +39 -4
  26. package/dist/agent/resolve.js +23 -6
  27. package/dist/agent/resolvePm.js +14 -3
  28. package/dist/agent/runEvidence.js +6 -2
  29. package/dist/agent/runner.d.ts +8 -2
  30. package/dist/agent/runner.js +363 -59
  31. package/dist/agent/scorecard.js +12 -4
  32. package/dist/agent/setups.js +57 -9
  33. package/dist/agent/skill.js +1 -1
  34. package/dist/agent/state.js +9 -4
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/agent/types.js +2 -1
  37. package/dist/agent/util.js +11 -4
  38. package/dist/agent/version.d.ts +1 -1
  39. package/dist/agent/version.js +1 -1
  40. package/dist/client.d.ts +51 -0
  41. package/dist/client.js +30 -3
  42. package/dist/executionPolicy.d.ts +2 -0
  43. package/dist/executionPolicy.js +21 -0
  44. package/dist/http.js +13 -3
  45. package/dist/tools.d.ts +23 -0
  46. package/dist/tools.js +796 -39
  47. package/package.json +86 -78
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";
@@ -43,12 +43,16 @@ function pinWarnings(path) {
43
43
  if (!existsSync(pin))
44
44
  return [];
45
45
  const parsed = parseYaml(readFileSync(pin, "utf8"));
46
- const v = parsed?.api?.openapiVersion;
47
- if (v && v !== COINRITHM_API.openapiVersion) {
48
- return [
49
- `⚠ functionality/coinrithm.yaml pins API ${v}; current is ${COINRITHM_API.openapiVersion} (warning only, not a block)`,
50
- ];
46
+ const warnings = [];
47
+ const openapi = parsed?.api?.openapiVersion;
48
+ if (openapi && openapi !== COINRITHM_API.openapiVersion) {
49
+ warnings.push(`⚠ functionality/coinrithm.yaml pins API ${openapi}; current is ${COINRITHM_API.openapiVersion} (warning only, not a block)`);
51
50
  }
51
+ const mcp = parsed?.api?.mcpVersion;
52
+ if (mcp && mcp !== COINRITHM_API.mcpVersion) {
53
+ warnings.push(`⚠ functionality/coinrithm.yaml pins MCP ${mcp}; current is ${COINRITHM_API.mcpVersion} (warning only, not a block)`);
54
+ }
55
+ return warnings;
52
56
  }
53
57
  catch {
54
58
  /* ignore */
@@ -62,7 +66,9 @@ export function cmdNew(targetPath, opts = {}) {
62
66
  }
63
67
  const preset = (opts.preset ?? "conservative");
64
68
  if (!PRESET_NAMES.includes(preset)) {
65
- return fail([`unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`]);
69
+ return fail([
70
+ `unknown preset "${preset}" (allowed: ${PRESET_NAMES.join(", ")})`,
71
+ ]);
66
72
  }
67
73
  const dir = resolvePath(targetPath);
68
74
  if (!dir || dir === resolvePath("."))
@@ -125,17 +131,25 @@ export function cmdLock(path) {
125
131
  ...drift.map((i) => ` [${i.code}] ${i.path ? `${i.path}: ` : ""}${i.message}`),
126
132
  ]
127
133
  : [];
128
- return { ok: true, code: 0, lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn] };
134
+ return {
135
+ ok: true,
136
+ code: 0,
137
+ lines: [`wrote ${out}`, `configHash ${manifest.configHash}`, ...warn],
138
+ };
129
139
  }
130
140
  export function cmdEject(path) {
131
141
  const agentDir = agentDirOf(path);
132
142
  const abs = resolvePath(path);
133
- const keystone = existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "agent.md") : abs;
143
+ const keystone = existsSync(abs) && statSync(abs).isDirectory()
144
+ ? join(abs, "agent.md")
145
+ : abs;
134
146
  if (!existsSync(keystone))
135
147
  return fail([`no agent.md at ${keystone}`]);
136
148
  const { data: fm, body } = parseFrontmatter(readFileSync(keystone, "utf8"));
137
149
  if (Array.isArray(fm.extends)) {
138
- return fail(["agent already uses `extends` (already ejected?) — nothing to do"]);
150
+ return fail([
151
+ "agent already uses `extends` (already ejected?) — nothing to do",
152
+ ]);
139
153
  }
140
154
  const before = buildSpec(fm);
141
155
  const { files } = ejectFiles(fm, body);
@@ -149,13 +163,17 @@ export function cmdEject(path) {
149
163
  after = buildSpec(resolveAgent(agentDir).rawFrontmatter);
150
164
  }
151
165
  catch (e) {
152
- return fail([`ejected folder failed to re-resolve: ${e.message}`]);
166
+ return fail([
167
+ `ejected folder failed to re-resolve: ${e.message}`,
168
+ ]);
153
169
  }
154
170
  const same = stableStringify(before) === stableStringify(after);
155
171
  const lines = [
156
172
  `ejected into ${agentDir}`,
157
173
  ...Object.keys(files).map((f) => ` + ${f}`),
158
- same ? "✓ resolved spec unchanged" : "✗ WARNING: resolved spec CHANGED after eject",
174
+ same
175
+ ? "✓ resolved spec unchanged"
176
+ : "✗ WARNING: resolved spec CHANGED after eject",
159
177
  ];
160
178
  return { ok: same, code: same ? 0 : 1, lines };
161
179
  }
@@ -170,7 +188,10 @@ export function cmdInspect(path, json = false) {
170
188
  throw e;
171
189
  }
172
190
  const spec = buildSpec(resolved.rawFrontmatter);
173
- const lint = [...strictLint(resolved.rawFrontmatter), ...checkCapabilityDrift(resolved, spec)];
191
+ const lint = [
192
+ ...strictLint(resolved.rawFrontmatter),
193
+ ...checkCapabilityDrift(resolved, spec),
194
+ ];
174
195
  const v = validateSkill({ spec, body: resolved.mergedProse, raw: resolved.rawFrontmatter }, "self-host");
175
196
  const output = {
176
197
  resolvedConfig: resolved.rawFrontmatter,
@@ -179,7 +200,12 @@ export function cmdInspect(path, json = false) {
179
200
  validation: { valid: v.valid, issues: v.issues, lint },
180
201
  };
181
202
  if (json) {
182
- return { ok: v.valid, code: 0, lines: [JSON.stringify(output, null, 2)], data: output };
203
+ return {
204
+ ok: v.valid,
205
+ code: 0,
206
+ lines: [JSON.stringify(output, null, 2)],
207
+ data: output,
208
+ };
183
209
  }
184
210
  const lines = [
185
211
  `name: ${spec.name}`,
@@ -278,7 +304,9 @@ export async function cmdRun(path, opts = {}) {
278
304
  }
279
305
  const apiKey = process.env.COINRITHM_API_KEY;
280
306
  if (!apiKey)
281
- return fail(["COINRITHM_API_KEY is not set (needed to read your paper account)"]);
307
+ return fail([
308
+ "COINRITHM_API_KEY is not set (needed to read your paper account)",
309
+ ]);
282
310
  let provider;
283
311
  try {
284
312
  provider = selectProvider(loaded.spec, process.env, fetch);
@@ -286,11 +314,16 @@ export async function cmdRun(path, opts = {}) {
286
314
  catch (e) {
287
315
  return fail([e.message]);
288
316
  }
289
- const client = new CoinRithmClient({ apiKey, baseUrl: process.env.COINRITHM_API_URL });
317
+ const client = new CoinRithmClient({
318
+ apiKey,
319
+ baseUrl: process.env.COINRITHM_API_URL,
320
+ });
290
321
  const stateFile = opts.stateFile ?? join(agentDirOf(path), ".agent.state.json");
291
322
  const release = acquireLock(stateFile);
292
323
  if (!release) {
293
- return fail([`another runner holds ${stateFile}.lock — only one runner per agent at a time`]);
324
+ return fail([
325
+ `another runner holds ${stateFile}.lock — only one runner per agent at a time`,
326
+ ]);
294
327
  }
295
328
  // A self-host `run` is a cadence-paced loop users stop with Ctrl-C. Node exits
296
329
  // on SIGINT/SIGTERM WITHOUT unwinding the finally across the awaited loop, so
@@ -315,7 +348,9 @@ export async function cmdRun(path, opts = {}) {
315
348
  return fail([e.message]);
316
349
  }
317
350
  if (state.disabled) {
318
- return fail([`agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`]);
351
+ return fail([
352
+ `agent is disabled: ${state.disabledReason ?? "kill-switch"} — clear ${stateFile} to reset`,
353
+ ]);
319
354
  }
320
355
  const live = !!opts.live;
321
356
  const lines = [
@@ -399,7 +434,10 @@ export async function main(argv) {
399
434
  let r;
400
435
  switch (cmd) {
401
436
  case "new":
402
- r = cmdNew(pos[0] ?? "", { template: flags.template, preset: flags.preset });
437
+ r = cmdNew(pos[0] ?? "", {
438
+ template: flags.template,
439
+ preset: flags.preset,
440
+ });
403
441
  break;
404
442
  case "validate":
405
443
  r = cmdValidate(pos[0] ?? ".", flags.hosted ? "hosted" : "self-host");
@@ -416,7 +454,11 @@ export async function main(argv) {
416
454
  case "run": {
417
455
  // dry-run is the default; only --live (or LIVE=1) AND not --dry-run trades.
418
456
  const live = (!!flags.live || process.env.LIVE === "1") && !flags.dryRun;
419
- r = await cmdRun(pos[0] ?? ".", { once: flags.once, live, stateFile: flags.state });
457
+ r = await cmdRun(pos[0] ?? ".", {
458
+ once: flags.once,
459
+ live,
460
+ stateFile: flags.state,
461
+ });
420
462
  break;
421
463
  }
422
464
  case undefined:
@@ -426,10 +468,13 @@ export async function main(argv) {
426
468
  r = { ok: true, code: 0, lines: usageLines() };
427
469
  break;
428
470
  default:
429
- r = { ok: false, code: 1, lines: [`unknown command "${cmd}"`, ...usageLines()] };
471
+ r = {
472
+ ok: false,
473
+ code: 1,
474
+ lines: [`unknown command "${cmd}"`, ...usageLines()],
475
+ };
430
476
  }
431
477
  for (const line of r.lines) {
432
- // eslint-disable-next-line no-console
433
478
  console.log(line);
434
479
  }
435
480
  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[];