llm-chess-mcp 0.4.8 → 0.4.9
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/.env.example +1 -1
- package/README.md +4 -1
- package/dist/chess-copy.js +39 -0
- package/dist/chess.d.ts +1 -1
- package/dist/chess.js +1 -1
- package/dist/engines/stockfish.js +55 -12
- package/dist/explorer-core.d.ts +1 -0
- package/dist/explorer-core.js +1 -0
- package/dist/explorer-limiter.d.ts +2 -0
- package/dist/explorer-limiter.js +48 -7
- package/dist/explorer-response.js +3 -0
- package/dist/explorer-retry.js +13 -7
- package/dist/explorer.d.ts +2 -2
- package/dist/explorer.js +45 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/intents.js +74 -9
- package/dist/pgn.d.ts +2 -0
- package/dist/pgn.js +766 -21
- package/dist/services.js +13 -12
- package/dist/tool-inputs.js +36 -6
- package/dist/tool-schemas.js +100 -35
- package/docs/architecture.md +6 -1
- package/package.json +3 -3
package/.env.example
CHANGED
|
@@ -5,5 +5,5 @@ LICHESS_TOKEN=
|
|
|
5
5
|
# The published package bundles only Maia3 5m (~20 MB). Leave at 5m.
|
|
6
6
|
MAIA3_MODEL=5m
|
|
7
7
|
|
|
8
|
-
# Stockfish flavor: lite-single (default, ~7MB) | lite | single | full
|
|
8
|
+
# Stockfish flavor: lite-single (default, ~7MB) | single-lite | lite | single | full | asm
|
|
9
9
|
STOCKFISH_FLAVOR=lite-single
|
package/README.md
CHANGED
|
@@ -264,7 +264,10 @@ rejected:
|
|
|
264
264
|
|
|
265
265
|
- Up to 1,000 games are retained per process; idle games expire after one hour.
|
|
266
266
|
- `move_evaluate` accepts at most 10 moves per call.
|
|
267
|
-
- Imported PGNs are limited to 1 MiB and 4,096 plies
|
|
267
|
+
- Imported PGNs are limited to 1 MiB, 256 headers, and 4,096 plies across the
|
|
268
|
+
mainline and variations, plus 32,768 structural elements and 16 KiB per
|
|
269
|
+
lexical token. Every variation is legality-checked; game state retains the
|
|
270
|
+
mainline. UTF-8 BOMs and standard escaped header values are supported.
|
|
268
271
|
- Custom FENs reject inconsistent castling/en-passant metadata and impossible
|
|
269
272
|
pawn or promotion material.
|
|
270
273
|
- Stockfish accepts up to 32 active or queued analyses.
|
package/dist/chess-copy.js
CHANGED
|
@@ -8,6 +8,36 @@ const ORIGINAL_PIECES = {
|
|
|
8
8
|
function squareColor(square) {
|
|
9
9
|
return ((square.charCodeAt(0) - 97 + Number(square[1])) % 2);
|
|
10
10
|
}
|
|
11
|
+
function minimumPawnCaptures(chess, color) {
|
|
12
|
+
const pawns = chess
|
|
13
|
+
.findPiece({ type: "p", color })
|
|
14
|
+
.map((square) => ({
|
|
15
|
+
advances: color === "w" ? Number(square[1]) - 2 : 7 - Number(square[1]),
|
|
16
|
+
file: square.charCodeAt(0) - 97,
|
|
17
|
+
}))
|
|
18
|
+
.sort((left, right) => left.file - right.file);
|
|
19
|
+
let costs = new Map([[0, 0]]);
|
|
20
|
+
for (const pawn of pawns) {
|
|
21
|
+
const next = new Map();
|
|
22
|
+
for (const [mask, cost] of costs) {
|
|
23
|
+
for (let original = 0; original < 8; original += 1) {
|
|
24
|
+
const bit = 1 << original;
|
|
25
|
+
if (mask & bit)
|
|
26
|
+
continue;
|
|
27
|
+
const captures = Math.abs(original - pawn.file);
|
|
28
|
+
if (captures > pawn.advances)
|
|
29
|
+
continue;
|
|
30
|
+
const nextMask = mask | bit;
|
|
31
|
+
next.set(nextMask, Math.min(next.get(nextMask) ?? Infinity, cost + captures));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
costs = next;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(...costs.values());
|
|
37
|
+
}
|
|
38
|
+
function nonKingMaterial(chess, color) {
|
|
39
|
+
return ["p", "q", "r", "b", "n"].reduce((total, type) => total + chess.findPiece({ type, color }).length, 0);
|
|
40
|
+
}
|
|
11
41
|
function hasPiece(chess, square, type, color) {
|
|
12
42
|
const piece = chess.get(square);
|
|
13
43
|
return piece?.type === type && piece.color === color;
|
|
@@ -92,6 +122,14 @@ export function assertLegalPosition(chess) {
|
|
|
92
122
|
throw new ChessError("INVALID_FEN", "FEN contains more promoted material than missing pawns allow");
|
|
93
123
|
}
|
|
94
124
|
assertCastlingPosition(chess, color);
|
|
125
|
+
const opponent = color === "w" ? "b" : "w";
|
|
126
|
+
const opponentPawns = chess.findPiece({ type: "p", color: opponent }).length;
|
|
127
|
+
const missingOpponentMaterial = 15 - nonKingMaterial(chess, opponent);
|
|
128
|
+
const possibleOpponentPromotions = 8 - opponentPawns;
|
|
129
|
+
if (minimumPawnCaptures(chess, color) >
|
|
130
|
+
missingOpponentMaterial + possibleOpponentPromotions) {
|
|
131
|
+
throw new ChessError("INVALID_FEN", "FEN pawn files require more captures than opposing material allows");
|
|
132
|
+
}
|
|
95
133
|
}
|
|
96
134
|
assertEnPassantPosition(chess);
|
|
97
135
|
const turn = chess.turn();
|
|
@@ -107,6 +145,7 @@ export function snapshotChess(chess) {
|
|
|
107
145
|
const initialFen = history[0]?.before ?? chess.fen();
|
|
108
146
|
assertSafeFenCounters(initialFen);
|
|
109
147
|
const snapshot = new Chess(initialFen);
|
|
148
|
+
assertLegalPosition(snapshot);
|
|
110
149
|
const comments = new Map(chess.getComments().map(({ fen, comment }) => [fen, comment]));
|
|
111
150
|
for (const [key, value] of Object.entries(chess.getHeaders())) {
|
|
112
151
|
snapshot.setHeader(key, value);
|
package/dist/chess.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
2
|
import type { Move } from "chess.js";
|
|
3
3
|
export { assertLegalPosition, assertSafeFenCounters, snapshotChess, } from "./chess-copy.js";
|
|
4
|
-
export { MAX_PGN_BYTES, MAX_PGN_PLIES, parseImportedPgn, pgnOf } from "./pgn.js";
|
|
4
|
+
export { MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, pgnOf, } from "./pgn.js";
|
|
5
5
|
import type { ChessState, DrawResult } from "./domain.js";
|
|
6
6
|
export declare const MAX_EVALUATED_MOVES = 10;
|
|
7
7
|
export declare function drawResult(chess: Chess): DrawResult | null;
|
package/dist/chess.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
2
|
export { assertLegalPosition, assertSafeFenCounters, snapshotChess, } from "./chess-copy.js";
|
|
3
|
-
export { MAX_PGN_BYTES, MAX_PGN_PLIES, parseImportedPgn, pgnOf } from "./pgn.js";
|
|
3
|
+
export { MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, pgnOf, } from "./pgn.js";
|
|
4
4
|
import { ChessError } from "./errors.js";
|
|
5
5
|
export const MAX_EVALUATED_MOVES = 10;
|
|
6
6
|
function moveDescriptor(move) {
|
|
@@ -19,6 +19,7 @@ const DEFAULT_TIMEOUTS = {
|
|
|
19
19
|
stopGrace: 2000,
|
|
20
20
|
};
|
|
21
21
|
const DEFAULT_MAX_QUEUE = 32;
|
|
22
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
22
23
|
export function resolveStockfishFlavor(value) {
|
|
23
24
|
const normalized = (value || DEFAULT_FLAVOR).toLowerCase();
|
|
24
25
|
if (!FLAVORS.has(normalized)) {
|
|
@@ -56,6 +57,22 @@ function asError(error) {
|
|
|
56
57
|
function abortError(signal) {
|
|
57
58
|
return asError(signal.reason ?? "stockfish request cancelled");
|
|
58
59
|
}
|
|
60
|
+
function ownOption(options, name) {
|
|
61
|
+
return Object.hasOwn(options, name) ? options[name] : undefined;
|
|
62
|
+
}
|
|
63
|
+
function mergeTimeouts(value) {
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
return { ...DEFAULT_TIMEOUTS };
|
|
66
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
67
|
+
throw new Error("stockfish timeouts must be an object");
|
|
68
|
+
}
|
|
69
|
+
const timeouts = { ...DEFAULT_TIMEOUTS };
|
|
70
|
+
for (const name of Object.keys(DEFAULT_TIMEOUTS)) {
|
|
71
|
+
if (Object.hasOwn(value, name))
|
|
72
|
+
timeouts[name] = value[name];
|
|
73
|
+
}
|
|
74
|
+
return timeouts;
|
|
75
|
+
}
|
|
59
76
|
export class Stockfish {
|
|
60
77
|
session = null;
|
|
61
78
|
queue = [];
|
|
@@ -74,16 +91,18 @@ export class Stockfish {
|
|
|
74
91
|
maxQueue;
|
|
75
92
|
timeouts;
|
|
76
93
|
constructor(options = {}) {
|
|
77
|
-
this.initEngine = options
|
|
78
|
-
this.configuredFlavor = options
|
|
79
|
-
this.maxQueue = options
|
|
80
|
-
this.timeouts =
|
|
94
|
+
this.initEngine = ownOption(options, "init");
|
|
95
|
+
this.configuredFlavor = ownOption(options, "flavor");
|
|
96
|
+
this.maxQueue = ownOption(options, "maxQueue") ?? DEFAULT_MAX_QUEUE;
|
|
97
|
+
this.timeouts = mergeTimeouts(ownOption(options, "timeouts"));
|
|
81
98
|
if (!Number.isInteger(this.maxQueue) || this.maxQueue < 1) {
|
|
82
99
|
throw new Error("stockfish maxQueue must be a positive integer");
|
|
83
100
|
}
|
|
84
101
|
for (const [name, timeout] of Object.entries(this.timeouts)) {
|
|
85
|
-
if (!Number.
|
|
86
|
-
|
|
102
|
+
if (!Number.isSafeInteger(timeout) ||
|
|
103
|
+
timeout < 1 ||
|
|
104
|
+
timeout > MAX_TIMER_DELAY_MS) {
|
|
105
|
+
throw new Error(`stockfish ${name} timeout must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
@@ -113,7 +132,9 @@ export class Stockfish {
|
|
|
113
132
|
const engine = (this.initEngine ?? loadStockfish())(selectedFlavor, (error, initializedEngine) => {
|
|
114
133
|
callbackCalled = true;
|
|
115
134
|
if (this.session !== session) {
|
|
116
|
-
this.
|
|
135
|
+
if (initializedEngine !== this.session?.engine) {
|
|
136
|
+
this.terminate(initializedEngine);
|
|
137
|
+
}
|
|
117
138
|
return;
|
|
118
139
|
}
|
|
119
140
|
if (session.readySettled) {
|
|
@@ -121,8 +142,21 @@ export class Stockfish {
|
|
|
121
142
|
this.terminate(initializedEngine);
|
|
122
143
|
return;
|
|
123
144
|
}
|
|
124
|
-
if (
|
|
125
|
-
this.
|
|
145
|
+
if (this.terminations.has(initializedEngine)) {
|
|
146
|
+
this.failSession(session, new Error("stockfish initializer reused a terminated engine"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const replacedEngine = session.engine;
|
|
150
|
+
if (replacedEngine && replacedEngine !== initializedEngine) {
|
|
151
|
+
this.terminate(replacedEngine);
|
|
152
|
+
if (this.session !== session ||
|
|
153
|
+
session.readySettled ||
|
|
154
|
+
session.engine !== replacedEngine) {
|
|
155
|
+
if (initializedEngine !== this.session?.engine) {
|
|
156
|
+
this.terminate(initializedEngine);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
126
160
|
}
|
|
127
161
|
session.engine = initializedEngine;
|
|
128
162
|
if (error) {
|
|
@@ -141,12 +175,21 @@ export class Stockfish {
|
|
|
141
175
|
}, (handshakeError) => this.failSession(session, asError(handshakeError)));
|
|
142
176
|
});
|
|
143
177
|
if (callbackCalled) {
|
|
144
|
-
if (engine !== session.engine)
|
|
178
|
+
if (engine !== session.engine && engine !== this.session?.engine) {
|
|
145
179
|
this.terminate(engine);
|
|
180
|
+
}
|
|
146
181
|
}
|
|
147
182
|
else if (this.session === session && !session.readySettled) {
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
if (this.terminations.has(engine)) {
|
|
184
|
+
this.failSession(session, new Error("stockfish initializer reused a terminated engine"));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
session.engine = engine;
|
|
188
|
+
engine.listener = () => { };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (engine !== this.session?.engine) {
|
|
192
|
+
this.terminate(engine);
|
|
150
193
|
}
|
|
151
194
|
}
|
|
152
195
|
catch (error) {
|
package/dist/explorer-core.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const EXPLORER_RATE_LIMIT_COOLDOWN_MS = 60000;
|
|
|
10
10
|
export declare const EXPLORER_MAX_RESPONSE_BYTES: number;
|
|
11
11
|
export declare const EXPLORER_MAX_MOVES = 256;
|
|
12
12
|
export declare const EXPLORER_MAX_STRING_LENGTH = 256;
|
|
13
|
+
export declare const EXPLORER_MAX_COOLDOWN_MS = 2147483647;
|
|
13
14
|
export declare class ExplorerError extends Error {
|
|
14
15
|
readonly kind: ExplorerErrorKind;
|
|
15
16
|
readonly status?: number | undefined;
|
package/dist/explorer-core.js
CHANGED
|
@@ -8,6 +8,7 @@ export const EXPLORER_RATE_LIMIT_COOLDOWN_MS = 60_000;
|
|
|
8
8
|
export const EXPLORER_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
9
9
|
export const EXPLORER_MAX_MOVES = 256;
|
|
10
10
|
export const EXPLORER_MAX_STRING_LENGTH = 256;
|
|
11
|
+
export const EXPLORER_MAX_COOLDOWN_MS = 2_147_483_647;
|
|
11
12
|
export class ExplorerError extends Error {
|
|
12
13
|
kind;
|
|
13
14
|
status;
|
package/dist/explorer-limiter.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { awaitWithAbort, explorerError, throwIfAborted, } from "./explorer-core.js";
|
|
1
|
+
import { awaitWithAbort, explorerError, EXPLORER_MAX_COOLDOWN_MS, throwIfAborted, } from "./explorer-core.js";
|
|
2
|
+
const cooldownErrors = new WeakSet();
|
|
3
|
+
function cooldownError() {
|
|
4
|
+
const error = explorerError("rate_limited");
|
|
5
|
+
cooldownErrors.add(error);
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
export function isExplorerCooldownError(error) {
|
|
9
|
+
return cooldownErrors.has(error);
|
|
10
|
+
}
|
|
2
11
|
class RequestLimiter {
|
|
3
12
|
now;
|
|
4
13
|
#active = false;
|
|
5
|
-
#cooldownUntil
|
|
14
|
+
#cooldownUntil;
|
|
15
|
+
#lastNow;
|
|
6
16
|
#queue = [];
|
|
7
17
|
constructor(now) {
|
|
8
18
|
this.now = now;
|
|
@@ -11,7 +21,22 @@ class RequestLimiter {
|
|
|
11
21
|
return this.#queue.length;
|
|
12
22
|
}
|
|
13
23
|
cooldown(ms, _now) {
|
|
14
|
-
|
|
24
|
+
if (!Number.isSafeInteger(ms) ||
|
|
25
|
+
ms < 0 ||
|
|
26
|
+
ms > EXPLORER_MAX_COOLDOWN_MS) {
|
|
27
|
+
throw explorerError("invalid_input");
|
|
28
|
+
}
|
|
29
|
+
const now = this.#readNow();
|
|
30
|
+
const cooldownUntil = now + ms;
|
|
31
|
+
if (!Number.isFinite(cooldownUntil) ||
|
|
32
|
+
Math.abs(cooldownUntil) > Number.MAX_SAFE_INTEGER ||
|
|
33
|
+
(ms > 0 && cooldownUntil <= now)) {
|
|
34
|
+
throw explorerError("invalid_input");
|
|
35
|
+
}
|
|
36
|
+
this.#cooldownUntil =
|
|
37
|
+
this.#cooldownUntil === undefined
|
|
38
|
+
? cooldownUntil
|
|
39
|
+
: Math.max(this.#cooldownUntil, cooldownUntil);
|
|
15
40
|
}
|
|
16
41
|
run(options, request) {
|
|
17
42
|
throwIfAborted(options.callerSignal);
|
|
@@ -66,18 +91,34 @@ class RequestLimiter {
|
|
|
66
91
|
waiter.start();
|
|
67
92
|
}
|
|
68
93
|
#waitForCooldown(options) {
|
|
94
|
+
const now = this.#readNow();
|
|
69
95
|
const cooldownUntil = this.#cooldownUntil;
|
|
70
|
-
|
|
96
|
+
if (cooldownUntil === undefined)
|
|
97
|
+
return;
|
|
98
|
+
const delay = Math.ceil(cooldownUntil - now);
|
|
71
99
|
if (delay <= 0)
|
|
72
100
|
return;
|
|
73
101
|
if (delay >= options.deadline - options.now()) {
|
|
74
|
-
throw
|
|
102
|
+
throw cooldownError();
|
|
75
103
|
}
|
|
76
104
|
return awaitWithAbort(options.callerSignal, () => options.sleep(delay)).then(() => {
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
const now = this.#readNow();
|
|
106
|
+
if (this.#cooldownUntil !== undefined && this.#cooldownUntil > now) {
|
|
107
|
+
throw cooldownError();
|
|
108
|
+
}
|
|
109
|
+
this.#cooldownUntil = undefined;
|
|
79
110
|
});
|
|
80
111
|
}
|
|
112
|
+
#readNow() {
|
|
113
|
+
const now = this.now();
|
|
114
|
+
if (!Number.isFinite(now) ||
|
|
115
|
+
Math.abs(now) > Number.MAX_SAFE_INTEGER ||
|
|
116
|
+
(this.#lastNow !== undefined && now < this.#lastNow)) {
|
|
117
|
+
throw explorerError("invalid_input");
|
|
118
|
+
}
|
|
119
|
+
this.#lastNow = now;
|
|
120
|
+
return now;
|
|
121
|
+
}
|
|
81
122
|
}
|
|
82
123
|
export function createExplorerLimiter(now = () => performance.now()) {
|
|
83
124
|
return new RequestLimiter(now);
|
|
@@ -118,6 +118,9 @@ export async function normalizeExplorerResponse(response, signal, options) {
|
|
|
118
118
|
if (ucis.has(move.uci) || options.legalMoves.get(move.uci) !== move.san) {
|
|
119
119
|
throw explorerError("invalid_response");
|
|
120
120
|
}
|
|
121
|
+
if (sumCounts(move.white, move.draws, move.black) === 0) {
|
|
122
|
+
throw explorerError("invalid_response");
|
|
123
|
+
}
|
|
121
124
|
ucis.add(move.uci);
|
|
122
125
|
white = sumCounts(white, move.white);
|
|
123
126
|
draws = sumCounts(draws, move.draws);
|
package/dist/explorer-retry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { awaitWithAbort, explorerError, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_RATE_LIMIT_COOLDOWN_MS, } from "./explorer-core.js";
|
|
1
|
+
import { awaitWithAbort, explorerError, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_MAX_COOLDOWN_MS, EXPLORER_RATE_LIMIT_COOLDOWN_MS, } from "./explorer-core.js";
|
|
2
|
+
import { isExplorerCooldownError } from "./explorer-limiter.js";
|
|
2
3
|
const SHORT_WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
3
4
|
const LONG_WEEKDAYS = [
|
|
4
5
|
"Sunday",
|
|
@@ -25,7 +26,7 @@ const MONTHS = [
|
|
|
25
26
|
];
|
|
26
27
|
const IMF_DATE = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
27
28
|
const RFC850_DATE = /^(Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
28
|
-
const ASCTIME_DATE = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( [1-9]|[12]\d|3[01]) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
|
|
29
|
+
const ASCTIME_DATE = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( [1-9]|0[1-9]|[12]\d|3[01]) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
|
|
29
30
|
function httpDate(weekday, year, month, day, hour, minute, second) {
|
|
30
31
|
const date = new Date(0);
|
|
31
32
|
date.setUTCFullYear(year, month, day);
|
|
@@ -100,10 +101,10 @@ export function retryAfterMs(value, now) {
|
|
|
100
101
|
return parseRetryAfterMs(value, now) ?? EXPLORER_DEFAULT_RETRY_DELAY_MS;
|
|
101
102
|
}
|
|
102
103
|
export function rateLimitCooldownMs(value, now) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
104
|
+
const delay = value === null || value.trim() === ""
|
|
105
|
+
? EXPLORER_RATE_LIMIT_COOLDOWN_MS
|
|
106
|
+
: (parseRetryAfterMs(value, now) ?? EXPLORER_RATE_LIMIT_COOLDOWN_MS);
|
|
107
|
+
return Math.min(delay, EXPLORER_MAX_COOLDOWN_MS);
|
|
107
108
|
}
|
|
108
109
|
async function waitForRetry(options, delay, originalError) {
|
|
109
110
|
if (delay >= options.deadline - options.now())
|
|
@@ -118,7 +119,12 @@ export async function retryExplorer(options, attemptRequest) {
|
|
|
118
119
|
const outcome = await attemptRequest();
|
|
119
120
|
if (outcome.type === "success")
|
|
120
121
|
return outcome.result;
|
|
121
|
-
lastError =
|
|
122
|
+
lastError =
|
|
123
|
+
isExplorerCooldownError(outcome.error) &&
|
|
124
|
+
lastError?.kind === "rate_limited" &&
|
|
125
|
+
lastError.status !== undefined
|
|
126
|
+
? lastError
|
|
127
|
+
: outcome.error;
|
|
122
128
|
if (outcome.retry === "stop" || attempt + 1 >= options.maxAttempts) {
|
|
123
129
|
throw lastError;
|
|
124
130
|
}
|
package/dist/explorer.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError } from "./explorer-core.js";
|
|
3
|
+
import { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_COOLDOWN_MS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError } from "./explorer-core.js";
|
|
4
4
|
import type { ExplorerErrorKind, ExplorerFetch, ExplorerResult } from "./explorer-core.js";
|
|
5
5
|
import { createExplorerLimiter } from "./explorer-limiter.js";
|
|
6
6
|
import type { ExplorerLimiter, ExplorerLimiterOptions } from "./explorer-limiter.js";
|
|
7
|
-
export { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, createExplorerLimiter, };
|
|
7
|
+
export { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_COOLDOWN_MS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, createExplorerLimiter, };
|
|
8
8
|
export type { ExplorerErrorKind, ExplorerFetch, ExplorerLimiter, ExplorerLimiterOptions, ExplorerResult, };
|
|
9
9
|
export declare const LICHESS_SPEEDS: readonly ["ultraBullet", "bullet", "blitz", "rapid", "classical", "correspondence"];
|
|
10
10
|
export declare const LICHESS_RATINGS: readonly [0, 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2500];
|
package/dist/explorer.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
|
-
import { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, explorerError, throwIfAborted, } from "./explorer-core.js";
|
|
2
|
+
import { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_COOLDOWN_MS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, explorerError, throwIfAborted, } from "./explorer-core.js";
|
|
3
3
|
import { createExplorerLimiter } from "./explorer-limiter.js";
|
|
4
4
|
import { normalizeExplorerResponse } from "./explorer-response.js";
|
|
5
5
|
import { rateLimitCooldownMs, retryAfterMs, retryExplorer, } from "./explorer-retry.js";
|
|
6
6
|
import { requestExplorerTransport } from "./explorer-transport.js";
|
|
7
7
|
const BASE = "https://explorer.lichess.org";
|
|
8
|
-
export { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, createExplorerLimiter, };
|
|
8
|
+
export { EXPLORER_ATTEMPT_TIMEOUT_MS, EXPLORER_DEFAULT_RETRY_DELAY_MS, EXPLORER_ERROR_KINDS, EXPLORER_MAX_ATTEMPTS, EXPLORER_MAX_COOLDOWN_MS, EXPLORER_MAX_MOVES, EXPLORER_MAX_RESPONSE_BYTES, EXPLORER_MAX_STRING_LENGTH, EXPLORER_RATE_LIMIT_COOLDOWN_MS, EXPLORER_TOTAL_TIMEOUT_MS, ExplorerError, createExplorerLimiter, };
|
|
9
9
|
export const LICHESS_SPEEDS = [
|
|
10
10
|
"ultraBullet",
|
|
11
11
|
"bullet",
|
|
@@ -32,6 +32,29 @@ export const lichessRatingSchema = z.union([
|
|
|
32
32
|
const speedSet = new Set(LICHESS_SPEEDS);
|
|
33
33
|
const ratingSet = new Set(LICHESS_RATINGS);
|
|
34
34
|
const dbSet = new Set(["lichess", "masters"]);
|
|
35
|
+
function snapshotArray(value) {
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
throw explorerError("invalid_input");
|
|
38
|
+
try {
|
|
39
|
+
return Array.from(value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw explorerError("invalid_input");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function requestClock(now) {
|
|
46
|
+
let previous;
|
|
47
|
+
return () => {
|
|
48
|
+
const current = now();
|
|
49
|
+
if (!Number.isFinite(current) ||
|
|
50
|
+
Math.abs(current) > Number.MAX_SAFE_INTEGER ||
|
|
51
|
+
(previous !== undefined && current < previous)) {
|
|
52
|
+
throw explorerError("invalid_input");
|
|
53
|
+
}
|
|
54
|
+
previous = current;
|
|
55
|
+
return current;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
35
58
|
export function explorerEnabled() {
|
|
36
59
|
return (process.env.LICHESS_TOKEN || "").length > 0;
|
|
37
60
|
}
|
|
@@ -44,30 +67,37 @@ function setupExplorerRequest(chess, db, speeds, ratings, options) {
|
|
|
44
67
|
throw explorerError("disabled");
|
|
45
68
|
if (!dbSet.has(db))
|
|
46
69
|
throw explorerError("invalid_input");
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
new Set(
|
|
70
|
+
const speedValues = snapshotArray(speeds);
|
|
71
|
+
if (!speedValues.every((speed) => speedSet.has(speed)) ||
|
|
72
|
+
new Set(speedValues).size !== speedValues.length) {
|
|
50
73
|
throw explorerError("invalid_input");
|
|
51
74
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
new Set(
|
|
75
|
+
const ratingValues = snapshotArray(ratings);
|
|
76
|
+
if (!ratingValues.every((rating) => ratingSet.has(rating)) ||
|
|
77
|
+
new Set(ratingValues).size !== ratingValues.length) {
|
|
55
78
|
throw explorerError("invalid_input");
|
|
56
79
|
}
|
|
57
|
-
if (db === "masters" && (
|
|
80
|
+
if (db === "masters" && (speedValues.length > 0 || ratingValues.length > 0)) {
|
|
58
81
|
throw explorerError("invalid_input");
|
|
59
82
|
}
|
|
60
83
|
const params = new URLSearchParams();
|
|
61
84
|
params.set("fen", chess.fen());
|
|
62
|
-
if (
|
|
63
|
-
params.set("speeds",
|
|
64
|
-
if (
|
|
65
|
-
params.set("ratings",
|
|
66
|
-
const now = options.now ?? (() => performance.now());
|
|
85
|
+
if (speedValues.length)
|
|
86
|
+
params.set("speeds", speedValues.join(","));
|
|
87
|
+
if (ratingValues.length)
|
|
88
|
+
params.set("ratings", ratingValues.join(","));
|
|
89
|
+
const now = requestClock(options.now ?? (() => performance.now()));
|
|
90
|
+
const startedAt = now();
|
|
91
|
+
const deadline = startedAt + EXPLORER_TOTAL_TIMEOUT_MS;
|
|
92
|
+
if (!Number.isFinite(deadline) ||
|
|
93
|
+
Math.abs(deadline) > Number.MAX_SAFE_INTEGER ||
|
|
94
|
+
deadline <= startedAt) {
|
|
95
|
+
throw explorerError("invalid_input");
|
|
96
|
+
}
|
|
67
97
|
return {
|
|
68
98
|
callerSignal,
|
|
69
99
|
db,
|
|
70
|
-
deadline
|
|
100
|
+
deadline,
|
|
71
101
|
legalMoves: new Map(chess.moves({ verbose: true }).map((move) => [move.lan, move.san])),
|
|
72
102
|
limiter: options.limiter ?? processExplorerLimiter,
|
|
73
103
|
now,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { buildServer } from "./server.js";
|
|
|
3
3
|
export { serveHttp } from "./http.js";
|
|
4
4
|
export type { HttpServerHandle, HttpServerOptions } from "./http.js";
|
|
5
5
|
export type { AnalysisServices, AppServices, CandidateServices, ExplorerServices, GameServices, LifecycleServices, } from "./services.js";
|
|
6
|
-
export { drawResult, MAX_EVALUATED_MOVES, MAX_PGN_BYTES, MAX_PGN_PLIES, parseImportedPgn, snapshotChess, } from "./chess.js";
|
|
6
|
+
export { drawResult, MAX_EVALUATED_MOVES, MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, snapshotChess, } from "./chess.js";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { serveHttp } from "./http.js";
|
|
|
8
8
|
import { buildServer } from "./server.js";
|
|
9
9
|
export { buildServer } from "./server.js";
|
|
10
10
|
export { serveHttp } from "./http.js";
|
|
11
|
-
export { drawResult, MAX_EVALUATED_MOVES, MAX_PGN_BYTES, MAX_PGN_PLIES, parseImportedPgn, snapshotChess, } from "./chess.js";
|
|
11
|
+
export { drawResult, MAX_EVALUATED_MOVES, MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, snapshotChess, } from "./chess.js";
|
|
12
12
|
function installShutdown(closeTransport) {
|
|
13
13
|
let shutdown;
|
|
14
14
|
const close = () => (shutdown ??= closeTransport());
|
package/dist/intents.js
CHANGED
|
@@ -8,8 +8,8 @@ function safeDifference(left, right) {
|
|
|
8
8
|
return result;
|
|
9
9
|
}
|
|
10
10
|
function safeSum(values) {
|
|
11
|
-
if (!values.every(Number.isSafeInteger)) {
|
|
12
|
-
throw new RangeError("chess count must be a safe integer");
|
|
11
|
+
if (!values.every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
12
|
+
throw new RangeError("chess count must be a non-negative safe integer");
|
|
13
13
|
}
|
|
14
14
|
const result = values.reduce((sum, value) => sum + value, 0);
|
|
15
15
|
if (!Number.isSafeInteger(result)) {
|
|
@@ -17,6 +17,18 @@ function safeSum(values) {
|
|
|
17
17
|
}
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
20
|
+
function assertLichessMove(move) {
|
|
21
|
+
if (move.count < 1) {
|
|
22
|
+
throw new RangeError("chess move count must be positive");
|
|
23
|
+
}
|
|
24
|
+
if (move.count !== safeSum([move.white, move.draws, move.black])) {
|
|
25
|
+
throw new RangeError("derived chess move count is inconsistent");
|
|
26
|
+
}
|
|
27
|
+
if (move.averageRating !== null &&
|
|
28
|
+
(!Number.isSafeInteger(move.averageRating) || move.averageRating < 0)) {
|
|
29
|
+
throw new RangeError("chess average rating must be a non-negative safe integer");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
function toSan(chess, uci) {
|
|
21
33
|
const m = chess.moves({ verbose: true }).find((x) => x.lan === uci);
|
|
22
34
|
return m ? m.san : uci;
|
|
@@ -49,42 +61,93 @@ function objectiveFromLine(line, turn, bestCp) {
|
|
|
49
61
|
};
|
|
50
62
|
}
|
|
51
63
|
export function explorerCandidateData(result) {
|
|
64
|
+
const totals = [result.white, result.draws, result.black];
|
|
65
|
+
let white = 0;
|
|
66
|
+
let draws = 0;
|
|
67
|
+
let black = 0;
|
|
68
|
+
const ucis = new Set();
|
|
52
69
|
for (const move of result.moves) {
|
|
53
|
-
if (
|
|
54
|
-
throw new RangeError("
|
|
70
|
+
if (ucis.has(move.uci)) {
|
|
71
|
+
throw new RangeError("duplicate chess move");
|
|
55
72
|
}
|
|
73
|
+
ucis.add(move.uci);
|
|
74
|
+
assertLichessMove(move);
|
|
75
|
+
white = safeSum([white, move.white]);
|
|
76
|
+
draws = safeSum([draws, move.draws]);
|
|
77
|
+
black = safeSum([black, move.black]);
|
|
78
|
+
}
|
|
79
|
+
if (white > result.white || draws > result.draws || black > result.black) {
|
|
80
|
+
throw new RangeError("chess move totals exceed explorer totals");
|
|
56
81
|
}
|
|
57
82
|
return {
|
|
58
83
|
status: result.moves.length > 0 ? "available" : "no_data",
|
|
59
|
-
totalGames: safeSum(
|
|
84
|
+
totalGames: safeSum(totals),
|
|
60
85
|
moves: result.moves,
|
|
61
86
|
};
|
|
62
87
|
}
|
|
63
88
|
export function candidateSetFromData(chess, elo, sfLines, maiaMoves, lichessResult) {
|
|
89
|
+
if ((lichessResult.status === "disabled" ||
|
|
90
|
+
lichessResult.status === "unavailable") &&
|
|
91
|
+
lichessResult.moves.length > 0) {
|
|
92
|
+
throw new RangeError(`${lichessResult.status} explorer data cannot contain moves`);
|
|
93
|
+
}
|
|
94
|
+
if ((lichessResult.status === "available") !==
|
|
95
|
+
(lichessResult.moves.length > 0)) {
|
|
96
|
+
throw new RangeError(`${lichessResult.status} explorer data has inconsistent moves`);
|
|
97
|
+
}
|
|
64
98
|
if (chess.isGameOver()) {
|
|
65
99
|
return {
|
|
66
100
|
candidates: [],
|
|
67
101
|
moveSensitivity: { level: "low", topMoveSpreadCp: null },
|
|
68
102
|
};
|
|
69
103
|
}
|
|
70
|
-
const turn = chess.turn();
|
|
71
|
-
const maiaByUci = new Map(maiaMoves.map((move) => [move.uci, move.prob]));
|
|
72
104
|
const legalUcis = new Set(chess.moves({ verbose: true }).map((move) => move.lan));
|
|
105
|
+
const turn = chess.turn();
|
|
106
|
+
const maiaByUci = new Map();
|
|
107
|
+
const maiaUcis = new Set();
|
|
108
|
+
for (const move of maiaMoves) {
|
|
109
|
+
if (maiaUcis.has(move.uci))
|
|
110
|
+
throw new RangeError("duplicate Maia move");
|
|
111
|
+
maiaUcis.add(move.uci);
|
|
112
|
+
if (!Number.isFinite(move.prob) || move.prob < 0 || move.prob > 1) {
|
|
113
|
+
throw new RangeError("Maia move probability must be between 0 and 1");
|
|
114
|
+
}
|
|
115
|
+
if (!legalUcis.has(move.uci))
|
|
116
|
+
continue;
|
|
117
|
+
maiaByUci.set(move.uci, move.prob);
|
|
118
|
+
}
|
|
73
119
|
const sfByUci = new Map();
|
|
74
120
|
for (const line of sfLines) {
|
|
75
121
|
const uci = line.pv[0];
|
|
76
122
|
const evaluation = toEval(line);
|
|
77
123
|
if (uci !== undefined && legalUcis.has(uci) && evaluation !== null) {
|
|
124
|
+
if (sfByUci.has(uci))
|
|
125
|
+
throw new RangeError("duplicate Stockfish move");
|
|
78
126
|
sfByUci.set(uci, { line, evaluation });
|
|
79
127
|
}
|
|
80
128
|
}
|
|
81
|
-
const
|
|
129
|
+
const totalGames = lichessResult.totalGames ?? 0;
|
|
130
|
+
safeSum([totalGames]);
|
|
131
|
+
const lichessByUci = new Map();
|
|
132
|
+
const lichessUcis = new Set();
|
|
133
|
+
for (const move of lichessResult.moves) {
|
|
134
|
+
if (lichessUcis.has(move.uci)) {
|
|
135
|
+
throw new RangeError("duplicate explorer move");
|
|
136
|
+
}
|
|
137
|
+
lichessUcis.add(move.uci);
|
|
138
|
+
assertLichessMove(move);
|
|
139
|
+
if (move.count > totalGames) {
|
|
140
|
+
throw new RangeError("chess move count exceeds explorer total");
|
|
141
|
+
}
|
|
142
|
+
if (!legalUcis.has(move.uci))
|
|
143
|
+
continue;
|
|
144
|
+
lichessByUci.set(move.uci, move);
|
|
145
|
+
}
|
|
82
146
|
const normalizedSfLines = [...sfByUci.values()];
|
|
83
147
|
const evals = normalizedSfLines.map(({ evaluation }) => evaluation);
|
|
84
148
|
const bestCp = evals.length
|
|
85
149
|
? Math.max(...evals.map((value) => evalToCp(value)))
|
|
86
150
|
: null;
|
|
87
|
-
const totalGames = lichessResult.totalGames ?? 0;
|
|
88
151
|
const ucis = new Set([
|
|
89
152
|
...sfByUci.keys(),
|
|
90
153
|
...maiaByUci.keys(),
|
|
@@ -167,6 +230,7 @@ export function createCandidateComputation(dependencies) {
|
|
|
167
230
|
return await work();
|
|
168
231
|
}
|
|
169
232
|
catch (error) {
|
|
233
|
+
workSignal.throwIfAborted();
|
|
170
234
|
controller.abort(error);
|
|
171
235
|
throw error;
|
|
172
236
|
}
|
|
@@ -194,6 +258,7 @@ export function createCandidateComputation(dependencies) {
|
|
|
194
258
|
fatal(() => dependencies.humanMoveDistribution(chess, elo, elo, maiaTopN, workSignal)),
|
|
195
259
|
fatal(explorer),
|
|
196
260
|
]);
|
|
261
|
+
workSignal.throwIfAborted();
|
|
197
262
|
return candidateSetFromData(chess, elo, sfLines, maiaMoves, lichessResult);
|
|
198
263
|
};
|
|
199
264
|
}
|