@coinrithm/mcp-trading 0.7.7 → 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.
@@ -3,6 +3,7 @@
3
3
  // JSON, an unknown action type, a free-form endpoint/tool name, extra unknown
4
4
  // fields, or a missing required field — fails closed (the runner skips).
5
5
  import { z } from "zod";
6
+ import { coerceThesis } from "./thesis.js";
6
7
  // Small models (especially Llama 3.1 8B) frequently emit numbers as JSON strings
7
8
  // ("12345", "0.8", "62000"). Coerce a *clean* numeric string to a number before
8
9
  // validating; leave anything else untouched so genuine garbage ("abc", "pos#5")
@@ -31,6 +32,15 @@ const forecastProbability = z
31
32
  return typeof n === "number" && Number.isFinite(n) ? n : undefined;
32
33
  })
33
34
  .optional();
35
+ // The thesis an OPEN is made on (slice 2, 2026-09-02): {summary, invalidation}.
36
+ // TOLERANT like forecastProbability: a missing / malformed thesis becomes
37
+ // undefined and never fails an otherwise-valid open. The prompt requires it and
38
+ // the structured-output schema below lists it as required; the runner binds it
39
+ // to the position the server returns and evaluates it every cycle.
40
+ const thesis = z
41
+ .any()
42
+ .transform((v) => coerceThesis(v))
43
+ .optional();
34
44
  const futuresOpen = z
35
45
  .object({
36
46
  type: z.literal("futures_open"),
@@ -42,6 +52,7 @@ const futuresOpen = z
42
52
  takeProfitPrice: num(z.number()).nullable().optional(),
43
53
  confidence,
44
54
  rationaleSummary: z.string().optional(),
55
+ thesis,
45
56
  })
46
57
  .strict();
47
58
  const futuresClose = z
@@ -51,6 +62,11 @@ const futuresClose = z
51
62
  fraction: num(z.number().positive().max(1)).optional(),
52
63
  confidence,
53
64
  rationaleSummary: z.string().optional(),
65
+ // Accepted-and-unused: models copy the open-side `thesis` onto every
66
+ // action exactly as they copied `confidence` (the .strict() fail-closed
67
+ // lesson of 2026-08-19). The structured-output schema never asks for it
68
+ // here; a BYO model that echoes it must not zero the whole cycle.
69
+ thesis,
54
70
  })
55
71
  .strict();
56
72
  const futuresSetSltp = z
@@ -66,6 +82,7 @@ const futuresSetSltp = z
66
82
  // trade actions already have.
67
83
  confidence,
68
84
  rationaleSummary: z.string().optional(),
85
+ thesis, // accepted-and-unused, see futures_close
69
86
  })
70
87
  .strict();
71
88
  const spotOrder = z
@@ -79,6 +96,7 @@ const spotOrder = z
79
96
  stopPrice: num(z.number().positive()).optional(),
80
97
  confidence,
81
98
  rationaleSummary: z.string().optional(),
99
+ thesis,
82
100
  })
83
101
  .strict();
84
102
  const spotCancel = z
@@ -88,6 +106,7 @@ const spotCancel = z
88
106
  // Same accepted-and-unused tolerance as futures_set_sltp above.
89
107
  confidence,
90
108
  rationaleSummary: z.string().optional(),
109
+ thesis,
91
110
  })
92
111
  .strict();
93
112
  // pm_open accepts EITHER a short ref (pm1…pmN, what the prompt now asks for) OR
@@ -107,6 +126,7 @@ const pmOpen = z
107
126
  confidence,
108
127
  rationaleSummary: z.string().optional(),
109
128
  forecastProbability,
129
+ thesis,
110
130
  })
111
131
  .strict();
