@coinrithm/mcp-trading 0.2.0 → 0.4.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 +49 -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 +40 -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/scorecard.d.ts +24 -0
- package/dist/agent/scorecard.js +177 -0
- 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
|
@@ -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,9 +429,24 @@ function resolveDirectory(dir) {
|
|
|
420
429
|
});
|
|
421
430
|
}
|
|
422
431
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
}
|
|
449
|
+
const mergedProse = mergeProseParts(proseParts);
|
|
426
450
|
checkSizing(ctx, rawFrontmatter);
|
|
427
451
|
scanSecrets(ctx, rawFrontmatter, mergedProse);
|
|
428
452
|
if (ctx.issues.length)
|
|
@@ -446,6 +470,8 @@ function resolveDirectory(dir) {
|
|
|
446
470
|
// permitted, tighten-only; anything else is rejected (no permission expansion).
|
|
447
471
|
function applySkillPatch(ctx, rawFrontmatter, patch, sourceLabel) {
|
|
448
472
|
for (const key of Object.keys(patch)) {
|
|
473
|
+
if (SKILL_METADATA_KEYS.has(key))
|
|
474
|
+
continue;
|
|
449
475
|
if (key === "risk" || key === "limits") {
|
|
450
476
|
const caps = key === "risk" ? RISK_CAPS : LIMIT_CAPS;
|
|
451
477
|
const base = rawFrontmatter[key] ?? {};
|
|
@@ -472,6 +498,17 @@ function applySkillPatch(ctx, rawFrontmatter, patch, sourceLabel) {
|
|
|
472
498
|
}
|
|
473
499
|
}
|
|
474
500
|
// ── entry point ──────────────────────────────────────────────────────────────
|
|
501
|
+
// Is this prose part a tactic skill? (Used by the skills ablation kill-switch to
|
|
502
|
+
// drop skill bodies from the run-time prompt without touching the resolver.)
|
|
503
|
+
export function isSkillProseSource(source) {
|
|
504
|
+
return source.startsWith("character/skills/");
|
|
505
|
+
}
|
|
506
|
+
// The canonical prose assembly: each part labelled with its source, joined by a
|
|
507
|
+
// blank line. The resolver uses this for mergedProse; the run path reuses it to
|
|
508
|
+
// re-assemble a skills-ablated prompt deterministically.
|
|
509
|
+
export function mergeProseParts(parts) {
|
|
510
|
+
return parts.map((p) => `<!-- ${p.source} -->\n${p.text.trim()}`).join("\n\n");
|
|
511
|
+
}
|
|
475
512
|
export function resolveAgent(inputPath) {
|
|
476
513
|
const abs = resolvePath(inputPath);
|
|
477
514
|
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[]>;
|