@coinrithm/mcp-trading 0.7.14 → 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.
Files changed (72) hide show
  1. package/CHANGELOG.md +811 -613
  2. package/README.md +415 -366
  3. package/dist/agent/bench/bench.d.ts +59 -0
  4. package/dist/agent/bench/bench.js +319 -0
  5. package/dist/agent/bench/cassette.d.ts +97 -0
  6. package/dist/agent/bench/cassette.js +181 -0
  7. package/dist/agent/bench/costs.d.ts +28 -0
  8. package/dist/agent/bench/costs.js +60 -0
  9. package/dist/agent/bench/labelBuilder.d.ts +123 -0
  10. package/dist/agent/bench/labelBuilder.js +282 -0
  11. package/dist/agent/bench/labels.d.ts +75 -0
  12. package/dist/agent/bench/labels.js +259 -0
  13. package/dist/agent/bench/recordingClient.d.ts +51 -0
  14. package/dist/agent/bench/recordingClient.js +193 -0
  15. package/dist/agent/bench/replayClient.d.ts +41 -0
  16. package/dist/agent/bench/replayClient.js +262 -0
  17. package/dist/agent/bench/report.d.ts +115 -0
  18. package/dist/agent/bench/report.js +435 -0
  19. package/dist/agent/cli.d.ts +25 -0
  20. package/dist/agent/cli.js +300 -2
  21. package/dist/agent/client.d.ts +14 -0
  22. package/dist/agent/client.js +113 -4
  23. package/dist/agent/configurationWarnings.js +11 -0
  24. package/dist/agent/decision.d.ts +3 -0
  25. package/dist/agent/decision.js +85 -3
  26. package/dist/agent/decisionReceipt.d.ts +5 -1
  27. package/dist/agent/decisionReceipt.js +52 -3
  28. package/dist/agent/decisionValidator.d.ts +30 -0
  29. package/dist/agent/decisionValidator.js +147 -2
  30. package/dist/agent/engine.d.ts +3 -3
  31. package/dist/agent/engine.js +3 -3
  32. package/dist/agent/futuresEligibility.d.ts +36 -0
  33. package/dist/agent/futuresEligibility.js +90 -0
  34. package/dist/agent/observe.d.ts +35 -2
  35. package/dist/agent/observe.js +969 -128
  36. package/dist/agent/pmContext.d.ts +10 -1
  37. package/dist/agent/pmContext.js +163 -1
  38. package/dist/agent/prompt.d.ts +18 -1
  39. package/dist/agent/prompt.js +229 -33
  40. package/dist/agent/promptTables.d.ts +1 -0
  41. package/dist/agent/promptTables.js +33 -0
  42. package/dist/agent/providerCapabilities.d.ts +9 -0
  43. package/dist/agent/providerCapabilities.js +57 -4
  44. package/dist/agent/providers.d.ts +28 -7
  45. package/dist/agent/providers.js +200 -50
  46. package/dist/agent/resolve.js +3 -0
  47. package/dist/agent/runner.d.ts +8 -0
  48. package/dist/agent/runner.js +159 -26
  49. package/dist/agent/setups.d.ts +2 -1
  50. package/dist/agent/setups.js +17 -16
  51. package/dist/agent/signals.d.ts +33 -0
  52. package/dist/agent/signals.js +89 -0
  53. package/dist/agent/skill.js +37 -0
  54. package/dist/agent/skillValidator.js +33 -0
  55. package/dist/agent/strictLint.js +29 -0
  56. package/dist/agent/templates.js +30 -30
  57. package/dist/agent/types.d.ts +266 -13
  58. package/dist/agent/types.js +4 -0
  59. package/dist/agent/universe.d.ts +78 -0
  60. package/dist/agent/universe.js +215 -0
  61. package/dist/client.d.ts +9 -2
  62. package/dist/client.js +22 -10
  63. package/dist/http.d.ts +1 -0
  64. package/dist/http.js +17 -2
  65. package/dist/httpCompletion.d.ts +1 -1
  66. package/dist/httpCompletion.js +1 -0
  67. package/dist/index.js +2 -1
  68. package/dist/instructions.d.ts +1 -0
  69. package/dist/instructions.js +16 -0
  70. package/dist/tools.d.ts +1 -0
  71. package/dist/tools.js +150 -66
  72. package/package.json +98 -97
