@coinrithm/mcp-trading 0.7.6 → 0.7.8

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 (42) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +37 -8
  3. package/dist/agent/act.js +19 -6
  4. package/dist/agent/capitalSizing.d.ts +32 -0
  5. package/dist/agent/capitalSizing.js +257 -0
  6. package/dist/agent/client.d.ts +2 -0
  7. package/dist/agent/client.js +4 -0
  8. package/dist/agent/decision.d.ts +392 -0
  9. package/dist/agent/decision.js +177 -0
  10. package/dist/agent/decisionProbe.d.ts +17 -0
  11. package/dist/agent/decisionProbe.js +70 -0
  12. package/dist/agent/decisionReceipt.d.ts +45 -0
  13. package/dist/agent/decisionReceipt.js +595 -0
  14. package/dist/agent/decisionValidator.d.ts +19 -2
  15. package/dist/agent/decisionValidator.js +97 -3
  16. package/dist/agent/engine.d.ts +5 -1
  17. package/dist/agent/engine.js +8 -1
  18. package/dist/agent/observe.js +194 -35
  19. package/dist/agent/pmContext.d.ts +13 -0
  20. package/dist/agent/pmContext.js +136 -0
  21. package/dist/agent/prompt.d.ts +14 -2
  22. package/dist/agent/prompt.js +232 -35
  23. package/dist/agent/providerCapabilities.d.ts +23 -0
  24. package/dist/agent/providerCapabilities.js +105 -0
  25. package/dist/agent/providers.d.ts +29 -1
  26. package/dist/agent/providers.js +159 -88
  27. package/dist/agent/resolve.d.ts +1 -1
  28. package/dist/agent/resolve.js +21 -1
  29. package/dist/agent/runner.d.ts +4 -1
  30. package/dist/agent/runner.js +418 -47
  31. package/dist/agent/scorecard.js +7 -1
  32. package/dist/agent/skill.js +23 -0
  33. package/dist/agent/skillValidator.d.ts +1 -0
  34. package/dist/agent/skillValidator.js +63 -0
  35. package/dist/agent/state.js +7 -1
  36. package/dist/agent/strictLint.js +20 -0
  37. package/dist/agent/thesis.d.ts +40 -0
  38. package/dist/agent/thesis.js +319 -0
  39. package/dist/agent/types.d.ts +151 -0
  40. package/dist/http.js +21 -0
  41. package/dist/tools.js +10 -10
  42. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ import { DECISION_JSON_SCHEMA } from "./decision.js";
