@coinrithm/mcp-trading 0.7.15 → 0.7.16
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 +127 -8
- package/README.md +18 -3
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +1 -0
- package/dist/agent/client.js +6 -0
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +85 -3
- package/dist/agent/decisionValidator.d.ts +30 -0
- package/dist/agent/decisionValidator.js +147 -2
- package/dist/agent/engine.d.ts +3 -3
- package/dist/agent/engine.js +3 -3
- package/dist/agent/observe.d.ts +27 -2
- package/dist/agent/observe.js +529 -89
- package/dist/agent/prompt.d.ts +4 -1
- package/dist/agent/prompt.js +98 -7
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +6 -0
- package/dist/agent/providerCapabilities.js +23 -0
- package/dist/agent/providers.d.ts +25 -7
- package/dist/agent/providers.js +188 -48
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +59 -15
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +37 -0
- package/dist/agent/skillValidator.js +33 -0
- package/dist/agent/strictLint.js +29 -0
- package/dist/agent/types.d.ts +149 -12
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/package.json +3 -2
package/dist/agent/providers.js
CHANGED
|
@@ -3,7 +3,64 @@
|
|
|
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
5
|
import { retryAfterSeconds } from "../retryAfter.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseDecision } from "./decision.js";
|
|
7
|
+
import { chatShapeFor, buildChatBody, withJsonContentTransport, canRetrySuperToolOutput, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
|
|
8
|
+
// A token count as Anthropic reports it: a non-negative safe integer. Zero
|
|
9
|
+
// is a real count; anything else (missing, null, negative, NaN, fractional,
|
|
10
|
+
// a string) is malformed.
|
|
11
|
+
function exactCount(value) {
|
|
12
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
13
|
+
? value
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
// Anthropic usage, including prompt-cache reads and writes (and the 5m/1h
|
|
17
|
+
// write split) when reported. Strict, because paid metering prices it (root
|
|
18
|
+
// review of #118): input_tokens and output_tokens are mandatory, and a
|
|
19
|
+
// malformed reported field (mandatory or cache) makes the WHOLE usage unknown
|
|
20
|
+
// (undefined), never zero, so the call is metered uncertain instead of free.
|
|
21
|
+
// Cache fields the response does not carry at all stay absent. A reported
|
|
22
|
+
// write total must equal its 5m/1h split; a split without a total implies it.
|
|
23
|
+
export function anthropicUsage(raw) {
|
|
24
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
25
|
+
return undefined;
|
|
26
|
+
const u = raw;
|
|
27
|
+
const promptTokens = exactCount(u.input_tokens);
|
|
28
|
+
const completionTokens = exactCount(u.output_tokens);
|
|
29
|
+
if (promptTokens === undefined || completionTokens === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const usage = { promptTokens, completionTokens };
|
|
32
|
+
const optional = (value) => value === undefined ? undefined : (exactCount(value) ?? null);
|
|
33
|
+
const cacheRead = optional(u.cache_read_input_tokens);
|
|
34
|
+
const cacheWrite = optional(u.cache_creation_input_tokens);
|
|
35
|
+
let write5m;
|
|
36
|
+
let write1h;
|
|
37
|
+
if (u.cache_creation !== undefined) {
|
|
38
|
+
const split = u.cache_creation;
|
|
39
|
+
if (typeof split !== "object" || split === null || Array.isArray(split))
|
|
40
|
+
return undefined;
|
|
41
|
+
const s = split;
|
|
42
|
+
write5m = optional(s.ephemeral_5m_input_tokens);
|
|
43
|
+
write1h = optional(s.ephemeral_1h_input_tokens);
|
|
44
|
+
}
|
|
45
|
+
if ([cacheRead, cacheWrite, write5m, write1h].includes(null))
|
|
46
|
+
return undefined;
|
|
47
|
+
if (write5m != null || write1h != null) {
|
|
48
|
+
const splitTotal = (write5m ?? 0) + (write1h ?? 0);
|
|
49
|
+
if (cacheWrite != null && cacheWrite !== splitTotal)
|
|
50
|
+
return undefined;
|
|
51
|
+
usage.cacheWriteTokens = splitTotal;
|
|
52
|
+
if (write5m != null)
|
|
53
|
+
usage.cacheWrite5mTokens = write5m;
|
|
54
|
+
if (write1h != null)
|
|
55
|
+
usage.cacheWrite1hTokens = write1h;
|
|
56
|
+
}
|
|
57
|
+
else if (cacheWrite != null) {
|
|
58
|
+
usage.cacheWriteTokens = cacheWrite;
|
|
59
|
+
}
|
|
60
|
+
if (cacheRead != null)
|
|
61
|
+
usage.cacheReadTokens = cacheRead;
|
|
62
|
+
return usage;
|
|
63
|
+
}
|
|
7
64
|
// NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
|
|
8
65
|
// endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
|
|
9
66
|
const NVIDIA_BASE_URL = CAP_NVIDIA_BASE_URL;
|
|
@@ -31,6 +88,8 @@ const DEFAULT_TIMEOUT_MS = 300_000;
|
|
|
31
88
|
const RETRYABLE_SERVER_STATUSES = new Set([500, 502, 503, 504]);
|
|
32
89
|
/** Keep direct and routed cycle evidence consistent about NVIDIA backpressure. */
|
|
33
90
|
export function classifyProviderFailure(result) {
|
|
91
|
+
if (result.failureClass === "malformed")
|
|
92
|
+
return "malformed";
|
|
34
93
|
if (result.status === 429 ||
|
|
35
94
|
(result.status === 503 &&
|
|
36
95
|
/resourceexhausted|worker local total request limit/i.test(result.error ?? "")))
|
|
@@ -186,15 +245,27 @@ class AnthropicProvider {
|
|
|
186
245
|
}
|
|
187
246
|
const json = (await res.json());
|
|
188
247
|
const text = json.content?.map((c) => c.text ?? "").join("") ?? "";
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
// Absent or malformed usage is undefined (unknown), never zero.
|
|
249
|
+
const usage = anthropicUsage(json.usage);
|
|
250
|
+
if (json.stop_reason === "max_tokens" ||
|
|
251
|
+
json.stop_reason === "model_context_window_exceeded") {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
failureClass: "malformed",
|
|
255
|
+
error: json.stop_reason === "max_tokens"
|
|
256
|
+
? "provider returned incomplete decision (output token limit)"
|
|
257
|
+
: "provider returned incomplete decision (context window limit)",
|
|
258
|
+
usage,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
195
261
|
return text
|
|
196
262
|
? { ok: true, text, usage, responseSource: "content" }
|
|
197
|
-
:
|
|
263
|
+
: // Answered (and billed) with nothing usable: keep the usage.
|
|
264
|
+
{
|
|
265
|
+
ok: false,
|
|
266
|
+
error: "anthropic returned empty content",
|
|
267
|
+
...(usage ? { usage } : {}),
|
|
268
|
+
};
|
|
198
269
|
});
|
|
199
270
|
}
|
|
200
271
|
catch (err) {
|
|
@@ -213,24 +284,40 @@ class AnthropicProvider {
|
|
|
213
284
|
}
|
|
214
285
|
}
|
|
215
286
|
}
|
|
287
|
+
function wantsJsonContent(options) {
|
|
288
|
+
const v = options.nemotronJsonContent;
|
|
289
|
+
if (typeof v !== "function")
|
|
290
|
+
return v === true;
|
|
291
|
+
try {
|
|
292
|
+
return v() === true;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
216
298
|
class OpenAiCompatProvider {
|
|
217
299
|
provider;
|
|
218
300
|
model;
|
|
219
301
|
apiKey;
|
|
220
302
|
baseUrl;
|
|
221
303
|
fetchFn;
|
|
304
|
+
options;
|
|
222
305
|
label;
|
|
223
|
-
constructor(provider, model, apiKey, baseUrl, fetchFn) {
|
|
306
|
+
constructor(provider, model, apiKey, baseUrl, fetchFn, options = {}) {
|
|
224
307
|
this.provider = provider;
|
|
225
308
|
this.model = model;
|
|
226
309
|
this.apiKey = apiKey;
|
|
227
310
|
this.baseUrl = baseUrl;
|
|
228
311
|
this.fetchFn = fetchFn;
|
|
312
|
+
this.options = options;
|
|
229
313
|
this.label = `${baseUrl}/${model}`;
|
|
230
314
|
}
|
|
231
315
|
async decide(input) {
|
|
232
316
|
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
233
|
-
const
|
|
317
|
+
const baseShape = chatShapeFor(this.provider, this.model, this.baseUrl);
|
|
318
|
+
const shape = wantsJsonContent(this.options)
|
|
319
|
+
? withJsonContentTransport(baseShape)
|
|
320
|
+
: baseShape;
|
|
234
321
|
let failureResponse;
|
|
235
322
|
try {
|
|
236
323
|
return await withProviderTimeout(timeoutMs, async (signal) => {
|
|
@@ -273,6 +360,14 @@ class OpenAiCompatProvider {
|
|
|
273
360
|
completionTokens: json.usage.completion_tokens ?? 0,
|
|
274
361
|
}
|
|
275
362
|
: undefined;
|
|
363
|
+
if (json.choices?.[0]?.finish_reason === "length") {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
failureClass: "malformed",
|
|
367
|
+
error: "provider returned incomplete decision (output token limit)",
|
|
368
|
+
usage,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
276
371
|
return text
|
|
277
372
|
? {
|
|
278
373
|
ok: true,
|
|
@@ -303,20 +398,24 @@ class OpenAiCompatProvider {
|
|
|
303
398
|
}
|
|
304
399
|
}
|
|
305
400
|
}
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// receive this wrapper. No
|
|
310
|
-
//
|
|
401
|
+
// Direct NVIDIA recovery has one shared two-call/deadline budget. Explicit
|
|
402
|
+
// server refusals retry the original request; rejected Super tool encoding
|
|
403
|
+
// retries JSON content on the same configured route. The shared router owns
|
|
404
|
+
// its own admission budget and MUST NOT receive this wrapper. No model
|
|
405
|
+
// substitution or repair/coercion of a rejected decision is allowed.
|
|
311
406
|
class SameModelRetryProvider {
|
|
312
407
|
delegate;
|
|
313
408
|
provider;
|
|
314
409
|
model;
|
|
410
|
+
contentRetry;
|
|
411
|
+
baseUrl;
|
|
315
412
|
label;
|
|
316
|
-
constructor(delegate, provider, model) {
|
|
413
|
+
constructor(delegate, provider, model, contentRetry, baseUrl) {
|
|
317
414
|
this.delegate = delegate;
|
|
318
415
|
this.provider = provider;
|
|
319
416
|
this.model = model;
|
|
417
|
+
this.contentRetry = contentRetry;
|
|
418
|
+
this.baseUrl = baseUrl;
|
|
320
419
|
this.label = delegate.label;
|
|
321
420
|
}
|
|
322
421
|
async decide(input) {
|
|
@@ -324,56 +423,95 @@ class SameModelRetryProvider {
|
|
|
324
423
|
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
325
424
|
const first = await this.delegate.decide(input);
|
|
326
425
|
const firstFinished = Date.now();
|
|
327
|
-
|
|
426
|
+
const parsedFirst = first.ok ? parseDecision(first.text) : undefined;
|
|
427
|
+
const retryContent = first.ok &&
|
|
428
|
+
parsedFirst?.ok === false &&
|
|
429
|
+
this.contentRetry !== undefined &&
|
|
430
|
+
canRetrySuperToolOutput({ provider: this.provider, model: this.model, baseUrl: this.baseUrl }, first.responseSource, parsedFirst.actionsStringDiagnostic);
|
|
431
|
+
if (!retryContent && (first.ok || !first.retryableHttpFailure)) {
|
|
328
432
|
return first;
|
|
329
433
|
}
|
|
330
|
-
const delayMs =
|
|
434
|
+
const delayMs = retryContent
|
|
435
|
+
? 0
|
|
436
|
+
: Math.max(1000, first.ok ? 0 : (first.retryAfterMs ?? 0));
|
|
331
437
|
// Honor the provider's cooldown; a long one waits for the next cycle. Both
|
|
332
438
|
// attempts and this backoff share the ORIGINAL deadline, never two 5m caps.
|
|
333
439
|
if (delayMs > 5000 || delayMs >= timeoutMs - (firstFinished - started)) {
|
|
334
440
|
return first;
|
|
335
441
|
}
|
|
336
|
-
|
|
442
|
+
if (delayMs > 0)
|
|
443
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
337
444
|
const retryStarted = Date.now();
|
|
338
445
|
const remainingMs = timeoutMs - (retryStarted - started);
|
|
339
446
|
if (remainingMs <= 0)
|
|
340
447
|
return first;
|
|
341
|
-
const
|
|
448
|
+
const retryResult = await (retryContent ? this.contentRetry : this.delegate).decide({
|
|
342
449
|
...input,
|
|
343
450
|
timeoutMs: remainingMs,
|
|
344
451
|
});
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
:
|
|
362
|
-
|
|
452
|
+
const parsedRetry = retryResult.ok
|
|
453
|
+
? parseDecision(retryResult.text)
|
|
454
|
+
: undefined;
|
|
455
|
+
const result = retryResult.ok && parsedRetry?.ok === false
|
|
456
|
+
? {
|
|
457
|
+
ok: false,
|
|
458
|
+
failureClass: "malformed",
|
|
459
|
+
error: parsedRetry.error,
|
|
460
|
+
usage: retryResult.usage,
|
|
461
|
+
}
|
|
462
|
+
: retryResult;
|
|
463
|
+
const attempt = (res, latencyMs) => {
|
|
464
|
+
const parsed = res.ok ? parseDecision(res.text) : undefined;
|
|
465
|
+
return {
|
|
466
|
+
provider: this.provider,
|
|
467
|
+
model: this.model,
|
|
468
|
+
outcome: res.ok && parsed?.ok ? "success" : "failed",
|
|
469
|
+
latencyMs,
|
|
470
|
+
...(res.ok ? { responseSource: res.responseSource } : {}),
|
|
471
|
+
...(parsed?.ok === false
|
|
472
|
+
? {
|
|
473
|
+
failureClass: "malformed",
|
|
474
|
+
error: parsed.error,
|
|
475
|
+
actionsStringDiagnostic: parsed.actionsStringDiagnostic,
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
478
|
+
...(!res.ok
|
|
479
|
+
? {
|
|
480
|
+
error: res.error,
|
|
481
|
+
status: res.status,
|
|
482
|
+
retryAfterMs: res.retryAfterMs,
|
|
483
|
+
failureClass: res.failureClass ??
|
|
484
|
+
(classifyProviderFailure(res) === "capacity"
|
|
485
|
+
? "capacity"
|
|
486
|
+
: res.status !== undefined && res.status < 500
|
|
487
|
+
? "permanent"
|
|
488
|
+
: "transient"),
|
|
489
|
+
}
|
|
490
|
+
: {}),
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
// Sum reported usage from both physical calls, including rejected output.
|
|
494
|
+
// Missing usage is unknown, not evidence that a request consumed no tokens.
|
|
495
|
+
const usage = first.usage || result.usage
|
|
496
|
+
? {
|
|
497
|
+
promptTokens: (first.usage?.promptTokens ?? 0) +
|
|
498
|
+
(result.usage?.promptTokens ?? 0),
|
|
499
|
+
completionTokens: (first.usage?.completionTokens ?? 0) +
|
|
500
|
+
(result.usage?.completionTokens ?? 0),
|
|
501
|
+
}
|
|
502
|
+
: undefined;
|
|
363
503
|
return {
|
|
364
|
-
// Usage, when present, describes only the final response. The preceding
|
|
365
|
-
// 5xx did not report usage; attempt evidence is not total billed tokens
|
|
366
|
-
// or a certificate that returned text passed the trading-decision parser.
|
|
367
504
|
...result,
|
|
505
|
+
...(usage ? { usage } : {}),
|
|
368
506
|
route: {
|
|
369
|
-
policyVersion: "coinrithm.configured-same-model-retry.
|
|
507
|
+
policyVersion: "coinrithm.configured-same-model-retry.v3",
|
|
370
508
|
profile: "configured",
|
|
371
509
|
effectiveProvider: this.provider,
|
|
372
510
|
effectiveModel: this.model,
|
|
373
511
|
reason: "configured_direct",
|
|
374
512
|
attempts: [
|
|
375
513
|
attempt(first, firstFinished - started),
|
|
376
|
-
attempt(
|
|
514
|
+
attempt(retryResult, Date.now() - retryStarted),
|
|
377
515
|
],
|
|
378
516
|
},
|
|
379
517
|
};
|
|
@@ -382,7 +520,7 @@ class SameModelRetryProvider {
|
|
|
382
520
|
// Build the provider from the spec's model block + env key. Throws a clear
|
|
383
521
|
// error if no model is configured (self-host requires one) or the env key is
|
|
384
522
|
// missing. fetch is injectable for tests.
|
|
385
|
-
export function selectProvider(spec, env, fetchFn = fetch) {
|
|
523
|
+
export function selectProvider(spec, env, fetchFn = fetch, options = {}) {
|
|
386
524
|
if (!spec.model) {
|
|
387
525
|
throw new Error("no model configured: set model.provider + model.name in the agent (self-host needs an explicit model)");
|
|
388
526
|
}
|
|
@@ -411,15 +549,17 @@ export function selectProvider(spec, env, fetchFn = fetch) {
|
|
|
411
549
|
if (!resolvedBase) {
|
|
412
550
|
throw new Error("openai-compatible provider needs model.baseUrl");
|
|
413
551
|
}
|
|
414
|
-
const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn);
|
|
552
|
+
const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn, options);
|
|
415
553
|
return provider === "nvidia"
|
|
416
|
-
? new SameModelRetryProvider(direct, provider, name
|
|
554
|
+
? new SameModelRetryProvider(direct, provider, name, new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn, {
|
|
555
|
+
nemotronJsonContent: true,
|
|
556
|
+
}), baseUrl)
|
|
417
557
|
: direct;
|
|
418
558
|
}
|
|
419
559
|
// Build a provider for an EXPLICIT route + raw key (no spec, no env) — the
|
|
420
560
|
// decision probe's entry point. Same classes as selectProvider, so a probe
|
|
421
561
|
// exercises byte-identical request shapes to a real cycle.
|
|
422
|
-
export function providerForRoute(route, apiKey, fetchFn = fetch) {
|
|
562
|
+
export function providerForRoute(route, apiKey, fetchFn = fetch, options = {}) {
|
|
423
563
|
if (route.provider === "mechanical")
|
|
424
564
|
return new MechanicalProvider(route.model);
|
|
425
565
|
if (route.provider === "anthropic")
|
|
@@ -428,5 +568,5 @@ export function providerForRoute(route, apiKey, fetchFn = fetch) {
|
|
|
428
568
|
if (!resolvedBase) {
|
|
429
569
|
throw new Error("openai-compatible route needs a baseUrl");
|
|
430
570
|
}
|
|
431
|
-
return new OpenAiCompatProvider(route.provider, route.model, apiKey, resolvedBase, fetchFn);
|
|
571
|
+
return new OpenAiCompatProvider(route.provider, route.model, apiKey, resolvedBase, fetchFn, options);
|
|
432
572
|
}
|
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 ObserveOptions } from "./observe.js";
|
|
4
5
|
import { type DecisionInputRecord } from "./decisionReceipt.js";
|
|
5
6
|
export interface RunnerDeps {
|
|
6
7
|
client: CoinRithmClient;
|
|
@@ -13,6 +14,13 @@ export interface RunnerDeps {
|
|
|
13
14
|
log?: (line: string) => void;
|
|
14
15
|
/** Optional private storage hook. Its failure never changes cycle execution. */
|
|
15
16
|
onDecisionInputRecord?: (record: DecisionInputRecord) => void;
|
|
17
|
+
/** Hosted shared-pool admission only; protective checks still run every cycle. */
|
|
18
|
+
minModelIntervalSeconds?: number;
|
|
19
|
+
/** Opt-in hosted canary; changes presentation only, preserving all values. */
|
|
20
|
+
compactPromptTables?: boolean;
|
|
21
|
+
/** Bench replay only (bench.ts): observation overrides. Live runs never
|
|
22
|
+
* set it. */
|
|
23
|
+
observeOptions?: ObserveOptions;
|
|
16
24
|
}
|
|
17
25
|
export declare function houseAgentForecastEnabled(): boolean;
|
|
18
26
|
export declare function agentOpportunityCaptureEnabled(): boolean;
|
package/dist/agent/runner.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// spot, futures, and prediction markets. Dry-run never writes. Live uses
|
|
3
3
|
// idempotency keys + agentTrace and exports run evidence. The client + provider
|
|
4
4
|
// are injected so the loop is fully unit-testable with no network/model calls.
|
|
5
|
+
import { signalThresholdsOf } from "./signals.js";
|
|
5
6
|
import { classifyProviderFailure } from "./providers.js";
|
|
6
7
|
import { COINRITHM_API } from "./version.js";
|
|
7
8
|
import { spotBuyCost, DEFAULT_TRIGGER_POLICY, } from "./types.js";
|
|
@@ -15,7 +16,7 @@ import { createOpportunityReporter } from "./opportunityReporter.js";
|
|
|
15
16
|
import { enrichWhaleContext, observe } from "./observe.js";
|
|
16
17
|
import { buildDailyRiskBudget, buildFuturesCapacity, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
|
|
17
18
|
import { parseDecision } from "./decision.js";
|
|
18
|
-
import { validateAction, isRiskIncreasingAction, } from "./decisionValidator.js";
|
|
19
|
+
import { validateAction, isRiskIncreasingAction, pmEventKey, pmCoinHorizonOf, pmReferenceMs, } from "./decisionValidator.js";
|
|
19
20
|
import { resolvePmRef } from "./resolvePm.js";
|
|
20
21
|
import { fetchQuote, executeAction } from "./act.js";
|
|
21
22
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
@@ -482,7 +483,7 @@ async function runCycleCore(deps, capture) {
|
|
|
482
483
|
log,
|
|
483
484
|
});
|
|
484
485
|
// OBSERVE
|
|
485
|
-
const obs = await observe(client, spec, state, baseTrace);
|
|
486
|
+
const obs = await observe(client, spec, state, baseTrace, deps.observeOptions);
|
|
486
487
|
const observation = obs.observation;
|
|
487
488
|
const nowMs = Date.now();
|
|
488
489
|
// Slice 2: attach to each open position the thesis it was opened on (from
|
|
@@ -632,7 +633,7 @@ async function runCycleCore(deps, capture) {
|
|
|
632
633
|
// receipt over the observation the model actually decides on. The exit
|
|
633
634
|
// traces above keep the pre-exit receipt they were decided on.
|
|
634
635
|
observation.openPositions = observation.openPositions.filter((p) => !closedByThesis.includes(p.id));
|
|
635
|
-
observation.setups = scanSetups(observation.watch, observation.openPositions);
|
|
636
|
+
observation.setups = scanSetups(observation.watch, observation.openPositions, signalThresholdsOf(spec));
|
|
636
637
|
observationReceipt = buildObservationReceipt(observation);
|
|
637
638
|
Object.assign(baseTrace, observationReceipt);
|
|
638
639
|
}
|
|
@@ -642,6 +643,17 @@ async function runCycleCore(deps, capture) {
|
|
|
642
643
|
// so it touches NO kill-switch counter; it just records the cheap cycle.
|
|
643
644
|
const policy = spec.triggerPolicy ?? DEFAULT_TRIGGER_POLICY;
|
|
644
645
|
const gate = evaluateGate(observation, state, policy, nowMs);
|
|
646
|
+
const minimumMs = Number.isFinite(deps.minModelIntervalSeconds)
|
|
647
|
+
? Math.max(0, deps.minModelIntervalSeconds ?? 0) * 1000
|
|
648
|
+
: 0;
|
|
649
|
+
if (gate.fire &&
|
|
650
|
+
spec.model?.provider !== "mechanical" &&
|
|
651
|
+
minimumMs > 0 &&
|
|
652
|
+
state.lastLlmCallAt != null &&
|
|
653
|
+
nowMs - state.lastLlmCallAt < minimumMs) {
|
|
654
|
+
gate.fire = false;
|
|
655
|
+
gate.reason = `shared pool model interval (${Math.ceil(minimumMs / 1000)}s minimum)`;
|
|
656
|
+
}
|
|
645
657
|
const providerName = spec.model?.provider ?? "nvidia";
|
|
646
658
|
if (!gate.fire) {
|
|
647
659
|
capture({
|
|
@@ -664,8 +676,8 @@ async function runCycleCore(deps, capture) {
|
|
|
664
676
|
tokensIn: 0,
|
|
665
677
|
tokensOut: 0,
|
|
666
678
|
estimatedCostUsd: 0,
|
|
667
|
-
writeAttempted: 0,
|
|
668
|
-
writeAccepted:
|
|
679
|
+
writeAttempted: live ? exitPlanned.length : 0,
|
|
680
|
+
writeAccepted: exitPlanned.filter((p) => p.executed).length,
|
|
669
681
|
...observationReceipt,
|
|
670
682
|
};
|
|
671
683
|
}
|
|
@@ -725,13 +737,20 @@ async function runCycleCore(deps, capture) {
|
|
|
725
737
|
includeForecast: forecastEnabled,
|
|
726
738
|
...actionAvailability,
|
|
727
739
|
});
|
|
728
|
-
const
|
|
740
|
+
const userOptions = {
|
|
729
741
|
venues: spec.venues,
|
|
730
742
|
dailyRiskBudget: buildDailyRiskBudget(spec, state),
|
|
731
743
|
...(usesCapitalSizing(spec) ? { capitalSizing: spec.capitalSizing } : {}),
|
|
732
744
|
...(futuresCapacity ? { futuresCapacity } : {}),
|
|
733
745
|
...actionAvailability,
|
|
734
|
-
}
|
|
746
|
+
};
|
|
747
|
+
const user = buildUserPrompt(observation, state.journal, userOptions);
|
|
748
|
+
const compactUser = deps.compactPromptTables
|
|
749
|
+
? buildUserPrompt(observation, state.journal, {
|
|
750
|
+
...userOptions,
|
|
751
|
+
compactTables: true,
|
|
752
|
+
})
|
|
753
|
+
: undefined;
|
|
735
754
|
const tokensInEst = Math.round((system.length + user.length) / 4);
|
|
736
755
|
// Prompt-size + trigger visibility in the live terminal.
|
|
737
756
|
log(`prompt ~${tokensInEst} tok ` +
|
|
@@ -745,6 +764,7 @@ async function runCycleCore(deps, capture) {
|
|
|
745
764
|
const res = await provider.decide({
|
|
746
765
|
system,
|
|
747
766
|
user,
|
|
767
|
+
...(compactUser && compactUser !== user ? { compactUser } : {}),
|
|
748
768
|
...actionAvailability,
|
|
749
769
|
});
|
|
750
770
|
const route = res.route;
|
|
@@ -761,12 +781,11 @@ async function runCycleCore(deps, capture) {
|
|
|
761
781
|
// Metering: prefer provider-reported usage; fall back to a chars/4 estimate.
|
|
762
782
|
const tokensIn = !actualCallMade
|
|
763
783
|
? 0
|
|
764
|
-
: res.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
: 0;
|
|
784
|
+
: (res.usage?.promptTokens ?? tokensInEst);
|
|
785
|
+
const tokensOut = !actualCallMade
|
|
786
|
+
? 0
|
|
787
|
+
: (res.usage?.completionTokens ??
|
|
788
|
+
(res.ok ? Math.round(res.text.length / 4) : 0));
|
|
770
789
|
const estimatedCostUsd = estimateCostUsd(effectiveProvider ?? providerName, tokensIn, tokensOut);
|
|
771
790
|
meter = {
|
|
772
791
|
triggerCodes: gate.codes,
|
|
@@ -782,6 +801,8 @@ async function runCycleCore(deps, capture) {
|
|
|
782
801
|
// hosted shared provider use this same path. Attribute only actual calls.
|
|
783
802
|
routeReason: route?.reason ?? (actualCallMade ? "configured_direct" : undefined),
|
|
784
803
|
routeAttempts: route?.attempts,
|
|
804
|
+
// Provider-reported usage only, never the estimate above.
|
|
805
|
+
...(actualCallMade && res.usage ? { providerUsage: res.usage } : {}),
|
|
785
806
|
};
|
|
786
807
|
if (!res.ok) {
|
|
787
808
|
// Upstream 429s are expected provider backpressure, not evidence that the
|
|
@@ -800,7 +821,9 @@ async function runCycleCore(deps, capture) {
|
|
|
800
821
|
decision: "skip",
|
|
801
822
|
skipReason: actualCallMade
|
|
802
823
|
? "provider rate-limited; retry next cycle"
|
|
803
|
-
: "
|
|
824
|
+
: res.error === "shared pool owner budget unavailable"
|
|
825
|
+
? "shared pool owner budget deferred"
|
|
826
|
+
: "provider capacity deferred",
|
|
804
827
|
planned: exitPlanned,
|
|
805
828
|
modelFailed: false,
|
|
806
829
|
live,
|
|
@@ -811,7 +834,6 @@ async function runCycleCore(deps, capture) {
|
|
|
811
834
|
...observationReceipt,
|
|
812
835
|
};
|
|
813
836
|
}
|
|
814
|
-
state.consecutiveModelFailures += 1;
|
|
815
837
|
// Permanent-failure classification: a 404/410/model_not_found is a
|
|
816
838
|
// DECOMMISSIONED or misconfigured model that will fail every cycle
|
|
817
839
|
// until something changes (live-measured 2026-08-26: NVIDIA EOL'd the
|
|
@@ -874,6 +896,12 @@ async function runCycleCore(deps, capture) {
|
|
|
874
896
|
}
|
|
875
897
|
}
|
|
876
898
|
else {
|
|
899
|
+
// Availability failures have their own route-specific hold counter.
|
|
900
|
+
// Charging them to the generic kill-switch as well disables BYO and
|
|
901
|
+
// self-hosted agents that keep retrying outside the shared circuit.
|
|
902
|
+
// Preserve prior transient/invalid-output failures until a valid
|
|
903
|
+
// decision clears them; a provider hold is not evidence of recovery.
|
|
904
|
+
state.consecutiveModelFailures += 1;
|
|
877
905
|
state.consecutivePermanentModelErrors = 0;
|
|
878
906
|
delete state.permanentModelErrorRoute;
|
|
879
907
|
}
|
|
@@ -997,6 +1025,8 @@ async function runCycleCore(deps, capture) {
|
|
|
997
1025
|
const realizedLossTodayMusd = Math.max(0, -state.realizedPnlTodayMusd);
|
|
998
1026
|
const targetedPositionIds = [];
|
|
999
1027
|
const targetedOrderIds = [];
|
|
1028
|
+
const pmEventsOpenedThisCycle = [];
|
|
1029
|
+
const pmCoinHorizonsOpenedThisCycle = [];
|
|
1000
1030
|
let anyAccepted = false;
|
|
1001
1031
|
let anyExecuted = false;
|
|
1002
1032
|
let anyExecFailed = false;
|
|
@@ -1197,6 +1227,9 @@ async function runCycleCore(deps, capture) {
|
|
|
1197
1227
|
realizedLossTodayMusd,
|
|
1198
1228
|
targetedPositionIds,
|
|
1199
1229
|
targetedOrderIds,
|
|
1230
|
+
pmEventsOpenedThisCycle,
|
|
1231
|
+
pmCoinHorizonsOpenedThisCycle,
|
|
1232
|
+
nowMs: Date.now(),
|
|
1200
1233
|
};
|
|
1201
1234
|
const v = validateAction(action, ctx);
|
|
1202
1235
|
if (!v.valid) {
|
|
@@ -1233,6 +1266,17 @@ async function runCycleCore(deps, capture) {
|
|
|
1233
1266
|
if (action.type === "spot_cancel") {
|
|
1234
1267
|
targetedOrderIds.push(action.orderId);
|
|
1235
1268
|
}
|
|
1269
|
+
if (action.type === "pm_open") {
|
|
1270
|
+
pmEventsOpenedThisCycle.push(pmEventKey(action.source, action.slug));
|
|
1271
|
+
const opened = observation.pmMarkets.find((m) => m.source === action.source.toLowerCase() &&
|
|
1272
|
+
m.slug === action.slug.toLowerCase() &&
|
|
1273
|
+
m.outcomeExternalMarketId === action.outcomeExternalMarketId);
|
|
1274
|
+
const horizon = opened
|
|
1275
|
+
? pmCoinHorizonOf(opened, pmReferenceMs(observation.asOf, ctx.nowMs))
|
|
1276
|
+
: null;
|
|
1277
|
+
if (horizon)
|
|
1278
|
+
pmCoinHorizonsOpenedThisCycle.push(horizon);
|
|
1279
|
+
}
|
|
1236
1280
|
if (!live) {
|
|
1237
1281
|
planned.push({
|
|
1238
1282
|
action,
|
package/dist/agent/setups.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SignalThresholds } from "./signals.js";
|
|
1
2
|
import { OpenPosition, SetupSignal, WatchEntry } from "./types.js";
|
|
2
3
|
export declare function baseSymbol(s: string | undefined): string;
|
|
3
|
-
export declare function scanSetups(watch: WatchEntry[], openPositions?: OpenPosition[]): SetupSignal[];
|
|
4
|
+
export declare function scanSetups(watch: WatchEntry[], openPositions?: OpenPosition[], thresholds?: SignalThresholds): SetupSignal[];
|