pokertools-arena 0.13.0 → 0.16.2

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/dist/app.js CHANGED
@@ -114149,7 +114149,7 @@ var BASE_CONFIG = {
114149
114149
  bigBlind: 50,
114150
114150
  ante: 5,
114151
114151
  handsPerLevel: 8,
114152
- blindMultiplier: 1.5
114152
+ blindMultiplier: 2
114153
114153
  },
114154
114154
  timing: {
114155
114155
  actionSeconds: 30,
@@ -114339,6 +114339,99 @@ async function fetchJsonWithRetry(url, options, { maxRetries = 1 } = {}) {
114339
114339
  throw new ArenaRequestError(message, { status: response.status, category: requestErrorCategory(response.status), payload, incidents });
114340
114340
  }
114341
114341
  }
114342
+ async function readStreamingPayload(response, onDelta) {
114343
+ const contentType = response.headers?.get?.("content-type") || "";
114344
+ if (!contentType.includes("text/event-stream") || typeof response.body?.getReader !== "function") {
114345
+ return { payload: await response.json().catch(() => ({})), reasoning: "" };
114346
+ }
114347
+ const reader = response.body.getReader();
114348
+ const decoder = new TextDecoder();
114349
+ const toolCalls = /* @__PURE__ */ new Map();
114350
+ let buffer = "", content = "", reasoning = "", finishReason = null, usage = null, model = null, provider = null;
114351
+ while (true) {
114352
+ const { value, done } = await reader.read();
114353
+ if (done) break;
114354
+ buffer += decoder.decode(value, { stream: true });
114355
+ let reasoningChanged = false, contentChanged = false;
114356
+ let newline;
114357
+ while ((newline = buffer.indexOf("\n")) >= 0) {
114358
+ const line = buffer.slice(0, newline).trim();
114359
+ buffer = buffer.slice(newline + 1);
114360
+ if (!line.startsWith("data:")) continue;
114361
+ const data = line.slice(5).trim();
114362
+ if (!data || data === "[DONE]") continue;
114363
+ let chunk;
114364
+ try {
114365
+ chunk = JSON.parse(data);
114366
+ } catch {
114367
+ continue;
114368
+ }
114369
+ if (chunk.model) model = chunk.model;
114370
+ if (chunk.provider) provider = chunk.provider;
114371
+ if (chunk.usage) usage = chunk.usage;
114372
+ const choice = chunk.choices?.[0];
114373
+ if (!choice) continue;
114374
+ if (choice.finish_reason) finishReason = choice.finish_reason;
114375
+ const delta = choice.delta || {};
114376
+ if (typeof delta.reasoning === "string" && delta.reasoning) {
114377
+ reasoning += delta.reasoning;
114378
+ reasoningChanged = true;
114379
+ }
114380
+ if (typeof delta.content === "string" && delta.content) {
114381
+ content += delta.content;
114382
+ contentChanged = true;
114383
+ }
114384
+ for (const call of delta.tool_calls || []) {
114385
+ const index = Number(call.index ?? 0);
114386
+ const current = toolCalls.get(index) || { id: "", name: "", args: "" };
114387
+ if (call.id) current.id = call.id;
114388
+ if (call.function?.name) current.name += call.function.name;
114389
+ if (typeof call.function?.arguments === "string") current.args += call.function.arguments;
114390
+ toolCalls.set(index, current);
114391
+ }
114392
+ }
114393
+ if (reasoningChanged) onDelta?.({ channel: "reasoning", text: reasoning });
114394
+ if (contentChanged) onDelta?.({ channel: "content", text: content });
114395
+ }
114396
+ const calls = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]).map(([, call]) => ({ id: call.id, type: "function", function: { name: call.name, arguments: call.args } }));
114397
+ return {
114398
+ payload: {
114399
+ model,
114400
+ provider,
114401
+ usage: usage ?? null,
114402
+ choices: [{ finish_reason: finishReason, message: { role: "assistant", content: content || null, ...calls.length ? { tool_calls: calls } : {} } }]
114403
+ },
114404
+ reasoning
114405
+ };
114406
+ }
114407
+ async function fetchStreamingJson(url, options, { maxRetries = 1, onDelta = null } = {}) {
114408
+ const incidents = [];
114409
+ for (let attempt = 0; ; attempt++) {
114410
+ if (attempt > 0) diagnosticsCounters.retries++;
114411
+ diagnosticsCounters.requests++;
114412
+ let response;
114413
+ try {
114414
+ response = await fetch(url, options);
114415
+ } catch (err) {
114416
+ if (err?.name === "AbortError") throw err;
114417
+ throw new ArenaRequestError(summarizeError(err), { category: "provider", incidents });
114418
+ }
114419
+ if (response.ok) return { ...await readStreamingPayload(response, onDelta), incidents, retryCount: attempt };
114420
+ const payload = await response.json().catch(() => ({}));
114421
+ const message = payload?.error?.message ?? payload?.message ?? `HTTP ${response.status}`;
114422
+ const retryable = response.status === 429 || response.status >= 500;
114423
+ if (retryable && attempt < maxRetries) {
114424
+ if (response.status === 429) diagnosticsCounters.rateLimits++;
114425
+ else diagnosticsCounters.providerErrors++;
114426
+ incidents.push({ category: requestErrorCategory(response.status), status: response.status, message: String(message).slice(0, 180) });
114427
+ await sleepWithSignal(retryDelayMs(response), options?.signal);
114428
+ continue;
114429
+ }
114430
+ if (response.status === 429) diagnosticsCounters.rateLimits++;
114431
+ else if (response.status >= 500) diagnosticsCounters.providerErrors++;
114432
+ throw new ArenaRequestError(message, { status: response.status, category: requestErrorCategory(response.status), payload, incidents });
114433
+ }
114434
+ }
114342
114435
  function isUnsupportedToolChoiceError(err) {
114343
114436
  const message = summarizeError(err);
114344
114437
  return [400, 404, 422].includes(Number(err?.status)) && /tool[_ -]?choice|tool call|tools?.*(?:unsupported|support)|no endpoints?.*support/i.test(message);
@@ -114417,6 +114510,9 @@ function isReasoningModel(model) {
114417
114510
  const id2 = String(model || "").toLowerCase();
114418
114511
  return /(?:^|[\/._:-])(qwen3|qwq|deepseek-[rv]\d|magistral|glm-(?:\d+[a-z]*|latest|flash-latest)|gpt-oss|nemotron|reason(?:ing)?|thinking|o1|o3|o4)(?:$|[\/._:-])/.test(id2);
114419
114512
  }
114513
+ function wantsReasoning(agent) {
114514
+ return isReasoningModel(agent?.model) || agent?.captureReasoning === true;
114515
+ }
114420
114516
  function effectiveProtocol(agent, connection) {
114421
114517
  if (isOpenRouterConnection(connection) && isJevModel(agent?.model)) return "jev_decisions";
114422
114518
  if (connection?.kind === "typesafe") return "jev_native";
@@ -114534,29 +114630,57 @@ function totalPot(state) {
114534
114630
  else if (bets && typeof bets === "object") for (const amount of Object.values(bets)) live += asNumber(amount);
114535
114631
  return settled + live;
114536
114632
  }
114537
- function clockwiseSeats(state) {
114538
- const seated = (state?.players ?? []).map((p, seat) => ({ p, seat })).filter(({ p }) => p && p.status !== "BUSTED" && p.status !== "SITTING_OUT").map(({ seat }) => seat).sort((a, b) => a - b);
114539
- if (!seated.length || state?.buttonSeat == null) return seated;
114540
- const idx = seated.indexOf(state.buttonSeat);
114541
- if (idx < 0) return seated;
114542
- return [...seated.slice(idx), ...seated.slice(0, idx)];
114633
+ function integerGcd(values) {
114634
+ const gcd = (a, b) => {
114635
+ while (b) [a, b] = [b, a % b];
114636
+ return Math.abs(a);
114637
+ };
114638
+ return values.map((value) => Math.abs(Math.round(Number(value) || 0))).filter(Boolean).reduce(gcd, 0) || 1;
114639
+ }
114640
+ function potChipBreakdown(pot, { smallBlind = 0, bigBlind = 0, ante = 0 } = {}) {
114641
+ let remaining = Math.max(0, Math.round(Number(pot) || 0));
114642
+ if (!remaining) return [];
114643
+ const base = integerGcd([smallBlind, bigBlind, ante]);
114644
+ const denominations = [
114645
+ { color: "gold", value: base * 100 },
114646
+ { color: "black", value: base * 25 },
114647
+ { color: "blue", value: base * 5 },
114648
+ { color: "red", value: base }
114649
+ ];
114650
+ if (base > 1) denominations.push({ color: "white", value: 1 });
114651
+ return denominations.flatMap(({ color, value }) => {
114652
+ const count = Math.floor(remaining / value);
114653
+ remaining -= count * value;
114654
+ return count ? [{ color, value, count, shown: Math.min(5, count) }] : [];
114655
+ });
114656
+ }
114657
+ function handSeats(state) {
114658
+ return (state?.players ?? []).map((player, seat) => ({ player, seat })).filter(({ player }) => player && (playerStack(player) > 0 || player.hand != null || asNumber(player.totalInvestedThisHand) > 0)).map(({ seat }) => seat);
114659
+ }
114660
+ function tableMarkerSeats(state) {
114661
+ const seats = handSeats(state);
114662
+ const buttonSeat = Number.isInteger(state?.buttonSeat) ? state.buttonSeat : null;
114663
+ const bigBlindSeat = Number.isInteger(state?.bigBlindSeat) ? state.bigBlindSeat : null;
114664
+ const maxPlayers = Math.max(1, asNumber(state?.maxPlayers, state?.players?.length || 1));
114665
+ const smallBlindSeat = buttonSeat == null ? null : seats.length === 2 ? buttonSeat : (buttonSeat + 1) % maxPlayers;
114666
+ return { buttonSeat, smallBlindSeat, bigBlindSeat, headsUp: seats.length === 2 };
114543
114667
  }
114544
- var POSITION_TABLE = {
114545
- 2: ["BTN/SB", "BB"],
114546
- 3: ["BTN", "SB", "BB"],
114547
- 4: ["BTN", "SB", "BB", "UTG"],
114548
- 5: ["BTN", "SB", "BB", "UTG", "CO"],
114549
- 6: ["BTN", "SB", "BB", "UTG", "HJ", "CO"],
114550
- 7: ["BTN", "SB", "BB", "UTG", "MP", "HJ", "CO"],
114551
- 8: ["BTN", "SB", "BB", "UTG", "UTG+1", "MP", "HJ", "CO"],
114552
- 9: ["BTN", "SB", "BB", "UTG", "UTG+1", "MP", "MP+1", "HJ", "CO"],
114553
- 10: ["BTN", "SB", "BB", "UTG", "UTG+1", "UTG+2", "MP", "MP+1", "HJ", "CO"]
114554
- };
114555
114668
  function positionForSeat(state, seat) {
114556
- const order = clockwiseSeats(state);
114557
- const labels = POSITION_TABLE[order.length] ?? order.map((_, i) => i === 0 ? "BTN" : `P${i}`);
114558
- const idx = order.indexOf(seat);
114559
- return idx >= 0 ? labels[idx] : `Seat ${seat + 1}`;
114669
+ const playing = handSeats(state);
114670
+ if (!playing.includes(seat)) return `Seat ${seat + 1}`;
114671
+ const markers = tableMarkerSeats(state);
114672
+ if (seat === markers.buttonSeat) return markers.headsUp ? "BTN/SB" : "BTN";
114673
+ if (seat === markers.smallBlindSeat) return "SB";
114674
+ if (seat === markers.bigBlindSeat) return "BB";
114675
+ const maxPlayers = Math.max(1, asNumber(state?.maxPlayers, state?.players?.length || 1));
114676
+ const afterBigBlind = [];
114677
+ for (let offset = 1; offset <= maxPlayers; offset++) {
114678
+ const candidate = ((markers.bigBlindSeat ?? -1) + offset + maxPlayers) % maxPlayers;
114679
+ if (playing.includes(candidate) && ![markers.buttonSeat, markers.smallBlindSeat, markers.bigBlindSeat].includes(candidate)) afterBigBlind.push(candidate);
114680
+ }
114681
+ const labels = afterBigBlind.length === 1 ? ["UTG"] : afterBigBlind.length === 2 ? ["UTG", "CO"] : ["UTG", ...Array.from({ length: Math.max(0, afterBigBlind.length - 3) }, (_, i) => `MP${i ? `+${i}` : ""}`), "HJ", "CO"];
114682
+ const index = afterBigBlind.indexOf(seat);
114683
+ return index >= 0 ? labels[index] : `Seat ${seat + 1}`;
114560
114684
  }
114561
114685
  function describeAction(type, amount, state, seat = null) {
114562
114686
  const bb = Math.max(1, asNumber(state.bigBlind, 1));
@@ -115123,7 +115247,7 @@ async function decideJevNative({ agent, connection, state, legalActions, timeout
115123
115247
  cancel();
115124
115248
  }
115125
115249
  }
115126
- function buildOpenAICompatibleBody({ agent, connection, state, legalActions, decisionId, protocol = "tool", instructions = null, criteria = null, representationText = null, maxTokens = null, reasoning = null }) {
115250
+ function buildOpenAICompatibleBody({ agent, connection, state, legalActions, decisionId, protocol = "tool", instructions = null, criteria = null, representationText = null, maxTokens = null, reasoning = null, stream = false }) {
115127
115251
  const isOpenRouter = isOpenRouterConnection(connection);
115128
115252
  const criteriaKeys = criteria ? Object.keys(criteria) : null;
115129
115253
  const useChoice = Boolean(criteriaKeys?.length);
@@ -115148,7 +115272,7 @@ Respond with one of: ${criteriaKeys.map((k) => `${k} = ${criteria[k]}`).join(";
115148
115272
  { role: "system", content: "You are one seat in an autonomous poker benchmark. Commit exactly one legal move." },
115149
115273
  { role: "user", content: userContent }
115150
115274
  ],
115151
- max_tokens: maxTokens ?? (isReasoningModel(agent.model) ? 2048 : 320)
115275
+ max_tokens: maxTokens ?? (wantsReasoning(agent) ? 2048 : 320)
115152
115276
  };
115153
115277
  const temperature = resolveTemperature(agent.model, agent.temperature);
115154
115278
  if (temperature !== void 0) body.temperature = temperature;
@@ -115163,26 +115287,30 @@ Respond with one of: ${criteriaKeys.map((k) => `${k} = ${criteria[k]}`).join(";
115163
115287
  }
115164
115288
  if (isOpenRouter) {
115165
115289
  body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
115166
- if (isReasoningModel(agent.model)) body.reasoning = reasoning ?? { max_tokens: 256, exclude: true };
115290
+ if (wantsReasoning(agent)) body.reasoning = reasoning ?? { max_tokens: 256, exclude: false };
115291
+ }
115292
+ if (stream) {
115293
+ body.stream = true;
115294
+ body.stream_options = { include_usage: true };
115167
115295
  }
115168
115296
  return body;
115169
115297
  }
115170
- async function decideOpenAICompatible({ agent, connection, state, legalActions, decisionId, timeoutMs, abortSignal, pauseClock, protocol: protocolOverride = null, instructions = null, criteria = null, representationText = null }) {
115298
+ async function decideOpenAICompatible({ agent, connection, state, legalActions, decisionId, timeoutMs, abortSignal, pauseClock, protocol: protocolOverride = null, instructions = null, criteria = null, representationText = null, onDelta = null }) {
115171
115299
  const { signal, cancel } = combineAbort(timeoutMs, abortSignal, pauseClock);
115172
115300
  const started = performance.now();
115173
115301
  const requestedProtocol = protocolOverride || agent.protocol;
115174
115302
  const capabilityKey = `${normalizeBaseUrl(connection.baseUrl)}|${agent.model}|${agent.provider || "auto"}|tool`;
115175
115303
  const isOpenRouter = isOpenRouterConnection(connection);
115176
115304
  const cachedProtocol = requestedProtocol === "tool" && isOpenRouter && !criteria ? protocolCapabilityCache.get(capabilityKey) : null;
115177
- const buildBody = (protocol, override = {}) => buildOpenAICompatibleBody({ agent, connection, state, legalActions, decisionId, protocol, instructions, criteria, representationText, maxTokens: override.maxTokens ?? null, reasoning: override.reasoning ?? null });
115305
+ const buildBody = (protocol, override = {}) => buildOpenAICompatibleBody({ agent, connection, state, legalActions, decisionId, protocol, instructions, criteria, representationText, maxTokens: override.maxTokens ?? null, reasoning: override.reasoning ?? null, stream: Boolean(onDelta) });
115306
+ const request = (body) => {
115307
+ const options = { method: "POST", headers: makeHeaders(connection), body: JSON.stringify(body), signal };
115308
+ const url = completionsUrl(connection.baseUrl);
115309
+ return onDelta ? fetchStreamingJson(url, options, { maxRetries: 1, onDelta: (delta) => onDelta({ stage: "flat", ...delta }) }) : fetchJsonWithRetry(url, options, { maxRetries: 1 });
115310
+ };
115178
115311
  async function execute(protocol, override = {}) {
115179
115312
  const body = buildBody(protocol, override);
115180
- const { payload, incidents, retryCount } = await fetchJsonWithRetry(completionsUrl(connection.baseUrl), {
115181
- method: "POST",
115182
- headers: makeHeaders(connection),
115183
- body: JSON.stringify(body),
115184
- signal
115185
- }, { maxRetries: 1 });
115313
+ const { payload, incidents, retryCount, reasoning } = await request(body);
115186
115314
  const message = payload?.choices?.[0]?.message;
115187
115315
  const finishReason = payload?.choices?.[0]?.finish_reason;
115188
115316
  let obj;
@@ -115203,7 +115331,7 @@ async function decideOpenAICompatible({ agent, connection, state, legalActions,
115203
115331
  throw new Error(`Model response was not valid JSON: ${content.slice(0, 180)}`);
115204
115332
  }
115205
115333
  }
115206
- return { obj, payload, incidents, retryCount, protocol };
115334
+ return { obj, payload, incidents, retryCount, protocol, reasoning };
115207
115335
  }
115208
115336
  try {
115209
115337
  let actualProtocol = cachedProtocol || requestedProtocol;
@@ -115231,6 +115359,7 @@ async function decideOpenAICompatible({ agent, connection, state, legalActions,
115231
115359
  latencyMs: Math.round(performance.now() - started),
115232
115360
  model: result.payload?.model || agent.model,
115233
115361
  usage: result.payload?.usage ?? null,
115362
+ reasoning: result.reasoning ?? null,
115234
115363
  meta: {
115235
115364
  method: result.protocol,
115236
115365
  requestedMethod: requestedProtocol,
@@ -115376,13 +115505,13 @@ async function requestJevStage({ connection, model, state, questionKey, question
115376
115505
  gap: ranked.length >= 2 ? Number(ranked[0][1]) - Number(ranked[1][1]) : null
115377
115506
  };
115378
115507
  }
115379
- async function requestChatStage({ agent, connection, schema, toolName, systemPrompt, userPrompt, signal, requestedProtocol, temperature }) {
115508
+ async function requestChatStage({ agent, connection, schema, toolName, systemPrompt, userPrompt, signal, requestedProtocol, temperature, onDelta = null }) {
115380
115509
  const isOpenRouter = isOpenRouterConnection(connection);
115381
115510
  const buildBody = (protocol, override = {}) => {
115382
115511
  const body = {
115383
115512
  model: agent.model,
115384
115513
  messages: [{ role: "system", content: systemPrompt }, { role: "user", content: userPrompt }],
115385
- max_tokens: override.maxTokens ?? (isReasoningModel(agent.model) ? 2048 : 320)
115514
+ max_tokens: override.maxTokens ?? (wantsReasoning(agent) ? 2048 : 320)
115386
115515
  };
115387
115516
  const resolvedTemperature = resolveTemperature(agent.model, temperature);
115388
115517
  if (resolvedTemperature !== void 0) body.temperature = resolvedTemperature;
@@ -115397,7 +115526,11 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
115397
115526
  }
115398
115527
  if (isOpenRouter) {
115399
115528
  body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
115400
- if (isReasoningModel(agent.model)) body.reasoning = override.reasoning ?? { max_tokens: 256, exclude: true };
115529
+ if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? { max_tokens: 256, exclude: false };
115530
+ }
115531
+ if (onDelta) {
115532
+ body.stream = true;
115533
+ body.stream_options = { include_usage: true };
115401
115534
  }
115402
115535
  return body;
115403
115536
  };
@@ -115405,12 +115538,9 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
115405
115538
  const cachedProtocol = requestedProtocol === "tool" && isOpenRouter ? protocolCapabilityCache.get(capabilityKey) : null;
115406
115539
  const execute = async (protocol, override = {}) => {
115407
115540
  const started = performance.now();
115408
- const { payload, incidents, retryCount } = await fetchJsonWithRetry(completionsUrl(connection.baseUrl), {
115409
- method: "POST",
115410
- headers: makeHeaders(connection),
115411
- body: JSON.stringify(buildBody(protocol, override)),
115412
- signal
115413
- }, { maxRetries: 1 });
115541
+ const options = { method: "POST", headers: makeHeaders(connection), body: JSON.stringify(buildBody(protocol, override)), signal };
115542
+ const url = completionsUrl(connection.baseUrl);
115543
+ const { payload, incidents, retryCount, reasoning } = onDelta ? await fetchStreamingJson(url, options, { maxRetries: 1, onDelta }) : await fetchJsonWithRetry(url, options, { maxRetries: 1 });
115414
115544
  const message = payload?.choices?.[0]?.message;
115415
115545
  const finishReason = payload?.choices?.[0]?.finish_reason;
115416
115546
  let obj;
@@ -115427,7 +115557,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
115427
115557
  if (!content) throw truncatedError(finishReason, `Model returned no ${toolName} content`);
115428
115558
  obj = JSON.parse(content);
115429
115559
  }
115430
- return { obj, protocol, latencyMs: Math.round(performance.now() - started), model: payload?.model || agent.model, usage: payload?.usage ?? null, incidents, retryCount };
115560
+ return { obj, protocol, latencyMs: Math.round(performance.now() - started), model: payload?.model || agent.model, usage: payload?.usage ?? null, incidents, retryCount, reasoning };
115431
115561
  };
115432
115562
  let actual = cachedProtocol || requestedProtocol;
115433
115563
  let protocolFallback = cachedProtocol ? "cached json_schema" : null;
@@ -115460,7 +115590,8 @@ async function decideHierarchical({
115460
115590
  representationMode = DEFAULT_REPRESENTATION_MODE,
115461
115591
  protocol: protocolOverride = null,
115462
115592
  sizesForFamily = null,
115463
- onStage = null
115593
+ onStage = null,
115594
+ onDelta = null
115464
115595
  }) {
115465
115596
  if (!connection) throw new Error(`Connection not found: ${agent.connectionId}`);
115466
115597
  const protocol = protocolOverride || effectiveProtocol(agent, connection);
@@ -115509,7 +115640,8 @@ async function decideHierarchical({
115509
115640
  userPrompt,
115510
115641
  signal,
115511
115642
  requestedProtocol: protocol,
115512
- temperature: agent.temperature
115643
+ temperature: agent.temperature,
115644
+ onDelta: onDelta ? (delta) => onDelta({ stage: "family", index: 1, of: 2, ...delta }) : null
115513
115645
  });
115514
115646
  familyResult = { ...result, choice: normalizeFamilyChoice(result.obj, families, decisionId) };
115515
115647
  }
@@ -115554,7 +115686,8 @@ async function decideHierarchical({
115554
115686
  userPrompt,
115555
115687
  signal,
115556
115688
  requestedProtocol: protocol,
115557
- temperature: agent.temperature
115689
+ temperature: agent.temperature,
115690
+ onDelta: onDelta ? (delta) => onDelta({ stage: "size", index: 2, of: 2, family: familyResult.choice, ...delta }) : null
115558
115691
  });
115559
115692
  sizingResult = { ...result, choice: normalizeSizeChoice(result.obj, sizes, decisionId) };
115560
115693
  }
@@ -115590,6 +115723,7 @@ async function decideHierarchical({
115590
115723
  publicReason: "",
115591
115724
  model: familyResult.model || agent.model,
115592
115725
  usage: usage.total_tokens ? usage : familyResult.usage ?? null,
115726
+ reasoning: { family: familyResult.reasoning ?? null, sizing: sizingResult?.reasoning ?? null },
115593
115727
  meta: {
115594
115728
  decisionArchitecture: DECISION_ARCHITECTURE_VERSION,
115595
115729
  method: isJev ? protocol === "jev_native" ? "jev-native-hierarchical" : "openrouter-decisions-hierarchical" : `${familyResult.protocol ?? protocol}-hierarchical`,
@@ -115672,6 +115806,7 @@ var els = {
115672
115806
  decisionActionLabel: $("#decisionActionLabel"),
115673
115807
  decisionActionHint: $("#decisionActionHint"),
115674
115808
  legalActions: $("#legalActions"),
115809
+ decisionReasoning: $("#decisionReasoning"),
115675
115810
  decisionLabelHand: $("#decisionLabelHand"),
115676
115811
  decisionLabelStreet: $("#decisionLabelStreet"),
115677
115812
  decisionLabelPosition: $("#decisionLabelPosition"),
@@ -115707,6 +115842,7 @@ var els = {
115707
115842
  refreshModelsBtn: $("#refreshModelsBtn"),
115708
115843
  seatProtocol: $("#seatProtocol"),
115709
115844
  seatProvider: $("#seatProvider"),
115845
+ seatCaptureReasoning: $("#seatCaptureReasoning"),
115710
115846
  seatError: $("#seatError"),
115711
115847
  removeSeatBtn: $("#removeSeatBtn"),
115712
115848
  cancelSeatBtn: $("#cancelSeatBtn"),
@@ -115733,6 +115869,7 @@ var els = {
115733
115869
  replaySummary: $("#replaySummary"),
115734
115870
  replayAction: $("#replayAction"),
115735
115871
  replayReason: $("#replayReason"),
115872
+ replayReasoning: $("#replayReasoning"),
115736
115873
  replayHistory: $("#replayHistory"),
115737
115874
  replayLegal: $("#replayLegal"),
115738
115875
  replayLegalSummary: $("#replayLegalSummary"),
@@ -116060,7 +116197,8 @@ function injectedEnvironmentConfig() {
116060
116197
  connectionId: String(player.connectionId || connections[0]?.id || ""),
116061
116198
  model: String(player.model || ""),
116062
116199
  protocol: String(player.protocol || "tool"),
116063
- provider: String(player.provider || "")
116200
+ provider: String(player.provider || ""),
116201
+ captureReasoning: Boolean(player.captureReasoning)
116064
116202
  }))
116065
116203
  };
116066
116204
  }
@@ -116169,28 +116307,8 @@ function tableCardHtml(card, kind = "board") {
116169
116307
  const red = suit === "\u2665" || suit === "\u2666";
116170
116308
  return `<span class="playing-card ${kind === "hole" ? "hole-card" : "board-card"} ${red ? "red" : ""}" data-rank="${escapeHtml(rank2)}" data-suit="${escapeHtml(suit)}" aria-label="${escapeHtml(rank2 + suit)}"></span>`;
116171
116309
  }
116172
- function integerGcd(values) {
116173
- const gcd = (a, b) => {
116174
- while (b) [a, b] = [b, a % b];
116175
- return Math.abs(a);
116176
- };
116177
- return values.map((value) => Math.abs(Math.round(Number(value) || 0))).filter(Boolean).reduce(gcd, 0) || 1;
116178
- }
116179
116310
  function potChipsHtml(pot, { smallBlind = 0, bigBlind = 0, ante = 0 } = {}) {
116180
- let remaining = Math.max(0, Math.round(Number(pot) || 0));
116181
- if (!remaining) return "";
116182
- const base = integerGcd([smallBlind, bigBlind, ante]);
116183
- const denominations = [
116184
- { color: "gold", value: base * 100 },
116185
- { color: "black", value: base * 25 },
116186
- { color: "blue", value: base * 5 },
116187
- { color: "red", value: base }
116188
- ];
116189
- return denominations.map(({ color, value }) => {
116190
- const count = Math.floor(remaining / value);
116191
- remaining -= count * value;
116192
- if (!count) return "";
116193
- const shown = Math.min(5, count);
116311
+ return potChipBreakdown(pot, { smallBlind, bigBlind, ante }).map(({ color, value, count, shown }) => {
116194
116312
  const chips = Array.from({ length: shown }, () => `<span class="pot-chip chip-${color}"></span>`).join("");
116195
116313
  const overflow = count > shown ? ` data-chip-count="\xD7${count}"` : "";
116196
116314
  return `<span class="chip-stack"${overflow} title="${count} \xD7 ${fmt(value)} chips" aria-label="${count} ${color} chips worth ${fmt(value)} each">${chips}</span>`;
@@ -116324,6 +116442,7 @@ function spectatorState(engine, tournamentMeta = {}) {
116324
116442
  status: p.status,
116325
116443
  position: positionForSeat(state, seat)
116326
116444
  } : null),
116445
+ markers: tableMarkerSeats(state),
116327
116446
  winners: state.winners ?? null
116328
116447
  };
116329
116448
  }
@@ -116396,7 +116515,7 @@ function normalizeConfig(input = {}) {
116396
116515
  if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
116397
116516
  const model = String(raw.model || "").trim();
116398
116517
  if (!model) throw new Error(`Model missing for ${name}`);
116399
- return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
116518
+ return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
116400
116519
  })
116401
116520
  };
116402
116521
  }
@@ -116472,7 +116591,7 @@ var TournamentDirector = class {
116472
116591
  for (const player of this.config.players) {
116473
116592
  this.engine.sit(player.seat, player.id, player.name, this.config.startingStack);
116474
116593
  this.timeBanks[player.id] = this.config.timeBankSeconds * 1e3;
116475
- this.stats[player.id] = { decisions: 0, invalid: 0, modelErrors: 0, providerErrors: 0, rateLimits: 0, timeouts: 0, autoFallbacks: 0, protocolFallbacks: 0, retries: 0, totalLatencyMs: 0, lastAction: null, lastReason: "" };
116594
+ this.stats[player.id] = { decisions: 0, invalid: 0, modelErrors: 0, providerErrors: 0, rateLimits: 0, timeouts: 0, autoFallbacks: 0, protocolFallbacks: 0, retries: 0, totalLatencyMs: 0, lastAction: null, lastActionType: null, lastActionAmount: null, lastReason: "" };
116476
116595
  }
116477
116596
  this.logEvent("TOURNAMENT_START", { config: this.publicConfig() });
116478
116597
  this.broadcast();
@@ -116524,7 +116643,7 @@ var TournamentDirector = class {
116524
116643
  const eliminatedPlayerIds = this.eliminations.map((e) => e.playerId);
116525
116644
  return {
116526
116645
  handNumber: this.handNumber,
116527
- playersRemaining: Math.max(0, startingPlayers - eliminatedPlayerIds.length),
116646
+ playersRemaining: this.playersRemaining().length,
116528
116647
  startingPlayers,
116529
116648
  levelIndex: this.engine?.state?.blindLevel ?? 0,
116530
116649
  eliminatedPlayerIds,
@@ -116611,31 +116730,28 @@ var TournamentDirector = class {
116611
116730
  }
116612
116731
  }
116613
116732
  pruneBustedSeats() {
116733
+ const failures = [];
116614
116734
  for (const p of this.engine?.state?.players ?? []) {
116615
116735
  if (!p || playerStack(p) > 0) continue;
116616
116736
  try {
116617
116737
  this.engine.stand(p.id);
116618
- } catch {
116738
+ } catch (error) {
116739
+ failures.push(`${p.name}: ${summarizeError(error)}`);
116619
116740
  }
116620
116741
  }
116742
+ if (failures.length) throw new Error(`Could not remove busted player(s): ${failures.join("; ")}`);
116621
116743
  }
116622
116744
  async startHand() {
116623
116745
  await this.waitIfPaused();
116624
116746
  this.pruneBustedSeats();
116625
- this.handNumber++;
116626
- for (const stat of Object.values(this.stats)) stat.lastAction = null;
116627
- this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
116628
- try {
116629
- this.engine.deal();
116630
- } catch {
116631
- for (const p of this.engine.state.players ?? []) if (p && playerStack(p) <= 0) {
116632
- try {
116633
- this.engine.stand(p.id);
116634
- } catch {
116635
- }
116636
- }
116637
- this.engine.deal();
116747
+ for (const stat of Object.values(this.stats)) {
116748
+ stat.lastAction = null;
116749
+ stat.lastActionType = null;
116750
+ stat.lastActionAmount = null;
116638
116751
  }
116752
+ this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
116753
+ this.engine.deal();
116754
+ this.handNumber = this.engine.state.handNumber;
116639
116755
  this.logEvent("HAND_START", {
116640
116756
  handNumber: this.handNumber,
116641
116757
  buttonSeat: this.engine.state.buttonSeat,
@@ -116741,6 +116857,19 @@ var TournamentDirector = class {
116741
116857
  if (incident?.category === "rate_limit") stats.rateLimits++;
116742
116858
  else if (incident?.category === "provider") stats.providerErrors++;
116743
116859
  };
116860
+ const liveReasoning = { family: "", sizing: "", flat: "" };
116861
+ let reasoningPaintAt = 0;
116862
+ const onDelta = (info) => {
116863
+ if (this.currentDecision?.id !== decisionId) return;
116864
+ const key = info.stage === "size" ? "sizing" : info.stage === "flat" ? "flat" : "family";
116865
+ if (info.channel === "reasoning") liveReasoning[key] = info.text;
116866
+ this.currentDecision.reasoning = { stage: info.stage, channel: info.channel, text: reasoningTail(info.text) };
116867
+ const now = Date.now();
116868
+ if (now - reasoningPaintAt >= 120) {
116869
+ reasoningPaintAt = now;
116870
+ this.broadcast();
116871
+ }
116872
+ };
116744
116873
  try {
116745
116874
  const before = performance.now();
116746
116875
  if (architecture === "hierarchical") {
@@ -116760,10 +116889,11 @@ var TournamentDirector = class {
116760
116889
  this.currentDecision.stage = info;
116761
116890
  this.broadcast();
116762
116891
  }
116763
- }
116892
+ },
116893
+ onDelta
116764
116894
  });
116765
116895
  } else {
116766
- result = await decide(agent, connection, { state: stateForAgent, legalActions, decisionId, timeoutMs: totalMs, abortSignal: this.abortController?.signal, pauseClock: this.currentDecision, representationMode: this.config.representation });
116896
+ result = await decide(agent, connection, { state: stateForAgent, legalActions, decisionId, timeoutMs: totalMs, abortSignal: this.abortController?.signal, pauseClock: this.currentDecision, representationMode: this.config.representation, onDelta });
116767
116897
  }
116768
116898
  elapsed = Math.max(0, Math.round(performance.now() - before - pausedTotal()));
116769
116899
  for (const incident of result?.meta?.incidents || []) recordIncident(incident);
@@ -116823,9 +116953,12 @@ var TournamentDirector = class {
116823
116953
  stats.decisions++;
116824
116954
  stats.totalLatencyMs += reportedLatency;
116825
116955
  stats.lastAction = chosen.description;
116956
+ stats.lastActionType = chosen.type;
116957
+ stats.lastActionAmount = chosen.amount ?? null;
116826
116958
  this.decisionCount++;
116827
116959
  if (this.decisionBudget > 0 && this.decisionCount >= this.decisionBudget) this.budgetReached = true;
116828
116960
  stats.lastReason = result?.publicReason || typedReason || (forced ? `Automatic ${chosen.description} after ${fallbackReason}.` : "");
116961
+ const reasoningLog = architecture === "hierarchical" ? { family: capReasoning(result?.reasoning?.family ?? liveReasoning.family), sizing: capReasoning(result?.reasoning?.sizing ?? liveReasoning.sizing) } : { flat: capReasoning(typeof result?.reasoning === "string" ? result.reasoning : liveReasoning.flat) };
116829
116962
  const decisionMeta = result?.meta ? {
116830
116963
  ...result.meta,
116831
116964
  decisionArchitecture: architecture === "hierarchical" ? DECISION_ARCHITECTURE_VERSION : "flat-v1",
@@ -116858,6 +116991,7 @@ var TournamentDirector = class {
116858
116991
  publicReason: stats.lastReason,
116859
116992
  usage: result?.usage ?? null,
116860
116993
  decisionMeta,
116994
+ reasoning: reasoningLog,
116861
116995
  benchmarkMode: this.config.benchmarkMode,
116862
116996
  decisionArchitecture: architecture,
116863
116997
  representation: this.config.representation,
@@ -117139,7 +117273,8 @@ function defaultSeatDraft(seatIndex) {
117139
117273
  connectionId,
117140
117274
  model: conn?.kind === "typesafe" ? "jev-latest" : "",
117141
117275
  protocol: conn?.kind === "typesafe" ? "jev_native" : "tool",
117142
- provider: ""
117276
+ provider: "",
117277
+ captureReasoning: false
117143
117278
  };
117144
117279
  }
117145
117280
  function applySeatProtocolRules() {
@@ -117147,7 +117282,14 @@ function applySeatProtocolRules() {
117147
117282
  const kind = conn?.kind || "openrouter";
117148
117283
  const openRouter = isOpenRouterConnection(conn);
117149
117284
  const jevViaOpenRouter = openRouter && isJevModel(els.seatModel.value);
117285
+ const jevSeat = kind === "typesafe" || jevViaOpenRouter;
117150
117286
  document.querySelector(".seat-provider-field")?.classList.toggle("hidden", !openRouter || jevViaOpenRouter);
117287
+ document.querySelector(".seat-reasoning-field")?.classList.toggle("hidden", jevSeat);
117288
+ if (els.seatCaptureReasoning) {
117289
+ els.seatCaptureReasoning.disabled = jevSeat;
117290
+ const alreadyReasoning = isReasoningModel(els.seatModel.value.trim());
117291
+ document.querySelector(".seat-reasoning-help")?.classList.toggle("hidden", !els.seatCaptureReasoning.checked || alreadyReasoning);
117292
+ }
117151
117293
  [...els.seatProtocol.options].forEach((option) => {
117152
117294
  option.disabled = option.value === "jev_native" && kind !== "typesafe" || option.value === "jev_decisions" && !openRouter;
117153
117295
  });
@@ -117188,12 +117330,14 @@ function openSeatEditor(seatIndex) {
117188
117330
  els.seatModel.value = draft.model || "";
117189
117331
  els.seatProtocol.value = draft.protocol || "tool";
117190
117332
  els.seatProvider.value = draft.provider || "";
117333
+ if (els.seatCaptureReasoning) els.seatCaptureReasoning.checked = Boolean(draft.captureReasoning);
117191
117334
  applySeatProtocolRules();
117192
117335
  syncSeatNameFromModel();
117193
117336
  void refreshSeatModelCatalog();
117194
117337
  els.seatError.classList.add("hidden");
117195
117338
  els.seatLockNotice.classList.toggle("hidden", !locked);
117196
117339
  for (const control of [els.seatName, els.seatConnection, els.seatModel, els.seatProtocol, els.seatProvider]) control.disabled = locked || control === els.seatProvider && (!isOpenRouterConnection(connectionById(els.seatConnection.value)) || isJevModel(els.seatModel.value));
117340
+ if (els.seatCaptureReasoning) els.seatCaptureReasoning.disabled = locked || els.seatCaptureReasoning.disabled;
117197
117341
  els.saveSeatBtn.disabled = locked;
117198
117342
  els.removeSeatBtn.disabled = locked || !seatAssignments[seatIndex];
117199
117343
  if (!els.seatDialog.open) els.seatDialog.showModal();
@@ -117210,7 +117354,8 @@ function readSeatDraft() {
117210
117354
  connectionId: els.seatConnection.value,
117211
117355
  model: els.seatModel.value.trim(),
117212
117356
  protocol: els.seatProtocol.value,
117213
- provider: els.seatProvider.disabled ? "" : els.seatProvider.value.trim()
117357
+ provider: els.seatProvider.disabled ? "" : els.seatProvider.value.trim(),
117358
+ captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked)
117214
117359
  };
117215
117360
  }
117216
117361
  function readSeatPlayers() {
@@ -117238,7 +117383,8 @@ function restoreSeatAssignments(rows = []) {
117238
117383
  connectionId,
117239
117384
  model: player.model || "",
117240
117385
  protocol: player.protocol || "tool",
117241
- provider: player.provider || ""
117386
+ provider: player.provider || "",
117387
+ captureReasoning: player.captureReasoning === true
117242
117388
  };
117243
117389
  });
117244
117390
  updateSeatSummary();
@@ -117386,7 +117532,7 @@ function seatPositionOnFelt(seat, slot, tableRect, feltRect, density, lobby) {
117386
117532
  const feltLeft = feltRect.left - tableRect.left;
117387
117533
  const feltTop = feltRect.top - tableRect.top;
117388
117534
  const centerX = feltLeft + feltRect.width / 2;
117389
- const centerY = feltTop + feltRect.height / 2;
117535
+ const centerY = feltTop + feltRect.height / 2 - (lobby ? 30 : 20);
117390
117536
  const scaleByDensity = {
117391
117537
  roomy: [1.22, 1.34],
117392
117538
  compact: [1.2, 1.31],
@@ -117422,10 +117568,14 @@ function positionTableMarker(marker, seat, tangentOffset = 0) {
117422
117568
  marker.style.top = `${feltRect.height / 2 + dy * edgeScale + dx / length * tangentOffset}px`;
117423
117569
  }
117424
117570
  function updateTableMarkers() {
117425
- const seats = [...els.seatsLayer.querySelectorAll(".table-seat[data-position]")];
117426
- const dealer = seats.find((seat) => tablePositionBadge(seat.dataset.position) === "D");
117427
- const sb = seats.find((seat) => tablePositionBadge(seat.dataset.position) === "SB");
117428
- const bb = seats.find((seat) => tablePositionBadge(seat.dataset.position) === "BB");
117571
+ const seats = [...els.seatsLayer.querySelectorAll(".table-seat[data-seat]")];
117572
+ const markerSeat = (role) => {
117573
+ const target = currentState?.table?.markers?.[role];
117574
+ return Number.isInteger(target) ? seats.find((seat) => Number(seat.dataset.seat) === target) : null;
117575
+ };
117576
+ const dealer = markerSeat("buttonSeat");
117577
+ const sb = markerSeat("smallBlindSeat");
117578
+ const bb = markerSeat("bigBlindSeat");
117429
117579
  const shared = dealer && dealer === sb;
117430
117580
  positionTableMarker(els.dealerMarker, dealer, shared ? -11 : 0);
117431
117581
  positionTableMarker(els.smallBlindMarker, sb, shared ? 11 : 0);
@@ -117537,24 +117687,24 @@ function renderTable(s) {
117537
117687
  els.pokerTable.classList.remove("lobby-mode");
117538
117688
  const players = table.players.filter(Boolean);
117539
117689
  const configuredPlayers = Array.isArray(s.config?.players) ? s.config.players : [];
117540
- const visualCount = Math.max(TABLE_MIN_PLAYERS, Math.min(MAX_LOBBY_SEATS, configuredPlayers.length || players.length));
117690
+ const visualCount = Math.max(TABLE_MIN_PLAYERS, Math.min(MAX_LOBBY_SEATS, players.length));
117541
117691
  const eliminatedIds = new Set((s.eliminations ?? []).map((e) => e.playerId));
117542
117692
  els.seatsLayer.innerHTML = players.map((p, i) => {
117543
117693
  const cfg = configuredPlayers.find((x) => x.id === p.id) || {}, stat = s.stats?.[p.id] || {};
117544
- const configuredIndex = configuredPlayers.findIndex((x) => x.id === p.id);
117545
- const visualIndex = configuredIndex >= 0 ? configuredIndex : i;
117694
+ const visualIndex = i;
117546
117695
  const active = table.actionTo === p.seat || s.currentDecision?.playerId === p.id, isWinner = s.status === "FINISHED" && s.winner?.playerId === p.id, busted = !isWinner && (eliminatedIds.has(p.id) || p.status === "BUSTED"), allIn = p.status === "ALL_IN" && !busted;
117547
117696
  const elimination = (s.eliminations ?? []).find((e) => e.playerId === p.id);
117548
117697
  const actionText = s.status === "FINISHED" ? isWinner ? "WINNER \xB7 1ST" : elimination ? `${ordinal(elimination.place)} \xB7 ELIMINATED` : stat.lastAction || "" : stat.lastAction || (busted ? "ELIMINATED" : allIn ? "ALL IN" : "");
117549
- const actionKind = isWinner ? "winner" : /raise/i.test(actionText) ? "raise" : /bet/i.test(actionText) ? "bet" : /call/i.test(actionText) ? "call" : /fold/i.test(actionText) ? "fold" : /check/i.test(actionText) ? "check" : "";
117550
- const action2 = tableActionParts(active && s.status !== "FINISHED" ? "THINKING" : actionText, active ? "thinking" : actionKind);
117551
- return `<div class="table-seat ${active ? "turn-active" : ""} ${isWinner ? "winner" : ""} ${busted ? "busted" : ""} ${allIn ? "all-in" : ""}" data-player-id="${escapeHtml(p.id)}" data-lobby-seat="${Number(cfg.lobbySeat ?? i)}" data-visual-index="${visualIndex}" data-visual-count="${visualCount}" data-position="${escapeHtml(p.position || "")}">
117698
+ const actionKind = isWinner ? "winner" : String(stat.lastActionType || "").toLowerCase();
117699
+ const shownAction = active && s.status !== "FINISHED" ? "THINKING" : actionText;
117700
+ const action2 = tableActionParts(shownAction, active ? "thinking" : actionKind, active ? null : stat.lastActionAmount);
117701
+ return `<div class="table-seat ${active ? "turn-active" : ""} ${isWinner ? "winner" : ""} ${busted ? "busted" : ""} ${allIn ? "all-in" : ""}" data-player-id="${escapeHtml(p.id)}" data-seat="${p.seat}" data-lobby-seat="${Number(cfg.lobbySeat ?? i)}" data-visual-index="${visualIndex}" data-visual-count="${visualCount}" data-position="${escapeHtml(p.position || "")}">
117552
117702
  <div class="seat-card">
117553
117703
  <i class="seat-turn" aria-hidden="true"></i><span class="seat-name">${escapeHtml(visiblePlayerName(p.name, cfg.model))}</span><span class="seat-stack">${fmt(p.stack)}</span>
117554
117704
  <span class="seat-model">${escapeHtml(shortModel(cfg.model))}</span><span class="seat-bb">${Number(p.stackBB || 0).toFixed(1)} BB</span>
117555
117705
  </div>
117556
117706
  <span class="hole-cards" aria-hidden="true">${[0, 1].map((k) => tableCardHtml(p.cards?.[k], "hole")).join("")}</span>
117557
- ${action2 ? `<span class="table-action ${active ? "thinking" : actionKind}" aria-label="Latest action"><span class="table-action-icon">${action2.icon}</span><span class="table-action-name">${escapeHtml(action2.name)}</span>${action2.value ? `<span class="table-action-value">${escapeHtml(action2.value)}</span>` : ""}</span>` : ""}
117707
+ ${action2 ? `<span class="table-action ${active ? "thinking" : actionKind}" aria-label="${escapeHtml(shownAction)}"><span class="table-action-icon">${action2.icon}</span><span class="table-action-name">${escapeHtml(action2.name)}</span>${action2.value ? `<span class="table-action-value">${escapeHtml(action2.value)}</span>` : ""}</span>` : ""}
117558
117708
  </div>`;
117559
117709
  }).join("");
117560
117710
  layoutTableSeats();
@@ -117571,18 +117721,13 @@ function renderTable(s) {
117571
117721
  els.levelValue.textContent = Number(table.blindLevel ?? 0) + 1;
117572
117722
  els.levelValue.title = `Level ${Number(table.blindLevel ?? 0) + 1}`;
117573
117723
  }
117574
- function tablePositionBadge(position) {
117575
- const value = String(position || "").trim().toUpperCase();
117576
- if (value === "BTN" || value === "BUTTON" || value === "D" || value === "DEALER") return "D";
117577
- return value === "SB" || value === "BB" ? value : "";
117578
- }
117579
- function tableActionParts(text, kind = "") {
117724
+ function tableActionParts(text, kind = "", actionAmount = null) {
117580
117725
  const value = String(text || "").trim();
117581
117726
  if (!value) return null;
117582
- const amount = value.match(/(?:^|\s)([\d,.]+(?:K|M)?)(?:\s|$)/i)?.[1] || "";
117583
- const label = { thinking: "THINKING", raise: "RAISE", bet: "BET", call: "CALL", check: "CHECK", fold: "FOLD", winner: "WINNER" }[kind];
117584
- const name = label || value.replace(amount, "").replace(/[·:\-]+$/g, "").trim() || value;
117585
- const icon = kind === "fold" ? "\xD7" : kind === "check" ? "\u2713" : kind === "call" ? "\u2192" : kind === "raise" ? "\u2197" : kind === "bet" ? "\u2191" : kind === "winner" ? "\u2605" : "\u2022";
117727
+ const amount = actionAmount == null ? value.match(/(?:^|\s)([\d,.]+(?:K|M)?)(?:\s|$)/i)?.[1] || "" : fmt(actionAmount);
117728
+ const label = { thinking: "THINKING", raise: "RAISE", bet: "BET", call: "CALL", check: "CHECK", fold: "FOLD", winner: "WINNER", all_in: "ALL IN" }[kind];
117729
+ const name = label || value;
117730
+ const icon = kind === "fold" ? "\xD7" : kind === "check" ? "\u2713" : kind === "call" ? "\u2192" : kind === "raise" ? "\u2197" : kind === "bet" || kind === "all_in" ? "\u2191" : kind === "winner" ? "\u2605" : "\u2022";
117586
117731
  return { icon, name, value: amount };
117587
117732
  }
117588
117733
  function clearTurnRings(keep = null) {
@@ -117694,6 +117839,37 @@ function decisionTelemetryHtml(event, { limit = 4 } = {}) {
117694
117839
  if (facts.length) parts.push(`<div class="decision-facts">${facts.join("")}</div>`);
117695
117840
  return parts.join("");
117696
117841
  }
117842
+ function reasoningTail(text, limit = 420) {
117843
+ const value = String(text || "");
117844
+ return value.length > limit ? `\u2026${value.slice(-limit)}` : value;
117845
+ }
117846
+ function capReasoning(text, limit = 8e3) {
117847
+ const value = String(text ?? "").trim();
117848
+ if (!value) return null;
117849
+ return value.length > limit ? `${value.slice(0, limit)}\u2026` : value;
117850
+ }
117851
+ function decisionReasoningText(event) {
117852
+ const reasoning = event?.reasoning;
117853
+ if (!reasoning) return "";
117854
+ return [reasoning.family, reasoning.sizing, reasoning.flat].filter(Boolean).join("\n\n");
117855
+ }
117856
+ function reasoningExcerpt(text, limit = 220) {
117857
+ const value = String(text || "").replace(/\s+/g, " ").trim();
117858
+ return value.length > limit ? `${value.slice(0, limit)}\u2026` : value;
117859
+ }
117860
+ function renderLiveReasoning(d) {
117861
+ const node = els.decisionReasoning;
117862
+ if (!node) return;
117863
+ const live = d?.reasoning ?? null;
117864
+ const text = String(live?.text || "").trim();
117865
+ node.classList.toggle("hidden", !text);
117866
+ if (!text) {
117867
+ node.innerHTML = "";
117868
+ return;
117869
+ }
117870
+ const label = live?.stage === "size" ? "Size reasoning" : live?.stage === "flat" ? "Reasoning" : "Action reasoning";
117871
+ node.innerHTML = `<span class="decision-reasoning-label">${escapeHtml(label)}${live?.channel === "content" ? " \xB7 answer" : ""}</span><span class="decision-reasoning-text">${escapeHtml(text)}</span>`;
117872
+ }
117697
117873
  function setDecisionContext({ hand = "\u2014", street = "\u2014", position = "\u2014", options = "\u2014", label = "Legal actions", hint = "Choose one", labels = null } = {}) {
117698
117874
  els.decisionHand.textContent = hand ?? "\u2014";
117699
117875
  els.decisionStreet.textContent = String(street ?? "\u2014").replaceAll("_", " ");
@@ -117743,6 +117919,7 @@ function renderDecision(s) {
117743
117919
  els.decisionPlayer.textContent = displayModelName(d.model);
117744
117920
  els.decisionPlayer.title = d.model;
117745
117921
  els.decisionModel.textContent = `${d.connection || "Connection"} \xB7 ${protocolDisplay(d.protocol)}`;
117922
+ renderLiveReasoning(d);
117746
117923
  if (d.architecture === "hierarchical") {
117747
117924
  const stage = d.stage ?? d.hierarchy?.stage1 ?? null;
117748
117925
  const families = stage?.families ?? d.hierarchy?.families ?? [];
@@ -117762,6 +117939,7 @@ function renderDecision(s) {
117762
117939
  startClock(d);
117763
117940
  return;
117764
117941
  }
117942
+ renderLiveReasoning(null);
117765
117943
  stopClock();
117766
117944
  if (finished) {
117767
117945
  const winnerLast = [...s.events || []].reverse().find((e) => e.type === "DECISION" && e.playerId === s.winner.playerId) || last;
@@ -117820,11 +117998,21 @@ function renderDecision(s) {
117820
117998
  els.legalActions.innerHTML = '<span class="action-chip">Start the tournament to stream model actions here.</span>';
117821
117999
  }
117822
118000
  }
118001
+ function liveFeedRowHtml(d) {
118002
+ const model = d.model || "";
118003
+ const stage = d.stage?.stage === "size" ? `${String(d.stage.family || "").toUpperCase()} size` : d.architecture === "hierarchical" ? "Action family" : "Legal actions";
118004
+ const reasoning = String(d.reasoning?.text || "").trim();
118005
+ const isAnswer = d.reasoning?.channel === "content";
118006
+ const bodyHtml = reasoning ? `<div class="decision-reasoning-excerpt live"><span class="decision-reasoning-label">${isAnswer ? "Streaming answer" : "Streaming reasoning"}</span>${escapeHtml(reasoningExcerpt(reasoning, 260))}</div>` : '<div class="decision-reason live"><span class="live-pulse" aria-hidden="true"></span>Thinking\u2026</div>';
118007
+ const elapsed = d.startedAt ? Math.max(0, Date.now() - d.startedAt - (d.pausedMs || 0)) : 0;
118008
+ return `<div class="decision-item decision-history-item live" aria-label="Live decision by ${escapeHtml(displayModelName(model))}"><div class="decision-item-head"><strong title="${escapeHtml(model)}">${escapeHtml(displayModelName(model))}</strong><span class="decision-item-action live-action"><span class="live-pulse" aria-hidden="true"></span>LIVE</span></div><div class="decision-reason">${escapeHtml(stage)}${d.street ? ` \xB7 ${escapeHtml(String(d.street))}` : ""}${d.position ? ` \xB7 ${escapeHtml(d.position)}` : ""}</div>` + bodyHtml + `<div class="decision-meta mono">${escapeHtml(d.connection || "")} \xB7 ${escapeHtml(shortModel(model))} \xB7 ${(elapsed / 1e3).toFixed(1)}s<span class="decision-replay-hint">streaming</span></div></div>`;
118009
+ }
117823
118010
  function renderFeed(s) {
117824
118011
  const feedArchive = eventArchive(s);
117825
118012
  const events = feedArchive.filter((e) => e.type === "DECISION").slice(-HISTORY_CONFIG.recentDecisions).reverse();
117826
118013
  const explanations = new Map(feedArchive.filter((e) => e.type === "SPECTATOR_EXPLANATION").map((e) => [e.decisionId, e.text]));
117827
- els.decisionFeed.innerHTML = events.length ? events.map((e) => {
118014
+ const liveHtml = s?.currentDecision ? liveFeedRowHtml(s.currentDecision) : "";
118015
+ const rowsHtml = events.map((e) => {
117828
118016
  const infra = [];
117829
118017
  if (e.protocolFallback) infra.push(e.protocolFallback);
117830
118018
  if (e.decisionMeta?.retryCount) infra.push(`retry ${e.decisionMeta.retryCount}`);
@@ -117832,9 +118020,14 @@ function renderFeed(s) {
117832
118020
  if (e.forced) infra.push("AUTO FALLBACK");
117833
118021
  const telemetry = decisionTelemetryHtml(e);
117834
118022
  const reason = explanations.get(e.decisionId) || e.publicReason || (e.decisionMeta?.family ? "Typed decision (no text rationale)" : "No public rationale");
117835
- return `<button type="button" class="decision-item decision-history-item ${e.error ? "error" : ""}" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))} decision"><div class="decision-item-head"><strong title="${escapeHtml(e.configuredModel || e.resolvedModel || "")}">${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))}</strong><span class="decision-item-action">${escapeHtml(e.action?.description || "\u2014")}</span></div><div class="decision-reason">${escapeHtml(reason)}</div>${telemetry ? `<div class="decision-item-telemetry">${telemetry}</div>` : ""}<div class="decision-meta mono">${escapeHtml(e.connection || "")} \xB7 ${escapeHtml(shortModel(e.resolvedModel || e.configuredModel || ""))} \xB7 ${e.primaryDecisionLatencyMs || e.latencyMs || 0}ms${infra.length ? ` \xB7 ${escapeHtml(infra.join(" \xB7 "))}` : ""}${e.error ? ` \xB7 ${escapeHtml(e.error)}` : ""}<span class="decision-replay-hint">View hand \u2197</span></div></button>`;
117836
- }).join("") : '<div class="empty-state">No decisions yet.</div>';
117837
- if (els.decisionFeedSummary) els.decisionFeedSummary.textContent = events.length ? `${events.length} most recent` : "Newest first";
118023
+ const reasoning = decisionReasoningText(e);
118024
+ const reasoningHtml = reasoning ? `<div class="decision-reasoning-excerpt" title="${escapeHtml(reasoning)}"><span class="decision-reasoning-label">Reasoning</span>${escapeHtml(reasoningExcerpt(reasoning))}</div>` : "";
118025
+ return `<button type="button" class="decision-item decision-history-item ${e.error ? "error" : ""}" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))} decision"><div class="decision-item-head"><strong title="${escapeHtml(e.configuredModel || e.resolvedModel || "")}">${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))}</strong><span class="decision-item-action">${escapeHtml(e.action?.description || "\u2014")}</span></div><div class="decision-reason">${escapeHtml(reason)}</div>${reasoningHtml}${telemetry ? `<div class="decision-item-telemetry">${telemetry}</div>` : ""}<div class="decision-meta mono">${escapeHtml(e.connection || "")} \xB7 ${escapeHtml(shortModel(e.resolvedModel || e.configuredModel || ""))} \xB7 ${e.primaryDecisionLatencyMs || e.latencyMs || 0}ms${infra.length ? ` \xB7 ${escapeHtml(infra.join(" \xB7 "))}` : ""}${e.error ? ` \xB7 ${escapeHtml(e.error)}` : ""}<span class="decision-replay-hint">View hand \u2197</span></div></button>`;
118026
+ }).join("");
118027
+ els.decisionFeed.innerHTML = liveHtml || rowsHtml ? `${liveHtml}${rowsHtml}` : '<div class="empty-state">No decisions yet.</div>';
118028
+ if (els.decisionFeedSummary) {
118029
+ els.decisionFeedSummary.textContent = s?.currentDecision ? `${events.length} settled \xB7 1 live` : events.length ? `${events.length} most recent` : "Newest first";
118030
+ }
117838
118031
  }
117839
118032
  function eventArchive(s = currentState) {
117840
118033
  if (director?.events?.length) return director.events;
@@ -117923,7 +118116,7 @@ function renderStats(s) {
117923
118116
  function tableRenderSignature(s) {
117924
118117
  const running = ["RUNNING", "PAUSED"].includes(s?.status);
117925
118118
  if (lobbyVisible && !running) {
117926
- return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider] : null)]);
118119
+ return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning)] : null)]);
117927
118120
  }
117928
118121
  const t = s?.table;
117929
118122
  if (!t) return JSON.stringify(["empty", s?.status || "IDLE"]);
@@ -117952,7 +118145,10 @@ function decisionRenderSignature(s) {
117952
118145
  return JSON.stringify([d ? [d.id, d.playerId, d.model, d.protocol, d.provider, d.startedAt, d.baseMs, d.timeBankMs, d.pausedMs || 0, Boolean(d.pausedAt), d.architecture || null, d.stage || null, d.legalActions] : null, last?.id || null, s?.status || "IDLE", seatAssignments.filter(Boolean).length]);
117953
118146
  }
117954
118147
  function feedRenderSignature(s) {
117955
- return (s?.events || []).filter((e) => e.type === "DECISION" || e.type === "SPECTATOR_EXPLANATION").slice(-24).map((e) => `${e.type}:${e.id}`).join("|");
118148
+ const completed = (s?.events || []).filter((e) => e.type === "DECISION" || e.type === "SPECTATOR_EXPLANATION").slice(-24).map((e) => `${e.type}:${e.id}`);
118149
+ const live = s?.currentDecision;
118150
+ if (live) completed.push(`LIVE:${live.id}:${live.stage?.stage ?? ""}:${live.reasoning?.text ?? ""}`);
118151
+ return completed.join("|");
117956
118152
  }
117957
118153
  function eventsRenderSignature(s) {
117958
118154
  const ev = eventArchive(s);
@@ -118328,10 +118524,23 @@ function createCaptureVideo(stream) {
118328
118524
  pointerEvents: "none",
118329
118525
  zIndex: "-1"
118330
118526
  });
118331
- video.srcObject = stream;
118527
+ video.srcObject = new MediaStream(stream.getVideoTracks());
118332
118528
  document.body.append(video);
118333
118529
  return video;
118334
118530
  }
118531
+ function createCaptureAudioMonitor(track) {
118532
+ if (!track || track.getSettings?.().suppressLocalAudioPlayback !== true) return null;
118533
+ const audio = document.createElement("audio");
118534
+ audio.autoplay = true;
118535
+ audio.controls = false;
118536
+ audio.setAttribute("aria-hidden", "true");
118537
+ Object.assign(audio.style, { position: "fixed", width: "1px", height: "1px", opacity: "0", pointerEvents: "none" });
118538
+ audio.srcObject = new MediaStream([track]);
118539
+ document.body.append(audio);
118540
+ void audio.play().catch(() => {
118541
+ });
118542
+ return audio;
118543
+ }
118335
118544
  function waitForCaptureVideo(video, track) {
118336
118545
  return new Promise((resolve, reject) => {
118337
118546
  let settled = false;
@@ -118474,6 +118683,14 @@ function disposeTableRecording(recording) {
118474
118683
  recording.captureVideo.srcObject = null;
118475
118684
  recording.captureVideo.remove();
118476
118685
  }
118686
+ try {
118687
+ recording.audioMonitor?.pause?.();
118688
+ } catch {
118689
+ }
118690
+ if (recording.audioMonitor) {
118691
+ recording.audioMonitor.srcObject = null;
118692
+ recording.audioMonitor.remove();
118693
+ }
118477
118694
  recording.canvasStream?.getTracks?.().forEach((track) => track.stop());
118478
118695
  try {
118479
118696
  recording.outputVideoTrack?.stop?.();
@@ -118501,6 +118718,7 @@ async function startTableRecording() {
118501
118718
  }
118502
118719
  let captureStream = null;
118503
118720
  let captureVideo = null;
118721
+ let audioMonitor = null;
118504
118722
  let recording = null;
118505
118723
  try {
118506
118724
  captureStream = await navigator.mediaDevices.getDisplayMedia({
@@ -118536,10 +118754,21 @@ async function startTableRecording() {
118536
118754
  const sourceAudioTrack = captureStream.getAudioTracks()[0] || null;
118537
118755
  if (sourceAudioTrack?.applyConstraints) {
118538
118756
  try {
118539
- await sourceAudioTrack.applyConstraints({ suppressLocalAudioPlayback: false });
118757
+ await sourceAudioTrack.applyConstraints({ suppressLocalAudioPlayback: { exact: false } });
118540
118758
  } catch {
118759
+ try {
118760
+ await sourceAudioTrack.applyConstraints({ suppressLocalAudioPlayback: false });
118761
+ } catch {
118762
+ }
118541
118763
  }
118542
118764
  }
118765
+ if (soundEnabled) {
118766
+ try {
118767
+ await getAudioContext()?.resume();
118768
+ } catch {
118769
+ }
118770
+ }
118771
+ audioMonitor = createCaptureAudioMonitor(sourceAudioTrack);
118543
118772
  const audioTrack = sourceAudioTrack;
118544
118773
  captureVideo = createCaptureVideo(captureStream);
118545
118774
  await captureVideo.play().catch(() => {
@@ -118567,6 +118796,7 @@ async function startTableRecording() {
118567
118796
  sourceHeight,
118568
118797
  audioTrack,
118569
118798
  sourceAudioTrack,
118799
+ audioMonitor,
118570
118800
  chunks: [],
118571
118801
  stopping: false,
118572
118802
  pipelineError: null
@@ -118623,6 +118853,14 @@ async function startTableRecording() {
118623
118853
  } catch (err) {
118624
118854
  if (recording) disposeTableRecording(recording);
118625
118855
  else {
118856
+ try {
118857
+ audioMonitor?.pause?.();
118858
+ } catch {
118859
+ }
118860
+ if (audioMonitor) {
118861
+ audioMonitor.srcObject = null;
118862
+ audioMonitor.remove();
118863
+ }
118626
118864
  captureStream?.getTracks?.().forEach((track) => track.stop());
118627
118865
  if (captureVideo) {
118628
118866
  captureVideo.srcObject = null;
@@ -118686,6 +118924,11 @@ function openDecisionReplay(eventId) {
118686
118924
  els.replaySubtitle.textContent = replay ? "Exact decision snapshot captured immediately before the model acted." : "This older decision does not contain a replay snapshot.";
118687
118925
  els.replayAction.textContent = event.action?.description || event.action?.type || "\u2014";
118688
118926
  els.replayReason.textContent = replayReasonText(event);
118927
+ const reasoningText = decisionReasoningText(event);
118928
+ if (els.replayReasoning) {
118929
+ els.replayReasoning.classList.toggle("hidden", !reasoningText);
118930
+ els.replayReasoning.innerHTML = reasoningText ? `<span class="decision-reasoning-label">Model reasoning \xB7 raw, unvetted scratch work</span><p>${escapeHtml(reasoningText)}</p>` : "";
118931
+ }
118689
118932
  if (!replay) {
118690
118933
  els.replayOpponents.innerHTML = "";
118691
118934
  els.replayStreet.textContent = event.street || "\u2014";
@@ -119256,6 +119499,7 @@ els.seatName.addEventListener("input", () => {
119256
119499
  seatNameAuto = false;
119257
119500
  });
119258
119501
  els.seatProtocol.addEventListener("change", applySeatProtocolRules);
119502
+ els.seatCaptureReasoning?.addEventListener("change", applySeatProtocolRules);
119259
119503
  els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
119260
119504
  els.removeSeatBtn.addEventListener("click", () => {
119261
119505
  if (editingSeatIndex == null || director && ["RUNNING", "PAUSED"].includes(director.status)) return;