@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.
- package/CHANGELOG.md +811 -613
- package/README.md +415 -366
- 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 +14 -0
- package/dist/agent/client.js +113 -4
- 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/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +52 -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/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/observe.d.ts +35 -2
- package/dist/agent/observe.js +969 -128
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +18 -1
- package/dist/agent/prompt.js +229 -33
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +9 -0
- package/dist/agent/providerCapabilities.js +57 -4
- package/dist/agent/providers.d.ts +28 -7
- package/dist/agent/providers.js +200 -50
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +159 -26
- 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/templates.js +30 -30
- package/dist/agent/types.d.ts +266 -13
- package/dist/agent/types.js +4 -0
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/dist/client.d.ts +9 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- 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
|
-
...(
|
|
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:
|
|
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:
|
|
141
|
+
response_format: jsonSchema
|
|
89
142
|
? {
|
|
90
143
|
type: "json_schema",
|
|
91
144
|
json_schema: {
|
|
92
145
|
name: "coinrithm_trading_decision",
|
|
93
|
-
schema:
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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;
|
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
|
-
? { ok: true, text, usage }
|
|
197
|
-
:
|
|
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
|
|
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
|
-
? {
|
|
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
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
// receive this wrapper. No
|
|
300
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
448
|
+
const retryResult = await (retryContent ? this.contentRetry : this.delegate).decide({
|
|
332
449
|
...input,
|
|
333
450
|
timeoutMs: remainingMs,
|
|
334
451
|
});
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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.
|
|
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(
|
|
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
|
}
|
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;
|