2
+ export const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
3
+ export const DECISION_TOOL_NAME = "submit_trading_decision";
4
+ // gpt-5*, o1/o3/o4* — the OpenAI reasoning-API family, wherever it is served.
5
+ const OPENAI_REASONING_MODEL = /^(gpt-5|o[0-9])/i;
6
+ const NEMOTRON_MODEL = /nemotron/i;
7
+ export function chatShapeFor(provider, model, baseUrl) {
8
+ if (provider === "anthropic") {
9
+ return {
10
+ family: "anthropic",
11
+ tokenParam: "max_tokens",
12
+ allowsTemperature: true,
13
+ jsonResponseFormat: false,
14
+ minProbeCompletionTokens: 1024,
15
+ };
16
+ }
17
+ if (provider === "openai" || OPENAI_REASONING_MODEL.test(model)) {
18
+ return {
19
+ family: "openai-reasoning",
20
+ tokenParam: "max_completion_tokens",
21
+ allowsTemperature: false,
22
+ jsonResponseFormat: true,
23
+ minProbeCompletionTokens: 1024,
24
+ };
25
+ }
26
+ if (NEMOTRON_MODEL.test(model)) {
27
+ const isNvidiaEndpoint = baseUrl === NVIDIA_BASE_URL;
28
+ return {
29
+ family: "nvidia-nemotron",
30
+ tokenParam: "max_tokens",
31
+ allowsTemperature: true,
32
+ jsonResponseFormat: true,
33
+ jsonSchema: isNvidiaEndpoint
34
+ ? DECISION_JSON_SCHEMA
35
+ : undefined,
36
+ // integrate.api.nvidia.com currently ignores both response_format
37
+ // json_schema and guided_json for these hosted models. Its forced tool
38
+ // call path is the live-probed contract-enforcing transport.
39
+ jsonSchemaTransport: isNvidiaEndpoint ? "tool_call" : undefined,
40
+ // The kwargs switch is only honored (and only safe to send) on the NVIDIA
41
+ // endpoint; the system hint helps on any endpoint serving a Nemotron.
42
+ extraBody: isNvidiaEndpoint
43
+ ? { chat_template_kwargs: { enable_thinking: false } }
44
+ : undefined,
45
+ systemHint: "detailed thinking off",
46
+ minProbeCompletionTokens: 1024,
47
+ };
48
+ }
49
+ return {
50
+ family: "openai-compat",
51
+ tokenParam: "max_tokens",
52
+ allowsTemperature: true,
53
+ jsonResponseFormat: true,
54
+ minProbeCompletionTokens: 1024,
55
+ };
56
+ }
57
+ /** Build the chat-completions body for a route from its capability shape. */
58
+ export function buildChatBody(shape, args) {
59
+ const system = shape.systemHint
60
+ ? `${shape.systemHint}\n\n${args.system}`
61
+ : args.system;
62
+ return {
63
+ model: args.model,
64
+ ...(shape.allowsTemperature
65
+ ? { temperature: args.temperature ?? 0.2 }
66
+ : {}),
67
+ [shape.tokenParam]: args.maxTokens,
68
+ ...(shape.jsonSchema && shape.jsonSchemaTransport === "tool_call"
69
+ ? {
70
+ tools: [
71
+ {
72
+ type: "function",
73
+ function: {
74
+ name: DECISION_TOOL_NAME,
75
+ description: "Submit the complete CoinRithm paper-trading decision for this cycle.",
76
+ parameters: shape.jsonSchema,
77
+ },
78
+ },
79
+ ],
80
+ tool_choice: {
81
+ type: "function",
82
+ function: { name: DECISION_TOOL_NAME },
83
+ },
84
+ }
85
+ : {}),
86
+ ...(shape.jsonResponseFormat && shape.jsonSchemaTransport !== "tool_call"
87
+ ? {
88
+ response_format: shape.jsonSchema
89
+ ? {
90
+ type: "json_schema",
91
+ json_schema: {
92
+ name: "coinrithm_trading_decision",
93
+ schema: shape.jsonSchema,
94
+ },
95
+ }
96
+ : { type: "json_object" },
97
+ }
98
+ : {}),
99
+ ...(shape.extraBody ?? {}),
100
+ messages: [
101
+ { role: "system", content: system },
102
+ { role: "user", content: args.user },
103
+ ],
104
+ };
105
+ }
@@ -1,10 +1,28 @@
1
- import { AgentSpec } from "./types.js";
1
+ import { AgentSpec, ProviderName } from "./types.js";
2
2
  export interface DecideInput {
3
3
  system: string;
4
4
  user: string;
5
5
  maxTokens?: number;
6
6
  timeoutMs?: number;
7
7
  }
8
+ export interface DecideRouteAttempt {
9
+ provider: string;
10
+ model: string;
11
+ outcome: "success" | "failed" | "deferred";
12
+ failureClass?: "capacity" | "permanent" | "transient" | "malformed";
13
+ status?: number;
14
+ retryAfterMs?: number;
15
+ latencyMs: number;
16
+ error?: string;
17
+ }
18
+ export interface DecideRouteMeta {
19
+ policyVersion: string;
20
+ profile: "fast" | "strong" | "configured";
21
+ effectiveProvider?: string;
22
+ effectiveModel?: string;
23
+ reason: "configured" | "circuit_fallback" | "capacity_fallback" | "provider_fallback" | "malformed_fallback" | "byo";
24
+ attempts: DecideRouteAttempt[];
25
+ }
8
26
  export type DecideResult = {
9
27
  ok: true;
10
28
  text: string;
@@ -12,9 +30,14 @@ export type DecideResult = {
12
30
  promptTokens: number;
13
31
  completionTokens: number;
14
32
  };
33
+ route?: DecideRouteMeta;
15
34
  } | {
16
35
  ok: false;
17
36
  error: string;
37
+ status?: number;
38
+ retryAfterMs?: number;
39
+ deferred?: boolean;
40
+ route?: DecideRouteMeta;
18
41
  };
