@coinrithm/mcp-trading 0.2.0 → 0.3.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 +32 -0
- package/README.md +2 -2
- package/dist/agent/act.d.ts +4 -0
- package/dist/agent/act.js +58 -9
- package/dist/agent/capabilityGuard.d.ts +2 -0
- package/dist/agent/capabilityGuard.js +131 -0
- package/dist/agent/cli.d.ts +21 -0
- package/dist/agent/cli.js +28 -6
- package/dist/agent/client.d.ts +107 -0
- package/dist/agent/client.js +26 -0
- package/dist/agent/decision.d.ts +137 -0
- package/dist/agent/decision.js +37 -4
- package/dist/agent/decisionValidator.d.ts +16 -0
- package/dist/agent/decisionValidator.js +118 -10
- package/dist/agent/engine.d.ts +10 -0
- package/dist/agent/engine.js +16 -0
- package/dist/agent/extract.d.ts +4 -0
- package/dist/agent/frontmatter.d.ts +5 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/indicators.d.ts +44 -0
- package/dist/agent/indicators.js +135 -0
- package/dist/agent/manifest.d.ts +15 -0
- package/dist/agent/mergeRules.d.ts +11 -0
- package/dist/agent/observe.d.ts +7 -0
- package/dist/agent/observe.js +142 -8
- package/dist/agent/prompt.d.ts +3 -0
- package/dist/agent/prompt.js +39 -8
- package/dist/agent/providers.d.ts +25 -0
- package/dist/agent/providers.js +10 -1
- package/dist/agent/resolve.d.ts +11 -0
- package/dist/agent/resolve.js +12 -3
- package/dist/agent/runEvidence.d.ts +6 -0
- package/dist/agent/runner.d.ts +19 -0
- package/dist/agent/runner.js +102 -19
- package/dist/agent/skill.d.ts +12 -0
- package/dist/agent/skill.js +6 -2
- package/dist/agent/skillValidator.d.ts +7 -0
- package/dist/agent/state.d.ts +7 -0
- package/dist/agent/state.js +3 -1
- package/dist/agent/strictLint.d.ts +3 -0
- package/dist/agent/strictLint.js +2 -1
- package/dist/agent/templates.d.ts +14 -0
- package/dist/agent/types.d.ts +286 -0
- package/dist/agent/types.js +39 -1
- package/dist/agent/util.d.ts +13 -0
- package/dist/agent/util.js +4 -0
- package/dist/agent/version.d.ts +11 -0
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +162 -0
- package/dist/http.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/tools.d.ts +3 -0
- package/dist/version.d.ts +1 -0
- package/package.json +7 -2
package/dist/agent/observe.js
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
// before any write (polledBeforeWrite=true only after that succeeds). If a
|
|
3
3
|
// required read fails, or no watchlist symbol resolves, the cycle SKIPS writes.
|
|
4
4
|
import { asObj, asArr, asNum, asStr } from "./extract.js";
|
|
5
|
+
import { computeIndicators } from "./indicators.js";
|
|
6
|
+
// Candle granularity feeding the indicators: the 1D range = 5-minute candles
|
|
7
|
+
// (~5-min fresh, ~288 bars — ample for EMA50/RSI14/Bollinger20), which suits the
|
|
8
|
+
// short cadence the hosted house agents run on. Probe-verified 2026-06-17.
|
|
9
|
+
const INDICATOR_RANGE = "1D";
|
|
10
|
+
// Fetch candles for one coin and reduce them to a compact indicator bundle.
|
|
11
|
+
// Tolerant by design: any failure (HTTP error, malformed/sparse candles) returns
|
|
12
|
+
// null so the cycle proceeds with price-only context rather than skipping.
|
|
13
|
+
async function fetchIndicators(client, coinId, trace) {
|
|
14
|
+
const cr = await client.candles(coinId, INDICATOR_RANGE, trace);
|
|
15
|
+
if (!cr.ok)
|
|
16
|
+
return null;
|
|
17
|
+
// Endpoint shape: { candles: [{ t, o, h, l, c, v }] } ascending (oldest first).
|
|
18
|
+
const candles = [];
|
|
19
|
+
for (const raw of asArr(asObj(cr.data).candles)) {
|
|
20
|
+
const c = asObj(raw);
|
|
21
|
+
const open = asNum(c.o);
|
|
22
|
+
const high = asNum(c.h);
|
|
23
|
+
const low = asNum(c.l);
|
|
24
|
+
const close = asNum(c.c);
|
|
25
|
+
if (open == null || high == null || low == null || close == null)
|
|
26
|
+
continue;
|
|
27
|
+
candles.push({ open, high, low, close, volume: asNum(c.v) ?? undefined });
|
|
28
|
+
}
|
|
29
|
+
return computeIndicators(candles);
|
|
30
|
+
}
|
|
5
31
|
function freshnessOf(block) {
|
|
6
32
|
const fr = asObj(block.freshness);
|
|
7
33
|
const status = asStr(fr.status);
|
|
@@ -14,6 +40,9 @@ function emptyObservation(state, scopes = []) {
|
|
|
14
40
|
cashAvailableMusd: null,
|
|
15
41
|
equityMusd: null,
|
|
16
42
|
openPositions: [],
|
|
43
|
+
openOrders: [],
|
|
44
|
+
pmPositions: [],
|
|
45
|
+
pmMarkets: [],
|
|
17
46
|
watch: [],
|
|
18
47
|
syncCursor: state.cursor,
|
|
19
48
|
newClosedTrades: [],
|
|
@@ -23,7 +52,10 @@ function emptyObservation(state, scopes = []) {
|
|
|
23
52
|
export async function observe(client, spec, state, trace) {
|
|
24
53
|
const meR = await client.me(trace);
|
|
25
54
|
if (!meR.ok)
|
|
26
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
observation: emptyObservation(state),
|
|
57
|
+
skip: `me failed (HTTP ${meR.status})`,
|
|
58
|
+
};
|
|
27
59
|
const scopes = asArr(asObj(meR.data).scopes).filter((s) => typeof s === "string");
|
|
28
60
|
const [portR, walletR, posR] = await Promise.all([
|
|
29
61
|
client.portfolio(trace),
|
|
@@ -31,7 +63,10 @@ export async function observe(client, spec, state, trace) {
|
|
|
31
63
|
client.futuresPositions(undefined, trace),
|
|
32
64
|
]);
|
|
33
65
|
if (!portR.ok || !walletR.ok || !posR.ok) {
|
|
34
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
observation: emptyObservation(state, scopes),
|
|
68
|
+
skip: "required reads failed (portfolio/wallet/positions)",
|
|
69
|
+
};
|
|
35
70
|
}
|
|
36
71
|
const usdt = asObj(asObj(walletR.data).usdt);
|
|
37
72
|
const equity = asObj(asObj(portR.data).equity);
|
|
@@ -51,7 +86,11 @@ export async function observe(client, spec, state, trace) {
|
|
|
51
86
|
unrealizedPnlMusd: asNum(p.unrealizedPnlMusd),
|
|
52
87
|
}));
|
|
53
88
|
// Sync poll: /trades since the persisted cursor.
|
|
54
|
-
const tradesR = await client.trades({
|
|
89
|
+
const tradesR = await client.trades({
|
|
90
|
+
venue: "futures",
|
|
91
|
+
updatedSince: state.cursor ?? undefined,
|
|
92
|
+
limit: state.cursor ? undefined : 1,
|
|
93
|
+
}, trace);
|
|
55
94
|
let polledBeforeWrite = false;
|
|
56
95
|
let newClosedTrades = [];
|
|
57
96
|
let syncCursor = state.cursor;
|
|
@@ -66,6 +105,7 @@ export async function observe(client, spec, state, trace) {
|
|
|
66
105
|
// Watchlist market context.
|
|
67
106
|
const watch = [];
|
|
68
107
|
let resolvedAny = false;
|
|
108
|
+
const wantIndicators = spec.capabilities.includes("indicators");
|
|
69
109
|
for (const symbol of spec.risk.watchlist) {
|
|
70
110
|
const rs = await client.resolve(symbol, trace);
|
|
71
111
|
const match = asObj(asObj(rs.data).match);
|
|
@@ -78,7 +118,7 @@ export async function observe(client, spec, state, trace) {
|
|
|
78
118
|
const mk = await client.market(coinId, trace);
|
|
79
119
|
const m = asObj(mk.data);
|
|
80
120
|
const price = asObj(m.price);
|
|
81
|
-
|
|
121
|
+
const entry = {
|
|
82
122
|
symbol,
|
|
83
123
|
coinId,
|
|
84
124
|
name: asStr(match.name),
|
|
@@ -88,7 +128,89 @@ export async function observe(client, spec, state, trace) {
|
|
|
88
128
|
change7d: asNum(price.change7d),
|
|
89
129
|
// Freshness lives under the response's `observation` block.
|
|
90
130
|
freshness: freshnessOf(asObj(m.observation)),
|
|
91
|
-
}
|
|
131
|
+
};
|
|
132
|
+
// `indicators` capability: enrich the observation with computed TA so the
|
|
133
|
+
// model reasons over structure (trend/momentum/volatility/breakout) instead
|
|
134
|
+
// of price + %change alone. Backed by the candles endpoint's shared cache.
|
|
135
|
+
if (wantIndicators) {
|
|
136
|
+
const ind = await fetchIndicators(client, coinId, trace);
|
|
137
|
+
if (ind)
|
|
138
|
+
entry.indicators = ind;
|
|
139
|
+
}
|
|
140
|
+
watch.push(entry);
|
|
141
|
+
}
|
|
142
|
+
// Spot resting orders (for cancel + affordability) — only if spot is enabled.
|
|
143
|
+
const wantSpot = spec.venues.includes("spot");
|
|
144
|
+
const wantPm = spec.venues.includes("pm");
|
|
145
|
+
let openOrders = [];
|
|
146
|
+
if (wantSpot) {
|
|
147
|
+
const ordR = await client.openOrders(undefined, trace);
|
|
148
|
+
if (ordR.ok) {
|
|
149
|
+
const od = asObj(ordR.data);
|
|
150
|
+
openOrders = asArr(od.orders ?? od.openOrders)
|
|
151
|
+
.map(asObj)
|
|
152
|
+
.filter((o) => (asStr(o.status) ?? "open") === "open")
|
|
153
|
+
.map((o) => ({
|
|
154
|
+
id: Number(asNum(o.id) ?? o.id),
|
|
155
|
+
coinId: asStr(o.coinId),
|
|
156
|
+
symbol: asStr(o.symbol),
|
|
157
|
+
side: asStr(o.side),
|
|
158
|
+
orderType: asStr(o.orderType),
|
|
159
|
+
quantity: asNum(o.quantity),
|
|
160
|
+
status: asStr(o.status) ?? "open",
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// PM open positions + discovered quote-ready candidates — only if pm enabled.
|
|
165
|
+
let pmPositions = [];
|
|
166
|
+
let pmMarkets = [];
|
|
167
|
+
if (wantPm) {
|
|
168
|
+
const [pmPosR, pmDiscR] = await Promise.all([
|
|
169
|
+
client.pmPositions(undefined, trace),
|
|
170
|
+
client.discoverPmMarkets({ limit: 8 }, trace),
|
|
171
|
+
]);
|
|
172
|
+
if (pmPosR.ok) {
|
|
173
|
+
pmPositions = asArr(asObj(pmPosR.data).positions)
|
|
174
|
+
.map(asObj)
|
|
175
|
+
.filter((p) => (asStr(p.status) ?? "open") === "open")
|
|
176
|
+
.map((p) => ({
|
|
177
|
+
id: Number(asNum(p.id) ?? p.id),
|
|
178
|
+
source: asStr(p.source),
|
|
179
|
+
slug: asStr(p.slug),
|
|
180
|
+
outcomeExternalMarketId: asStr(p.outcomeExternalMarketId),
|
|
181
|
+
stakeMusd: asNum(p.stakeMusd),
|
|
182
|
+
status: asStr(p.status) ?? "open",
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
if (pmDiscR.ok) {
|
|
186
|
+
const dd = asObj(pmDiscR.data);
|
|
187
|
+
// Real /api/agent/pm/discover payload: { data: [event], pagination, meta }.
|
|
188
|
+
// Each EVENT carries source/slug/title/freshness at the top level and the
|
|
189
|
+
// quoteable id NESTED at outcomes[].externalMarketId — so expand one
|
|
190
|
+
// PmMarket per quoteable outcome. (Tolerant `markets`/`results` and flat
|
|
191
|
+
// `outcomeExternalMarketId` fallbacks kept for older/mocked shapes.)
|
|
192
|
+
pmMarkets = asArr(dd.data ?? dd.markets ?? dd.results)
|
|
193
|
+
.map(asObj)
|
|
194
|
+
.flatMap((ev) => {
|
|
195
|
+
const source = (asStr(ev.source) ?? "").toLowerCase();
|
|
196
|
+
const slug = (asStr(ev.slug) ?? "").toLowerCase();
|
|
197
|
+
const title = asStr(ev.title) ?? asStr(ev.question);
|
|
198
|
+
const freshness = freshnessOf(ev); // freshness is event-level
|
|
199
|
+
const outcomes = asArr(ev.outcomes).map(asObj);
|
|
200
|
+
// A market with no outcomes array still round-trips a flat fallback row.
|
|
201
|
+
const rows = outcomes.length > 0 ? outcomes : [ev];
|
|
202
|
+
return rows.map((o) => ({
|
|
203
|
+
source,
|
|
204
|
+
slug,
|
|
205
|
+
outcomeExternalMarketId: asStr(o.externalMarketId) ??
|
|
206
|
+
asStr(o.outcomeExternalMarketId) ??
|
|
207
|
+
"",
|
|
208
|
+
title,
|
|
209
|
+
freshness,
|
|
210
|
+
}));
|
|
211
|
+
})
|
|
212
|
+
.filter((m) => m.source && m.slug && m.outcomeExternalMarketId);
|
|
213
|
+
}
|
|
92
214
|
}
|
|
93
215
|
const observation = {
|
|
94
216
|
asOf: syncCursor ?? new Date().toISOString(),
|
|
@@ -96,15 +218,27 @@ export async function observe(client, spec, state, trace) {
|
|
|
96
218
|
cashAvailableMusd,
|
|
97
219
|
equityMusd,
|
|
98
220
|
openPositions,
|
|
221
|
+
openOrders,
|
|
222
|
+
pmPositions,
|
|
223
|
+
pmMarkets,
|
|
99
224
|
watch,
|
|
100
225
|
syncCursor,
|
|
101
226
|
newClosedTrades,
|
|
102
227
|
polledBeforeWrite,
|
|
103
228
|
};
|
|
104
|
-
|
|
105
|
-
|
|
229
|
+
// Skip only when there is NOTHING actionable: no coin resolved (futures/spot)
|
|
230
|
+
// AND no PM candidate (pm). A pm-only agent proceeds on its discovered markets.
|
|
231
|
+
if (!resolvedAny && pmMarkets.length === 0) {
|
|
232
|
+
return {
|
|
233
|
+
observation,
|
|
234
|
+
skip: "no watchlist coin resolved and no PM markets available",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
106
237
|
if (spec.sync.requirePollBeforeWrite && !polledBeforeWrite) {
|
|
107
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
observation,
|
|
240
|
+
skip: "poll-before-write required but /trades poll failed",
|
|
241
|
+
};
|
|
108
242
|
}
|
|
109
243
|
return { observation };
|
|
110
244
|
}
|
package/dist/agent/prompt.js
CHANGED
|
@@ -4,24 +4,52 @@
|
|
|
4
4
|
// so the prompt states the caps but never relies on the model to honor them.
|
|
5
5
|
export function buildSystemPrompt(spec, mergedProse) {
|
|
6
6
|
const r = spec.risk;
|
|
7
|
+
const v = spec.venues;
|
|
8
|
+
const actions = [];
|
|
9
|
+
if (v.includes("futures")) {
|
|
10
|
+
actions.push('{"type":"futures_open","symbol","side":"long"|"short","leverage","marginMusd","stopLossPrice","takeProfitPrice","confidence":0..1}', '{"type":"futures_close","positionId","fraction"}', '{"type":"futures_set_sltp","positionId","stopLossPrice","takeProfitPrice"}');
|
|
11
|
+
}
|
|
12
|
+
if (v.includes("spot")) {
|
|
13
|
+
actions.push('{"type":"spot_order","symbol","side":"buy"|"sell","orderType":"market"|"limit"|"stop","quantity","limitPrice","stopPrice","confidence":0..1}', '{"type":"spot_cancel","orderId"}');
|
|
14
|
+
}
|
|
15
|
+
if (v.includes("pm")) {
|
|
16
|
+
actions.push('{"type":"pm_open","source","slug","outcomeExternalMarketId","stakeMusd","confidence":0..1} (ONLY a market from observation.pmMarkets; stakeMusd >= 10)');
|
|
17
|
+
}
|
|
7
18
|
return [
|
|
8
|
-
"You operate a CoinRithm PAPER-TRADING
|
|
19
|
+
"You operate a CoinRithm PAPER-TRADING agent (simulated 50,000 mUSD; not real money, not financial advice).",
|
|
9
20
|
"You only PROPOSE actions as structured JSON. A separate runner re-validates every action against hard caps and executes it; you cannot bypass a cap.",
|
|
10
21
|
"",
|
|
11
22
|
"## Your strategy (your borders)",
|
|
12
23
|
mergedProse.trim() || "(no strategy prose provided)",
|
|
13
24
|
"",
|
|
14
25
|
"## Hard caps the runner enforces (do not exceed; proposing over a cap wastes the cycle)",
|
|
15
|
-
`- venues: ${
|
|
16
|
-
`-
|
|
17
|
-
`-
|
|
26
|
+
`- venues you may act in: ${v.join(", ")}`,
|
|
27
|
+
`- perTradeMarginMusd ${r.perTradeMarginMusd} is the per-trade SIZE cap (futures margin / spot buy notional / PM stake)`,
|
|
28
|
+
`- futures: maxLeverage ${r.maxLeverage}, maxConcurrentPositions ${r.maxConcurrentPositions}, requireStopLoss ${r.requireStopLoss} (long stop below entry, short stop above)`,
|
|
29
|
+
`- watchlist (spot + futures use ONLY these): ${r.watchlist.join(", ")}`,
|
|
30
|
+
...(r.blocklist && r.blocklist.length > 0
|
|
31
|
+
? [
|
|
32
|
+
`- deny-list (NEVER open these, even if on the watchlist): ${r.blocklist.join(", ")}`,
|
|
33
|
+
]
|
|
34
|
+
: []),
|
|
35
|
+
"- prediction markets: pick ONLY a market listed in observation.pmMarkets; minimum stake 10 mUSD",
|
|
18
36
|
`- abstention.minConfidence ${spec.abstention.minConfidence}; a skipped cycle is correct and cheap`,
|
|
37
|
+
...(spec.capabilities.includes("indicators")
|
|
38
|
+
? [
|
|
39
|
+
"",
|
|
40
|
+
"## Signals — each watch entry may carry `indicators` (computed from 5-minute candles)",
|
|
41
|
+
"- rsi14: momentum (>70 overbought, <30 oversold); ema20 & ema50: trend; atr14: volatility (size stops off it); bollinger {upper,mid,lower}; recent20 {high,low}: breakout levels.",
|
|
42
|
+
"- boolean reads: aboveEma20, ema20AboveEma50 (uptrend when both true), brokeRecentHigh (breakout), brokeRecentLow (breakdown).",
|
|
43
|
+
"- a null field = not enough data; ignore it. These INFORM your decision; they never widen a cap.",
|
|
44
|
+
]
|
|
45
|
+
: []),
|
|
19
46
|
"",
|
|
20
47
|
"## Output contract — return ONLY this JSON object, nothing else:",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
48
|
+
'{"decision":"skip"|"act","confidence":0..1,"reason":"short","actions":[]}',
|
|
49
|
+
"Each action is one of:",
|
|
50
|
+
...actions.map((a) => `- ${a}`),
|
|
51
|
+
`Set each opening action's "confidence" (0..1) to your honest conviction — the runner REJECTS any open below abstention.minConfidence (${spec.abstention.minConfidence}). The decision-level "confidence" is the fallback when an action omits its own.`,
|
|
52
|
+
"Prefer skip when the signal is weak or data is stale.",
|
|
25
53
|
].join("\n");
|
|
26
54
|
}
|
|
27
55
|
export function buildUserPrompt(obs) {
|
|
@@ -34,6 +62,9 @@ export function buildUserPrompt(obs) {
|
|
|
34
62
|
cashAvailableMusd: obs.cashAvailableMusd,
|
|
35
63
|
equityMusd: obs.equityMusd,
|
|
36
64
|
openPositions: obs.openPositions,
|
|
65
|
+
openOrders: obs.openOrders,
|
|
66
|
+
pmPositions: obs.pmPositions,
|
|
67
|
+
pmMarkets: obs.pmMarkets,
|
|
37
68
|
watch: obs.watch,
|
|
38
69
|
newClosedTrades: obs.newClosedTrades,
|
|
39
70
|
polledBeforeWrite: obs.polledBeforeWrite,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AgentSpec } from "./types.js";
|
|
2
|
+
export interface DecideInput {
|
|
3
|
+
system: string;
|
|
4
|
+
user: string;
|
|
5
|
+
maxTokens?: number;
|
|
6
|
+
}
|
|
7
|
+
export type DecideResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
export interface Provider {
|
|
15
|
+
label: string;
|
|
16
|
+
decide(input: DecideInput): Promise<DecideResult>;
|
|
17
|
+
}
|
|
18
|
+
export interface ProviderEnv {
|
|
19
|
+
ANTHROPIC_API_KEY?: string;
|
|
20
|
+
OPENAI_API_KEY?: string;
|
|
21
|
+
GROQ_API_KEY?: string;
|
|
22
|
+
NVIDIA_API_KEY?: string;
|
|
23
|
+
MODEL_API_KEY?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function selectProvider(spec: AgentSpec, env: ProviderEnv, fetchFn?: typeof fetch): Provider;
|
package/dist/agent/providers.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// never from an agent file. One call returns one chunk of text that must be a
|
|
3
3
|
// single structured-JSON decision (parsed in decision.ts). No free-form tool
|
|
4
4
|
// execution — the model only proposes; the runner disposes.
|
|
5
|
+
// NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
|
|
6
|
+
// endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
|
|
7
|
+
const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
|
5
8
|
function envKey(provider, env) {
|
|
6
9
|
switch (provider) {
|
|
7
10
|
case "anthropic":
|
|
@@ -10,6 +13,8 @@ function envKey(provider, env) {
|
|
|
10
13
|
return env.OPENAI_API_KEY;
|
|
11
14
|
case "groq":
|
|
12
15
|
return env.GROQ_API_KEY;
|
|
16
|
+
case "nvidia":
|
|
17
|
+
return env.NVIDIA_API_KEY ?? env.MODEL_API_KEY;
|
|
13
18
|
case "openai-compatible":
|
|
14
19
|
return env.MODEL_API_KEY ?? env.OPENAI_API_KEY;
|
|
15
20
|
}
|
|
@@ -20,6 +25,8 @@ function baseUrlFor(provider, configured) {
|
|
|
20
25
|
return "https://api.openai.com/v1";
|
|
21
26
|
case "groq":
|
|
22
27
|
return "https://api.groq.com/openai/v1";
|
|
28
|
+
case "nvidia":
|
|
29
|
+
return NVIDIA_BASE_URL;
|
|
23
30
|
case "openai-compatible":
|
|
24
31
|
return (configured ?? "").replace(/\/+$/, "");
|
|
25
32
|
case "anthropic":
|
|
@@ -121,7 +128,9 @@ export function selectProvider(spec, env, fetchFn = fetch) {
|
|
|
121
128
|
? "ANTHROPIC_API_KEY"
|
|
122
129
|
: provider === "groq"
|
|
123
130
|
? "GROQ_API_KEY"
|
|
124
|
-
:
|
|
131
|
+
: provider === "nvidia"
|
|
132
|
+
? "NVIDIA_API_KEY"
|
|
133
|
+
: "OPENAI_API_KEY / MODEL_API_KEY";
|
|
125
134
|
throw new Error(`missing model API key: set ${varName} in the environment (never in an agent file)`);
|
|
126
135
|
}
|
|
127
136
|
if (provider === "anthropic")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolvedAgent, ResolveIssue } from "./types.js";
|
|
2
|
+
export declare class ResolveError extends Error {
|
|
3
|
+
issues: ResolveIssue[];
|
|
4
|
+
constructor(issues: ResolveIssue[]);
|
|
5
|
+
}
|
|
6
|
+
export declare function isSkillProseSource(source: string): boolean;
|
|
7
|
+
export declare function mergeProseParts(parts: Array<{
|
|
8
|
+
source: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}>): string;
|
|
11
|
+
export declare function resolveAgent(inputPath: string): ResolvedAgent;
|
package/dist/agent/resolve.js
CHANGED
|
@@ -420,9 +420,7 @@ function resolveDirectory(dir) {
|
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
-
const mergedProse = proseParts
|
|
424
|
-
.map((p) => `<!-- ${p.source} -->\n${p.text.trim()}`)
|
|
425
|
-
.join("\n\n");
|
|
423
|
+
const mergedProse = mergeProseParts(proseParts);
|
|
426
424
|
checkSizing(ctx, rawFrontmatter);
|
|
427
425
|
scanSecrets(ctx, rawFrontmatter, mergedProse);
|
|
428
426
|
if (ctx.issues.length)
|
|
@@ -472,6 +470,17 @@ function applySkillPatch(ctx, rawFrontmatter, patch, sourceLabel) {
|
|
|
472
470
|
}
|
|
473
471
|
}
|
|
474
472
|
// ── entry point ──────────────────────────────────────────────────────────────
|
|
473
|
+
// Is this prose part a tactic skill? (Used by the skills ablation kill-switch to
|
|
474
|
+
// drop skill bodies from the run-time prompt without touching the resolver.)
|
|
475
|
+
export function isSkillProseSource(source) {
|
|
476
|
+
return source.startsWith("character/skills/");
|
|
477
|
+
}
|
|
478
|
+
// The canonical prose assembly: each part labelled with its source, joined by a
|
|
479
|
+
// blank line. The resolver uses this for mergedProse; the run path reuses it to
|
|
480
|
+
// re-assemble a skills-ablated prompt deterministically.
|
|
481
|
+
export function mergeProseParts(parts) {
|
|
482
|
+
return parts.map((p) => `<!-- ${p.source} -->\n${p.text.trim()}`).join("\n\n");
|
|
483
|
+
}
|
|
475
484
|
export function resolveAgent(inputPath) {
|
|
476
485
|
const abs = resolvePath(inputPath);
|
|
477
486
|
if (!existsSync(abs)) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CoinRithmClient } from "./client.js";
|
|
2
|
+
import { AgentSpec, AgentTrace } from "./types.js";
|
|
3
|
+
export declare function makeRunId(spec: AgentSpec): string;
|
|
4
|
+
export declare function makeDecisionId(cycle: number): string;
|
|
5
|
+
export declare function makeTrace(runId: string, decisionId: string, spec: AgentSpec, confidence?: number, rationaleSummary?: string): AgentTrace;
|
|
6
|
+
export declare function exportRunEvidence(client: CoinRithmClient, runId: string): Promise<unknown>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CoinRithmClient } from "./client.js";
|
|
2
|
+
import { Provider } from "./providers.js";
|
|
3
|
+
import { AgentSpec, RunState, CycleResult } from "./types.js";
|
|
4
|
+
export interface RunnerDeps {
|
|
5
|
+
client: CoinRithmClient;
|
|
6
|
+
provider: Provider;
|
|
7
|
+
spec: AgentSpec;
|
|
8
|
+
mergedProse: string;
|
|
9
|
+
state: RunState;
|
|
10
|
+
live: boolean;
|
|
11
|
+
stateFile?: string;
|
|
12
|
+
log?: (line: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function runCycle(deps: RunnerDeps): Promise<CycleResult>;
|
|
15
|
+
export interface LoopOptions {
|
|
16
|
+
once?: boolean;
|
|
17
|
+
maxCycles?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function runLoop(deps: RunnerDeps, opts?: LoopOptions): Promise<CycleResult[]>;
|
package/dist/agent/runner.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// The execution loop: observe -> decide (BYO model) -> validate -> act
|
|
2
|
-
// futures
|
|
3
|
-
// and exports run evidence. The client + provider
|
|
4
|
-
// fully unit-testable with no network
|
|
1
|
+
// The execution loop: observe -> decide (BYO model) -> validate -> act across
|
|
2
|
+
// spot, futures, and prediction markets. Dry-run never writes. Live uses
|
|
3
|
+
// idempotency keys + agentTrace and exports run evidence. The client + provider
|
|
4
|
+
// are injected so the loop is fully unit-testable with no network/model calls.
|
|
5
|
+
import { spotBuyCost, } from "./types.js";
|
|
5
6
|
import { observe } from "./observe.js";
|
|
6
7
|
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
7
8
|
import { parseDecision } from "./decision.js";
|
|
8
9
|
import { validateAction } from "./decisionValidator.js";
|
|
9
10
|
import { fetchQuote, executeAction } from "./act.js";
|
|
10
11
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
11
|
-
import { rollDay, checkKillSwitch, accrueRealized, saveState } from "./state.js";
|
|
12
|
+
import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
|
|
12
13
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
13
14
|
import { parseCadenceMs, sleep } from "./util.js";
|
|
14
15
|
// A stable idempotency-key component per distinct intent (so a lost response
|
|
@@ -23,7 +24,33 @@ function intentKeyOf(action) {
|
|
|
23
24
|
if (action.type === "futures_set_sltp") {
|
|
24
25
|
return `sltp:${action.positionId}`;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
if (action.type === "spot_order") {
|
|
28
|
+
return `spot:${action.symbol.toUpperCase()}:${action.side}:${action.orderType}:${action.quantity}:${action.limitPrice ?? ""}:${action.stopPrice ?? ""}`;
|
|
29
|
+
}
|
|
30
|
+
if (action.type === "spot_cancel") {
|
|
31
|
+
return `cancel:${action.orderId}`;
|
|
32
|
+
}
|
|
33
|
+
if (action.type === "pm_open") {
|
|
34
|
+
return `pm:${action.source.toLowerCase()}:${action.slug.toLowerCase()}:${action.outcomeExternalMarketId}:${action.stakeMusd}`;
|
|
35
|
+
}
|
|
36
|
+
return "other"; // unreachable: every action type is handled above
|
|
37
|
+
}
|
|
38
|
+
// Estimated cash a successful action consumes (for the running-cash guard):
|
|
39
|
+
// futures margin, spot buy notional, or a PM stake. Closes/cancels/sells free
|
|
40
|
+
// cash or are neutral, so they consume nothing here. Spot buys use the SAME
|
|
41
|
+
// `spotBuyCost` helper the validator gates on, so the gate and this decrement
|
|
42
|
+
// never diverge. The `?? 0` is unreachable for an EXECUTED buy: the validator
|
|
43
|
+
// fails closed (missing_quote_price) on any buy whose cost can't be sized, so
|
|
44
|
+
// nothing with an undefined cost ever reaches execution to be decremented.
|
|
45
|
+
function cashConsumed(action, quote) {
|
|
46
|
+
if (action.type === "futures_open")
|
|
47
|
+
return action.marginMusd;
|
|
48
|
+
if (action.type === "pm_open")
|
|
49
|
+
return action.stakeMusd;
|
|
50
|
+
if (action.type === "spot_order" && action.side === "buy") {
|
|
51
|
+
return spotBuyCost(action, quote) ?? 0;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
27
54
|
}
|
|
28
55
|
export async function runCycle(deps) {
|
|
29
56
|
const { client, provider, spec, mergedProse, state, live, stateFile } = deps;
|
|
@@ -37,7 +64,13 @@ export async function runCycle(deps) {
|
|
|
37
64
|
state.disabledReason = tripped;
|
|
38
65
|
saveState(stateFile, state);
|
|
39
66
|
log(`disabled: ${tripped}`);
|
|
40
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
decision: "skip",
|
|
69
|
+
planned: [],
|
|
70
|
+
disabled: true,
|
|
71
|
+
disabledReason: tripped,
|
|
72
|
+
live,
|
|
73
|
+
};
|
|
41
74
|
}
|
|
42
75
|
const runId = state.runId;
|
|
43
76
|
const decisionId = makeDecisionId(state.cyclesRun);
|
|
@@ -55,12 +88,19 @@ export async function runCycle(deps) {
|
|
|
55
88
|
// not only realized losses.
|
|
56
89
|
const unrealized = observation.openPositions.reduce((s, p) => s + (p.unrealizedPnlMusd ?? 0), 0);
|
|
57
90
|
if (spec.killSwitch.maxDrawdownMusd > 0 &&
|
|
58
|
-
state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >=
|
|
91
|
+
state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >=
|
|
92
|
+
spec.killSwitch.maxDrawdownMusd) {
|
|
59
93
|
state.disabled = true;
|
|
60
94
|
state.disabledReason = `equity drawdown >= ${spec.killSwitch.maxDrawdownMusd}`;
|
|
61
95
|
saveState(stateFile, state);
|
|
62
96
|
log(`disabled: ${state.disabledReason}`);
|
|
63
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
decision: "skip",
|
|
99
|
+
planned: [],
|
|
100
|
+
disabled: true,
|
|
101
|
+
disabledReason: state.disabledReason,
|
|
102
|
+
live,
|
|
103
|
+
};
|
|
64
104
|
}
|
|
65
105
|
if (obs.skip) {
|
|
66
106
|
state.consecutiveRejectCycles += 1;
|
|
@@ -77,14 +117,26 @@ export async function runCycle(deps) {
|
|
|
77
117
|
state.consecutiveModelFailures += 1;
|
|
78
118
|
saveState(stateFile, state);
|
|
79
119
|
log(`model error: ${res.error}`);
|
|
80
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
decision: "skip",
|
|
122
|
+
skipReason: `model error: ${res.error}`,
|
|
123
|
+
planned: [],
|
|
124
|
+
modelFailed: true,
|
|
125
|
+
live,
|
|
126
|
+
};
|
|
81
127
|
}
|
|
82
128
|
const parsed = parseDecision(res.text);
|
|
83
129
|
if (!parsed.ok) {
|
|
84
130
|
state.consecutiveModelFailures += 1;
|
|
85
131
|
saveState(stateFile, state);
|
|
86
132
|
log(`model output invalid: ${parsed.error}`);
|
|
87
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
decision: "skip",
|
|
135
|
+
skipReason: `model output invalid: ${parsed.error}`,
|
|
136
|
+
planned: [],
|
|
137
|
+
modelFailed: true,
|
|
138
|
+
live,
|
|
139
|
+
};
|
|
88
140
|
}
|
|
89
141
|
state.consecutiveModelFailures = 0;
|
|
90
142
|
const decision = parsed.decision;
|
|
@@ -92,7 +144,12 @@ export async function runCycle(deps) {
|
|
|
92
144
|
state.consecutiveRejectCycles += 1;
|
|
93
145
|
saveState(stateFile, state);
|
|
94
146
|
log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
|
|
95
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
decision: "skip",
|
|
149
|
+
skipReason: decision.reason ?? "model chose skip",
|
|
150
|
+
planned: [],
|
|
151
|
+
live,
|
|
152
|
+
};
|
|
96
153
|
}
|
|
97
154
|
// VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
|
|
98
155
|
const planned = [];
|
|
@@ -105,6 +162,7 @@ export async function runCycle(deps) {
|
|
|
105
162
|
let cashAvailableMusd = observation.cashAvailableMusd;
|
|
106
163
|
const realizedLossTodayMusd = Math.max(0, -state.realizedPnlTodayMusd);
|
|
107
164
|
const targetedPositionIds = [];
|
|
165
|
+
const targetedOrderIds = [];
|
|
108
166
|
let anyAccepted = false;
|
|
109
167
|
let anyExecuted = false;
|
|
110
168
|
let anyExecFailed = false;
|
|
@@ -112,6 +170,10 @@ export async function runCycle(deps) {
|
|
|
112
170
|
const quote = await fetchQuote(client, action, observation, baseTrace);
|
|
113
171
|
const ctx = {
|
|
114
172
|
spec,
|
|
173
|
+
// Inherit the decision-level confidence so the per-action abstention gate
|
|
174
|
+
// doesn't reject a model that reports conviction on the decision (the
|
|
175
|
+
// output contract) rather than on each action.
|
|
176
|
+
decisionConfidence: decision.confidence,
|
|
115
177
|
observation,
|
|
116
178
|
quote,
|
|
117
179
|
writesThisCycle,
|
|
@@ -121,10 +183,17 @@ export async function runCycle(deps) {
|
|
|
121
183
|
openMarginMusd,
|
|
122
184
|
realizedLossTodayMusd,
|
|
123
185
|
targetedPositionIds,
|
|
186
|
+
targetedOrderIds,
|
|
124
187
|
};
|
|
125
188
|
const v = validateAction(action, ctx);
|
|
126
189
|
if (!v.valid) {
|
|
127
|
-
planned.push({
|
|
190
|
+
planned.push({
|
|
191
|
+
action,
|
|
192
|
+
accepted: false,
|
|
193
|
+
code: v.code,
|
|
194
|
+
reason: v.reason,
|
|
195
|
+
quote,
|
|
196
|
+
});
|
|
128
197
|
log(`reject ${action.type}: ${v.code} (${v.reason})`);
|
|
129
198
|
continue;
|
|
130
199
|
}
|
|
@@ -132,6 +201,9 @@ export async function runCycle(deps) {
|
|
|
132
201
|
if (action.type === "futures_close" || action.type === "futures_set_sltp") {
|
|
133
202
|
targetedPositionIds.push(action.positionId);
|
|
134
203
|
}
|
|
204
|
+
if (action.type === "spot_cancel") {
|
|
205
|
+
targetedOrderIds.push(action.orderId);
|
|
206
|
+
}
|
|
135
207
|
if (!live) {
|
|
136
208
|
planned.push({ action, accepted: true, quote, executed: false });
|
|
137
209
|
log(`DRY-RUN: would ${action.type}`);
|
|
@@ -143,9 +215,15 @@ export async function runCycle(deps) {
|
|
|
143
215
|
const seq = state.intentSeq[intentKey] ?? 0;
|
|
144
216
|
const idem = `${runId}:${intentKey}:${seq}`;
|
|
145
217
|
const meta = action;
|
|
146
|
-
const trace = makeTrace(runId, decisionId, spec, meta.confidence, meta.rationaleSummary);
|
|
218
|
+
const trace = makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence, meta.rationaleSummary);
|
|
147
219
|
const r = await executeAction(client, action, observation, trace, idem);
|
|
148
|
-
planned.push({
|
|
220
|
+
planned.push({
|
|
221
|
+
action,
|
|
222
|
+
accepted: true,
|
|
223
|
+
quote,
|
|
224
|
+
executed: r.ok,
|
|
225
|
+
result: r.data,
|
|
226
|
+
});
|
|
149
227
|
if (r.ok) {
|
|
150
228
|
anyExecuted = true;
|
|
151
229
|
state.intentSeq[intentKey] = seq + 1;
|
|
@@ -154,9 +232,11 @@ export async function runCycle(deps) {
|
|
|
154
232
|
if (action.type === "futures_open") {
|
|
155
233
|
openCount += 1;
|
|
156
234
|
openMarginMusd += action.marginMusd;
|
|
157
|
-
if (cashAvailableMusd != null)
|
|
158
|
-
cashAvailableMusd -= action.marginMusd;
|
|
159
235
|
}
|
|
236
|
+
// Decrement running cash by what this action consumed (futures margin /
|
|
237
|
+
// spot buy notional / PM stake) so a later action this cycle sees it spent.
|
|
238
|
+
if (cashAvailableMusd != null)
|
|
239
|
+
cashAvailableMusd -= cashConsumed(action, quote);
|
|
160
240
|
}
|
|
161
241
|
else {
|
|
162
242
|
anyExecFailed = true;
|
|
@@ -167,8 +247,11 @@ export async function runCycle(deps) {
|
|
|
167
247
|
// live write is not progress, or a persistently failing live agent would
|
|
168
248
|
// never trip the kill-switch.
|
|
169
249
|
const progressed = live ? anyExecuted : anyAccepted;
|
|
170
|
-
state.consecutiveRejectCycles = progressed
|
|
171
|
-
|
|
250
|
+
state.consecutiveRejectCycles = progressed
|
|
251
|
+
? 0
|
|
252
|
+
: state.consecutiveRejectCycles + 1;
|
|
253
|
+
state.consecutiveExecFailures =
|
|
254
|
+
anyExecFailed && !anyExecuted ? state.consecutiveExecFailures + 1 : 0;
|
|
172
255
|
state.rateLimitHits = client.rateLimitHits ?? state.rateLimitHits;
|
|
173
256
|
saveState(stateFile, state);
|
|
174
257
|
if (live && anyExecuted)
|