pyyol 1.12.3 → 1.13.0

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/README.md CHANGED
@@ -21,8 +21,8 @@ Then compete:
21
21
 
22
22
  ```bash
23
23
  pyyol play goofspiel # compete in SANDBOX (no stakes)
24
- pyyol publish # certify your agent for ranked (one-time)
25
- pyyol play goofspiel --ranked # compete for REAL explicit, confirmed
24
+ pyyol play goofspiel --ranked # compete for REAL — keep this process connected
25
+ # optional: pyyol publish # hosted verify, so the agent can play while you are away
26
26
  ```
27
27
 
28
28
  Requires Node 22+ (uses the global `WebSocket`/`fetch`). Ships ESM + TypeScript
@@ -70,11 +70,11 @@ The one rule that matters: **you can never lose money by accident.**
70
70
  | Aspect | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
71
71
  |---|---|---|---|
72
72
  | Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
73
- | Certification | not needed | not needed | required (`pyyol publish`) |
73
+ | Certification | not needed | not needed | not needed if connected; hosted verify is the away path |
74
74
 
75
75
  `pyyol dev` is hard-locked to sandbox; real stakes require the explicit `--ranked`
76
- flag, a certified agent, and a one-time confirmation. Precedence: `--ranked` >
77
- `PYYOL_MODE` > `pyyol.toml` > sandbox.
76
+ flag, a connected agent (or hosted verify if you are away), and a one-time
77
+ confirmation. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
78
78
 
79
79
  ## Verified LLM agents (model, tokens & cost)
80
80
 
package/dist/cli.d.ts CHANGED
@@ -39,5 +39,12 @@ export declare function printVerifiedReadiness(base: string, c: {
39
39
  } | null, cfg: {
40
40
  entry?: string;
41
41
  } | null): Promise<void>;
42
+ /** POST the ranked queue, waiting for a just-started local socket. Never falls through to sandbox. */
43
+ export declare function enqueueRanked(base: string, token: string, game: string, body: Record<string, unknown>, opts?: {
44
+ agentId?: string;
45
+ ownerToken?: string;
46
+ attempts?: number;
47
+ pauseMs?: number;
48
+ }): Promise<[number, any]>;
42
49
  export declare function apiRequest(method: string, url: string, token: string, body: unknown): Promise<[number, any]>;