112
132
  export const actionSchema = z.discriminatedUnion("type", [
@@ -117,6 +137,163 @@ export const actionSchema = z.discriminatedUnion("type", [
117
137
  spotCancel,
118
138
  pmOpen,
119
139
  ]);
140
+ // Request-side structured-output contract for providers that support JSON
141
+ // Schema. Keep this beside the Zod parser: the provider must not be allowed to
142
+ // emit an `act` with no actions (the Nano incident on 2026-08-27), while a real
143
+ // skip must carry an empty action list. The runner still parses with Zod and
144
+ // re-validates every action against live quotes/caps before any write.
145
+ //
146
+ // `pm_open` deliberately requires the short per-cycle `ref` here. The parser
147
+ // remains backwards-compatible with the full source/slug/id triple, but the
148
+ // hosted prompt asks models for refs and that is the only reliable generated
149
+ // shape.
150
+ const nullableNumber = { anyOf: [{ type: "number" }, { type: "null" }] };
151
+ const optionalConfidence = { type: "number", minimum: 0, maximum: 1 };
152
+ const optionalRationaleSummary = { type: "string" };
153
+ // Thesis contract for the structured-output transports (the NVIDIA endpoint
154
+ // forces the tool call from this schema, so a field absent here can never be
155
+ // emitted). One shared shape; the runner drops the fields that do not apply to
156
+ // the venue and re-signs nothing.
157
+ const thesisSchema = {
158
+ type: "object",
159
+ properties: {
160
+ summary: { type: "string", maxLength: 200 },
161
+ invalidation: {
162
+ type: "object",
163
+ properties: {
164
+ priceBelow: { type: "number", exclusiveMinimum: 0 },
165
+ priceAbove: { type: "number", exclusiveMinimum: 0 },
166
+ probabilityBelow: { type: "number", exclusiveMinimum: 0, maximum: 100 },
167
+ probabilityAbove: { type: "number", exclusiveMinimum: 0, maximum: 100 },
168
+ maxHoldMinutes: { type: "number", exclusiveMinimum: 0 },
169
+ catalyst: { type: "string", maxLength: 160 },
170
+ },
171
+ additionalProperties: false,
172
+ },
173
+ },
174
+ required: ["summary", "invalidation"],
175
+ additionalProperties: false,
176
+ };
177
+ const generatedActionSchemas = [
178
+ {
179
+ type: "object",
180
+ properties: {
181
+ type: { const: "futures_open" },
182
+ symbol: { type: "string", minLength: 1 },
183
+ side: { enum: ["long", "short"] },
184
+ leverage: { type: "number", exclusiveMinimum: 0 },
185
+ marginMusd: { type: "number", exclusiveMinimum: 0 },
186
+ stopLossPrice: nullableNumber,
187
+ takeProfitPrice: nullableNumber,
188
+ confidence: optionalConfidence,
189
+ rationaleSummary: optionalRationaleSummary,
190
+ thesis: thesisSchema,
191
+ },
192
+ required: ["type", "symbol", "side", "leverage", "marginMusd", "thesis"],
193
+ additionalProperties: false,
194
+ },
195
+ {
196
+ type: "object",
197
+ properties: {
198
+ type: { const: "futures_close" },
199
+ positionId: { type: "number" },
200
+ fraction: { type: "number", exclusiveMinimum: 0, maximum: 1 },
201
+ confidence: optionalConfidence,
202
+ rationaleSummary: optionalRationaleSummary,
203
+ },
204
+ required: ["type", "positionId"],
205
+ additionalProperties: false,
206
+ },
207
+ {
208
+ type: "object",
209
+ properties: {
210
+ type: { const: "futures_set_sltp" },
211
+ positionId: { type: "number" },
212
+ stopLossPrice: nullableNumber,
213
+ takeProfitPrice: nullableNumber,
214
+ confidence: optionalConfidence,
215
+ rationaleSummary: optionalRationaleSummary,
216
+ },
217
+ required: ["type", "positionId"],
218
+ additionalProperties: false,
219
+ },
220
+ {
221
+ type: "object",
222
+ properties: {
223
+ type: { const: "spot_order" },
224
+ symbol: { type: "string", minLength: 1 },
225
+ side: { enum: ["buy", "sell"] },
226
+ orderType: { enum: ["market", "limit", "stop"] },
227
+ quantity: { type: "number", exclusiveMinimum: 0 },
228
+ limitPrice: { type: "number", exclusiveMinimum: 0 },
229
+ stopPrice: { type: "number", exclusiveMinimum: 0 },
230
+ confidence: optionalConfidence,
231
+ rationaleSummary: optionalRationaleSummary,
232
+ thesis: thesisSchema,
233
+ },
234
+ required: ["type", "symbol", "side", "orderType", "quantity", "thesis"],
235
+ additionalProperties: false,
236
+ },
237
+ {
238
+ type: "object",
239
+ properties: {
240
+ type: { const: "spot_cancel" },
241
+ orderId: { type: "number" },
242
+ confidence: optionalConfidence,
243
+ rationaleSummary: optionalRationaleSummary,
244
+ },
245
+ required: ["type", "orderId"],
246
+ additionalProperties: false,
247
+ },
248
+ {
249
+ type: "object",
250
+ properties: {
251
+ type: { const: "pm_open" },
252
+ ref: { type: "string", minLength: 1 },
253
+ stakeMusd: { type: "number", exclusiveMinimum: 0 },
254
+ confidence: optionalConfidence,
255
+ rationaleSummary: optionalRationaleSummary,
256
+ forecastProbability: { type: "number" },
257
+ thesis: thesisSchema,
258
+ },
259
+ required: ["type", "ref", "stakeMusd", "thesis"],
260
+ additionalProperties: false,
261
+ },
262
+ ];
263
+ const generatedDecisionProperties = {
264
+ confidence: optionalConfidence,
265
+ reason: { type: "string" },
266
+ rationale: { type: "string", maxLength: 1200 },
267
+ };
268
+ export const DECISION_JSON_SCHEMA = {
269
+ type: "object",
270
+ properties: {
271
+ decision: { enum: ["skip", "act"] },
272
+ ...generatedDecisionProperties,
273
+ actions: {
274
+ type: "array",
275
+ items: { oneOf: generatedActionSchemas },
276
+ },
277
+ },
278
+ required: ["decision", "actions"],
279
+ additionalProperties: false,
280
+ allOf: [
281
+ {
282
+ if: {
283
+ properties: { decision: { const: "act" } },
284
+ required: ["decision"],
285
+ },
286
+ then: { properties: { actions: { minItems: 1 } } },
287
+ },
288
+ {
289
+ if: {
290
+ properties: { decision: { const: "skip" } },
291
+ required: ["decision"],
292
+ },
293
+ then: { properties: { actions: { maxItems: 0 } } },
294
+ },
295
+ ],
296
+ };
120
297
  const decisionSchema = z
121
298
  .object({
122
299
  decision: z.enum(["skip", "act"]),
@@ -0,0 +1,45 @@
1
+ import type { AgentSpec, Observation, RunState } from "./types.js";
2
+ import { type DailyRiskBudget } from "./prompt.js";
3
+ export declare const DECISION_INPUT_MAX_BYTES: number;
4
+ export type DecisionInputPhase = "before_observation" | "observed" | "decision_input";
5
+ type Scalar = string | number | boolean | null;
6
+ type Row = Record<string, Scalar | string[] | Record<string, Scalar | string[]>>;
7
+ export interface DecisionInputRecord {
8
+ version: "coinrithm.decision-input.v1";
9
+ visibility: "private";
10
+ completeness: "partial";
11
+ phase: DecisionInputPhase;
12
+ outcome: "pending" | "returned" | "runtime_error";
13
+ runId: string | null;
14
+ decisionId: string | null;
15
+ configFingerprint: string | null;
16
+ observationFingerprint: string | null;
17
+ preThesisObservationFingerprint: string | null;
18
+ dailyRiskBudget: DailyRiskBudget | null;
19
+ guardState: Record<string, number | boolean | null>;
20
+ account: Record<string, number | boolean | string | null> | null;
21
+ lists: Record<string, Row[]>;
22
+ counts: Record<string, {
23
+ source: number;
24
+ retained: number;
25
+ omitted: number;
26
+ }>;
27
+ omissions: string[];
28
+ }
29
+ export interface DecisionInputCapture {
30
+ phase: DecisionInputPhase;
31
+ runId: string;
32
+ decisionId: string;
33
+ spec: AgentSpec;
34
+ mergedProse: string;
35
+ state: RunState;
36
+ observation?: Observation;
37
+ observationFingerprint?: string;
38
+ preThesisObservationFingerprint?: string;
39
+ }
40
+ export declare function buildDecisionInputRecord(input: DecisionInputCapture): DecisionInputRecord;
41
+ export declare function unavailableDecisionInputRecord(): DecisionInputRecord;
42
+ /** Defense at the storage boundary. Reject rather than preserve an arbitrary
43
+ * extension field. Detached JSON copy also prevents post-check mutation. */
44
+ export declare function sanitizeDecisionInputRecord(value: unknown): DecisionInputRecord | undefined;
45
+ export {};