@coinrithm/mcp-trading 0.3.0 → 0.5.0
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 +54 -1
- package/README.md +36 -9
- package/dist/agent/act.js +8 -1
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +58 -4
- package/dist/agent/client.d.ts +8 -1
- package/dist/agent/client.js +14 -2
- package/dist/agent/decision.d.ts +66 -63
- package/dist/agent/decision.js +95 -24
- package/dist/agent/decisionValidator.js +41 -1
- package/dist/agent/deploymentOverlay.d.ts +22 -0
- package/dist/agent/deploymentOverlay.js +55 -0
- package/dist/agent/gate.d.ts +9 -0
- package/dist/agent/gate.js +114 -0
- package/dist/agent/indicators.js +22 -7
- package/dist/agent/observe.js +201 -18
- package/dist/agent/prompt.d.ts +6 -2
- package/dist/agent/prompt.js +116 -26
- package/dist/agent/providers.d.ts +6 -0
- package/dist/agent/providers.js +89 -12
- package/dist/agent/resolve.js +28 -0
- package/dist/agent/resolvePm.d.ts +14 -0
- package/dist/agent/resolvePm.js +69 -0
- package/dist/agent/runner.d.ts +6 -1
- package/dist/agent/runner.js +312 -10
- package/dist/agent/scorecard.d.ts +24 -0
- package/dist/agent/scorecard.js +177 -0
- package/dist/agent/setups.d.ts +3 -0
- package/dist/agent/setups.js +133 -0
- package/dist/agent/skill.d.ts +1 -0
- package/dist/agent/skill.js +21 -3
- package/dist/agent/skillValidator.js +4 -2
- package/dist/agent/state.js +10 -2
- package/dist/agent/templates.js +8 -4
- package/dist/agent/types.d.ts +75 -2
- package/dist/agent/types.js +14 -1
- package/dist/agent/version.d.ts +2 -2
- package/dist/agent/version.js +12 -2
- package/dist/client.d.ts +2 -0
- package/dist/tools.js +28 -5
- package/package.json +1 -1
package/dist/agent/providers.js
CHANGED
|
@@ -5,6 +5,53 @@
|
|
|
5
5
|
// NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
|
|
6
6
|
// endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
|
|
7
7
|
const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
|
8
|
+
// Gemini exposes an OpenAI-compatible surface, so the `gemini` preset hard-wires
|
|
9
|
+
// its hosted endpoint — an agent only needs `{ provider: gemini, name: "gemini-2.0-flash" }`
|
|
10
|
+
// plus a GEMINI_API_KEY. The free tier (no credit card, generous Flash quota) makes
|
|
11
|
+
// it the easiest key a USER can bring for their OWN agent — and a per-user key means
|
|
12
|
+
// each agent draws on its own quota, which is how the fleet actually scales.
|
|
13
|
+
const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
14
|
+
// Cap any single model call so a hung provider can't block an agent forever — but
|
|
15
|
+
// generously. The scheduler runs cycles SEQUENTIALLY per agent (it locks the row
|
|
16
|
+
// while a cycle runs and schedules the next from COMPLETION, not from claim), so a
|
|
17
|
+
// slow call no longer overlaps the next cadence; it just delays it. That lets the
|
|
18
|
+
// cap be long enough for a large model (e.g. a 70B on a busy free key) to finish
|
|
19
|
+
// and ACT, instead of being killed mid-thought and counted as a failure. On a real
|
|
20
|
+
// timeout the call still aborts -> a model failure -> retried next cadence (and the
|
|
21
|
+
// disable threshold is now far more tolerant of the odd flaky cycle).
|
|
22
|
+
// 5 minutes. Sequential scheduling (the scheduler RUN-LOCKS an agent while a cycle
|
|
23
|
+
// runs and reschedules the NEXT from completion) means a slow call never overlaps
|
|
24
|
+
// the next cadence — it just delays it. So a generous timeout lets a busy free 70B
|
|
25
|
+
// finish and ACT instead of being killed mid-thought and counted as a failure
|
|
26
|
+
// (the recurring Leo/70B timeout). A real hang still aborts -> retried next cadence.
|
|
27
|
+
// MUST stay below the scheduler's RUN_LOCK_SECONDS and HEARTBEAT_STALE_MS.
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
29
|
+
// Reasoning models (NVIDIA Nemotron) DEFAULT to emitting a long <think> chain:
|
|
30
|
+
// measured ~30-60s/call and a JSON-leak risk. The documented toggle is a
|
|
31
|
+
// "detailed thinking off" line in the system prompt, which drops them to
|
|
32
|
+
// instruct mode (measured ~3-4s, clean JSON). Apply it automatically for any
|
|
33
|
+
// nemotron model so a per-cadence decision never blows the cadence.
|
|
34
|
+
function applyReasoningToggle(model, system) {
|
|
35
|
+
return /nemotron/i.test(model) ? `detailed thinking off\n\n${system}` : system;
|
|
36
|
+
}
|
|
37
|
+
// fetch with a hard timeout via AbortController. A custom fetchFn (tests) that
|
|
38
|
+
// ignores `signal` still works — the timer just never fires for it.
|
|
39
|
+
async function fetchWithTimeout(fetchFn, url, init, timeoutMs) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
return await fetchFn(url, { ...init, signal: controller.signal });
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function callError(err, timeoutMs) {
|
|
50
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
51
|
+
return `model call timed out after ${timeoutMs}ms`;
|
|
52
|
+
}
|
|
53
|
+
return err instanceof Error ? err.message : String(err);
|
|
54
|
+
}
|
|
8
55
|
function envKey(provider, env) {
|
|
9
56
|
switch (provider) {
|
|
10
57
|
case "anthropic":
|
|
@@ -15,6 +62,8 @@ function envKey(provider, env) {
|
|
|
15
62
|
return env.GROQ_API_KEY;
|
|
16
63
|
case "nvidia":
|
|
17
64
|
return env.NVIDIA_API_KEY ?? env.MODEL_API_KEY;
|
|
65
|
+
case "gemini":
|
|
66
|
+
return env.GEMINI_API_KEY ?? env.MODEL_API_KEY;
|
|
18
67
|
case "openai-compatible":
|
|
19
68
|
return env.MODEL_API_KEY ?? env.OPENAI_API_KEY;
|
|
20
69
|
}
|
|
@@ -27,6 +76,8 @@ function baseUrlFor(provider, configured) {
|
|
|
27
76
|
return "https://api.groq.com/openai/v1";
|
|
28
77
|
case "nvidia":
|
|
29
78
|
return NVIDIA_BASE_URL;
|
|
79
|
+
case "gemini":
|
|
80
|
+
return GEMINI_BASE_URL;
|
|
30
81
|
case "openai-compatible":
|
|
31
82
|
return (configured ?? "").replace(/\/+$/, "");
|
|
32
83
|
case "anthropic":
|
|
@@ -45,8 +96,9 @@ class AnthropicProvider {
|
|
|
45
96
|
this.label = `anthropic/${model}`;
|
|
46
97
|
}
|
|
47
98
|
async decide(input) {
|
|
99
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
48
100
|
try {
|
|
49
|
-
const res = await this.fetchFn
|
|
101
|
+
const res = await fetchWithTimeout(this.fetchFn, "https://api.anthropic.com/v1/messages", {
|
|
50
102
|
method: "POST",
|
|
51
103
|
headers: {
|
|
52
104
|
"x-api-key": this.apiKey,
|
|
@@ -59,15 +111,26 @@ class AnthropicProvider {
|
|
|
59
111
|
system: input.system,
|
|
60
112
|
messages: [{ role: "user", content: input.user }],
|
|
61
113
|
}),
|
|
62
|
-
});
|
|
114
|
+
}, timeoutMs);
|
|
63
115
|
if (!res.ok)
|
|
64
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
119
|
+
// unbounded provider error page must not bloat the ledger row.
|
|
120
|
+
error: `anthropic HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
|
|
121
|
+
};
|
|
65
122
|
const json = (await res.json());
|
|
66
123
|
const text = json.content?.map((c) => c.text ?? "").join("") ?? "";
|
|
67
|
-
|
|
124
|
+
const usage = json.usage
|
|
125
|
+
? {
|
|
126
|
+
promptTokens: json.usage.input_tokens ?? 0,
|
|
127
|
+
completionTokens: json.usage.output_tokens ?? 0,
|
|
128
|
+
}
|
|
129
|
+
: undefined;
|
|
130
|
+
return text ? { ok: true, text, usage } : { ok: false, error: "anthropic returned empty content" };
|
|
68
131
|
}
|
|
69
132
|
catch (err) {
|
|
70
|
-
return { ok: false, error: err
|
|
133
|
+
return { ok: false, error: callError(err, timeoutMs) };
|
|
71
134
|
}
|
|
72
135
|
}
|
|
73
136
|
}
|
|
@@ -85,8 +148,9 @@ class OpenAiCompatProvider {
|
|
|
85
148
|
this.label = `${baseUrl}/${model}`;
|
|
86
149
|
}
|
|
87
150
|
async decide(input) {
|
|
151
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
88
152
|
try {
|
|
89
|
-
const res = await this.fetchFn
|
|
153
|
+
const res = await fetchWithTimeout(this.fetchFn, `${this.baseUrl}/chat/completions`, {
|
|
90
154
|
method: "POST",
|
|
91
155
|
headers: {
|
|
92
156
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -98,19 +162,30 @@ class OpenAiCompatProvider {
|
|
|
98
162
|
max_tokens: input.maxTokens ?? 1024,
|
|
99
163
|
response_format: { type: "json_object" },
|
|
100
164
|
messages: [
|
|
101
|
-
{ role: "system", content: input.system },
|
|
165
|
+
{ role: "system", content: applyReasoningToggle(this.model, input.system) },
|
|
102
166
|
{ role: "user", content: input.user },
|
|
103
167
|
],
|
|
104
168
|
}),
|
|
105
|
-
});
|
|
169
|
+
}, timeoutMs);
|
|
106
170
|
if (!res.ok)
|
|
107
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
174
|
+
// unbounded provider error page must not bloat the ledger row.
|
|
175
|
+
error: `provider HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
|
|
176
|
+
};
|
|
108
177
|
const json = (await res.json());
|
|
109
178
|
const text = json.choices?.[0]?.message?.content ?? "";
|
|
110
|
-
|
|
179
|
+
const usage = json.usage
|
|
180
|
+
? {
|
|
181
|
+
promptTokens: json.usage.prompt_tokens ?? 0,
|
|
182
|
+
completionTokens: json.usage.completion_tokens ?? 0,
|
|
183
|
+
}
|
|
184
|
+
: undefined;
|
|
185
|
+
return text ? { ok: true, text, usage } : { ok: false, error: "provider returned empty content" };
|
|
111
186
|
}
|
|
112
187
|
catch (err) {
|
|
113
|
-
return { ok: false, error: err
|
|
188
|
+
return { ok: false, error: callError(err, timeoutMs) };
|
|
114
189
|
}
|
|
115
190
|
}
|
|
116
191
|
}
|
|
@@ -130,7 +205,9 @@ export function selectProvider(spec, env, fetchFn = fetch) {
|
|
|
130
205
|
? "GROQ_API_KEY"
|
|
131
206
|
: provider === "nvidia"
|
|
132
207
|
? "NVIDIA_API_KEY"
|
|
133
|
-
:
|
|
208
|
+
: provider === "gemini"
|
|
209
|
+
? "GEMINI_API_KEY"
|
|
210
|
+
: "OPENAI_API_KEY / MODEL_API_KEY";
|
|
134
211
|
throw new Error(`missing model API key: set ${varName} in the environment (never in an agent file)`);
|
|
135
212
|
}
|
|
136
213
|
if (provider === "anthropic")
|
package/dist/agent/resolve.js
CHANGED
|
@@ -48,6 +48,7 @@ const JOURNAL_MAX_LINES = 200;
|
|
|
48
48
|
const JOURNAL_MAX_BYTES = 8_000;
|
|
49
49
|
// Optional prose files (markdown the LLM reads), in assembly order.
|
|
50
50
|
const PROSE_FILES = ["character/thesis.md", "character/persona.md"];
|
|
51
|
+
const FUNCTIONALITY_PIN = "functionality/coinrithm.yaml";
|
|
51
52
|
// Enforced cap field names. sizing.yaml is SOFT guidance and must NOT contain
|
|
52
53
|
// any of these (or a user could think a limit binds when it does not).
|
|
53
54
|
const ENFORCED_FIELD_NAMES = new Set([
|
|
@@ -58,6 +59,7 @@ const ENFORCED_FIELD_NAMES = new Set([
|
|
|
58
59
|
"maxConsecutiveModelFailures",
|
|
59
60
|
"onRateLimitPressure",
|
|
60
61
|
]);
|
|
62
|
+
const SKILL_METADATA_KEYS = new Set(["type", "title", "description", "tags"]);
|
|
61
63
|
// A $ref must be a LOCAL, RELATIVE path inside the agent folder — never a URL,
|
|
62
64
|
// an absolute path, a home/drive path, or a Windows backslash path.
|
|
63
65
|
function refSyntaxIssue(ref) {
|
|
@@ -391,6 +393,13 @@ function resolveDirectory(dir) {
|
|
|
391
393
|
// a skill file may be pure prose (no frontmatter) — treat whole as body.
|
|
392
394
|
body = readFileSync(abs, "utf8");
|
|
393
395
|
}
|
|
396
|
+
for (const f of scanForSecrets(patch)) {
|
|
397
|
+
ctx.issues.push({
|
|
398
|
+
code: "secret_in_frontmatter",
|
|
399
|
+
path: refPath,
|
|
400
|
+
message: `${f} (skill frontmatter is committable metadata — remove secrets)`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
394
403
|
includeOrder.push(name);
|
|
395
404
|
skillProse.push({ source: refPath, text: body });
|
|
396
405
|
applySkillPatch(ctx, rawFrontmatter, patch, refPath);
|
|
@@ -420,6 +429,23 @@ function resolveDirectory(dir) {
|
|
|
420
429
|
});
|
|
421
430
|
}
|
|
422
431
|
}
|
|
432
|
+
// Optional API/tool contract pin. It is locked for reproducibility and stale
|
|
433
|
+
// warnings, but it is not part of AgentSpec and is never sent to the model.
|
|
434
|
+
const functionalityPath = join(dir, FUNCTIONALITY_PIN);
|
|
435
|
+
if (existsSync(functionalityPath)) {
|
|
436
|
+
const abs = safePath(ctx, FUNCTIONALITY_PIN, "functionality pin");
|
|
437
|
+
if (abs) {
|
|
438
|
+
const parsed = parseYamlSafe(ctx, readHashed(ctx, abs), FUNCTIONALITY_PIN);
|
|
439
|
+
for (const f of scanForSecrets(parsed)) {
|
|
440
|
+
ctx.issues.push({
|
|
441
|
+
code: "secret_in_functionality",
|
|
442
|
+
path: FUNCTIONALITY_PIN,
|
|
443
|
+
message: `${f} (the functionality pin is committable metadata — remove secrets)`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
sources.functionality = FUNCTIONALITY_PIN;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
423
449
|
const mergedProse = mergeProseParts(proseParts);
|
|
424
450
|
checkSizing(ctx, rawFrontmatter);
|
|
425
451
|
scanSecrets(ctx, rawFrontmatter, mergedProse);
|
|
@@ -444,6 +470,8 @@ function resolveDirectory(dir) {
|
|
|
444
470
|
// permitted, tighten-only; anything else is rejected (no permission expansion).
|
|
445
471
|
function applySkillPatch(ctx, rawFrontmatter, patch, sourceLabel) {
|
|
446
472
|
for (const key of Object.keys(patch)) {
|
|
473
|
+
if (SKILL_METADATA_KEYS.has(key))
|
|
474
|
+
continue;
|
|
447
475
|
if (key === "risk" || key === "limits") {
|
|
448
476
|
const caps = key === "risk" ? RISK_CAPS : LIMIT_CAPS;
|
|
449
477
|
const base = rawFrontmatter[key] ?? {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PmMarket, ProposedAction } from "./types.js";
|
|
2
|
+
type PmOpen = Extract<ProposedAction, {
|
|
3
|
+
type: "pm_open";
|
|
4
|
+
}>;
|
|
5
|
+
export type PmRefResolution = {
|
|
6
|
+
ok: true;
|
|
7
|
+
action: PmOpen;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
code: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolvePmRef(action: PmOpen, pmMarkets: PmMarket[]): PmRefResolution;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Resolve a short pm ref (pm1…pmN) on a pm_open action back to the canonical
|
|
2
|
+
// {source, slug, outcomeExternalMarketId} triple, using the cycle's discovered
|
|
3
|
+
// pmMarkets. This is the reliability fix for prediction-market trading: small
|
|
4
|
+
// free models (Llama 3.1 8B) reliably copy a 3-char ref but mis-copy the long
|
|
5
|
+
// outcomeExternalMarketId, which previously made every PM open fail closed with
|
|
6
|
+
// pm_market_not_discovered. The prompt now asks the model for a ref; the runner
|
|
7
|
+
// resolves it here BEFORE validation so the validator/act phase still see the
|
|
8
|
+
// full triple they require.
|
|
9
|
+
//
|
|
10
|
+
// Back-compat: a model that sends the full triple (no ref) passes through
|
|
11
|
+
// unchanged. Robustness: a model that drops the ref into one of the id fields
|
|
12
|
+
// (a common 8B mistake) is still detected and resolved.
|
|
13
|
+
const REF_RE = /^pm\d+$/i;
|
|
14
|
+
const trimLower = (v) => typeof v === "string" ? v.trim().toLowerCase() : "";
|
|
15
|
+
// Find a pmN-shaped token the model may have placed in `ref` or — for sloppy
|
|
16
|
+
// small models — in one of the id fields. Real Kalshi/Polymarket ids are long
|
|
17
|
+
// hex/uuid strings and never match /^pm\d+$/, so this can't false-positive on a
|
|
18
|
+
// genuine id.
|
|
19
|
+
function extractRef(action) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
action.ref,
|
|
22
|
+
action.outcomeExternalMarketId,
|
|
23
|
+
action.slug,
|
|
24
|
+
action.source,
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates) {
|
|
27
|
+
const t = trimLower(c);
|
|
28
|
+
if (REF_RE.test(t))
|
|
29
|
+
return t;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function resolvePmRef(action, pmMarkets) {
|
|
34
|
+
const ref = extractRef(action);
|
|
35
|
+
if (ref) {
|
|
36
|
+
const mkt = pmMarkets.find((m) => trimLower(m.ref) === ref);
|
|
37
|
+
if (!mkt) {
|
|
38
|
+
const known = pmMarkets.length
|
|
39
|
+
? `pm1..pm${pmMarkets.length}`
|
|
40
|
+
: "(none discovered this cycle)";
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
code: "pm_ref_unknown",
|
|
44
|
+
reason: `ref ${ref} is not one of this cycle's listed PM markets ${known}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Canonicalise: take the triple from the matched market, drop the ref.
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
action: {
|
|
51
|
+
...action,
|
|
52
|
+
ref: undefined,
|
|
53
|
+
source: mkt.source,
|
|
54
|
+
slug: mkt.slug,
|
|
55
|
+
outcomeExternalMarketId: mkt.outcomeExternalMarketId,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// No ref: require the full back-compat triple. Empty/missing → reject with a
|
|
60
|
+
// clear, model-actionable message rather than a downstream crash.
|
|
61
|
+
if (action.source && action.slug && action.outcomeExternalMarketId) {
|
|
62
|
+
return { ok: true, action };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
code: "pm_ref_missing",
|
|
67
|
+
reason: "pm_open needs a ref (pmN) copied from one observation.pmMarkets entry, or the full source+slug+outcomeExternalMarketId",
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CoinRithmClient } from "./client.js";
|
|
2
2
|
import { Provider } from "./providers.js";
|
|
3
|
-
import { AgentSpec, RunState, CycleResult } from "./types.js";
|
|
3
|
+
import { AgentSpec, RunState, CycleResult, ProposedAction, QuoteEvidence } from "./types.js";
|
|
4
4
|
export interface RunnerDeps {
|
|
5
5
|
client: CoinRithmClient;
|
|
6
6
|
provider: Provider;
|
|
@@ -11,6 +11,11 @@ export interface RunnerDeps {
|
|
|
11
11
|
stateFile?: string;
|
|
12
12
|
log?: (line: string) => void;
|
|
13
13
|
}
|
|
14
|
+
export declare function repairFuturesTakeProfit(action: ProposedAction, quote?: QuoteEvidence): {
|
|
15
|
+
action: ProposedAction;
|
|
16
|
+
repaired: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function rationaleForAction(a: ProposedAction, decisionRationale: string | undefined, perActionSummary: string | undefined, totalActions: number): string | undefined;
|
|
14
19
|
export declare function runCycle(deps: RunnerDeps): Promise<CycleResult>;
|
|
15
20
|
export interface LoopOptions {
|
|
16
21
|
once?: boolean;
|