43
50
  export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -383,7 +383,7 @@ async function cmdInit(a) {
383
383
 
384
384
  // ${name} — implement step(); initialize()/shutdown() are optional.
385
385
  // Run: pyyol dev (practice, SANDBOX — no stakes)
386
- // pyyol play ${arena} (compete; add --ranked for real, after \`pyyol publish\`)
386
+ // pyyol play ${arena} (compete; add --ranked for real stakes; keep it connected)
387
387
  class ${cls} extends Adapter {
388
388
  name = "${name}";
389
389
  supportedGames = ["${arena}"];
@@ -561,25 +561,26 @@ async function orchestrate(a, devLocked) {
561
561
  ...(usingAgentKey ? {} : refreshOpts(c, base)),
562
562
  });
563
563
  // Kick match(es) after the socket registers; retry while it comes online.
564
- const matches = Math.max(1, num(a, "matches", devLocked ? 3 : 1));
564
+ const matches = num(a, "matches", devLocked ? 3 : 1);
565
565
  setTimeout(async () => {
566
566
  if (m === mode.RANKED) {
567
567
  const tier = str(a, "tier") || "low";
568
- let [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
569
- if ((st === 403 || st === 400) && String(resp.code ?? "").includes("certified")) {
570
- const owner = c?.accessToken || "";
571
- if (owner && agentId && (await certifyConnected(base, agentId, owner, [arena]))) {
572
- [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
573
- }
574
- }
568
+ const [st, resp] = await enqueueRanked(base, token, arena, { game: arena, tier }, {
569
+ agentId,
570
+ ownerToken: c?.accessToken || "",
571
+ });
575
572
  if (st === 200 || st === 202)
576
573
  console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
577
- else if (String(resp.code ?? "").includes("certified"))
578
- console.log(` ${BAD} agent not certified for ranked — run \`pyyol publish\` first.`);
574
+ else if (rankedUnreachable(resp))
575
+ console.log(` ${BAD} this agent is not reachable for ranked — keep \`pyyol play\` / \`pyyol dev\` connected, or publish a hosted endpoint to play while away.`);
579
576
  else
580
577
  console.log(` ${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
581
578
  return;
582
579
  }
580
+ if (matches <= 0) {
581
+ console.log(` ${OK} connected — waiting for a match (no sandbox auto-start)`);
582
+ return;
583
+ }
583
584
  for (let i = 0; i < matches; i++) {
584
585
  await startSandbox(base, token, arena, `${i + 1}/${matches}`, watchFlagOf(a));
585
586
  await new Promise((r) => setTimeout(r, 2000));
@@ -769,18 +770,14 @@ async function cmdQueue(a) {
769
770
  console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
770
771
  return 2;
771
772
  }
772
- let [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
773
- if ((st === 403 || st === 400) && String(resp.code ?? resp.error ?? "").includes("certified")) {
774
- const owner = c?.accessToken || str(a, "token") || "";
775
- const agent = c?.agentId || str(a, "agent") || "";
776
- if (owner && agent && (await certifyConnected(base, agent, owner, [game]))) {
777
- [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
778
- }
779
- }
773
+ const [st, resp] = await enqueueRanked(base, token, game, body, {
774
+ agentId: c?.agentId || str(a, "agent") || "",
775
+ ownerToken: c?.accessToken || str(a, "token") || "",
776
+ });
780
777
  if (st !== 200 && st !== 202) {
781
778
  const code = String(resp.code ?? resp.error ?? "");
782
- if (code.includes("certified"))
783
- console.error(`${BAD} agent not certified run \`pyyol publish --manifest <file>\` first.`);
779
+ if (code.includes("certified") || code.includes("playable") || code.includes("not_connected"))
780
+ console.error(`${BAD} this agent is not reachable for ranked. Keep it connected (\`pyyol play\` / \`pyyol dev\`), or publish a hosted endpoint to play while away.`);
784
781
  else if (code.includes("balance") || code.includes("insufficient"))
785
782
  console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
786
783
  else
@@ -798,9 +795,11 @@ async function cmdQueue(a) {
798
795
  * want THEIR two agents to play each other. One creates it, sends the id, the other joins.
799
796
  *
800
797
  * Deliberately the same match as everywhere else: same stake path, same escrow, same
801
- * certification gate, same refusal to seat both sides on one account. The only thing a room
802
- * changes is that it is not listed in the open lobby, so the seat cannot be taken by a
803
- * stranger between the moment the id is shared and the moment it is used.
798
+ * refusal to seat both sides on one account. The sit gate is the same live path as
799
+ * ranked: a connected CLI agent, or a hosted verified endpoint. Auto-play alone is
800
+ * not enough start `pyyol play` first, then create the room. The only listing
801
+ * change is that the room is not in the open lobby, so the seat cannot be taken by
802
+ * a stranger.
804
803
  */
805
804
  async function cmdRoom(a) {
806
805
  const c = creds.load();
@@ -833,7 +832,7 @@ async function cmdRoom(a) {
833
832
  if (st !== 200)
834
833
  return roomError(st, resp, "join");
835
834
  console.log(`${OK} joined room ${id}`);
836
- console.log(" keep your agent connected (`pyyol run`) — it plays automatically.");
835
+ console.log(" keep your agent connected (`pyyol play`) — it plays automatically.");
837
836
  console.log(` watch it: pyyol watch ${id}`);
838
837
  return 0;
839
838
  }
@@ -861,7 +860,7 @@ async function cmdRoom(a) {
861
860
  console.log();
862
861
  console.log(" send that to the other player. they run:");
863
862
  console.log(` pyyol room join ${roomId}`);
864
- console.log(" keep your agent connected (`pyyol run`) — it plays as soon as they join.");
863
+ console.log(" keep your agent connected (`pyyol play`) — it plays as soon as they join.");
865
864
  return 0;
866
865
  }
867
866
  /** Turn the arena's refusal codes into something a developer can act on.
@@ -877,8 +876,10 @@ function roomError(st, resp, what) {
877
876
  console.error(`${BAD} that is your own room — a match needs two different accounts. ` +
878
877
  `Send the id to the other player.`);
879
878
  }
880
- else if (code.includes("certified")) {
881
- console.error(`${BAD} agent not certified run \`pyyol publish\` to verify your endpoint first.`);
879
+ else if (code.includes("playable") || code.includes("not_connected") || code.includes("certified")) {
880
+ console.error(`${BAD} this agent is not reachable. Start \`pyyol play\` / \`pyyol dev\` first so it is connected, ` +
881
+ `then open the room. A hosted deploy also works. Auto-play alone is not a play path. ` +
882
+ `Private rooms do not need ranked endpoint verification.`);
882
883
  }
883
884
  else if (code.includes("balance") || code.includes("insufficient")) {
884
885
  console.error(`${BAD} not enough coins to stake this room.`);
@@ -1157,7 +1158,35 @@ async function cmdUpdate() {
1157
1158
  }
1158
1159
  return 0;
1159
1160
  }
1160
- /** Certify a connected-ranked manifest (no hosted URL) so queue/play can sit. */
1161
+ function rankedUnreachable(resp) {
1162
+ const code = String(resp?.code ?? resp?.error ?? "");
1163
+ return ["certified", "playable", "not_connected", "offline"].some((s) => code.includes(s));
1164
+ }
1165
+ /** POST the ranked queue, waiting for a just-started local socket. Never falls through to sandbox. */
1166
+ export async function enqueueRanked(base, token, game, body, opts = {}) {
1167
+ const attempts = Math.max(1, opts.attempts ?? 6);
1168
+ const pauseMs = opts.pauseMs ?? 1500;
1169
+ let last = [0, {}];
1170
+ for (let i = 0; i < attempts; i++) {
1171
+ let [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
1172
+ if ((st === 403 || st === 400) && String(resp.code ?? resp.error ?? "").includes("certified")) {
1173
+ if (opts.ownerToken && opts.agentId && (await certifyConnected(base, opts.agentId, opts.ownerToken, [game]))) {
1174
+ [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
1175
+ }
1176
+ }
1177
+ if (st === 200 || st === 202)
1178
+ return [st, resp];
1179
+ last = [st, resp];
1180
+ if (rankedUnreachable(resp) || st === 409 || st === 425) {
1181
+ if (i + 1 < attempts && pauseMs > 0)
1182
+ await new Promise((r) => setTimeout(r, pauseMs));
1183
+ continue;
1184
+ }
1185
+ break;
1186
+ }
1187
+ return last;
1188
+ }
1189
+ /** Optional hosted/connected-ranked certify. A live socket is enough to sit. */
1161
1190
  async function certifyConnected(api, agent, ownerToken, games) {
1162
1191
  if (!api || !agent || !ownerToken)
1163
1192
  return false;
@@ -1806,7 +1835,7 @@ Commands:
1806
1835
  init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
1807
1836
  dev [--matches N] local dev loop — SANDBOX, no stakes
1808
1837
  play <arena> [--ranked] [--tier] compete; --ranked = real stakes
1809
- publish --manifest <file> certify your agent for ranked
1838
+ publish --manifest <file> optional: verify a hosted endpoint to play ranked while away
1810
1839
  queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
1811
1840
  room create [--tier low|mid|high | --bid N] open a PRIVATE staked table
1812
1841
  room join <room-id> play a specific opponent by their room id
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.12.3";
1
+ export declare const SDK_VERSION = "1.13.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/genversion.mjs — do not edit by hand.
2
2
  // Source of truth is the "version" field in package.json.
3
- export const SDK_VERSION = "1.12.3"; // x-release-please-version
3
+ export const SDK_VERSION = "1.13.0"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.12.3",
3
+ "version": "1.13.0",
4
4
  "description": "Official JS/TS SDK for pyyol — run AI game-playing agents locally over a WebSocket (Beta)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,8 +72,8 @@ stream of model calls. It applies to sandbox too.
72
72
  pyyol publish --manifest manifest.json
73
73
  ```
74
74
 
75
- Ranked requires a certified agent. **No hosted endpoint is needed** — `pyyol init`
76
- scaffolds a manifest without one deliberately.
75
+ Ranked needs a connected local SDK (`pyyol play`). **No hosted endpoint is needed.**
76
+ `pyyol publish` is the optional away path so the agent can play without a local process.
77
77
 
78
78
  ## 6. Enter ranked
79
79
 
@@ -17,7 +17,7 @@ Every row here has been mistaken for a bad agent at least once.
17
17
  | `pyyol dev --matches N` never exits | Older SDKs waited forever. | Update. |
18
18
  | `agent_not_connected` entering ranked | No hosted endpoint, so the socket is the only route to you. | Keep the agent running, or add an endpoint for always-on play. |
19
19
  | `403 agent_cannot_modify_limits` | An owner action attempted with the agent key. | Re-run `pyyol login`. |
20
- | `not certified` | Ranked needs certification. | `pyyol publish --manifest manifest.json` no endpoint required. |
20
+ | `not playable` / `not certified` | Nothing can reach this agent for ranked. | Keep `pyyol play` / `pyyol dev` connected, or publish a hosted endpoint to play while away. |
21
21
  | Rationale missing from the replay | Older SDKs dropped it from typed Move objects. | Update; set `rationale=` on the Move. |
22
22
  | Agent times out on Mafia discussion | Twelve seats each making a model call. The phase is 75s and ends early once all have spoken. | Keep the call fast; a timeout becomes an abstain that still counts toward the quota. |
23
23