@coinrithm/mcp-trading 0.7.14 → 0.7.16
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/CHANGELOG.md +811 -613
- package/README.md +415 -366
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +14 -0
- package/dist/agent/client.js +113 -4
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +85 -3
- package/dist/agent/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +52 -3
- package/dist/agent/decisionValidator.d.ts +30 -0
- package/dist/agent/decisionValidator.js +147 -2
- package/dist/agent/engine.d.ts +3 -3
- package/dist/agent/engine.js +3 -3
- package/dist/agent/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/observe.d.ts +35 -2
- package/dist/agent/observe.js +969 -128
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +18 -1
- package/dist/agent/prompt.js +229 -33
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +9 -0
- package/dist/agent/providerCapabilities.js +57 -4
- package/dist/agent/providers.d.ts +28 -7
- package/dist/agent/providers.js +200 -50
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +159 -26
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +37 -0
- package/dist/agent/skillValidator.js +33 -0
- package/dist/agent/strictLint.js +29 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +266 -13
- package/dist/agent/types.js +4 -0
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/dist/client.d.ts +9 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- package/package.json +98 -97
package/dist/agent/cli.js
CHANGED
|
@@ -18,12 +18,17 @@ import { buildManifest, writeManifest } from "./manifest.js";
|
|
|
18
18
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
19
19
|
import { renderFolderOfOne, ejectFiles, PRESET_NAMES, } from "./templates.js";
|
|
20
20
|
import { COINRITHM_API } from "./version.js";
|
|
21
|
-
import { stableStringify, envFlag } from "./util.js";
|
|
22
|
-
import { CoinRithmClient } from "./client.js";
|
|
21
|
+
import { stableStringify, envFlag, parseCadenceMs, sleep } from "./util.js";
|
|
22
|
+
import { CoinRithmClient, DEFAULT_BASE_URL } from "./client.js";
|
|
23
23
|
import { selectProvider } from "./providers.js";
|
|
24
24
|
import { runLoop } from "./runner.js";
|
|
25
25
|
import { loadState, saveState } from "./state.js";
|
|
26
26
|
import { makeRunId } from "./runEvidence.js";
|
|
27
|
+
import { readCorpus, writeCassette } from "./bench/cassette.js";
|
|
28
|
+
import { recordCassette } from "./bench/recordingClient.js";
|
|
29
|
+
import { runBench } from "./bench/bench.js";
|
|
30
|
+
import { buildPmLabels, buildPriceLabels, DEFAULT_LABEL_HORIZON_HOURS, } from "./bench/labelBuilder.js";
|
|
31
|
+
import { parseLabelFile } from "./bench/labels.js";
|
|
27
32
|
const fail = (lines) => ({ ok: false, code: 1, lines });
|
|
28
33
|
function issuesResult(issues, header) {
|
|
29
34
|
return {
|
|
@@ -438,8 +443,254 @@ export async function cmdRun(path, opts = {}) {
|
|
|
438
443
|
release();
|
|
439
444
|
}
|
|
440
445
|
}
|
|
446
|
+
const MAX_RECORD_CYCLES = 10_000;
|
|
447
|
+
// Record bench cassettes (see bench/cassette.ts). Read-only by construction:
|
|
448
|
+
// every cycle runs DRY-RUN through a recording transport that refuses all
|
|
449
|
+
// non-GET requests, with a brain that never calls a model, so only
|
|
450
|
+
// COINRITHM_API_KEY is needed and no model key is read. The agent's own state
|
|
451
|
+
// file is not touched; each recorded cycle starts from a fresh run state.
|
|
452
|
+
export async function cmdRecord(path, opts = {}) {
|
|
453
|
+
if (!opts.out)
|
|
454
|
+
return fail(["record needs --out <dir> for the cassettes"]);
|
|
455
|
+
const cycles = opts.cycles ?? 1;
|
|
456
|
+
if (!Number.isInteger(cycles) || cycles < 1 || cycles > MAX_RECORD_CYCLES)
|
|
457
|
+
return fail([`--cycles must be an integer from 1 to ${MAX_RECORD_CYCLES}`]);
|
|
458
|
+
let loaded;
|
|
459
|
+
try {
|
|
460
|
+
loaded = loadAgent(path, "self-host");
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
if (e instanceof ResolveError)
|
|
464
|
+
return issuesResult(e.issues, "resolve failed");
|
|
465
|
+
throw e;
|
|
466
|
+
}
|
|
467
|
+
const everyMs = opts.every !== undefined
|
|
468
|
+
? parseCadenceMs(opts.every)
|
|
469
|
+
: (parseCadenceMs(loaded.spec.trigger.cadence) ?? 3_600_000);
|
|
470
|
+
if (everyMs === null)
|
|
471
|
+
return fail([`--every "${opts.every}" is not a cadence like 5m or 1h`]);
|
|
472
|
+
const apiKey = process.env.COINRITHM_API_KEY;
|
|
473
|
+
if (!apiKey)
|
|
474
|
+
return fail([
|
|
475
|
+
"COINRITHM_API_KEY is not set (recording reads your paper account; it never writes)",
|
|
476
|
+
]);
|
|
477
|
+
const mergedProse = runtimeProse(loaded.resolved);
|
|
478
|
+
const lines = [
|
|
479
|
+
`record DRY-RUN (reads only, no model call): ${loaded.spec.name}, ${cycles} cycle(s)`,
|
|
480
|
+
];
|
|
481
|
+
const files = [];
|
|
482
|
+
for (let i = 0; i < cycles; i++) {
|
|
483
|
+
let cassette;
|
|
484
|
+
try {
|
|
485
|
+
cassette = await recordCassette({
|
|
486
|
+
spec: loaded.spec,
|
|
487
|
+
mergedProse,
|
|
488
|
+
apiKey,
|
|
489
|
+
baseUrl: process.env.COINRITHM_API_URL || undefined,
|
|
490
|
+
fetchFn: opts.fetchFn,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
lines.push(`✗ cycle ${i + 1} failed: ${e.message}`);
|
|
495
|
+
return { ok: false, code: 1, lines, data: files };
|
|
496
|
+
}
|
|
497
|
+
files.push(writeCassette(resolvePath(opts.out), cassette));
|
|
498
|
+
const cycle = cassette.recordCycle;
|
|
499
|
+
lines.push(`recorded ${cassette.id}: ${cassette.responses.length} read(s), asOf ${cassette.asOf}, cycle ${cycle.decisionType ?? cycle.decision}${cycle.skipReason ? ` (${cycle.skipReason})` : ""}` +
|
|
500
|
+
(cassette.refusedRequests.length
|
|
501
|
+
? `, ${cassette.refusedRequests.length} write(s) refused`
|
|
502
|
+
: ""));
|
|
503
|
+
if (i < cycles - 1)
|
|
504
|
+
await (opts.sleepFn ?? sleep)(everyMs);
|
|
505
|
+
}
|
|
506
|
+
return { ok: true, code: 0, lines, data: files };
|
|
507
|
+
}
|
|
508
|
+
// Write outcome labels for a recorded corpus (bench/labelBuilder.ts), reads
|
|
509
|
+
// only: price bars from candles published after each cassette's asOf (with
|
|
510
|
+
// COINRITHM_API_KEY), and PM settlement from the public event verdict
|
|
511
|
+
// (settlementEligible + "settle" + a provider won/lost outcome result; any
|
|
512
|
+
// other state stays unlabelled).
|
|
513
|
+
export async function cmdLabel(opts = {}) {
|
|
514
|
+
if (!opts.corpus)
|
|
515
|
+
return fail(["label needs --corpus <dir>"]);
|
|
516
|
+
const horizonHours = opts.horizonHours ?? DEFAULT_LABEL_HORIZON_HOURS;
|
|
517
|
+
if (!Number.isFinite(horizonHours) || horizonHours <= 0 || horizonHours > 720)
|
|
518
|
+
return fail(["--horizon-hours must be a number in (0, 720]"]);
|
|
519
|
+
const apiKey = process.env.COINRITHM_API_KEY;
|
|
520
|
+
if (!apiKey)
|
|
521
|
+
return fail([
|
|
522
|
+
"COINRITHM_API_KEY is not set (labels read public market candles through your key; nothing is written)",
|
|
523
|
+
]);
|
|
524
|
+
const dir = resolvePath(opts.corpus);
|
|
525
|
+
let corpus;
|
|
526
|
+
try {
|
|
527
|
+
corpus = readCorpus(dir);
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
return fail([e.message]);
|
|
531
|
+
}
|
|
532
|
+
const client = new CoinRithmClient({
|
|
533
|
+
apiKey,
|
|
534
|
+
baseUrl: process.env.COINRITHM_API_URL || undefined,
|
|
535
|
+
fetchFn: opts.fetchFn,
|
|
536
|
+
});
|
|
537
|
+
const results = await buildPriceLabels(corpus.cassettes, {
|
|
538
|
+
fetchCandles: (coinId, range) => client.candles(coinId, range),
|
|
539
|
+
horizonHours,
|
|
540
|
+
nowMs: opts.nowMs ?? Date.now(),
|
|
541
|
+
existing: corpus.labels,
|
|
542
|
+
overwrite: opts.overwrite,
|
|
543
|
+
});
|
|
544
|
+
const base = (process.env.COINRITHM_API_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
545
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
546
|
+
const pmResults = await buildPmLabels(corpus.cassettes, {
|
|
547
|
+
// The public event detail: the same verdict the event page shows.
|
|
548
|
+
fetchEvent: async (source, slug) => {
|
|
549
|
+
const res = await fetchFn(`${base}/api/prediction-markets/events/${encodeURIComponent(source)}/${encodeURIComponent(slug)}`, { method: "GET", signal: AbortSignal.timeout(15_000) });
|
|
550
|
+
return {
|
|
551
|
+
ok: res.ok,
|
|
552
|
+
status: res.status,
|
|
553
|
+
data: res.ok ? await res.json() : null,
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
existing: corpus.labels,
|
|
557
|
+
overwrite: opts.overwrite,
|
|
558
|
+
});
|
|
559
|
+
const lines = [
|
|
560
|
+
`label (reads only): ${corpus.cassettes.length} cassette(s), horizon ${horizonHours} h`,
|
|
561
|
+
];
|
|
562
|
+
let written = 0;
|
|
563
|
+
for (const r of results) {
|
|
564
|
+
const pm = pmResults.find((p) => p.id === r.id);
|
|
565
|
+
const notes = [];
|
|
566
|
+
let file;
|
|
567
|
+
if (r.status === "built") {
|
|
568
|
+
file = { ...r.file };
|
|
569
|
+
notes.push(`${r.range} bars for ${r.symbols.length} symbol(s)${r.missing.length ? `, missing ${r.missing.join(",")}` : ""}`);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
notes.push(`prices ${r.status}${r.status === "not_yet" ? ` (labelable after ${r.labelableAfter})` : ""}${r.status === "horizon_mismatch" ? ` (existing file labels ${r.existingHorizonHours ?? "no"} h, not ${horizonHours} h; rerun with --overwrite)` : ""}`);
|
|
573
|
+
}
|
|
574
|
+
if (pm) {
|
|
575
|
+
notes.push(`pm ${pm.labelled} new settled outcome(s) from ${pm.events} event(s)${pm.failed ? `, ${pm.failed} read(s) failed` : ""}`);
|
|
576
|
+
const hasPm = Object.keys(pm.pm).length > 0;
|
|
577
|
+
if (opts.overwrite) {
|
|
578
|
+
// --overwrite REPLACES the PM part, even with nothing: a label that
|
|
579
|
+
// was settled before but is void/unresolved now must not survive.
|
|
580
|
+
const base = file ?? corpus.labels[r.id];
|
|
581
|
+
if (base || hasPm) {
|
|
582
|
+
const { pm: _replaced, ...rest } = base ?? {};
|
|
583
|
+
file = hasPm ? { ...rest, pm: pm.pm } : rest;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else if (pm.labelled > 0 || (file && hasPm))
|
|
587
|
+
file = { ...(file ?? corpus.labels[r.id] ?? {}), pm: pm.pm };
|
|
588
|
+
}
|
|
589
|
+
lines.push(`${r.id}: ${notes.join("; ")}`);
|
|
590
|
+
if (!file)
|
|
591
|
+
continue;
|
|
592
|
+
const path = join(dir, "labels", `${r.id}.json`);
|
|
593
|
+
// The bench reads it back through the same validator; fail closed here.
|
|
594
|
+
parseLabelFile(file, `labels/${r.id}.json`);
|
|
595
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
596
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
597
|
+
written += 1;
|
|
598
|
+
}
|
|
599
|
+
lines.push(`wrote ${written} label file(s); PM outcomes without a settled platform verdict stay unlabelled`);
|
|
600
|
+
return { ok: true, code: 0, lines, data: { prices: results, pm: pmResults } };
|
|
601
|
+
}
|
|
602
|
+
// Compare agent variants on a recorded corpus (see bench/bench.ts). Each
|
|
603
|
+
// variant runs with its own model, built exactly as `run` builds it from the
|
|
604
|
+
// variant's model config and the environment's keys. Never writes.
|
|
605
|
+
export async function cmdBench(opts = {}) {
|
|
606
|
+
if (!opts.corpus)
|
|
607
|
+
return fail(["bench needs --corpus <dir>"]);
|
|
608
|
+
if (!opts.variants || opts.variants.length === 0)
|
|
609
|
+
return fail(["bench needs at least one --variant name=<agentPath>"]);
|
|
610
|
+
if (opts.seed !== undefined &&
|
|
611
|
+
(!Number.isSafeInteger(opts.seed) || opts.seed < 0))
|
|
612
|
+
return fail(["--seed must be a non-negative integer"]);
|
|
613
|
+
let corpus;
|
|
614
|
+
try {
|
|
615
|
+
corpus = readCorpus(resolvePath(opts.corpus));
|
|
616
|
+
}
|
|
617
|
+
catch (e) {
|
|
618
|
+
return fail([e.message]);
|
|
619
|
+
}
|
|
620
|
+
const variants = [];
|
|
621
|
+
for (const arg of opts.variants) {
|
|
622
|
+
const eq = arg.indexOf("=");
|
|
623
|
+
if (eq <= 0 || eq === arg.length - 1)
|
|
624
|
+
return fail([`--variant "${arg}" must look like name=<agentPath>`]);
|
|
625
|
+
const name = arg.slice(0, eq);
|
|
626
|
+
const agentPath = arg.slice(eq + 1);
|
|
627
|
+
let loaded;
|
|
628
|
+
try {
|
|
629
|
+
loaded = loadAgent(agentPath, "self-host");
|
|
630
|
+
}
|
|
631
|
+
catch (e) {
|
|
632
|
+
if (e instanceof ResolveError)
|
|
633
|
+
return issuesResult(e.issues, `variant ${name}: resolve failed`);
|
|
634
|
+
throw e;
|
|
635
|
+
}
|
|
636
|
+
let provider;
|
|
637
|
+
try {
|
|
638
|
+
provider = opts.providerFor
|
|
639
|
+
? opts.providerFor(loaded.spec)
|
|
640
|
+
: selectProvider(loaded.spec, process.env, fetch);
|
|
641
|
+
}
|
|
642
|
+
catch (e) {
|
|
643
|
+
return fail([`variant ${name}: ${e.message}`]);
|
|
644
|
+
}
|
|
645
|
+
variants.push({
|
|
646
|
+
name,
|
|
647
|
+
spec: loaded.spec,
|
|
648
|
+
mergedProse: runtimeProse(loaded.resolved),
|
|
649
|
+
provider,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
const lines = [
|
|
653
|
+
`bench DRY-RUN: ${corpus.cassettes.length} cassette(s) x ${variants.length} variant(s) x ${opts.repeats ?? 3} repeat(s)`,
|
|
654
|
+
];
|
|
655
|
+
if (corpus.ignored.length)
|
|
656
|
+
lines.push(`ignored non-cassette file(s): ${corpus.ignored.join(", ")}`);
|
|
657
|
+
let report;
|
|
658
|
+
try {
|
|
659
|
+
report = await runBench({
|
|
660
|
+
cassettes: corpus.cassettes,
|
|
661
|
+
labels: corpus.labels,
|
|
662
|
+
variants,
|
|
663
|
+
repeats: opts.repeats,
|
|
664
|
+
seed: opts.seed,
|
|
665
|
+
baselines: opts.baselines,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (e) {
|
|
669
|
+
return fail([...lines, `✗ ${e.message}`]);
|
|
670
|
+
}
|
|
671
|
+
const summary = report.variants;
|
|
672
|
+
for (const [name, v] of Object.entries(summary)) {
|
|
673
|
+
const codes = Object.entries(v.all.actions.rejectCodes)
|
|
674
|
+
.map(([c, n]) => `${c} ${n}`)
|
|
675
|
+
.join(", ");
|
|
676
|
+
lines.push(`${name}: ${v.all.cycles} cycle(s), accepted ${v.all.actions.accepted}, rejected ${v.all.actions.rejected}${codes ? ` (${codes})` : ""}, model failures ${v.all.modelFailures}, cycles missing inputs ${v.all.cyclesWithMissingInputs}, repeat consistency ${v.all.repeatConsistency ?? "n/a"}`);
|
|
677
|
+
}
|
|
678
|
+
for (const c of report.comparisons) {
|
|
679
|
+
const accepted = c.all.metrics.acceptedActions;
|
|
680
|
+
lines.push(`${c.a} vs ${c.b}: overlap ${c.all.actionOverlapJaccard ?? "n/a"}, accepted diff ${accepted.meanDiff ?? "n/a"} (95% CI ${accepted.ci95 ? `${accepted.ci95[0]}..${accepted.ci95[1]}` : "n/a"}, n=${accepted.n})`);
|
|
681
|
+
}
|
|
682
|
+
if (opts.out) {
|
|
683
|
+
writeFileSync(resolvePath(opts.out), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
684
|
+
lines.push(`wrote ${resolvePath(opts.out)}`);
|
|
685
|
+
}
|
|
686
|
+
lines.push(`contentHash ${report.contentHash}`);
|
|
687
|
+
return { ok: true, code: 0, lines, data: report };
|
|
688
|
+
}
|
|
441
689
|
function parseFlags(args) {
|
|
442
690
|
const out = { _: [] };
|
|
691
|
+
// A missing or non-numeric value becomes NaN so the command rejects it,
|
|
692
|
+
// never silently falls back to its default.
|
|
693
|
+
const num = (v) => (v === undefined ? NaN : Number(v));
|
|
443
694
|
for (let i = 0; i < args.length; i++) {
|
|
444
695
|
const a = args[i];
|
|
445
696
|
if (a === "--hosted")
|
|
@@ -462,6 +713,26 @@ function parseFlags(args) {
|
|
|
462
713
|
out.state = args[++i];
|
|
463
714
|
else if (a === "--expect-definition")
|
|
464
715
|
out.expectDefinition = args[++i] ?? "";
|
|
716
|
+
else if (a === "--out")
|
|
717
|
+
out.out = args[++i];
|
|
718
|
+
else if (a === "--cycles")
|
|
719
|
+
out.cycles = num(args[++i]);
|
|
720
|
+
else if (a === "--every")
|
|
721
|
+
out.every = args[++i] ?? "";
|
|
722
|
+
else if (a === "--corpus")
|
|
723
|
+
out.corpus = args[++i];
|
|
724
|
+
else if (a === "--variant")
|
|
725
|
+
out.variant = [...(out.variant ?? []), args[++i] ?? ""];
|
|
726
|
+
else if (a === "--repeats")
|
|
727
|
+
out.repeats = num(args[++i]);
|
|
728
|
+
else if (a === "--seed")
|
|
729
|
+
out.seed = num(args[++i]);
|
|
730
|
+
else if (a === "--no-baselines")
|
|
731
|
+
out.noBaselines = true;
|
|
732
|
+
else if (a === "--horizon-hours")
|
|
733
|
+
out.horizonHours = num(args[++i]);
|
|
734
|
+
else if (a === "--overwrite")
|
|
735
|
+
out.overwrite = true;
|
|
465
736
|
else
|
|
466
737
|
out._.push(a);
|
|
467
738
|
}
|
|
@@ -476,6 +747,9 @@ function usageLines() {
|
|
|
476
747
|
" eject <agent.md | dir>",
|
|
477
748
|
" lock <path>",
|
|
478
749
|
" run <path> [--once] [--live] [--dry-run] [--state <file>] [--expect-definition sha256:...] (dry-run by default)",
|
|
750
|
+
" record <path> --out <dir> [--cycles N] [--every 5m] (bench inputs; reads only, no model call)",
|
|
751
|
+
" bench --corpus <dir> --variant a=<path> --variant b=<path> [--repeats 3] [--seed N] [--no-baselines] [--out report.json]",
|
|
752
|
+
" label --corpus <dir> [--horizon-hours 24] [--overwrite] (price outcome labels after asOf; reads only)",
|
|
479
753
|
];
|
|
480
754
|
}
|
|
481
755
|
export async function main(argv) {
|
|
@@ -513,6 +787,30 @@ export async function main(argv) {
|
|
|
513
787
|
});
|
|
514
788
|
break;
|
|
515
789
|
}
|
|
790
|
+
case "record":
|
|
791
|
+
r = await cmdRecord(pos[0] ?? ".", {
|
|
792
|
+
out: flags.out,
|
|
793
|
+
cycles: flags.cycles,
|
|
794
|
+
every: flags.every,
|
|
795
|
+
});
|
|
796
|
+
break;
|
|
797
|
+
case "label":
|
|
798
|
+
r = await cmdLabel({
|
|
799
|
+
corpus: flags.corpus,
|
|
800
|
+
horizonHours: flags.horizonHours,
|
|
801
|
+
overwrite: flags.overwrite,
|
|
802
|
+
});
|
|
803
|
+
break;
|
|
804
|
+
case "bench":
|
|
805
|
+
r = await cmdBench({
|
|
806
|
+
corpus: flags.corpus,
|
|
807
|
+
variants: flags.variant,
|
|
808
|
+
repeats: flags.repeats,
|
|
809
|
+
seed: flags.seed,
|
|
810
|
+
out: flags.out,
|
|
811
|
+
baselines: !flags.noBaselines,
|
|
812
|
+
});
|
|
813
|
+
break;
|
|
516
814
|
case undefined:
|
|
517
815
|
case "help":
|
|
518
816
|
case "--help":
|
package/dist/agent/client.d.ts
CHANGED
|
@@ -40,13 +40,27 @@ export declare class CoinRithmClient {
|
|
|
40
40
|
rateLimitHits: number;
|
|
41
41
|
constructor(cfg: ClientConfig);
|
|
42
42
|
private request;
|
|
43
|
+
private publicRequest;
|
|
44
|
+
credentialFingerprint(): string;
|
|
43
45
|
me(trace?: AgentTrace): Promise<ApiResult>;
|
|
46
|
+
performance(trace?: AgentTrace, options?: {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
}): Promise<ApiResult>;
|
|
44
50
|
portfolio(trace?: AgentTrace): Promise<ApiResult>;
|
|
45
51
|
wallet(query?: {
|
|
46
52
|
coinId?: string;
|
|
47
53
|
}, trace?: AgentTrace): Promise<ApiResult>;
|
|
48
54
|
resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
49
55
|
cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
|
|
56
|
+
agentUniverse(query: Record<string, string | number>, trace?: AgentTrace): Promise<ApiResult>;
|
|
57
|
+
getPublicPmWhales(options?: {
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
limit?: number;
|
|
60
|
+
}): Promise<ApiResult>;
|
|
61
|
+
getPublicPmWhaleWallet(source: string, wallet: string, options?: {
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
}): Promise<ApiResult>;
|
|
50
64
|
market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
51
65
|
candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
52
66
|
trades(query?: {
|
package/dist/agent/client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// from an agent file). 429 backs off on Retry-After; 401/403/409/422 are
|
|
5
5
|
// FAIL-CLOSED cycle outcomes (returned, not retried). fetch + sleep are
|
|
6
6
|
// injectable so tests run with no network and no real waits.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
9
|
import { retryAfterSeconds } from "../retryAfter.js";
|
|
9
10
|
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
@@ -54,6 +55,16 @@ export class CoinRithmClient {
|
|
|
54
55
|
this.extraHeaders = cfg.extraHeaders;
|
|
55
56
|
}
|
|
56
57
|
async request(method, path, opts = {}) {
|
|
58
|
+
const requestTimeoutMs = opts.timeoutMs !== undefined &&
|
|
59
|
+
Number.isSafeInteger(opts.timeoutMs) &&
|
|
60
|
+
opts.timeoutMs >= 1
|
|
61
|
+
? Math.min(opts.timeoutMs, this.requestTimeoutMs)
|
|
62
|
+
: this.requestTimeoutMs;
|
|
63
|
+
const maxRetries = opts.maxRetries !== undefined &&
|
|
64
|
+
Number.isSafeInteger(opts.maxRetries) &&
|
|
65
|
+
opts.maxRetries >= 0
|
|
66
|
+
? Math.min(opts.maxRetries, this.maxRetries)
|
|
67
|
+
: this.maxRetries;
|
|
57
68
|
const url = new URL(this.baseUrl + path);
|
|
58
69
|
if (opts.query) {
|
|
59
70
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -76,7 +87,7 @@ export class CoinRithmClient {
|
|
|
76
87
|
const timer = setTimeout(() => {
|
|
77
88
|
timedOut = true;
|
|
78
89
|
controller.abort(new Error("API request deadline exceeded"));
|
|
79
|
-
},
|
|
90
|
+
}, requestTimeoutMs);
|
|
80
91
|
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
81
92
|
if (this.signal?.aborted)
|
|
82
93
|
cancel();
|
|
@@ -102,12 +113,12 @@ export class CoinRithmClient {
|
|
|
102
113
|
const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
|
|
103
114
|
if (res.status === 429)
|
|
104
115
|
this.rateLimitHits += 1;
|
|
105
|
-
if (res.status === 429 && attempt <
|
|
116
|
+
if (res.status === 429 && attempt < maxRetries) {
|
|
106
117
|
// Release this response before waiting so retries do not retain sockets.
|
|
107
118
|
void res.body?.cancel().catch(() => { });
|
|
108
119
|
const delayMs = (retryAfter ?? 5) * 1000;
|
|
109
120
|
// Never shorten a provider's Retry-After or overflow a Node timer.
|
|
110
|
-
if (delayMs >=
|
|
121
|
+
if (delayMs >= requestTimeoutMs)
|
|
111
122
|
await aborted;
|
|
112
123
|
else if (this.sleepFn)
|
|
113
124
|
await this.sleepFn(delayMs);
|
|
@@ -151,7 +162,7 @@ export class CoinRithmClient {
|
|
|
151
162
|
? "request_aborted"
|
|
152
163
|
: "network_error",
|
|
153
164
|
message: timedOut
|
|
154
|
-
? `API request exceeded ${
|
|
165
|
+
? `API request exceeded ${requestTimeoutMs}ms deadline`
|
|
155
166
|
: controller.signal.aborted
|
|
156
167
|
? "API request cancelled"
|
|
157
168
|
: err instanceof Error
|
|
@@ -166,10 +177,96 @@ export class CoinRithmClient {
|
|
|
166
177
|
controller.signal.removeEventListener("abort", rejectAborted);
|
|
167
178
|
}
|
|
168
179
|
}
|
|
180
|
+
// Keyless, bounded reads against the public PM surface. These deliberately
|
|
181
|
+
// bypass the authenticated request path so a runner key cannot leak into a
|
|
182
|
+
// public context lookup. No retries are added here; the enrichment is
|
|
183
|
+
// optional and its deadline includes response-body consumption.
|
|
184
|
+
async publicRequest(path, query, timeoutMs = 5_000) {
|
|
185
|
+
const url = new URL(this.baseUrl + path);
|
|
186
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
187
|
+
if (value !== undefined && value !== null && value !== "")
|
|
188
|
+
url.searchParams.set(key, String(value));
|
|
189
|
+
}
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
if (this.signal?.aborted) {
|
|
192
|
+
return { ok: false, status: 0, data: { error: "network_error" } };
|
|
193
|
+
}
|
|
194
|
+
const cancel = () => controller.abort();
|
|
195
|
+
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
196
|
+
const deadlineMs = Math.min(timeoutMs, this.requestTimeoutMs);
|
|
197
|
+
let timedOut = false;
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
timedOut = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
}, deadlineMs);
|
|
202
|
+
let rejectAborted;
|
|
203
|
+
const aborted = new Promise((_, reject) => {
|
|
204
|
+
rejectAborted = () => reject(new Error("public request aborted"));
|
|
205
|
+
controller.signal.addEventListener("abort", rejectAborted, {
|
|
206
|
+
once: true,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
if (this.signal?.aborted)
|
|
210
|
+
cancel();
|
|
211
|
+
try {
|
|
212
|
+
const perform = async () => {
|
|
213
|
+
const res = await this.fetchFn(url.toString(), {
|
|
214
|
+
method: "GET",
|
|
215
|
+
headers: { Accept: "application/json" },
|
|
216
|
+
signal: controller.signal,
|
|
217
|
+
});
|
|
218
|
+
const text = await res.text();
|
|
219
|
+
let data = text;
|
|
220
|
+
if (text) {
|
|
221
|
+
try {
|
|
222
|
+
data = JSON.parse(text);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* leave as text */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: res.ok, status: res.status, data };
|
|
229
|
+
};
|
|
230
|
+
return await Promise.race([perform(), aborted]);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
status: 0,
|
|
236
|
+
data: {
|
|
237
|
+
error: timedOut ? "timeout" : "network_error",
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
controller.signal.removeEventListener("abort", rejectAborted);
|
|
244
|
+
this.signal?.removeEventListener("abort", cancel);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A stable, non-reversible identity for this credential and API base, for
|
|
248
|
+
// per-agent in-memory caches that must outlive one client instance (the
|
|
249
|
+
// hosted scheduler builds a new client every cycle). Never the key itself.
|
|
250
|
+
credentialFingerprint() {
|
|
251
|
+
return createHash("sha256")
|
|
252
|
+
.update(`${this.baseUrl}\n${this.apiKey}`, "utf8")
|
|
253
|
+
.digest("hex")
|
|
254
|
+
.slice(0, 32);
|
|
255
|
+
}
|
|
169
256
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
170
257
|
me(trace) {
|
|
171
258
|
return this.request("GET", "/api/agent/me", { trace });
|
|
172
259
|
}
|
|
260
|
+
// GET /api/agent/performance: the calling key's own realized performance.
|
|
261
|
+
// Newer backends add an optional `pmCalibration` block (the key's settled PM
|
|
262
|
+
// forecast record), parsed defensively in observe.
|
|
263
|
+
performance(trace, options) {
|
|
264
|
+
return this.request("GET", "/api/agent/performance", {
|
|
265
|
+
trace,
|
|
266
|
+
timeoutMs: options?.timeoutMs,
|
|
267
|
+
maxRetries: options?.maxRetries,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
173
270
|
portfolio(trace) {
|
|
174
271
|
return this.request("GET", "/api/agent/portfolio", { trace });
|
|
175
272
|
}
|
|
@@ -186,6 +283,18 @@ export class CoinRithmClient {
|
|
|
186
283
|
? "/api/coins/top-losers"
|
|
187
284
|
: "/api/coins/top-gainers", { query: { limit }, trace });
|
|
188
285
|
}
|
|
286
|
+
// Market-boundaries screener (GET /api/agent/universe, read scope): rows
|
|
287
|
+
// inside the agent's declared rank band / volume floor / sectors, sorted by
|
|
288
|
+
// the agent's notion of an opportunity. Shared 60 s server cache.
|
|
289
|
+
agentUniverse(query, trace) {
|
|
290
|
+
return this.request("GET", "/api/agent/universe", { query, trace });
|
|
291
|
+
}
|
|
292
|
+
getPublicPmWhales(options) {
|
|
293
|
+
return this.publicRequest("/api/prediction-markets/whales", { limit: options?.limit }, options?.timeoutMs);
|
|
294
|
+
}
|
|
295
|
+
getPublicPmWhaleWallet(source, wallet, options) {
|
|
296
|
+
return this.publicRequest(`/api/prediction-markets/whales/wallets/${encodeURIComponent(source)}/${encodeURIComponent(wallet)}`, undefined, options?.timeoutMs);
|
|
297
|
+
}
|
|
189
298
|
market(coinId, trace) {
|
|
190
299
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
|
|
191
300
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
// Per-day counters roll over on the UTC date (util.ts dayKey), whatever the
|
|
2
|
+
// bundle declares. trigger.timezone is parsed but read by nothing else, so a
|
|
3
|
+
// non-UTC value would silently promise local-day limits.
|
|
4
|
+
const UTC_NAMES = new Set(["utc", "etc/utc", "z", "gmt", "etc/gmt"]);
|
|
1
5
|
// Advisory only: these fields remain loadable for backwards compatibility.
|
|
2
6
|
export function configurationWarnings(raw) {
|
|
3
7
|
const warnings = [];
|
|
8
|
+
const trigger = raw.trigger;
|
|
9
|
+
if (trigger && typeof trigger === "object" && !Array.isArray(trigger)) {
|
|
10
|
+
const tz = trigger.timezone;
|
|
11
|
+
if (typeof tz === "string" && !UTC_NAMES.has(tz.trim().toLowerCase())) {
|
|
12
|
+
warnings.push(`trigger.timezone "${tz}" is informational: maxTradesPerDay and maxDailyLossMusd reset at 00:00 UTC, not in this timezone`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
if (Array.isArray(raw.capabilities) &&
|
|
5
16
|
raw.capabilities.includes("websearch")) {
|
|
6
17
|
warnings.push("capabilities.websearch is reserved: the runner does not perform web searches");
|
package/dist/agent/decision.d.ts
CHANGED
|
@@ -537,11 +537,14 @@ export declare const DECISION_JSON_SCHEMA: {
|
|
|
537
537
|
};
|
|
538
538
|
}];
|
|
539
539
|
};
|
|
540
|
+
export declare const ACTIONS_STRING_DIAGNOSTICS: readonly ["json_array_empty_valid_decision", "json_array_nonempty_valid_decision", "json_array_invalid_decision", "json_nonarray", "invalid_json", "empty_string_valid_skip", "empty_string_other_decision", "non_json_array_text", "json5_array_valid_decision", "json5_array_invalid_decision", "over_size_limit"];
|
|
541
|
+
export type ActionsStringDiagnostic = (typeof ACTIONS_STRING_DIAGNOSTICS)[number];
|
|
540
542
|
export type ParseDecisionResult = {
|
|
541
543
|
ok: true;
|
|
542
544
|
decision: Decision;
|
|
543
545
|
} | {
|
|
544
546
|
ok: false;
|
|
545
547
|
error: string;
|
|
548
|
+
actionsStringDiagnostic?: ActionsStringDiagnostic;
|
|
546
549
|
};
|
|
547
550
|
export declare function parseDecision(text: string): ParseDecisionResult;
|