@coinrithm/mcp-trading 0.7.5 → 0.7.7
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 +310 -197
- package/README.md +319 -279
- package/dist/agent/cli.js +22 -1
- package/dist/agent/client.d.ts +3 -0
- package/dist/agent/client.js +11 -0
- package/dist/agent/decision.d.ts +12 -0
- package/dist/agent/decision.js +10 -0
- package/dist/agent/decisionProbe.d.ts +17 -0
- package/dist/agent/decisionProbe.js +70 -0
- package/dist/agent/decisionValidator.js +23 -0
- package/dist/agent/engine.d.ts +4 -1
- package/dist/agent/engine.js +7 -1
- package/dist/agent/observe.js +118 -6
- package/dist/agent/prompt.d.ts +3 -1
- package/dist/agent/prompt.js +114 -27
- package/dist/agent/providerCapabilities.d.ts +20 -0
- package/dist/agent/providerCapabilities.js +67 -0
- package/dist/agent/providers.d.ts +29 -1
- package/dist/agent/providers.js +50 -25
- package/dist/agent/resolve.d.ts +12 -0
- package/dist/agent/resolve.js +82 -3
- package/dist/agent/runner.js +113 -8
- package/dist/agent/skill.js +6 -0
- package/dist/agent/skillValidator.js +7 -0
- package/dist/agent/state.d.ts +5 -0
- package/dist/agent/state.js +20 -0
- package/dist/agent/strictLint.js +14 -0
- package/dist/agent/templates.js +6 -0
- package/dist/agent/types.d.ts +34 -1
- package/dist/agent/types.js +10 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -0
- package/dist/http.js +21 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +66 -10
- package/package.json +1 -1
package/dist/agent/resolve.js
CHANGED
|
@@ -47,6 +47,32 @@ const JOURNAL_MAX_LINES = 200;
|
|
|
47
47
|
const JOURNAL_MAX_BYTES = 8_000;
|
|
48
48
|
// Optional prose files (markdown the LLM reads), in assembly order.
|
|
49
49
|
const PROSE_FILES = ["character/thesis.md", "character/persona.md"];
|
|
50
|
+
// Prose files carry an OPTIONAL YAML frontmatter block (type/title/description/
|
|
51
|
+
// tags) that is authoring metadata, not doctrine — the model gains nothing from
|
|
52
|
+
// `tags: [agent, persona, mean-reversion]`. It was being merged verbatim into
|
|
53
|
+
// the system prompt: pure noise, and on the hosted path it also consumed the
|
|
54
|
+
// 8,000-char strategy budget (measured 2026-08-19: ~1.0k chars across a
|
|
55
|
+
// decomposed bundle's thesis/persona/journal). Skill files already strip theirs
|
|
56
|
+
// (their frontmatter is parsed for the cap patch), and guards.md strips too —
|
|
57
|
+
// this brings the remaining prose files in line. Files WITHOUT frontmatter are
|
|
58
|
+
// returned unchanged.
|
|
59
|
+
export const proseBody = (raw) => {
|
|
60
|
+
const src = raw.replace(/\r\n/g, "\n");
|
|
61
|
+
const m = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(src);
|
|
62
|
+
return (m ? m[1] : src).trim();
|
|
63
|
+
};
|
|
64
|
+
// First-class hard-guards file (2026-08-19, audit rank 7). User-authored
|
|
65
|
+
// behavioral borders that machine caps cannot express ("never open a short
|
|
66
|
+
// unless a qualifying pump preceded it") previously lived as an undocumented
|
|
67
|
+
// "Hard borders" paragraph buried mid-persona — present in only 5 of 8
|
|
68
|
+
// bundles and easy for a forker to miss. character/guards.md gets a dedicated
|
|
69
|
+
// slot: loaded LAST so the block lands at the END of the strategy prose,
|
|
70
|
+
// immediately adjacent to the system prompt's hard-caps section, wrapped in a
|
|
71
|
+
// high-salience header plus an explicit guards-win-conflicts rule.
|
|
72
|
+
export const GUARDS_FILE = "character/guards.md";
|
|
73
|
+
export const GUARDS_HEADER = "## HARD BEHAVIORAL GUARDS — never violate these";
|
|
74
|
+
export const GUARDS_FOOTER = "(These guards override every other instruction in this strategy. When a guard conflicts with an opportunity, the guard wins and the correct output is a skip that names the guard.)";
|
|
75
|
+
export const wrapGuardsProse = (body) => `${GUARDS_HEADER}\n\n${body.trim()}\n\n${GUARDS_FOOTER}`;
|
|
50
76
|
const FUNCTIONALITY_PIN = "functionality/coinrithm.yaml";
|
|
51
77
|
// Enforced cap field names. sizing.yaml is SOFT guidance and must NOT contain
|
|
52
78
|
// any of these (or a user could think a limit binds when it does not).
|
|
@@ -427,8 +453,9 @@ function resolveDirectory(dir) {
|
|
|
427
453
|
const p = join(dir, pf);
|
|
428
454
|
if (existsSync(p)) {
|
|
429
455
|
const abs = safePath(ctx, pf, "prose");
|
|
430
|
-
if (abs)
|
|
431
|
-
proseParts.push({ source: pf, text: readHashed(ctx, abs) });
|
|
456
|
+
if (abs) {
|
|
457
|
+
proseParts.push({ source: pf, text: proseBody(readHashed(ctx, abs)) });
|
|
458
|
+
}
|
|
432
459
|
}
|
|
433
460
|
}
|
|
434
461
|
proseParts.push(...skillProse);
|
|
@@ -437,13 +464,28 @@ function resolveDirectory(dir) {
|
|
|
437
464
|
if (existsSync(journalPath)) {
|
|
438
465
|
const abs = safePath(ctx, "journal/notes.md", "journal");
|
|
439
466
|
if (abs) {
|
|
440
|
-
const full = readHashed(ctx, abs);
|
|
467
|
+
const full = proseBody(readHashed(ctx, abs));
|
|
441
468
|
proseParts.push({
|
|
442
469
|
source: "journal/notes.md",
|
|
443
470
|
text: boundTail(full, JOURNAL_MAX_LINES, JOURNAL_MAX_BYTES),
|
|
444
471
|
});
|
|
445
472
|
}
|
|
446
473
|
}
|
|
474
|
+
// Hard behavioral guards — see GUARDS_FILE above. Pushed AFTER the journal
|
|
475
|
+
// so the wrapped block is the final prose the model reads before the caps
|
|
476
|
+
// section. Frontmatter is optional and stripped (only the body is doctrine);
|
|
477
|
+
// an empty body contributes nothing.
|
|
478
|
+
const guardsAbsPath = join(dir, GUARDS_FILE);
|
|
479
|
+
if (existsSync(guardsAbsPath)) {
|
|
480
|
+
const abs = safePath(ctx, GUARDS_FILE, "guards");
|
|
481
|
+
if (abs) {
|
|
482
|
+
const raw = readHashed(ctx, abs);
|
|
483
|
+
const body = (raw.startsWith("---") ? parseFrontmatter(raw).body : raw).trim();
|
|
484
|
+
if (body) {
|
|
485
|
+
proseParts.push({ source: GUARDS_FILE, text: wrapGuardsProse(body) });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
447
489
|
// Optional API/tool contract pin. It is locked for reproducibility and stale
|
|
448
490
|
// warnings, but it is not part of AgentSpec and is never sent to the model.
|
|
449
491
|
const functionalityPath = join(dir, FUNCTIONALITY_PIN);
|
|
@@ -542,3 +584,40 @@ export function resolveAgent(inputPath) {
|
|
|
542
584
|
{ code: "input_invalid", message: "input must be a file or a directory" },
|
|
543
585
|
]);
|
|
544
586
|
}
|
|
587
|
+
// ── Hosted strategy-prose budget ───────────────────────────────────────────
|
|
588
|
+
// The managed (Studio) deploy/edit API caps the merged strategy prose and
|
|
589
|
+
// reverts the save past the cap; the self-host runner has NO such limit. The
|
|
590
|
+
// cap is not arbitrary — the merged prose becomes the system prompt, and an
|
|
591
|
+
// oversized prompt is what previously pushed small free-tier models to ~69k
|
|
592
|
+
// tokens / 413s per cycle. Mirrored here (backend-v2
|
|
593
|
+
// controllers/agentManage.ts sanitizeStrategyProse) so `validate --hosted`
|
|
594
|
+
// can catch it before a user does.
|
|
595
|
+
//
|
|
596
|
+
// RAISED 8,000 -> 12,000 on 2026-08-21, from measurement rather than feel.
|
|
597
|
+
// 8,000 made the product's core promise impossible: forking a house template
|
|
598
|
+
// starts you at 7,967 (Olivia) / 7,931 (Carl) / 7,839 (Mia), so a user had
|
|
599
|
+
// 33 to 161 characters to write their own rules in. "Fork a template and make
|
|
600
|
+
// it yours" could not be done.
|
|
601
|
+
//
|
|
602
|
+
// The cap was justified by hosted inference cost. Measured over 8,060 LLM
|
|
603
|
+
// cycles in 24h on prod: average input is 9,038 tokens, of which the prose is
|
|
604
|
+
// only 12.6-40.2% (median ~25%) — the OBSERVATION is the other ~75%. Inputs
|
|
605
|
+
// already reached 17,065 tokens on Llama 3.1 8B and 16,437 on Nemotron 49B
|
|
606
|
+
// (measured on the since-retired NIM line; Nemotron 3 successors match), with
|
|
607
|
+
// ZERO rate-limit errors and estimated_cost_usd of 0.0000 (free NIM tier).
|
|
608
|
+
// +4,000 characters is ~+1,000 tokens/cycle (+11%), landing average input near
|
|
609
|
+
// 10,038 — still below what the fleet already handles at peak today.
|
|
610
|
+
//
|
|
611
|
+
// Self-host is deliberately NOT capped (runner.ts/prompt.ts enforce nothing):
|
|
612
|
+
// those agents run on the user's own model key, so their prompt size costs us
|
|
613
|
+
// nothing. This limit exists only where WE pay for the inference.
|
|
614
|
+
export const HOSTED_PROSE_MAX_CHARS = 12000;
|
|
615
|
+
/** PURE — exported for tests. Mirrors the backend's trim-then-measure. */
|
|
616
|
+
export const hostedProseBudget = (mergedProse) => {
|
|
617
|
+
const used = mergedProse.replace(/\r\n/g, "\n").trim().length;
|
|
618
|
+
return {
|
|
619
|
+
used,
|
|
620
|
+
over: Math.max(0, used - HOSTED_PROSE_MAX_CHARS),
|
|
621
|
+
fits: used <= HOSTED_PROSE_MAX_CHARS,
|
|
622
|
+
};
|
|
623
|
+
};
|
package/dist/agent/runner.js
CHANGED
|
@@ -14,7 +14,7 @@ import { validateAction } from "./decisionValidator.js";
|
|
|
14
14
|
import { resolvePmRef } from "./resolvePm.js";
|
|
15
15
|
import { fetchQuote, executeAction } from "./act.js";
|
|
16
16
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
17
|
-
import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
|
|
17
|
+
import { rollDay, checkKillSwitch, accrueRealized, saveState, isPermanentModelError, isAuthFailureSkip, PERMANENT_MODEL_ERROR_THRESHOLD, AUTH_FAILURE_THRESHOLD, } from "./state.js";
|
|
18
18
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
19
19
|
import { parseCadenceMs, sleep } from "./util.js";
|
|
20
20
|
import { buildObservationReceipt } from "./observationReceipt.js";
|
|
@@ -429,6 +429,29 @@ export async function runCycle(deps) {
|
|
|
429
429
|
}
|
|
430
430
|
if (obs.skip) {
|
|
431
431
|
state.consecutiveRejectCycles += 1;
|
|
432
|
+
// Permanent-failure classification: a revoked/invalid CoinRithm key
|
|
433
|
+
// answers 401 deterministically — after the threshold, disable with the
|
|
434
|
+
// machine-readable 'key_invalid' prefix the scheduler's self-heal exempts
|
|
435
|
+
// (the old path revived such agents into ~1,500 guaranteed-dead
|
|
436
|
+
// cycles/day). A transient rotation blip stays under the threshold.
|
|
437
|
+
if (isAuthFailureSkip(obs.skip)) {
|
|
438
|
+
state.consecutiveAuthFailures = (state.consecutiveAuthFailures ?? 0) + 1;
|
|
439
|
+
if (state.consecutiveAuthFailures >= AUTH_FAILURE_THRESHOLD) {
|
|
440
|
+
state.disabled = true;
|
|
441
|
+
state.disabledReason = `key_invalid: CoinRithm key rejected (HTTP 401) on ${state.consecutiveAuthFailures} consecutive cycles`;
|
|
442
|
+
saveState(stateFile, state);
|
|
443
|
+
log(`disabled: ${state.disabledReason}`);
|
|
444
|
+
return {
|
|
445
|
+
decision: "skip",
|
|
446
|
+
skipReason: obs.skip,
|
|
447
|
+
planned: [],
|
|
448
|
+
disabled: true,
|
|
449
|
+
disabledReason: state.disabledReason,
|
|
450
|
+
live,
|
|
451
|
+
...observationReceipt,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
432
455
|
saveState(stateFile, state);
|
|
433
456
|
log(`skip: ${obs.skip}`);
|
|
434
457
|
return {
|
|
@@ -439,6 +462,8 @@ export async function runCycle(deps) {
|
|
|
439
462
|
...observationReceipt,
|
|
440
463
|
};
|
|
441
464
|
}
|
|
465
|
+
// A full observation implies /me succeeded — the auth-failure streak is over.
|
|
466
|
+
state.consecutiveAuthFailures = 0;
|
|
442
467
|
// GATE (slice 2): only SPEND an LLM call when a deterministic trigger fires — a
|
|
443
468
|
// flagged entry setup or an open position to manage. No trigger => a cheap
|
|
444
469
|
// heartbeat (zero tokens). A heartbeat is neither a model reject nor a failure,
|
|
@@ -493,33 +518,112 @@ export async function runCycle(deps) {
|
|
|
493
518
|
};
|
|
494
519
|
}
|
|
495
520
|
else {
|
|
496
|
-
noteLlmCall(state, gate.codes, nowMs);
|
|
497
521
|
const system = buildSystemPrompt(spec, mergedProse, {
|
|
498
522
|
includeForecast: forecastEnabled,
|
|
499
523
|
});
|
|
500
|
-
const user = buildUserPrompt(observation, state.journal
|
|
524
|
+
const user = buildUserPrompt(observation, state.journal, {
|
|
525
|
+
venues: spec.venues,
|
|
526
|
+
});
|
|
501
527
|
const tokensInEst = Math.round((system.length + user.length) / 4);
|
|
502
528
|
// Prompt-size + trigger visibility in the live terminal.
|
|
503
529
|
log(`prompt ~${tokensInEst} tok ` +
|
|
504
530
|
`(pm ${observation.pmMarkets.length}, trades ${observation.newClosedTrades.length}, watch ${observation.watch.length}, setups ${observation.setups.length}, triggers ${gate.codes.join("|") || "none"})`);
|
|
505
531
|
const res = await provider.decide({ system, user });
|
|
532
|
+
const route = res.route;
|
|
533
|
+
const actualCallMade = route
|
|
534
|
+
? route.attempts.some((attempt) => attempt.outcome !== "deferred")
|
|
535
|
+
: true;
|
|
536
|
+
// Capacity deferral made no provider call, so it must not consume the
|
|
537
|
+
// runner's debounce/LLM budget or delay recovery after capacity returns.
|
|
538
|
+
if (actualCallMade)
|
|
539
|
+
noteLlmCall(state, gate.codes, nowMs);
|
|
540
|
+
const effectiveProvider = actualCallMade
|
|
541
|
+
? (route?.effectiveProvider ?? providerName)
|
|
542
|
+
: undefined;
|
|
506
543
|
// Metering: prefer provider-reported usage; fall back to a chars/4 estimate.
|
|
507
|
-
const tokensIn =
|
|
508
|
-
?
|
|
509
|
-
:
|
|
544
|
+
const tokensIn = !actualCallMade
|
|
545
|
+
? 0
|
|
546
|
+
: res.ok
|
|
547
|
+
? (res.usage?.promptTokens ?? tokensInEst)
|
|
548
|
+
: tokensInEst;
|
|
510
549
|
const tokensOut = res.ok
|
|
511
550
|
? (res.usage?.completionTokens ?? Math.round(res.text.length / 4))
|
|
512
551
|
: 0;
|
|
513
|
-
const estimatedCostUsd = estimateCostUsd(providerName, tokensIn, tokensOut);
|
|
552
|
+
const estimatedCostUsd = estimateCostUsd(effectiveProvider ?? providerName, tokensIn, tokensOut);
|
|
514
553
|
meter = {
|
|
515
554
|
triggerCodes: gate.codes,
|
|
516
|
-
llmCallMade:
|
|
555
|
+
llmCallMade: actualCallMade,
|
|
517
556
|
tokensIn,
|
|
518
557
|
tokensOut,
|
|
519
558
|
estimatedCostUsd,
|
|
559
|
+
effectiveProvider,
|
|
560
|
+
effectiveModel: actualCallMade
|
|
561
|
+
? (route?.effectiveModel ?? spec.model?.name)
|
|
562
|
+
: undefined,
|
|
563
|
+
routeReason: route?.reason,
|
|
564
|
+
routeAttempts: route?.attempts,
|
|
520
565
|
};
|
|
521
566
|
if (!res.ok) {
|
|
567
|
+
if (res.deferred || !actualCallMade) {
|
|
568
|
+
saveState(stateFile, state);
|
|
569
|
+
log(`capacity deferred: ${res.error}`);
|
|
570
|
+
return {
|
|
571
|
+
decision: "skip",
|
|
572
|
+
skipReason: "provider capacity deferred",
|
|
573
|
+
planned: [],
|
|
574
|
+
modelFailed: false,
|
|
575
|
+
live,
|
|
576
|
+
...meter,
|
|
577
|
+
decisionType: "gate_skip",
|
|
578
|
+
writeAttempted: 0,
|
|
579
|
+
writeAccepted: 0,
|
|
580
|
+
...observationReceipt,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
522
583
|
state.consecutiveModelFailures += 1;
|
|
584
|
+
// Permanent-failure classification: a 404/410/model_not_found is a
|
|
585
|
+
// DECOMMISSIONED or misconfigured model that will fail every cycle
|
|
586
|
+
// until something changes (live-measured 2026-08-26: NVIDIA EOL'd the
|
|
587
|
+
// whole Llama 3.x line and 35 agents died on the old disable path).
|
|
588
|
+
// Reliability slice 1: this class must NEVER disable the agent —
|
|
589
|
+
// provider failures are the PLATFORM's problem, not the user's. Three
|
|
590
|
+
// consecutive occurrences (rules out a routing fluke) now emit a
|
|
591
|
+
// providerHold: hosted, the scheduler folds holds into a fleet-wide
|
|
592
|
+
// (provider, model) circuit that skip-claims matching agents with
|
|
593
|
+
// backoff probes; self-host, the runner simply keeps retrying each
|
|
594
|
+
// cadence and recovers the moment the provider does. Disables remain
|
|
595
|
+
// for what deserves them: revoked credentials, drawdown, kill-switch,
|
|
596
|
+
// user action. Transient errors reset the permanent streak.
|
|
597
|
+
if (isPermanentModelError(res.error)) {
|
|
598
|
+
state.consecutivePermanentModelErrors =
|
|
599
|
+
(state.consecutivePermanentModelErrors ?? 0) + 1;
|
|
600
|
+
if (state.consecutivePermanentModelErrors >=
|
|
601
|
+
PERMANENT_MODEL_ERROR_THRESHOLD) {
|
|
602
|
+
const hold = {
|
|
603
|
+
provider: route?.effectiveProvider ?? spec.model?.provider ?? "unknown",
|
|
604
|
+
model: route?.effectiveModel ?? spec.model?.name ?? "unknown",
|
|
605
|
+
error: res.error.slice(0, 200),
|
|
606
|
+
};
|
|
607
|
+
saveState(stateFile, state);
|
|
608
|
+
log(`provider hold: ${hold.provider}/${hold.model} — ${res.error.slice(0, 120)}`);
|
|
609
|
+
return {
|
|
610
|
+
decision: "skip",
|
|
611
|
+
skipReason: `provider hold: ${res.error}`,
|
|
612
|
+
planned: [],
|
|
613
|
+
modelFailed: true,
|
|
614
|
+
providerHold: hold,
|
|
615
|
+
live,
|
|
616
|
+
...meter,
|
|
617
|
+
decisionType: "model_error",
|
|
618
|
+
writeAttempted: 0,
|
|
619
|
+
writeAccepted: 0,
|
|
620
|
+
...observationReceipt,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
state.consecutivePermanentModelErrors = 0;
|
|
626
|
+
}
|
|
523
627
|
saveState(stateFile, state);
|
|
524
628
|
log(`model error: ${res.error}`);
|
|
525
629
|
return {
|
|
@@ -557,6 +661,7 @@ export async function runCycle(deps) {
|
|
|
557
661
|
};
|
|
558
662
|
}
|
|
559
663
|
state.consecutiveModelFailures = 0;
|
|
664
|
+
state.consecutivePermanentModelErrors = 0;
|
|
560
665
|
decision = parsed.decision;
|
|
561
666
|
}
|
|
562
667
|
// Reasoning captured for the Arena terminal (keystone transparency): the
|
package/dist/agent/skill.js
CHANGED
|
@@ -92,6 +92,12 @@ export function buildSpec(raw) {
|
|
|
92
92
|
requireStopLoss: bool(risk.requireStopLoss, true),
|
|
93
93
|
watchlist: strArr(risk.watchlist),
|
|
94
94
|
blocklist: strArr(risk.blocklist),
|
|
95
|
+
// Only the two exact values pass; anything else stays undefined here and
|
|
96
|
+
// FAILS validation (skillValidator) — a typo like "shorts_only" must
|
|
97
|
+
// never silently mean "unrestricted".
|
|
98
|
+
direction: risk.direction === "long_only" || risk.direction === "short_only"
|
|
99
|
+
? risk.direction
|
|
100
|
+
: undefined,
|
|
95
101
|
},
|
|
96
102
|
limits: {
|
|
97
103
|
maxTradesPerDay: normalizeTradeCap(num(limits.maxTradesPerDay, DEFAULT_LIMITS.maxTradesPerDay)),
|
|
@@ -53,6 +53,13 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
53
53
|
add("skill_risk_sl", "risk.requireStopLoss must be true or false");
|
|
54
54
|
if (!Array.isArray(r.watchlist) || r.watchlist.length === 0)
|
|
55
55
|
add("skill_risk_watchlist", "risk.watchlist must be a non-empty list of symbols");
|
|
56
|
+
// Fail-closed on the side restriction: a typo ("shorts_only") must never
|
|
57
|
+
// silently mean "unrestricted" — that is exactly how a prose-only
|
|
58
|
+
// constraint failed live on 2026-08-24.
|
|
59
|
+
if (r.direction !== undefined &&
|
|
60
|
+
r.direction !== "long_only" &&
|
|
61
|
+
r.direction !== "short_only")
|
|
62
|
+
add("skill_risk_direction", 'risk.direction must be "long_only" or "short_only" (omit for both)');
|
|
56
63
|
}
|
|
57
64
|
// Model
|
|
58
65
|
if (raw.model === undefined) {
|
package/dist/agent/state.d.ts
CHANGED
|
@@ -4,4 +4,9 @@ export declare function loadState(file: string | undefined, runId: string): RunS
|
|
|
4
4
|
export declare function saveState(file: string | undefined, state: RunState): void;
|
|
5
5
|
export declare function rollDay(state: RunState): RunState;
|
|
6
6
|
export declare function accrueRealized(state: RunState, closedTrades: Record<string, unknown>[]): void;
|
|
7
|
+
export declare const PERMANENT_MODEL_ERROR_RE: RegExp;
|
|
8
|
+
export declare const PERMANENT_MODEL_ERROR_THRESHOLD = 3;
|
|
9
|
+
export declare const AUTH_FAILURE_THRESHOLD = 10;
|
|
10
|
+
export declare const isPermanentModelError: (error: string) => boolean;
|
|
11
|
+
export declare const isAuthFailureSkip: (skipReason: string) => boolean;
|
|
7
12
|
export declare function checkKillSwitch(spec: AgentSpec, state: RunState): string | null;
|
package/dist/agent/state.js
CHANGED
|
@@ -84,6 +84,26 @@ export function accrueRealized(state, closedTrades) {
|
|
|
84
84
|
// floored at this many consecutive failures regardless of an agent's own (lower)
|
|
85
85
|
// setting. The scheduler additionally auto-revives any model-failure disable.
|
|
86
86
|
const MODEL_FAILURE_FLOOR = 10;
|
|
87
|
+
// ── Permanent-failure classification (2026-08-19) ───────────────────────────
|
|
88
|
+
// The generic kill-switch treats every failure as transient — correct for
|
|
89
|
+
// timeouts/blips, catastrophic for DETERMINISTIC failures. Live-measured: one
|
|
90
|
+
// agent spent 93% of 782 cycles/24h on a Groq 404 (model decommissioned),
|
|
91
|
+
// revived 7 times in 3h by the self-heal; four others burned ~1,500 cycles/day
|
|
92
|
+
// on a revoked CoinRithm key (HTTP 401). These classifiers give such failures
|
|
93
|
+
// a fast, NON-revivable disable with a machine-readable reason prefix the
|
|
94
|
+
// scheduler's self-heal exempts ('model_unavailable' / 'key_invalid').
|
|
95
|
+
//
|
|
96
|
+
// Permanent model errors are deterministic, so the threshold is small — 3
|
|
97
|
+
// consecutive occurrences rules out a one-off routing fluke without burning a
|
|
98
|
+
// day. Auth failures get 10: a key rotation/propagation blip should not kill
|
|
99
|
+
// an agent, but nothing recovers from an actually-revoked key.
|
|
100
|
+
export const PERMANENT_MODEL_ERROR_RE = /model_not_found|model[_ ]decommissioned|has been decommissioned|\b404\b|\b410\b|reached (?:its )?end of life|no longer available|does not exist or you do not have access/i;
|
|
101
|
+
export const PERMANENT_MODEL_ERROR_THRESHOLD = 3;
|
|
102
|
+
export const AUTH_FAILURE_THRESHOLD = 10;
|
|
103
|
+
export const isPermanentModelError = (error) => PERMANENT_MODEL_ERROR_RE.test(error);
|
|
104
|
+
// The observe phase folds a rejected key into its required-reads skip reason
|
|
105
|
+
// as "... (HTTP 401)".
|
|
106
|
+
export const isAuthFailureSkip = (skipReason) => /HTTP 401/.test(skipReason);
|
|
87
107
|
// Returns a disable reason if any kill-switch condition is tripped, else null.
|
|
88
108
|
export function checkKillSwitch(spec, state) {
|
|
89
109
|
const ks = spec.killSwitch;
|
package/dist/agent/strictLint.js
CHANGED
|
@@ -25,8 +25,20 @@ const ALLOWED_KEYS = {
|
|
|
25
25
|
"capabilities",
|
|
26
26
|
"include",
|
|
27
27
|
"watchlist",
|
|
28
|
+
// Load-bearing since OKF v2 (skill.ts builds the full TriggerPolicy from
|
|
29
|
+
// it) but was missing here, so any bundle actually SETTING it got an
|
|
30
|
+
// unknown_key lint — the knob existed and was unreachable (audit rank 10).
|
|
31
|
+
"triggerPolicy",
|
|
28
32
|
],
|
|
29
33
|
trigger: ["cadence", "timezone", "events"],
|
|
34
|
+
triggerPolicy: [
|
|
35
|
+
"mode",
|
|
36
|
+
"skipLlmWhenNoTrigger",
|
|
37
|
+
"alwaysManageOpenPositions",
|
|
38
|
+
"maxLlmCallsPerHour",
|
|
39
|
+
"debounceMinutes",
|
|
40
|
+
"pmEvalCooldownMinutes",
|
|
41
|
+
],
|
|
30
42
|
model: ["provider", "name", "baseUrl"],
|
|
31
43
|
risk: [
|
|
32
44
|
"maxLeverage",
|
|
@@ -35,6 +47,7 @@ const ALLOWED_KEYS = {
|
|
|
35
47
|
"requireStopLoss",
|
|
36
48
|
"watchlist",
|
|
37
49
|
"blocklist",
|
|
50
|
+
"direction",
|
|
38
51
|
],
|
|
39
52
|
sizing: null,
|
|
40
53
|
limits: [
|
|
@@ -107,6 +120,7 @@ export function strictLint(raw) {
|
|
|
107
120
|
lintKeys("$root", raw, issues);
|
|
108
121
|
for (const block of [
|
|
109
122
|
"trigger",
|
|
123
|
+
"triggerPolicy",
|
|
110
124
|
"model",
|
|
111
125
|
"risk",
|
|
112
126
|
"limits",
|
package/dist/agent/templates.js
CHANGED
|
@@ -91,6 +91,12 @@ export function buildAgentObject(name, preset) {
|
|
|
91
91
|
trigger: { cadence: p.cadence, timezone: "UTC" },
|
|
92
92
|
model: { provider: "anthropic", name: "claude-sonnet-4-6" },
|
|
93
93
|
venues: ["futures"],
|
|
94
|
+
// Without `indicators` the event_driven gate has no setups to fire on and
|
|
95
|
+
// a fresh flat agent heartbeats forever with ZERO model calls (audit
|
|
96
|
+
// 2026-08-19: every scaffold was born dormant). Optional extras a user
|
|
97
|
+
// can add: "universe_scan" (top-movers discovery beyond the watchlist)
|
|
98
|
+
// and "news" (catalyst context for its coins).
|
|
99
|
+
capabilities: ["indicators"],
|
|
94
100
|
risk: {
|
|
95
101
|
maxLeverage: p.leverage,
|
|
96
102
|
perTradeMarginMusd: p.margin,
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface RiskConfig {
|
|
|
31
31
|
requireStopLoss: boolean;
|
|
32
32
|
watchlist: string[];
|
|
33
33
|
blocklist?: string[];
|
|
34
|
+
direction?: "long_only" | "short_only";
|
|
34
35
|
}
|
|
35
36
|
export interface LimitsConfig {
|
|
36
37
|
maxTradesPerDay: number;
|
|
@@ -61,7 +62,7 @@ export interface ObjectiveConfig {
|
|
|
61
62
|
secondary: string[];
|
|
62
63
|
horizon?: string;
|
|
63
64
|
}
|
|
64
|
-
export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news"];
|
|
65
|
+
export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan"];
|
|
65
66
|
export type Capability = (typeof ALLOWED_CAPABILITIES)[number];
|
|
66
67
|
export interface AgentSpec {
|
|
67
68
|
name: string;
|
|
@@ -106,6 +107,7 @@ export interface WatchEntry {
|
|
|
106
107
|
sentimentBullishPct?: number;
|
|
107
108
|
freshness?: Freshness;
|
|
108
109
|
indicators?: IndicatorSet;
|
|
110
|
+
discovered?: boolean;
|
|
109
111
|
}
|
|
110
112
|
export interface OpenPosition {
|
|
111
113
|
venue: Venue;
|
|
@@ -197,6 +199,12 @@ export interface Observation {
|
|
|
197
199
|
newClosedTrades: Array<Record<string, unknown>>;
|
|
198
200
|
polledBeforeWrite: boolean;
|
|
199
201
|
news?: NewsItem[];
|
|
202
|
+
universeMovers?: Array<{
|
|
203
|
+
symbol: string;
|
|
204
|
+
name?: string;
|
|
205
|
+
change24hPct?: number;
|
|
206
|
+
priceUsd?: number;
|
|
207
|
+
}>;
|
|
200
208
|
}
|
|
201
209
|
export type ProposedAction = {
|
|
202
210
|
type: "futures_open";
|
|
@@ -290,6 +298,8 @@ export interface RunState {
|
|
|
290
298
|
llmCallTimestamps?: number[];
|
|
291
299
|
lastLlmCallAt?: number;
|
|
292
300
|
lastTriggerFingerprint?: string;
|
|
301
|
+
consecutivePermanentModelErrors?: number;
|
|
302
|
+
consecutiveAuthFailures?: number;
|
|
293
303
|
journal?: Array<{
|
|
294
304
|
at: string;
|
|
295
305
|
did: string;
|
|
@@ -331,6 +341,16 @@ export interface CycleResult {
|
|
|
331
341
|
modelFailed?: boolean;
|
|
332
342
|
disabled?: boolean;
|
|
333
343
|
disabledReason?: string;
|
|
344
|
+
/** Reliability slice 1 (2026-08-26): a PERMANENT provider/model failure
|
|
345
|
+
* (404/410/decommission class) no longer disables the agent. The runner
|
|
346
|
+
* reports the hold; the scheduler aggregates holds into a fleet-wide
|
|
347
|
+
* provider circuit (skip-claiming + backoff probes). User pauses, revoked
|
|
348
|
+
* credentials, drawdown and safety stops keep using `disabled`. */
|
|
349
|
+
providerHold?: {
|
|
350
|
+
provider: string;
|
|
351
|
+
model: string;
|
|
352
|
+
error: string;
|
|
353
|
+
};
|
|
334
354
|
live: boolean;
|
|
335
355
|
observationHash?: string;
|
|
336
356
|
indicatorVersion?: string;
|
|
@@ -339,6 +359,19 @@ export interface CycleResult {
|
|
|
339
359
|
tokensIn?: number;
|
|
340
360
|
tokensOut?: number;
|
|
341
361
|
estimatedCostUsd?: number;
|
|
362
|
+
effectiveProvider?: string;
|
|
363
|
+
effectiveModel?: string;
|
|
364
|
+
routeReason?: string;
|
|
365
|
+
routeAttempts?: Array<{
|
|
366
|
+
provider: string;
|
|
367
|
+
model: string;
|
|
368
|
+
outcome: "success" | "failed" | "deferred";
|
|
369
|
+
failureClass?: "capacity" | "permanent" | "transient" | "malformed";
|
|
370
|
+
status?: number;
|
|
371
|
+
retryAfterMs?: number;
|
|
372
|
+
latencyMs: number;
|
|
373
|
+
error?: string;
|
|
374
|
+
}>;
|
|
342
375
|
decisionType?: "act" | "skip" | "gate_skip" | "model_error";
|
|
343
376
|
writeAttempted?: number;
|
|
344
377
|
writeAccepted?: number;
|
package/dist/agent/types.js
CHANGED
|
@@ -50,10 +50,20 @@ export const OBJECTIVE_PRIMARIES = [
|
|
|
50
50
|
// slice. `websearch` = external lookups (an injection surface + a cost — it can
|
|
51
51
|
// inform reasoning but NEVER widen a cap, since caps live in the runner);
|
|
52
52
|
// `indicators` = runner-computed RSI/MACD/etc. fed into the observation.
|
|
53
|
+
// `universe_scan` (2026-08-18, direct user request): each cycle the runner
|
|
54
|
+
// pulls the top 24h movers across CoinRithm's tracked coin universe, resolves
|
|
55
|
+
// the top few into FULL watch entries (price, sentiment, indicators when that
|
|
56
|
+
// capability is also on) and appends them to the observation marked
|
|
57
|
+
// `discovered: true`. Downstream is unchanged by design: a discovered entry
|
|
58
|
+
// passes through the exact same risk gates as a watchlist symbol (blocklist
|
|
59
|
+
// still wins, caps/SL rules unchanged) — the capability widens the CANDIDATE
|
|
60
|
+
// SET for one cycle, never any cap. Off by default; without it the universe
|
|
61
|
+
// is invisible and only manual watchlist pairs are analyzed.
|
|
53
62
|
export const ALLOWED_CAPABILITIES = [
|
|
54
63
|
"websearch",
|
|
55
64
|
"indicators",
|
|
56
65
|
"news",
|
|
66
|
+
"universe_scan",
|
|
57
67
|
];
|
|
58
68
|
export const ok = () => ({ valid: true });
|
|
59
69
|
export const fail = (code, reason) => ({
|
package/dist/agent/version.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const COINRITHM_API: {
|
|
|
5
5
|
readonly kind: "coinrithm-agent-api";
|
|
6
6
|
readonly baseUrl: "https://api.coinrithm.com";
|
|
7
7
|
readonly mcpUrl: "https://mcp.coinrithm.com/mcp";
|
|
8
|
-
readonly openapiVersion: "1.
|
|
8
|
+
readonly openapiVersion: "1.7.0";
|
|
9
9
|
readonly mcpPackage: "@coinrithm/mcp-trading";
|
|
10
10
|
readonly mcpVersion: string;
|
|
11
11
|
};
|
package/dist/agent/version.js
CHANGED
|
@@ -20,7 +20,7 @@ export const COINRITHM_API = {
|
|
|
20
20
|
mcpUrl: "https://mcp.coinrithm.com/mcp",
|
|
21
21
|
// The API CONTRACT version (openapi.yaml info.version). Versioned independently
|
|
22
22
|
// from the npm package below — hand-bump this when the OpenAPI contract changes.
|
|
23
|
-
openapiVersion: "1.
|
|
23
|
+
openapiVersion: "1.7.0",
|
|
24
24
|
mcpPackage: "@coinrithm/mcp-trading",
|
|
25
25
|
mcpVersion: PACKAGE_VERSION,
|
|
26
26
|
};
|
package/dist/client.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ export declare class CoinRithmClient {
|
|
|
83
83
|
}): Promise<ApiResult>;
|
|
84
84
|
getPublicPmCanonicalDetail(key: string): Promise<ApiResult>;
|
|
85
85
|
getPublicPmVolumeHistory(): Promise<ApiResult>;
|
|
86
|
+
getPublicCryptoMovers(direction: "gainers" | "losers", limit?: number): Promise<ApiResult>;
|
|
86
87
|
whoami(apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
|
|
87
88
|
getPortfolio(query?: {
|
|
88
89
|
fiat?: string;
|
package/dist/client.js
CHANGED
|
@@ -236,6 +236,16 @@ export class CoinRithmClient {
|
|
|
236
236
|
getPublicPmVolumeHistory() {
|
|
237
237
|
return this.publicRequest("/api/prediction-markets/volume-history");
|
|
238
238
|
}
|
|
239
|
+
// ---- public crypto data (no key required) ----
|
|
240
|
+
// Top 24h movers across the tracked coin universe (user feature request,
|
|
241
|
+
// 2026-08-18: agents previously could only analyze manually-added pairs).
|
|
242
|
+
// Backend caps limit at 100; rows are {ucid, symbol, name, slug, change24h,
|
|
243
|
+
// currentPrice} ordered by 24h change.
|
|
244
|
+
getPublicCryptoMovers(direction, limit) {
|
|
245
|
+
return this.publicRequest(direction === "losers"
|
|
246
|
+
? "/api/coins/top-losers"
|
|
247
|
+
: "/api/coins/top-gainers", { limit });
|
|
248
|
+
}
|
|
239
249
|
// Every method takes an optional trailing `apiKey` (the per-request key for
|
|
240
250
|
// the multi-user HTTP path). When omitted, the constructor key (stdio) is used.
|
|
241
251
|
// ---- reads (scope: read) ----
|
package/dist/http.js
CHANGED
|
@@ -79,6 +79,27 @@ async function main() {
|
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
+
// robots.txt for THIS host. robots.txt is per-HOST, so www.coinrithm.com's
|
|
83
|
+
// file never governed mcp.coinrithm.com — a separate origin that had no
|
|
84
|
+
// route of its own. The origin 404'd and Cloudflare answered with its
|
|
85
|
+
// managed content-signals boilerplate: 1,248 bytes of comments carrying ZERO
|
|
86
|
+
// User-agent/Disallow/Allow lines, which a crawler reads as "crawl
|
|
87
|
+
// everything". That is the identical failure that cost api.coinrithm.com
|
|
88
|
+
// 15.4% of the site's 90-day crawl budget (4,468 of 29,100 GSC requests)
|
|
89
|
+
// before it was closed on 2026-08-20.
|
|
90
|
+
//
|
|
91
|
+
// Nothing here is indexable: GET / is a JSON service descriptor, GET /mcp is
|
|
92
|
+
// a 405, and the real surface is POST-only streamable HTTP. The human-facing
|
|
93
|
+
// documentation crawlers should index lives on www.coinrithm.com
|
|
94
|
+
// (/en/agentic-trading, /en/prediction-markets/api), which links here.
|
|
95
|
+
//
|
|
96
|
+
// SAFE FOR MCP CLIENTS AND REGISTRIES: robots.txt is advisory to CRAWLERS
|
|
97
|
+
// only. MCP clients, Smithery and the MCP registry POST /mcp or GET /healthz
|
|
98
|
+
// directly and never consult robots.txt, so this cannot gate discovery,
|
|
99
|
+
// initialization or tool listing. Do not "fix" a registry problem here.
|
|
100
|
+
app.get("/robots.txt", (_req, res) => {
|
|
101
|
+
res.type("text/plain").send("User-agent: *\nDisallow: /\n");
|
|
102
|
+
});
|
|
82
103
|
app.get("/mcp", (_req, res) => {
|
|
83
104
|
res.status(405).json({
|
|
84
105
|
error: "method_not_allowed",
|
package/dist/tools.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function compactPublicPmEvents(data: unknown): unknown;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function compactPublicPmEvent(data: unknown): unknown;
|
|
16
16
|
export declare function compactPublicPmWhales(data: unknown, limit: number): unknown;
|
|
17
|
+
export declare function compactPublicCryptoMovers(data: unknown): unknown;
|
|
17
18
|
/**
|
|
18
19
|
* Keep cross-venue disagreement clusters small enough for an agent context
|
|
19
20
|
* window: each event is reduced to eventSummary (drops descriptions, images,
|