@@ -1,6 +1,16 @@
1
1
  import { DECISION_JSON_SCHEMA } from "./decision.js";
2
2
  export const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
3
3
  export const DECISION_TOOL_NAME = "submit_trading_decision";
4
+ // A retry changes transport only after the strict parser has rejected a known
5
+ // Super tool-argument encoding failure. Valid responses and other routes keep
6
+ // their original request shape. A custom endpoint is deliberately excluded.
7
+ export function canRetrySuperToolOutput(route, responseSource, actionsStringDiagnostic) {
8
+ return (route.provider === "nvidia" &&
9
+ route.model === "nvidia/nemotron-3-super-120b-a12b" &&
10
+ (route.baseUrl == null || route.baseUrl === NVIDIA_BASE_URL) &&
11
+ responseSource === "tool_call" &&
12
+ actionsStringDiagnostic !== undefined);
13
+ }
4
14
  // gpt-5*, o1/o3/o4* — the OpenAI reasoning-API family, wherever it is served.
5
15
  const OPENAI_REASONING_MODEL = /^(gpt-5|o[0-9])/i;
6
16
  const NEMOTRON_MODEL = /nemotron/i;
@@ -54,18 +64,61 @@ export function chatShapeFor(provider, model, baseUrl) {
54
64
  minProbeCompletionTokens: 1024,
55
65
  };
56
66
  }
67
+ // Opt-in alternative for a hosted Nemotron route: plain JSON content
68
+ // (`response_format: json_object`) instead of the forced decision tool call.
69
+ // Hosted Super has returned the forced tool call with `actions` as a string
70
+ // (519 of 533 malformed attempts in a 24h sample, 2026-10-05). In an 8-call
71
+ // synthetic smoke pilot the tool-call arms failed 0/4 with the same coarse
72
+ // string shape and the JSON-content arm parsed 2/2; that is not a rate
73
+ // measurement and the exact serialization cause is not established. Only the
74
+ // request transport changes: parseDecision still validates the full contract.
75
+ export function withJsonContentTransport(shape) {
76
+ if (shape.family !== "nvidia-nemotron")
77
+ return shape;
78
+ return { ...shape, jsonSchema: undefined, jsonSchemaTransport: undefined };
79
+ }
80
+ // Transport-only restriction: drop the excluded variants from the action oneOf
81
+ // a route decodes against. parseDecision keeps validating the FULL contract, so
82
+ // a route that does not enforce a schema can still propose the action and the
83
+ // validator rejects it. Never returns an empty oneOf.
84
+ export function restrictDecisionSchema(schema, exclude) {
85
+ if (!exclude?.length)
86
+ return schema;
87
+ const properties = schema.properties;
88
+ const actions = properties?.actions;
89
+ const variants = actions?.items?.oneOf;
90
+ if (!properties || !actions || !Array.isArray(variants))
91
+ return schema;
92
+ const kept = variants.filter((variant) => {
93
+ const type = variant
94
+ .properties?.type?.const;
95
+ return !exclude.includes(type);
96
+ });
97
+ if (kept.length === variants.length || kept.length === 0)
98
+ return schema;
99
+ return {
100
+ ...schema,
101
+ properties: {
102
+ ...properties,
103
+ actions: { ...actions, items: { ...actions.items, oneOf: kept } },
104
+ },
105
+ };
106
+ }
57
107
  /** Build the chat-completions body for a route from its capability shape. */
