@coinrithm/mcp-trading 0.7.7 → 0.7.9
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 +178 -0
- package/README.md +86 -69
- package/dist/agent/act.js +19 -6
- package/dist/agent/capitalSizing.d.ts +32 -0
- package/dist/agent/capitalSizing.js +270 -0
- package/dist/agent/client.d.ts +8 -1
- package/dist/agent/client.js +98 -39
- package/dist/agent/decision.d.ts +392 -0
- package/dist/agent/decision.js +177 -0
- package/dist/agent/decisionReceipt.d.ts +48 -0
- package/dist/agent/decisionReceipt.js +619 -0
- package/dist/agent/decisionValidator.d.ts +19 -2
- package/dist/agent/decisionValidator.js +74 -3
- package/dist/agent/engine.d.ts +1 -0
- package/dist/agent/engine.js +1 -0
- package/dist/agent/gate.js +14 -11
- package/dist/agent/observe.js +175 -34
- package/dist/agent/pmContext.d.ts +13 -0
- package/dist/agent/pmContext.js +136 -0
- package/dist/agent/prompt.d.ts +11 -1
- package/dist/agent/prompt.js +135 -7
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +41 -3
- package/dist/agent/providers.d.ts +2 -1
- package/dist/agent/providers.js +222 -86
- package/dist/agent/resolve.js +1 -0
- package/dist/agent/runner.d.ts +4 -1
- package/dist/agent/runner.js +369 -38
- package/dist/agent/scorecard.js +7 -1
- package/dist/agent/skill.js +17 -0
- package/dist/agent/skillValidator.d.ts +1 -0
- package/dist/agent/skillValidator.js +56 -0
- package/dist/agent/state.js +23 -2
- package/dist/agent/strictLint.js +19 -0
- package/dist/agent/templates.js +4 -0
- package/dist/agent/thesis.d.ts +40 -0
- package/dist/agent/thesis.js +319 -0
- package/dist/agent/types.d.ts +127 -0
- package/dist/client.js +3 -2
- package/dist/http.d.ts +7 -1
- package/dist/http.js +36 -16
- package/dist/httpCompletion.d.ts +33 -0
- package/dist/httpCompletion.js +220 -0
- package/dist/retryAfter.d.ts +1 -0
- package/dist/retryAfter.js +16 -0
- package/dist/tools.js +19 -1
- package/package.json +4 -2
package/dist/agent/providers.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { retryAfterSeconds } from "../retryAfter.js";
|
|
6
|
+
import { chatShapeFor, buildChatBody, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
|
|
6
7
|
// NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
|
|
7
8
|
// endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
|
|
8
9
|
const NVIDIA_BASE_URL = CAP_NVIDIA_BASE_URL;
|
|
@@ -27,16 +28,27 @@ const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai
|
|
|
27
28
|
// (the recurring Leo/70B timeout). A real hang still aborts -> retried next cadence.
|
|
28
29
|
// MUST stay below the scheduler's RUN_LOCK_SECONDS and HEARTBEAT_STALE_MS.
|
|
29
30
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
31
|
+
const RETRYABLE_SERVER_STATUSES = new Set([500, 502, 503, 504]);
|
|
30
32
|
// Per-route request quirks (reasoning toggles, token param, temperature) live
|
|
31
33
|
// in the capability table — providerCapabilities.ts is the single source; this
|
|
32
34
|
// module only assembles and sends.
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
35
|
+
// One deadline covers both headers AND response-body consumption. fetch resolves
|
|
36
|
+
// at headers, so clearing a fetch-only timer there leaves text/json unbounded.
|
|
37
|
+
// Abort native I/O and race the deadline as well: an injected implementation that
|
|
38
|
+
// ignores AbortSignal must still release the caller rather than its run lock.
|
|
39
|
+
async function withProviderTimeout(timeoutMs, operation) {
|
|
36
40
|
const controller = new AbortController();
|
|
37
|
-
|
|
41
|
+
let timer;
|
|
42
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
reject(Object.assign(new Error("model deadline exceeded"), {
|
|
45
|
+
name: "AbortError",
|
|
46
|
+
}));
|
|
47
|
+
controller.abort();
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
});
|
|
38
50
|
try {
|
|
39
|
-
return await
|
|
51
|
+
return await Promise.race([operation(controller.signal), deadline]);
|
|
40
52
|
}
|
|
41
53
|
finally {
|
|
42
54
|
clearTimeout(timer);
|
|
@@ -48,22 +60,24 @@ function callError(err, timeoutMs) {
|
|
|
48
60
|
}
|
|
49
61
|
return err instanceof Error ? err.message : String(err);
|
|
50
62
|
}
|
|
63
|
+
// Error text reaches retained cycle evidence. Redact before truncating so a
|
|
64
|
+
// credential crossing the size boundary cannot leave a partial key behind.
|
|
65
|
+
function safeProviderError(text, apiKey) {
|
|
66
|
+
return (apiKey ? text.split(apiKey).join("[redacted]") : text)
|
|
67
|
+
.replace(/Bearer\s+[^\s"'<>\\]+/gi, "Bearer [redacted]")
|
|
68
|
+
.replace(/(?:nvapi-|crk_live_|sk-|sk_live_|ghp_|AIza)[A-Za-z0-9_-]+/g, "[redacted]")
|
|
69
|
+
.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted]")
|
|
70
|
+
.replace(/((?:api[_-]?key|access[_-]?token|password|secret)["']?\s*[:=]\s*["']?)[^\s"',}&<>]+/gi, "$1[redacted]")
|
|
71
|
+
.slice(0, 2000);
|
|
72
|
+
}
|
|
51
73
|
// Parse a Retry-After header (delta-seconds or HTTP-date) into ms, capped at
|
|
52
74
|
// one hour — a provider asking for more is treated as "an hour, then re-probe".
|
|
53
75
|
const RETRY_AFTER_CAP_MS = 3_600_000;
|
|
54
76
|
function retryAfterMs(res) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (Number.isFinite(secs) && secs >= 0) {
|
|
60
|
-
return Math.min(Math.round(secs * 1000), RETRY_AFTER_CAP_MS);
|
|
61
|
-
}
|
|
62
|
-
const at = Date.parse(raw);
|
|
63
|
-
if (!Number.isFinite(at))
|
|
64
|
-
return undefined;
|
|
65
|
-
const ms = at - Date.now();
|
|
66
|
-
return ms > 0 ? Math.min(ms, RETRY_AFTER_CAP_MS) : 0;
|
|
77
|
+
const seconds = retryAfterSeconds(res.headers.get("retry-after"));
|
|
78
|
+
return seconds === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: Math.min(Math.round(seconds * 1000), RETRY_AFTER_CAP_MS);
|
|
67
81
|
}
|
|
68
82
|
function envKey(provider, env) {
|
|
69
83
|
switch (provider) {
|
|
@@ -131,44 +145,61 @@ class AnthropicProvider {
|
|
|
131
145
|
}
|
|
132
146
|
async decide(input) {
|
|
133
147
|
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
148
|
+
let failureResponse;
|
|
134
149
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
? {
|
|
162
|
-
promptTokens: json.usage.input_tokens ?? 0,
|
|
163
|
-
completionTokens: json.usage.output_tokens ?? 0,
|
|
150
|
+
return await withProviderTimeout(timeoutMs, async (signal) => {
|
|
151
|
+
const res = await this.fetchFn("https://api.anthropic.com/v1/messages", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
signal,
|
|
154
|
+
headers: {
|
|
155
|
+
"x-api-key": this.apiKey,
|
|
156
|
+
"anthropic-version": "2023-06-01",
|
|
157
|
+
"content-type": "application/json",
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
model: this.model,
|
|
161
|
+
max_tokens: input.maxTokens ?? 1024,
|
|
162
|
+
system: input.system,
|
|
163
|
+
messages: [{ role: "user", content: input.user }],
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
failureResponse = res;
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
171
|
+
// unbounded provider error page must not bloat the ledger row.
|
|
172
|
+
error: `anthropic HTTP ${res.status}: ${safeProviderError(await res.text(), this.apiKey)}`,
|
|
173
|
+
status: res.status,
|
|
174
|
+
retryAfterMs: retryAfterMs(res),
|
|
175
|
+
};
|
|
164
176
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
const json = (await res.json());
|
|
178
|
+
const text = json.content?.map((c) => c.text ?? "").join("") ?? "";
|
|
179
|
+
const usage = json.usage
|
|
180
|
+
? {
|
|
181
|
+
promptTokens: json.usage.input_tokens ?? 0,
|
|
182
|
+
completionTokens: json.usage.output_tokens ?? 0,
|
|
183
|
+
}
|
|
184
|
+
: undefined;
|
|
185
|
+
return text
|
|
186
|
+
? { ok: true, text, usage }
|
|
187
|
+
: { ok: false, error: "anthropic returned empty content" };
|
|
188
|
+
});
|
|
169
189
|
}
|
|
170
190
|
catch (err) {
|
|
171
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: safeProviderError(failureResponse
|
|
194
|
+
? `anthropic HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
|
|
195
|
+
: callError(err, timeoutMs), this.apiKey),
|
|
196
|
+
...(failureResponse
|
|
197
|
+
? {
|
|
198
|
+
status: failureResponse.status,
|
|
199
|
+
retryAfterMs: retryAfterMs(failureResponse),
|
|
200
|
+
}
|
|
201
|
+
: {}),
|
|
202
|
+
};
|
|
172
203
|
}
|
|
173
204
|
}
|
|
174
205
|
}
|
|
@@ -190,46 +221,148 @@ class OpenAiCompatProvider {
|
|
|
190
221
|
async decide(input) {
|
|
191
222
|
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
192
223
|
const shape = chatShapeFor(this.provider, this.model, this.baseUrl);
|
|
224
|
+
let failureResponse;
|
|
193
225
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
return await withProviderTimeout(timeoutMs, async (signal) => {
|
|
227
|
+
const res = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
signal,
|
|
230
|
+
headers: {
|
|
231
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
232
|
+
"content-type": "application/json",
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(buildChatBody(shape, {
|
|
235
|
+
model: this.model,
|
|
236
|
+
system: input.system,
|
|
237
|
+
user: input.user,
|
|
238
|
+
maxTokens: input.maxTokens ?? 1024,
|
|
239
|
+
})),
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
failureResponse = res;
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
246
|
+
// unbounded provider error page must not bloat the ledger row.
|
|
247
|
+
error: `provider HTTP ${res.status}: ${safeProviderError(await res.text(), this.apiKey)}`,
|
|
248
|
+
status: res.status,
|
|
249
|
+
retryAfterMs: retryAfterMs(res),
|
|
250
|
+
...(RETRYABLE_SERVER_STATUSES.has(res.status)
|
|
251
|
+
? { retryableHttpFailure: true }
|
|
252
|
+
: {}),
|
|
253
|
+
};
|
|
222
254
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
255
|
+
const json = (await res.json());
|
|
256
|
+
const message = json.choices?.[0]?.message;
|
|
257
|
+
const decisionArguments = message?.tool_calls?.find((call) => call.function?.name === DECISION_TOOL_NAME)?.function?.arguments;
|
|
258
|
+
const text = decisionArguments ?? message?.content ?? "";
|
|
259
|
+
const usage = json.usage
|
|
260
|
+
? {
|
|
261
|
+
promptTokens: json.usage.prompt_tokens ?? 0,
|
|
262
|
+
completionTokens: json.usage.completion_tokens ?? 0,
|
|
263
|
+
}
|
|
264
|
+
: undefined;
|
|
265
|
+
return text
|
|
266
|
+
? { ok: true, text, usage }
|
|
267
|
+
: { ok: false, error: "provider returned empty content" };
|
|
268
|
+
});
|
|
227
269
|
}
|
|
228
270
|
catch (err) {
|
|
229
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: safeProviderError(failureResponse
|
|
274
|
+
? `provider HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
|
|
275
|
+
: callError(err, timeoutMs), this.apiKey),
|
|
276
|
+
...(failureResponse
|
|
277
|
+
? {
|
|
278
|
+
status: failureResponse.status,
|
|
279
|
+
retryAfterMs: retryAfterMs(failureResponse),
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
282
|
+
};
|
|
230
283
|
}
|
|
231
284
|
}
|
|
232
285
|
}
|
|
286
|
+
// A direct NVIDIA 5xx previously lost the whole cycle until the next cadence.
|
|
287
|
+
// Retry only an explicit server refusal, once, on the identical configured
|
|
288
|
+
// route. The shared router owns its own attempt/capacity budget and MUST NOT
|
|
289
|
+
// receive this wrapper. No auth/404/429, network, timeout or decision repair
|
|
290
|
+
// retries; no model substitution or request-parameter changes.
|
|
291
|
+
class SameModelRetryProvider {
|
|
292
|
+
delegate;
|
|
293
|
+
provider;
|
|
294
|
+
model;
|
|
295
|
+
label;
|
|
296
|
+
constructor(delegate, provider, model) {
|
|
297
|
+
this.delegate = delegate;
|
|
298
|
+
this.provider = provider;
|
|
299
|
+
this.model = model;
|
|
300
|
+
this.label = delegate.label;
|
|
301
|
+
}
|
|
302
|
+
async decide(input) {
|
|
303
|
+
const started = Date.now();
|
|
304
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
305
|
+
const first = await this.delegate.decide(input);
|
|
306
|
+
const firstFinished = Date.now();
|
|
307
|
+
if (first.ok || !first.retryableHttpFailure) {
|
|
308
|
+
return first;
|
|
309
|
+
}
|
|
310
|
+
const delayMs = Math.max(1000, first.retryAfterMs ?? 0);
|
|
311
|
+
// Honor the provider's cooldown; a long one waits for the next cycle. Both
|
|
312
|
+
// attempts and this backoff share the ORIGINAL deadline, never two 5m caps.
|
|
313
|
+
if (delayMs > 5000 || delayMs >= timeoutMs - (firstFinished - started)) {
|
|
314
|
+
return first;
|
|
315
|
+
}
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
317
|
+
const retryStarted = Date.now();
|
|
318
|
+
const remainingMs = timeoutMs - (retryStarted - started);
|
|
319
|
+
if (remainingMs <= 0)
|
|
320
|
+
return first;
|
|
321
|
+
const result = await this.delegate.decide({
|
|
322
|
+
...input,
|
|
323
|
+
timeoutMs: remainingMs,
|
|
324
|
+
});
|
|
325
|
+
const attempt = (res, latencyMs) => ({
|
|
326
|
+
provider: this.provider,
|
|
327
|
+
model: this.model,
|
|
328
|
+
outcome: res.ok ? "success" : "failed",
|
|
329
|
+
latencyMs,
|
|
330
|
+
...(!res.ok
|
|
331
|
+
? {
|
|
332
|
+
error: res.error,
|
|
333
|
+
status: res.status,
|
|
334
|
+
retryAfterMs: res.retryAfterMs,
|
|
335
|
+
...(res.status !== undefined
|
|
336
|
+
? {
|
|
337
|
+
failureClass: res.status === 429
|
|
338
|
+
? "capacity"
|
|
339
|
+
: res.status >= 500
|
|
340
|
+
? "transient"
|
|
341
|
+
: "permanent",
|
|
342
|
+
}
|
|
343
|
+
: {}),
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
// Usage, when present, describes only the final response. The preceding
|
|
349
|
+
// 5xx did not report usage; attempt evidence is not total billed tokens
|
|
350
|
+
// or a certificate that returned text passed the trading-decision parser.
|
|
351
|
+
...result,
|
|
352
|
+
route: {
|
|
353
|
+
policyVersion: "coinrithm.configured-same-model-retry.v1",
|
|
354
|
+
profile: "configured",
|
|
355
|
+
effectiveProvider: this.provider,
|
|
356
|
+
effectiveModel: this.model,
|
|
357
|
+
reason: "configured_direct",
|
|
358
|
+
attempts: [
|
|
359
|
+
attempt(first, firstFinished - started),
|
|
360
|
+
attempt(result, Date.now() - retryStarted),
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
233
366
|
// Build the provider from the spec's model block + env key. Throws a clear
|
|
234
367
|
// error if no model is configured (self-host requires one) or the env key is
|
|
235
368
|
// missing. fetch is injectable for tests.
|
|
@@ -262,7 +395,10 @@ export function selectProvider(spec, env, fetchFn = fetch) {
|
|
|
262
395
|
if (!resolvedBase) {
|
|
263
396
|
throw new Error("openai-compatible provider needs model.baseUrl");
|
|
264
397
|
}
|
|
265
|
-
|
|
398
|
+
const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn);
|
|
399
|
+
return provider === "nvidia"
|
|
400
|
+
? new SameModelRetryProvider(direct, provider, name)
|
|
401
|
+
: direct;
|
|
266
402
|
}
|
|
267
403
|
// Build a provider for an EXPLICIT route + raw key (no spec, no env) — the
|
|
268
404
|
// decision probe's entry point. Same classes as selectProvider, so a probe
|
package/dist/agent/resolve.js
CHANGED
package/dist/agent/runner.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CoinRithmClient, ProvenanceReport } from "./client.js";
|
|
2
2
|
import { Provider } from "./providers.js";
|
|
3
3
|
import { AgentSpec, RunState, CycleResult, Decision, ProposedAction, PmMarket, PostedOpportunity, QuoteEvidence } from "./types.js";
|
|
4
|
+
import { type DecisionInputRecord } from "./decisionReceipt.js";
|
|
4
5
|
export interface RunnerDeps {
|
|
5
6
|
client: CoinRithmClient;
|
|
6
7
|
provider: Provider;
|
|
@@ -10,13 +11,15 @@ export interface RunnerDeps {
|
|
|
10
11
|
live: boolean;
|
|
11
12
|
stateFile?: string;
|
|
12
13
|
log?: (line: string) => void;
|
|
14
|
+
/** Optional private storage hook. Its failure never changes cycle execution. */
|
|
15
|
+
onDecisionInputRecord?: (record: DecisionInputRecord) => void;
|
|
13
16
|
}
|
|
14
17
|
export declare function houseAgentForecastEnabled(): boolean;
|
|
15
18
|
export declare function agentOpportunityCaptureEnabled(): boolean;
|
|
16
19
|
export declare function runnerRuntimeKind(): ProvenanceReport["runtimeKind"];
|
|
17
20
|
export declare function buildRunnerProvenance(spec: AgentSpec): ProvenanceReport;
|
|
18
21
|
export declare function sanitizeForecastProbability(raw: unknown): number | undefined;
|
|
19
|
-
export declare function repairFuturesTakeProfit(action: ProposedAction, quote?: QuoteEvidence): {
|
|
22
|
+
export declare function repairFuturesTakeProfit(action: ProposedAction, quote?: QuoteEvidence, capitalMinimumRewardRisk?: number): {
|
|
20
23
|
action: ProposedAction;
|
|
21
24
|
repaired: boolean;
|
|
22
25
|
};
|