19
42
  export interface Provider {
20
43
  label: string;
@@ -29,3 +52,8 @@ export interface ProviderEnv {
29
52
  MODEL_API_KEY?: string;
30
53
  }
31
54
  export declare function selectProvider(spec: AgentSpec, env: ProviderEnv, fetchFn?: typeof fetch): Provider;
55
+ export declare function providerForRoute(route: {
56
+ provider: ProviderName;
57
+ model: string;
58
+ baseUrl?: string | null;
59
+ }, apiKey: string, fetchFn?: typeof fetch): Provider;
@@ -2,9 +2,10 @@
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 { chatShapeFor, buildChatBody, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
5
6
  // NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
6
7
  // endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
7
- const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
8
+ const NVIDIA_BASE_URL = CAP_NVIDIA_BASE_URL;
8
9
  // Gemini exposes an OpenAI-compatible surface, so the `gemini` preset hard-wires
9
10
  // its hosted endpoint — an agent only needs `{ provider: gemini, name: "gemini-2.0-flash" }`
10
11
  // plus a GEMINI_API_KEY. The free tier (no credit card, generous Flash quota) makes
@@ -26,23 +27,26 @@ const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai
26
27
  // (the recurring Leo/70B timeout). A real hang still aborts -> retried next cadence.
27
28
  // MUST stay below the scheduler's RUN_LOCK_SECONDS and HEARTBEAT_STALE_MS.
28
29
  const DEFAULT_TIMEOUT_MS = 300_000;
29
- // Reasoning models (NVIDIA Nemotron) DEFAULT to emitting a long <think> chain:
30
- // measured ~30-60s/call and a JSON-leak risk. The documented toggle is a
31
- // "detailed thinking off" line in the system prompt, which drops them to
32
- // instruct mode (measured ~3-4s, clean JSON). Apply it automatically for any
33
- // nemotron model so a per-cadence decision never blows the cadence.
34
- function applyReasoningToggle(model, system) {
35
- return /nemotron/i.test(model)
36
- ? `detailed thinking off\n\n${system}`
37
- : system;
38
- }
39
- // fetch with a hard timeout via AbortController. A custom fetchFn (tests) that
40
- // ignores `signal` still works — the timer just never fires for it.
41
- async function fetchWithTimeout(fetchFn, url, init, timeoutMs) {
30
+ // Per-route request quirks (reasoning toggles, token param, temperature) live
31
+ // in the capability table — providerCapabilities.ts is the single source; this
32
+ // module only assembles and sends.
33
+ // One deadline covers both headers AND response-body consumption. fetch resolves
34
+ // at headers, so clearing a fetch-only timer there leaves text/json unbounded.
35
+ // Abort native I/O and race the deadline as well: an injected implementation that
36
+ // ignores AbortSignal must still release the caller rather than its run lock.
37
+ async function withProviderTimeout(timeoutMs, operation) {
42
38
  const controller = new AbortController();
43
- const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ let timer;
40
+ const deadline = new Promise((_resolve, reject) => {
41
+ timer = setTimeout(() => {
42
+ reject(Object.assign(new Error("model deadline exceeded"), {
43
+ name: "AbortError",
44
+ }));
45
+ controller.abort();
46
+ }, timeoutMs);
47
+ });
44
48
  try {
45
- return await fetchFn(url, { ...init, signal: controller.signal });
49
+ return await Promise.race([operation(controller.signal), deadline]);
46
50
  }
47
51
  finally {
48
52
  clearTimeout(timer);
@@ -54,6 +58,23 @@ function callError(err, timeoutMs) {
54
58
  }
55
59
  return err instanceof Error ? err.message : String(err);
56
60
  }
61
+ // Parse a Retry-After header (delta-seconds or HTTP-date) into ms, capped at
62
+ // one hour — a provider asking for more is treated as "an hour, then re-probe".
63
+ const RETRY_AFTER_CAP_MS = 3_600_000;
64
+ function retryAfterMs(res) {
65
+ const raw = res.headers.get("retry-after");
66
+ if (!raw)
67
+ return undefined;
68
+ const secs = Number(raw);
69
+ if (Number.isFinite(secs) && secs >= 0) {
70
+ return Math.min(Math.round(secs * 1000), RETRY_AFTER_CAP_MS);
71
+ }
72
+ const at = Date.parse(raw);
73
+ if (!Number.isFinite(at))
74
+ return undefined;
75
+ const ms = at - Date.now();
76
+ return ms > 0 ? Math.min(ms, RETRY_AFTER_CAP_MS) : 0;
77
+ }
57
78
  function envKey(provider, env) {
58
79
  switch (provider) {
59
80
  case "anthropic":
@@ -120,52 +141,73 @@ class AnthropicProvider {
120
141
  }
121
142
  async decide(input) {
122
143
  const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
144
+ let failureResponse;
123
145
  try {
124
- const res = await fetchWithTimeout(this.fetchFn, "https://api.anthropic.com/v1/messages", {
125
- method: "POST",
126
- headers: {
127
- "x-api-key": this.apiKey,
128
- "anthropic-version": "2023-06-01",
129
- "content-type": "application/json",
130
- },
131
- body: JSON.stringify({
132
- model: this.model,
133
- max_tokens: input.maxTokens ?? 1024,
134
- system: input.system,
135
- messages: [{ role: "user", content: input.user }],
136
- }),
137
- }, timeoutMs);
138
- if (!res.ok)
139
- return {
140
- ok: false,
141
- // Cap the upstream body: it lands in agent_cycles.skip_reason, so an
142
- // unbounded provider error page must not bloat the ledger row.
143
- error: `anthropic HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
144
- };
145
- const json = (await res.json());
146
- const text = json.content?.map((c) => c.text ?? "").join("") ?? "";
147
- const usage = json.usage
148
- ? {
149
- promptTokens: json.usage.input_tokens ?? 0,
150
- completionTokens: json.usage.output_tokens ?? 0,
146
+ return await withProviderTimeout(timeoutMs, async (signal) => {
147
+ const res = await this.fetchFn("https://api.anthropic.com/v1/messages", {
148
+ method: "POST",
149
+ signal,
150
+ headers: {
151
+ "x-api-key": this.apiKey,
152
+ "anthropic-version": "2023-06-01",
153
+ "content-type": "application/json",
154
+ },
155
+ body: JSON.stringify({
156
+ model: this.model,
157
+ max_tokens: input.maxTokens ?? 1024,
158
+ system: input.system,
159
+ messages: [{ role: "user", content: input.user }],
160
+ }),
161
+ });
162
+ if (!res.ok) {
163
+ failureResponse = res;
164
+ return {
165
+ ok: false,
166
+ // Cap the upstream body: it lands in agent_cycles.skip_reason, so an
167
+ // unbounded provider error page must not bloat the ledger row.
168
+ error: `anthropic HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
169
+ status: res.status,
170
+ retryAfterMs: retryAfterMs(res),
171
+ };
151
172
  }
152
- : undefined;
153
- return text
154
- ? { ok: true, text, usage }
155
- : { ok: false, error: "anthropic returned empty content" };
173
+ const json = (await res.json());
174
+ const text = json.content?.map((c) => c.text ?? "").join("") ?? "";
175
+ const usage = json.usage
176
+ ? {
177
+ promptTokens: json.usage.input_tokens ?? 0,
178
+ completionTokens: json.usage.output_tokens ?? 0,
179
+ }
180
+ : undefined;
181
+ return text
182
+ ? { ok: true, text, usage }
183
+ : { ok: false, error: "anthropic returned empty content" };
184
+ });
156
185
  }
157
186
  catch (err) {
158
- return { ok: false, error: callError(err, timeoutMs) };
187
+ return {
188
+ ok: false,
189
+ error: failureResponse
190
+ ? `anthropic HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
191
+ : callError(err, timeoutMs),
192
+ ...(failureResponse
193
+ ? {
194
+ status: failureResponse.status,
195
+ retryAfterMs: retryAfterMs(failureResponse),
196
+ }
197
+ : {}),
198
+ };
159
199
  }
160
200
  }
161
201
  }
162
202
  class OpenAiCompatProvider {
203
+ provider;
163
204
  model;
164
205
  apiKey;
165
206
  baseUrl;
166
207
  fetchFn;
167
208
  label;
168
- constructor(model, apiKey, baseUrl, fetchFn) {
209
+ constructor(provider, model, apiKey, baseUrl, fetchFn) {
210
+ this.provider = provider;
169
211
  this.model = model;
170
212
  this.apiKey = apiKey;
171
213
  this.baseUrl = baseUrl;
@@ -174,48 +216,63 @@ class OpenAiCompatProvider {
174
216
  }
175
217
  async decide(input) {
176
218
  const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
219
+ const shape = chatShapeFor(this.provider, this.model, this.baseUrl);
220
+ let failureResponse;
177
221
  try {
178
- const res = await fetchWithTimeout(this.fetchFn, `${this.baseUrl}/chat/completions`, {
179
- method: "POST",
180
- headers: {
181
- Authorization: `Bearer ${this.apiKey}`,
182
- "content-type": "application/json",
183
- },
184
- body: JSON.stringify({
185
- model: this.model,
186
- temperature: 0.2,
187
- max_tokens: input.maxTokens ?? 1024,
188
- response_format: { type: "json_object" },
189
- messages: [
190
- {
191
- role: "system",
192
- content: applyReasoningToggle(this.model, input.system),
193
- },
194
- { role: "user", content: input.user },
195
- ],
196
- }),
197
- }, timeoutMs);
198
- if (!res.ok)
199
- return {
200
- ok: false,
201
- // Cap the upstream body: it lands in agent_cycles.skip_reason, so an
202
- // unbounded provider error page must not bloat the ledger row.
203
- error: `provider HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
204
- };
205
- const json = (await res.json());
206
- const text = json.choices?.[0]?.message?.content ?? "";
207
- const usage = json.usage
208
- ? {
209
- promptTokens: json.usage.prompt_tokens ?? 0,
210
- completionTokens: json.usage.completion_tokens ?? 0,
222
+ return await withProviderTimeout(timeoutMs, async (signal) => {
223
+ const res = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
224
+ method: "POST",
225
+ signal,
226
+ headers: {
227
+ Authorization: `Bearer ${this.apiKey}`,
228
+ "content-type": "application/json",
229
+ },
230
+ body: JSON.stringify(buildChatBody(shape, {
231
+ model: this.model,
232
+ system: input.system,
233
+ user: input.user,
234
+ maxTokens: input.maxTokens ?? 1024,
235
+ })),
236
+ });
237
+ if (!res.ok) {
238
+ failureResponse = res;
239
+ return {
240
+ ok: false,
241
+ // Cap the upstream body: it lands in agent_cycles.skip_reason, so an
242
+ // unbounded provider error page must not bloat the ledger row.
243
+ error: `provider HTTP ${res.status}: ${(await res.text()).slice(0, 2000)}`,
244
+ status: res.status,
245
+ retryAfterMs: retryAfterMs(res),
246
+ };
211
247
  }
212
- : undefined;
213
- return text
214
- ? { ok: true, text, usage }
215
- : { ok: false, error: "provider returned empty content" };
248
+ const json = (await res.json());
249
+ const message = json.choices?.[0]?.message;
250
+ const decisionArguments = message?.tool_calls?.find((call) => call.function?.name === DECISION_TOOL_NAME)?.function?.arguments;
251
+ const text = decisionArguments ?? message?.content ?? "";
252
+ const usage = json.usage
253
+ ? {
254
+ promptTokens: json.usage.prompt_tokens ?? 0,
255
+ completionTokens: json.usage.completion_tokens ?? 0,
256
+ }
257
+ : undefined;
258
+ return text
259
+ ? { ok: true, text, usage }
260
+ : { ok: false, error: "provider returned empty content" };
261
+ });
216
262
  }
217
263
  catch (err) {
218
- return { ok: false, error: callError(err, timeoutMs) };
264
+ return {
265
+ ok: false,
266
+ error: failureResponse
267
+ ? `provider HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
268
+ : callError(err, timeoutMs),
269
+ ...(failureResponse
270
+ ? {
271
+ status: failureResponse.status,
272
+ retryAfterMs: retryAfterMs(failureResponse),
273
+ }
274
+ : {}),
275
+ };
219
276
  }
220
277
  }
221
278
  }
@@ -251,5 +308,19 @@ export function selectProvider(spec, env, fetchFn = fetch) {
251
308
  if (!resolvedBase) {
252
309
  throw new Error("openai-compatible provider needs model.baseUrl");
253
310
  }
254
- return new OpenAiCompatProvider(name, key, resolvedBase, fetchFn);
311
+ return new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn);
312
+ }
313
+ // Build a provider for an EXPLICIT route + raw key (no spec, no env) — the
314
+ // decision probe's entry point. Same classes as selectProvider, so a probe
315
+ // exercises byte-identical request shapes to a real cycle.
316
+ export function providerForRoute(route, apiKey, fetchFn = fetch) {
317
+ if (route.provider === "mechanical")
318
+ return new MechanicalProvider(route.model);
319
+ if (route.provider === "anthropic")
320
+ return new AnthropicProvider(route.model, apiKey, fetchFn);
321
+ const resolvedBase = baseUrlFor(route.provider, route.baseUrl ?? undefined);
322
+ if (!resolvedBase) {
323
+ throw new Error("openai-compatible route needs a baseUrl");
324
+ }
325
+ return new OpenAiCompatProvider(route.provider, route.model, apiKey, resolvedBase, fetchFn);
255
326
  }
@@ -14,7 +14,7 @@ export declare function mergeProseParts(parts: Array<{
14
14
  text: string;
15
15
  }>): string;
16
16
  export declare function resolveAgent(inputPath: string): ResolvedAgent;
17
- export declare const HOSTED_PROSE_MAX_CHARS = 8000;
17
+ export declare const HOSTED_PROSE_MAX_CHARS = 12000;
18
18
  /** PURE — exported for tests. Mirrors the backend's trim-then-measure. */
19
19
  export declare const hostedProseBudget: (mergedProse: string) => {
20
20
  used: number;
@@ -36,6 +36,7 @@ const CONFIG_BLOCKS = [
36
36
  "venues",
37
37
  "risk",
38
38
  "sizing",
39
+ "capitalSizing",
39
40
  "limits",
40
41
  "abstention",
41
42
  "sync",
@@ -592,7 +593,26 @@ export function resolveAgent(inputPath) {
592
593
  // tokens / 413s per cycle. Mirrored here (backend-v2
593
594
  // controllers/agentManage.ts sanitizeStrategyProse) so `validate --hosted`
594
595
  // can catch it before a user does.
595
- export const HOSTED_PROSE_MAX_CHARS = 8000;
596
+ //
597
+ // RAISED 8,000 -> 12,000 on 2026-08-21, from measurement rather than feel.
598
+ // 8,000 made the product's core promise impossible: forking a house template
599
+ // starts you at 7,967 (Olivia) / 7,931 (Carl) / 7,839 (Mia), so a user had
600
+ // 33 to 161 characters to write their own rules in. "Fork a template and make
601
+ // it yours" could not be done.
602
+ //
603
+ // The cap was justified by hosted inference cost. Measured over 8,060 LLM
604
+ // cycles in 24h on prod: average input is 9,038 tokens, of which the prose is
605
+ // only 12.6-40.2% (median ~25%) — the OBSERVATION is the other ~75%. Inputs
606
+ // already reached 17,065 tokens on Llama 3.1 8B and 16,437 on Nemotron 49B
607
+ // (measured on the since-retired NIM line; Nemotron 3 successors match), with
608
+ // ZERO rate-limit errors and estimated_cost_usd of 0.0000 (free NIM tier).
609
+ // +4,000 characters is ~+1,000 tokens/cycle (+11%), landing average input near
610
+ // 10,038 — still below what the fleet already handles at peak today.
611
+ //
612
+ // Self-host is deliberately NOT capped (runner.ts/prompt.ts enforce nothing):
613
+ // those agents run on the user's own model key, so their prompt size costs us
614
+ // nothing. This limit exists only where WE pay for the inference.
615
+ export const HOSTED_PROSE_MAX_CHARS = 12000;
596
616
  /** PURE — exported for tests. Mirrors the backend's trim-then-measure. */
597
617
  export const hostedProseBudget = (mergedProse) => {
598
618
  const used = mergedProse.replace(/\r\n/g, "\n").trim().length;
@@ -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
  };