58
108
  export function buildChatBody(shape, args) {
59
109
  const system = shape.systemHint
60
110
  ? `${shape.systemHint}\n\n${args.system}`
61
111
  : args.system;
112
+ const jsonSchema = shape.jsonSchema
113
+ ? restrictDecisionSchema(shape.jsonSchema, args.excludeActionTypes)
114
+ : undefined;
62
115
  return {
63
116
  model: args.model,
64
117
  ...(shape.allowsTemperature
65
118
  ? { temperature: args.temperature ?? 0.2 }
66
119
  : {}),
67
120
  [shape.tokenParam]: args.maxTokens,
68
- ...(shape.jsonSchema && shape.jsonSchemaTransport === "tool_call"
121
+ ...(jsonSchema && shape.jsonSchemaTransport === "tool_call"
69
122
  ? {
70
123
  tools: [
71
124
  {
@@ -73,7 +126,7 @@ export function buildChatBody(shape, args) {
73
126
  function: {
74
127
  name: DECISION_TOOL_NAME,
75
128
  description: "Submit the complete CoinRithm paper-trading decision for this cycle.",
76
- parameters: shape.jsonSchema,
129
+ parameters: jsonSchema,
77
130
  },
78
131
  },
79
132
  ],
@@ -85,12 +138,12 @@ export function buildChatBody(shape, args) {
85
138
  : {}),
86
139
  ...(shape.jsonResponseFormat && shape.jsonSchemaTransport !== "tool_call"
87
140
  ? {
88
- response_format: shape.jsonSchema
141
+ response_format: jsonSchema
89
142
  ? {
90
143
  type: "json_schema",
91
144
  json_schema: {
92
145
  name: "coinrithm_trading_decision",
93
- schema: shape.jsonSchema,
146
+ schema: jsonSchema,
94
147
  },
95
148
  }
96
149
  : { type: "json_object" },
@@ -1,9 +1,23 @@
1
1
  import { AgentSpec, ProviderName } from "./types.js";
2
+ import { type ActionsStringDiagnostic } from "./decision.js";
3
+ import { type DecisionActionExclusion } from "./providerCapabilities.js";
4
+ export interface ProviderUsage {
5
+ promptTokens: number;
6
+ completionTokens: number;
7
+ cacheReadTokens?: number;
8
+ cacheWriteTokens?: number;
9
+ cacheWrite5mTokens?: number;
10
+ cacheWrite1hTokens?: number;
11
+ }
12
+ export declare function anthropicUsage(raw: unknown): ProviderUsage | undefined;
2
13
  export interface DecideInput {
3
14
  system: string;
4
15
  user: string;
16
+ /** Optional equivalent presentation for explicitly verified hosted routes. */
17
+ compactUser?: string;
5
18
  maxTokens?: number;
6
19
  timeoutMs?: number;
20
+ excludeActionTypes?: readonly DecisionActionExclusion[];
7
21
  }
8
22
  export interface DecideRouteAttempt {
9
23
  provider: string;
@@ -14,6 +28,10 @@ export interface DecideRouteAttempt {
14
28
  retryAfterMs?: number;
15
29
  latencyMs: number;
16
30
  error?: string;
31
+ responseSource?: "tool_call" | "content_fallback" | "content";
32
+ actionsStringDiagnostic?: ActionsStringDiagnostic;
33
+ /** Scheduler admission categories, never free-form provider error text. */
34
+ admissionReasons?: Array<"request_budget" | "token_budget" | "concurrency" | "shared_key_cooldown" | "model_cooldown">;
17
35
  }
18
36
  export interface DecideRouteMeta {
19
37
  policyVersion: string;
@@ -26,14 +44,14 @@ export interface DecideRouteMeta {
26
44
  export type DecideResult = {
27
45
  ok: true;
28
46
  text: string;
29
- usage?: {
30
- promptTokens: number;
31
- completionTokens: number;
32
- };
47
+ responseSource?: "tool_call" | "content_fallback" | "content";
48
+ usage?: ProviderUsage;
33
49
  route?: DecideRouteMeta;
34
50
  } | {
35
51
  ok: false;
36
52
  error: string;
53
+ failureClass?: "malformed";
54
+ usage?: ProviderUsage;
37
55
  status?: number;
38
56
  retryAfterMs?: number;
39
57
  retryableHttpFailure?: true;
@@ -55,10 +73,13 @@ export interface ProviderEnv {
55
73
  /** Keep direct and routed cycle evidence consistent about NVIDIA backpressure. */
56
74
  export declare function classifyProviderFailure(result: Extract<DecideResult, {
57
75
  ok: false;
58
- }>): "capacity" | "permanent" | "transient";
59
- export declare function selectProvider(spec: AgentSpec, env: ProviderEnv, fetchFn?: typeof fetch): Provider;
76
+ }>): "capacity" | "permanent" | "transient" | "malformed";
77
+ export interface ProviderRouteOptions {
78
+ nemotronJsonContent?: boolean | (() => boolean);
79
+ }
80
+ export declare function selectProvider(spec: AgentSpec, env: ProviderEnv, fetchFn?: typeof fetch, options?: ProviderRouteOptions): Provider;
60
81
  export declare function providerForRoute(route: {
61
82
  provider: ProviderName;
62
83
  model: string;
63
84
  baseUrl?: string | null;
64
- }, apiKey: string, fetchFn?: typeof fetch): Provider;
85
+ }, apiKey: string, fetchFn?: typeof fetch, options?: ProviderRouteOptions): Provider;
@@ -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 { chatShapeFor, buildChatBody, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
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
- const usage = json.usage
190
- ? {
191
- promptTokens: json.usage.input_tokens ?? 0,
192
- completionTokens: json.usage.output_tokens ?? 0,
193
- }
194
- : undefined;
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
- ? { ok: true, text, usage }
197
- : { ok: false, error: "anthropic returned empty content" };
262
+ ? { ok: true, text, usage, responseSource: "content" }
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 shape = chatShapeFor(this.provider, this.model, this.baseUrl);
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) => {
@@ -246,6 +333,7 @@ class OpenAiCompatProvider {
246
333
  system: input.system,
247
334
  user: input.user,
248
335
  maxTokens: input.maxTokens ?? 1024,
336
+ excludeActionTypes: input.excludeActionTypes,
249
337
  })),
250
338
  });
251
339
  if (!res.ok) {
@@ -272,8 +360,25 @@ class OpenAiCompatProvider {
272
360
  completionTokens: json.usage.completion_tokens ?? 0,
273
361
  }
274
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
+ }
275
371
  return text
276
- ? { ok: true, text, usage }
372
+ ? {
373
+ ok: true,
374
+ text,
375
+ usage,
376
+ responseSource: decisionArguments != null
377
+ ? "tool_call"
378
+ : shape.jsonSchemaTransport === "tool_call"
379
+ ? "content_fallback"
380
+ : "content",
381
+ }
277
382
  : { ok: false, error: "provider returned empty content" };
278
383
  });
279
384
  }
@@ -293,20 +398,24 @@ class OpenAiCompatProvider {
293
398
  }
294
399
  }
295
400
  }
296
- // A direct NVIDIA 5xx previously lost the whole cycle until the next cadence.
297
- // Retry only an explicit server refusal, once, on the identical configured
298
- // route. The shared router owns its own attempt/capacity budget and MUST NOT
299
- // receive this wrapper. No auth/404/429, network, timeout or decision repair
300
- // retries; no model substitution or request-parameter changes.
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.
301
406
  class SameModelRetryProvider {
302
407
  delegate;
303
408
  provider;
304
409
  model;
410
+ contentRetry;
411
+ baseUrl;
305
412
  label;
306
- constructor(delegate, provider, model) {
413
+ constructor(delegate, provider, model, contentRetry, baseUrl) {
307
414
  this.delegate = delegate;
308
415
  this.provider = provider;
309
416
  this.model = model;
417
+ this.contentRetry = contentRetry;
418
+ this.baseUrl = baseUrl;
310
419
  this.label = delegate.label;
311
420
  }
312
421
  async decide(input) {
@@ -314,56 +423,95 @@ class SameModelRetryProvider {
314
423
  const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
315
424
  const first = await this.delegate.decide(input);
316
425
  const firstFinished = Date.now();
317
- if (first.ok || !first.retryableHttpFailure) {
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)) {
318
432
  return first;
319
433
  }
320
- const delayMs = Math.max(1000, first.retryAfterMs ?? 0);
434
+ const delayMs = retryContent
435
+ ? 0
436
+ : Math.max(1000, first.ok ? 0 : (first.retryAfterMs ?? 0));
321
437
  // Honor the provider's cooldown; a long one waits for the next cycle. Both
322
438
  // attempts and this backoff share the ORIGINAL deadline, never two 5m caps.
323
439
  if (delayMs > 5000 || delayMs >= timeoutMs - (firstFinished - started)) {
324
440
  return first;
325
441
  }
326
- await new Promise((resolve) => setTimeout(resolve, delayMs));
442
+ if (delayMs > 0)
443
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
327
444
  const retryStarted = Date.now();
328
445
  const remainingMs = timeoutMs - (retryStarted - started);
329
446
  if (remainingMs <= 0)
330
447
  return first;
331
- const result = await this.delegate.decide({
448
+ const retryResult = await (retryContent ? this.contentRetry : this.delegate).decide({
332
449
  ...input,
333
450
  timeoutMs: remainingMs,
334
451
  });
335
- const attempt = (res, latencyMs) => ({
336
- provider: this.provider,
337
- model: this.model,
338
- outcome: res.ok ? "success" : "failed",
339
- latencyMs,
340
- ...(!res.ok
341
- ? {
342
- error: res.error,
343
- status: res.status,
344
- retryAfterMs: res.retryAfterMs,
345
- failureClass: classifyProviderFailure(res) === "capacity"
346
- ? "capacity"
347
- : res.status !== undefined && res.status < 500
348
- ? "permanent"
349
- : "transient",
350
- }
351
- : {}),
352
- });
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;
353
503
  return {
354
- // Usage, when present, describes only the final response. The preceding
355
- // 5xx did not report usage; attempt evidence is not total billed tokens
356
- // or a certificate that returned text passed the trading-decision parser.
357
504
  ...result,
505
+ ...(usage ? { usage } : {}),
358
506
  route: {
359
- policyVersion: "coinrithm.configured-same-model-retry.v2",
507
+ policyVersion: "coinrithm.configured-same-model-retry.v3",
360
508
  profile: "configured",
361
509
  effectiveProvider: this.provider,
362
510
  effectiveModel: this.model,
363
511
  reason: "configured_direct",
364
512
  attempts: [
365
513
  attempt(first, firstFinished - started),
366
- attempt(result, Date.now() - retryStarted),
514
+ attempt(retryResult, Date.now() - retryStarted),
367
515
  ],
368
516
  },
369
517
  };
@@ -372,7 +520,7 @@ class SameModelRetryProvider {
372
520
  // Build the provider from the spec's model block + env key. Throws a clear
373
521
  // error if no model is configured (self-host requires one) or the env key is
374
522
  // missing. fetch is injectable for tests.
375
- export function selectProvider(spec, env, fetchFn = fetch) {
523
+ export function selectProvider(spec, env, fetchFn = fetch, options = {}) {
376
524
  if (!spec.model) {
377
525
  throw new Error("no model configured: set model.provider + model.name in the agent (self-host needs an explicit model)");
378
526
  }
@@ -401,15 +549,17 @@ export function selectProvider(spec, env, fetchFn = fetch) {
401
549
  if (!resolvedBase) {
402
550
  throw new Error("openai-compatible provider needs model.baseUrl");
403
551
  }
404
- const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn);
552
+ const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn, options);
405
553
  return provider === "nvidia"
406
- ? new SameModelRetryProvider(direct, provider, name)
554
+ ? new SameModelRetryProvider(direct, provider, name, new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn, {
555
+ nemotronJsonContent: true,
556
+ }), baseUrl)
407
557
  : direct;
408
558
  }
409
559
  // Build a provider for an EXPLICIT route + raw key (no spec, no env) — the
410
560
  // decision probe's entry point. Same classes as selectProvider, so a probe
411
561
  // exercises byte-identical request shapes to a real cycle.
412
- export function providerForRoute(route, apiKey, fetchFn = fetch) {
562
+ export function providerForRoute(route, apiKey, fetchFn = fetch, options = {}) {
413
563
  if (route.provider === "mechanical")
414
564
  return new MechanicalProvider(route.model);
415
565
  if (route.provider === "anthropic")
@@ -418,5 +568,5 @@ export function providerForRoute(route, apiKey, fetchFn = fetch) {
418
568
  if (!resolvedBase) {
419
569
  throw new Error("openai-compatible route needs a baseUrl");
420
570
  }
421
- return new OpenAiCompatProvider(route.provider, route.model, apiKey, resolvedBase, fetchFn);
571
+ return new OpenAiCompatProvider(route.provider, route.model, apiKey, resolvedBase, fetchFn, options);
422
572
  }
@@ -43,6 +43,9 @@ const CONFIG_BLOCKS = [
43
43
  "killSwitch",
44
44
  "objective",
45
45
  "capabilities",
46
+ "signals",
47
+ "data",
48
+ "universe",
46
49
  ];
47
50
  const JOURNAL_MAX_LINES = 200;
48
51
  const JOURNAL_MAX_BYTES = 8_000;
@@ -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;