pyyol 1.12.3 → 1.14.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 +5 -5
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +115 -39
- package/dist/intent.d.ts +35 -0
- package/dist/intent.js +87 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/rules/llms-full.txt +65 -35
- package/skill/references/setup.md +5 -4
- package/skill/references/troubleshooting.md +1 -1
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
|
|
25
|
-
pyyol
|
|
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 |
|
|
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
|
|
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
|
@@ -19,6 +19,7 @@ import { deriveConnectUrl, deviceLabel, runLoginFlow } from "./login.js";
|
|
|
19
19
|
import * as mode from "./mode.js";
|
|
20
20
|
import { RuntimeConnector } from "./runtime.js";
|
|
21
21
|
import { askWatch, watchUrl, WATCH_BROWSER, WATCH_TERMINAL } from "./watch.js";
|
|
22
|
+
import { INTENT_INVITE, friendsUrl, resolveStartupIntent, } from "./intent.js";
|
|
22
23
|
import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADER, computeSignature, } from "./signing.js";
|
|
23
24
|
import { SDK_VERSION } from "./version.js";
|
|
24
25
|
const OK = "✓";
|
|
@@ -383,7 +384,7 @@ async function cmdInit(a) {
|
|
|
383
384
|
|
|
384
385
|
// ${name} — implement step(); initialize()/shutdown() are optional.
|
|
385
386
|
// Run: pyyol dev (practice, SANDBOX — no stakes)
|
|
386
|
-
// pyyol play ${arena} (compete; add --ranked for real
|
|
387
|
+
// pyyol play ${arena} (compete; add --ranked for real stakes; keep it connected)
|
|
387
388
|
class ${cls} extends Adapter {
|
|
388
389
|
name = "${name}";
|
|
389
390
|
supportedGames = ["${arena}"];
|
|
@@ -560,26 +561,62 @@ async function orchestrate(a, devLocked) {
|
|
|
560
561
|
// agent key can't expire, so refresh is only wired when NOT using it.
|
|
561
562
|
...(usingAgentKey ? {} : refreshOpts(c, base)),
|
|
562
563
|
});
|
|
564
|
+
// Join vs Invite — only on `pyyol play` (not `dev`). Non-TTY / --queue / --ranked
|
|
565
|
+
// keep today's auto-start so CI and scripts never hang on a prompt.
|
|
566
|
+
let startup = "queue";
|
|
567
|
+
if (!devLocked) {
|
|
568
|
+
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
569
|
+
startup = await resolveStartupIntent({
|
|
570
|
+
ranked: m === mode.RANKED || bool(a, "ranked"),
|
|
571
|
+
mode: str(a, "mode") || null,
|
|
572
|
+
queueFlag: bool(a, "queue"),
|
|
573
|
+
inviteFlag: bool(a, "invite"),
|
|
574
|
+
isTty: tty,
|
|
575
|
+
});
|
|
576
|
+
if (startup === INTENT_INVITE) {
|
|
577
|
+
const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
578
|
+
const url = friendsUrl(dash);
|
|
579
|
+
console.log(` ${OK} invite mode — connected; not queueing a match`);
|
|
580
|
+
if (url) {
|
|
581
|
+
console.log(` ${OK} invite a friend: ${url}`);
|
|
582
|
+
const openMode = str(a, "open") || "auto";
|
|
583
|
+
if (openMode !== "never" && tty) {
|
|
584
|
+
try {
|
|
585
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
586
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
587
|
+
console.log(` ${OK} opened Play a friend in your browser`);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
/* link already printed */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
563
596
|
// Kick match(es) after the socket registers; retry while it comes online.
|
|
564
|
-
|
|
597
|
+
// Invite mode stays connected only — the developer hosts from /friends.
|
|
598
|
+
const matches = num(a, "matches", devLocked ? 3 : 1);
|
|
565
599
|
setTimeout(async () => {
|
|
600
|
+
if (!devLocked && startup === INTENT_INVITE)
|
|
601
|
+
return;
|
|
566
602
|
if (m === mode.RANKED) {
|
|
567
603
|
const tier = str(a, "tier") || "low";
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
[st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
604
|
+
const [st, resp] = await enqueueRanked(base, token, arena, { game: arena, tier }, {
|
|
605
|
+
agentId,
|
|
606
|
+
ownerToken: c?.accessToken || "",
|
|
607
|
+
});
|
|
575
608
|
if (st === 200 || st === 202)
|
|
576
609
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
577
|
-
else if (
|
|
578
|
-
console.log(` ${BAD} agent not
|
|
610
|
+
else if (rankedUnreachable(resp))
|
|
611
|
+
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
612
|
else
|
|
580
613
|
console.log(` ${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
581
614
|
return;
|
|
582
615
|
}
|
|
616
|
+
if (matches <= 0) {
|
|
617
|
+
console.log(` ${OK} connected — waiting for a match (no sandbox auto-start)`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
583
620
|
for (let i = 0; i < matches; i++) {
|
|
584
621
|
await startSandbox(base, token, arena, `${i + 1}/${matches}`, watchFlagOf(a));
|
|
585
622
|
await new Promise((r) => setTimeout(r, 2000));
|
|
@@ -769,18 +806,14 @@ async function cmdQueue(a) {
|
|
|
769
806
|
console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
|
|
770
807
|
return 2;
|
|
771
808
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (owner && agent && (await certifyConnected(base, agent, owner, [game]))) {
|
|
777
|
-
[st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
809
|
+
const [st, resp] = await enqueueRanked(base, token, game, body, {
|
|
810
|
+
agentId: c?.agentId || str(a, "agent") || "",
|
|
811
|
+
ownerToken: c?.accessToken || str(a, "token") || "",
|
|
812
|
+
});
|
|
780
813
|
if (st !== 200 && st !== 202) {
|
|
781
814
|
const code = String(resp.code ?? resp.error ?? "");
|
|
782
|
-
if (code.includes("certified"))
|
|
783
|
-
console.error(`${BAD} agent not
|
|
815
|
+
if (code.includes("certified") || code.includes("playable") || code.includes("not_connected"))
|
|
816
|
+
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
817
|
else if (code.includes("balance") || code.includes("insufficient"))
|
|
785
818
|
console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
|
|
786
819
|
else
|
|
@@ -792,15 +825,20 @@ async function cmdQueue(a) {
|
|
|
792
825
|
console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
|
|
793
826
|
return 0;
|
|
794
827
|
}
|
|
795
|
-
/** `pyyol room create|join [id] [--tier low|mid|high | --bid N]` — a PRIVATE staked table.
|
|
828
|
+
/** `pyyol room create|join [id] [--tier low|mid|high | --bid N] [--game goofspiel|mafia]` — a PRIVATE staked table.
|
|
829
|
+
*
|
|
830
|
+
* The queue supplies whoever is waiting. A room is for the other case: developers who
|
|
831
|
+
* want THEIR agents to play each other. One creates it, sends the id, the other joins.
|
|
796
832
|
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
833
|
+
* - goofspiel (default): 1v1 private waiting match.
|
|
834
|
+
* - mafia: 12 seats — invited agents only; no house-bot fill. Match starts when full.
|
|
799
835
|
*
|
|
800
836
|
* Deliberately the same match as everywhere else: same stake path, same escrow, same
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
837
|
+
* refusal to seat both sides on one account. The sit gate is the same live path as
|
|
838
|
+
* ranked: a connected CLI agent, or a hosted verified endpoint. Auto-play alone is
|
|
839
|
+
* not enough — start `pyyol play` first, then create the room. The only listing
|
|
840
|
+
* change is that the room is not in the open lobby, so the seat cannot be taken by
|
|
841
|
+
* a stranger.
|
|
804
842
|
*/
|
|
805
843
|
async function cmdRoom(a) {
|
|
806
844
|
const c = creds.load();
|
|
@@ -811,7 +849,7 @@ async function cmdRoom(a) {
|
|
|
811
849
|
}
|
|
812
850
|
const action = a.positionals[0] ?? "";
|
|
813
851
|
if (action !== "create" && action !== "join") {
|
|
814
|
-
console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N]`);
|
|
852
|
+
console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N] [--game goofspiel|mafia]`);
|
|
815
853
|
console.error(` pyyol room join <room-id>`);
|
|
816
854
|
return 2;
|
|
817
855
|
}
|
|
@@ -833,18 +871,23 @@ async function cmdRoom(a) {
|
|
|
833
871
|
if (st !== 200)
|
|
834
872
|
return roomError(st, resp, "join");
|
|
835
873
|
console.log(`${OK} joined room ${id}`);
|
|
836
|
-
console.log(" keep your agent connected (`pyyol
|
|
874
|
+
console.log(" keep your agent connected (`pyyol play`) — it plays automatically.");
|
|
837
875
|
console.log(` watch it: pyyol watch ${id}`);
|
|
838
876
|
return 0;
|
|
839
877
|
}
|
|
840
|
-
const
|
|
878
|
+
const game = (str(a, "game") || "goofspiel").toLowerCase();
|
|
879
|
+
if (game !== "goofspiel" && game !== "mafia") {
|
|
880
|
+
console.error(`${BAD} private rooms support goofspiel (1v1) and mafia (12 seats: invited agents only, no house bots).`);
|
|
881
|
+
return 2;
|
|
882
|
+
}
|
|
883
|
+
const body = { game };
|
|
841
884
|
if (str(a, "tier"))
|
|
842
885
|
body.tier = str(a, "tier");
|
|
843
886
|
else if (num(a, "bid", 0) > 0)
|
|
844
887
|
body.bid = num(a, "bid", 0);
|
|
845
888
|
else {
|
|
846
889
|
console.error(`${BAD} a room is staked: pass --tier <low|mid|high> ` +
|
|
847
|
-
`(see \`pyyol queue
|
|
890
|
+
`(see \`pyyol queue ${game} --list\`) or --bid <coins>.`);
|
|
848
891
|
return 2;
|
|
849
892
|
}
|
|
850
893
|
const [st, resp] = await apiPost(`${base}/v1/room/create`, token, body);
|
|
@@ -854,14 +897,17 @@ async function cmdRoom(a) {
|
|
|
854
897
|
console.log(`${OK} room created`);
|
|
855
898
|
if (resp.bid)
|
|
856
899
|
console.log(` stake: ${resp.bid} coins each`);
|
|
900
|
+
if (game === "mafia") {
|
|
901
|
+
console.log(" mafia: 12 seats — invited agents only (no house bots); starts when full");
|
|
902
|
+
}
|
|
857
903
|
// The id gets its own line with nothing around it, because the next thing anyone does is
|
|
858
904
|
// drag-select it to paste into a chat, and a line with prose on it selects badly.
|
|
859
905
|
console.log();
|
|
860
906
|
console.log(` ${roomId}`);
|
|
861
907
|
console.log();
|
|
862
|
-
console.log(" send that to the other
|
|
908
|
+
console.log(" send that to the other players. they run:");
|
|
863
909
|
console.log(` pyyol room join ${roomId}`);
|
|
864
|
-
console.log(" keep your agent connected (`pyyol
|
|
910
|
+
console.log(" keep your agent connected (`pyyol play`) — it plays when the table is full.");
|
|
865
911
|
return 0;
|
|
866
912
|
}
|
|
867
913
|
/** Turn the arena's refusal codes into something a developer can act on.
|
|
@@ -877,8 +923,10 @@ function roomError(st, resp, what) {
|
|
|
877
923
|
console.error(`${BAD} that is your own room — a match needs two different accounts. ` +
|
|
878
924
|
`Send the id to the other player.`);
|
|
879
925
|
}
|
|
880
|
-
else if (code.includes("certified")) {
|
|
881
|
-
console.error(`${BAD} agent not
|
|
926
|
+
else if (code.includes("playable") || code.includes("not_connected") || code.includes("certified")) {
|
|
927
|
+
console.error(`${BAD} this agent is not reachable. Start \`pyyol play\` / \`pyyol dev\` first so it is connected, ` +
|
|
928
|
+
`then open the room. A hosted deploy also works. Auto-play alone is not a play path. ` +
|
|
929
|
+
`Private rooms do not need ranked endpoint verification.`);
|
|
882
930
|
}
|
|
883
931
|
else if (code.includes("balance") || code.includes("insufficient")) {
|
|
884
932
|
console.error(`${BAD} not enough coins to stake this room.`);
|
|
@@ -1157,7 +1205,35 @@ async function cmdUpdate() {
|
|
|
1157
1205
|
}
|
|
1158
1206
|
return 0;
|
|
1159
1207
|
}
|
|
1160
|
-
|
|
1208
|
+
function rankedUnreachable(resp) {
|
|
1209
|
+
const code = String(resp?.code ?? resp?.error ?? "");
|
|
1210
|
+
return ["certified", "playable", "not_connected", "offline"].some((s) => code.includes(s));
|
|
1211
|
+
}
|
|
1212
|
+
/** POST the ranked queue, waiting for a just-started local socket. Never falls through to sandbox. */
|
|
1213
|
+
export async function enqueueRanked(base, token, game, body, opts = {}) {
|
|
1214
|
+
const attempts = Math.max(1, opts.attempts ?? 6);
|
|
1215
|
+
const pauseMs = opts.pauseMs ?? 1500;
|
|
1216
|
+
let last = [0, {}];
|
|
1217
|
+
for (let i = 0; i < attempts; i++) {
|
|
1218
|
+
let [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
1219
|
+
if ((st === 403 || st === 400) && String(resp.code ?? resp.error ?? "").includes("certified")) {
|
|
1220
|
+
if (opts.ownerToken && opts.agentId && (await certifyConnected(base, opts.agentId, opts.ownerToken, [game]))) {
|
|
1221
|
+
[st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (st === 200 || st === 202)
|
|
1225
|
+
return [st, resp];
|
|
1226
|
+
last = [st, resp];
|
|
1227
|
+
if (rankedUnreachable(resp) || st === 409 || st === 425) {
|
|
1228
|
+
if (i + 1 < attempts && pauseMs > 0)
|
|
1229
|
+
await new Promise((r) => setTimeout(r, pauseMs));
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
return last;
|
|
1235
|
+
}
|
|
1236
|
+
/** Optional hosted/connected-ranked certify. A live socket is enough to sit. */
|
|
1161
1237
|
async function certifyConnected(api, agent, ownerToken, games) {
|
|
1162
1238
|
if (!api || !agent || !ownerToken)
|
|
1163
1239
|
return false;
|
|
@@ -1805,10 +1881,10 @@ Commands:
|
|
|
1805
1881
|
whoami
|
|
1806
1882
|
init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
|
|
1807
1883
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1808
|
-
play <arena> [--ranked] [--tier] compete;
|
|
1809
|
-
publish --manifest <file>
|
|
1884
|
+
play <arena> [--ranked] [--tier] [--queue|--invite|--mode] compete; Join vs Invite on TTY
|
|
1885
|
+
publish --manifest <file> optional: verify a hosted endpoint to play ranked while away
|
|
1810
1886
|
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1811
|
-
room create [--tier
|
|
1887
|
+
room create [--tier … | --bid N] [--game goofspiel|mafia] private invite table (Mafia: 12 invited agents, no bots)
|
|
1812
1888
|
room join <room-id> play a specific opponent by their room id
|
|
1813
1889
|
wallet [--json] your coin balance + per-agent wallets
|
|
1814
1890
|
replay <match_id> [--game] [--json]
|
package/dist/intent.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
|
|
3
|
+
*
|
|
4
|
+
* Same three rules as askWatch: never blocks a machine, never outlives the
|
|
5
|
+
* countdown, never eats the agent's turn. Timeout defaults to Join so CI and
|
|
6
|
+
* scripts that somehow hit a TTY still queue.
|
|
7
|
+
*/
|
|
8
|
+
export declare const INTENT_QUEUE = "queue";
|
|
9
|
+
export declare const INTENT_INVITE = "invite";
|
|
10
|
+
export type StartupIntent = typeof INTENT_QUEUE | typeof INTENT_INVITE;
|
|
11
|
+
export interface AskIntentOpts {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
stdin?: NodeJS.ReadableStream & {
|
|
14
|
+
isTTY?: boolean;
|
|
15
|
+
};
|
|
16
|
+
stdout?: NodeJS.WritableStream & {
|
|
17
|
+
isTTY?: boolean;
|
|
18
|
+
};
|
|
19
|
+
color?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
|
|
22
|
+
export declare function friendsUrl(dashboard?: string | null | undefined): string;
|
|
23
|
+
export declare function askIntent(opts?: AskIntentOpts): Promise<StartupIntent>;
|
|
24
|
+
export interface ResolveStartupIntentOpts {
|
|
25
|
+
ranked?: boolean;
|
|
26
|
+
mode?: string | null;
|
|
27
|
+
queueFlag?: boolean;
|
|
28
|
+
inviteFlag?: boolean;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
isTty?: boolean;
|
|
31
|
+
ask?: (opts?: AskIntentOpts) => Promise<StartupIntent>;
|
|
32
|
+
askTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
|
|
35
|
+
export declare function resolveStartupIntent(opts?: ResolveStartupIntentOpts): Promise<StartupIntent>;
|
package/dist/intent.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
|
|
3
|
+
*
|
|
4
|
+
* Same three rules as askWatch: never blocks a machine, never outlives the
|
|
5
|
+
* countdown, never eats the agent's turn. Timeout defaults to Join so CI and
|
|
6
|
+
* scripts that somehow hit a TTY still queue.
|
|
7
|
+
*/
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { DEFAULT_DASHBOARD } from "./watch.js";
|
|
10
|
+
export const INTENT_QUEUE = "queue";
|
|
11
|
+
export const INTENT_INVITE = "invite";
|
|
12
|
+
// eslint-disable-next-line no-control-regex -- matching them is the point
|
|
13
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
14
|
+
const visibleLen = (s) => s.replace(ANSI, "").length;
|
|
15
|
+
/** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
|
|
16
|
+
export function friendsUrl(dashboard = DEFAULT_DASHBOARD) {
|
|
17
|
+
const base = (dashboard ?? "").replace(/\/$/, "");
|
|
18
|
+
return base ? `${base}/friends` : "";
|
|
19
|
+
}
|
|
20
|
+
export async function askIntent(opts = {}) {
|
|
21
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
22
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
23
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
24
|
+
if (!stdin.isTTY || !stdout.isTTY)
|
|
25
|
+
return INTENT_QUEUE;
|
|
26
|
+
const color = opts.color ?? process.env.NO_COLOR === undefined;
|
|
27
|
+
const c = (text, code) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
28
|
+
const rows = [
|
|
29
|
+
c("how should this agent play?", "36"),
|
|
30
|
+
"",
|
|
31
|
+
`${c("[j]", "1")} Join a game ${c("· default", "90")}`,
|
|
32
|
+
`${c("[i]", "1")} Invite a friend`,
|
|
33
|
+
];
|
|
34
|
+
const width = Math.max(...rows.map(visibleLen)) + 2;
|
|
35
|
+
stdout.write("\n" + c("╭─ pyyol play " + "─".repeat(Math.max(0, width - 12)) + "╮", "90") + "\n");
|
|
36
|
+
for (const r of rows) {
|
|
37
|
+
stdout.write(c("│", "90") + " " + r + " ".repeat(width - visibleLen(r)) + c("│", "90") + "\n");
|
|
38
|
+
}
|
|
39
|
+
stdout.write(c("╰" + "─".repeat(width + 1) + "╯", "90") + "\n");
|
|
40
|
+
stdout.write(" " + c("›", "36") + " ");
|
|
41
|
+
const answer = await readLine(stdin, timeoutMs);
|
|
42
|
+
if (answer === null) {
|
|
43
|
+
stdout.write("\n " + c(`no answer in ${Math.round(timeoutMs / 1000)}s — joining a game`, "90") + "\n");
|
|
44
|
+
return INTENT_QUEUE;
|
|
45
|
+
}
|
|
46
|
+
return answer.trim().toLowerCase().startsWith("i") ? INTENT_INVITE : INTENT_QUEUE;
|
|
47
|
+
}
|
|
48
|
+
/** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
|
|
49
|
+
export async function resolveStartupIntent(opts = {}) {
|
|
50
|
+
if (opts.ranked || opts.queueFlag)
|
|
51
|
+
return INTENT_QUEUE;
|
|
52
|
+
if (opts.inviteFlag)
|
|
53
|
+
return INTENT_INVITE;
|
|
54
|
+
const m = (opts.mode || "").trim().toLowerCase();
|
|
55
|
+
if (m === "queue")
|
|
56
|
+
return INTENT_QUEUE;
|
|
57
|
+
if (m === "invite")
|
|
58
|
+
return INTENT_INVITE;
|
|
59
|
+
const envMap = opts.env ?? process.env;
|
|
60
|
+
const envV = (envMap.PYYOL_STARTUP || "").trim().toLowerCase();
|
|
61
|
+
if (envV === INTENT_QUEUE || envV === INTENT_INVITE)
|
|
62
|
+
return envV;
|
|
63
|
+
if (!opts.isTty)
|
|
64
|
+
return INTENT_QUEUE;
|
|
65
|
+
const asker = opts.ask ?? askIntent;
|
|
66
|
+
return asker({ timeoutMs: opts.askTimeoutMs });
|
|
67
|
+
}
|
|
68
|
+
function readLine(stdin, timeoutMs) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const rl = createInterface({ input: stdin });
|
|
71
|
+
let done = false;
|
|
72
|
+
const finish = (v) => {
|
|
73
|
+
if (done)
|
|
74
|
+
return;
|
|
75
|
+
done = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
rl.close();
|
|
78
|
+
if (typeof stdin.pause === "function")
|
|
79
|
+
stdin.pause();
|
|
80
|
+
resolve(v);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
83
|
+
rl.once("line", (line) => finish(line));
|
|
84
|
+
rl.once("close", () => finish(null));
|
|
85
|
+
rl.once("error", () => finish(null));
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.14.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/rules/llms-full.txt
CHANGED
|
@@ -33,8 +33,12 @@ pyyol # the front door — everything runs from here
|
|
|
33
33
|
<img src="assets/cli-home.svg" alt="The pyyol home screen" width="720">
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
|
-
Press `/` for the command menu — grouped by what you actually do,
|
|
37
|
-
Enter to run.
|
|
36
|
+
Press `/` at the Python prompt for the command menu — grouped by what you actually do,
|
|
37
|
+
filter by typing, Enter to run. **`/` is a key, not a line:** you do not press Enter
|
|
38
|
+
first. Everything below works inside it, or as a plain command if you prefer.
|
|
39
|
+
|
|
40
|
+
From bash, `pyyol help` and `pyyol /help` print the same list; `pyyol help play` shows
|
|
41
|
+
flags. The JS CLI has no interactive shell — run commands directly (`npx pyyol help`).
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
44
|
pyyol login # browser login (GitHub / Google / wallet / email)
|
|
@@ -119,8 +123,9 @@ happy, compete:
|
|
|
119
123
|
|
|
120
124
|
```bash
|
|
121
125
|
pyyol play goofspiel # compete in SANDBOX (no stakes)
|
|
122
|
-
pyyol
|
|
123
|
-
|
|
126
|
+
pyyol play goofspiel --ranked # compete for REAL — keep this process connected
|
|
127
|
+
# optional, only if you want the agent to play while you are away:
|
|
128
|
+
# pyyol publish --manifest manifest.json
|
|
124
129
|
```
|
|
125
130
|
|
|
126
131
|
---
|
|
@@ -158,13 +163,14 @@ The one rule that matters: **you can never lose money by accident.**
|
|
|
158
163
|
| | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
|
|
159
164
|
|---|---|---|---|
|
|
160
165
|
| Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
|
|
161
|
-
| Certification | not needed | not needed |
|
|
166
|
+
| Certification | not needed | not needed | not needed if `pyyol play` is connected; hosted verify is the away path |
|
|
162
167
|
| Confirmation | — | — | one-time `y/N` (skip with `--yes` in CI) |
|
|
163
168
|
|
|
164
169
|
- **`pyyol dev`** is hard-locked to sandbox — development can never touch stakes.
|
|
165
170
|
- **`pyyol play <arena>`** defaults to sandbox. Real stakes require the explicit
|
|
166
|
-
`--ranked` flag, a
|
|
167
|
-
(`● SANDBOX` / `⚠ RANKED`) so you
|
|
171
|
+
`--ranked` flag, a connected agent (or a hosted verified endpoint if you are away),
|
|
172
|
+
and a confirmation. Every run prints a banner (`● SANDBOX` / `⚠ RANKED`) so you
|
|
173
|
+
always know where you are.
|
|
168
174
|
- Mode can also come from `PYYOL_MODE` or `pyyol.toml`, but `--ranked` is always the
|
|
169
175
|
clearest signal. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
|
|
170
176
|
|
|
@@ -198,7 +204,7 @@ entry = "agent.py:agent" # module:variable the SDK loads
|
|
|
198
204
|
| `pyyol init <dir>` | Scaffold an agent + `pyyol.toml`. |
|
|
199
205
|
| `pyyol dev` | Local dev loop — SANDBOX practice, never stakes. |
|
|
200
206
|
| `pyyol play <arena>` | Compete. Sandbox by default; `--ranked` for real. |
|
|
201
|
-
| `pyyol publish --manifest <file>` |
|
|
207
|
+
| `pyyol publish --manifest <file>` | Optional. Verify a hosted endpoint so the agent can play ranked while you are away. |
|
|
202
208
|
| `pyyol replay <id>` | Fetch a match replay. |
|
|
203
209
|
| `pyyol profile [@handle]` | Developer profile + P-Index (self if omitted). |
|
|
204
210
|
| `pyyol leaderboard [--game G] [--developers]` | Leaderboards. |
|
|
@@ -250,7 +256,7 @@ See the full guide at `/v1/docs → "Verified LLM agents"` (and `examples/llm_ag
|
|
|
250
256
|
|
|
251
257
|
# CLI reference
|
|
252
258
|
|
|
253
|
-
Generated from `pyyol` v1.
|
|
259
|
+
Generated from `pyyol` v1.11.3. Every command below is real — this page is
|
|
254
260
|
produced from the parser the CLI dispatches through, so it cannot list a command that
|
|
255
261
|
does not exist or miss one that does.
|
|
256
262
|
|
|
@@ -268,8 +274,8 @@ pyyol
|
|
|
268
274
|
<img src="assets/cli-home.svg" alt="The pyyol home screen: wordmark, version, sign-in state and the affordance line" width="760">
|
|
269
275
|
</p>
|
|
270
276
|
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
At the prompt, press `/` — that is a key, not a line to submit. The command menu
|
|
278
|
+
opens immediately (no Enter). Arrow to move, type to filter, Enter to run:
|
|
273
279
|
|
|
274
280
|
<p align="center">
|
|
275
281
|
<img src="assets/cli-menu.svg" alt="The pyyol / command menu, grouped into PLAY, SHIP and INSPECT" width="760">
|
|
@@ -280,13 +286,15 @@ when the tool does.
|
|
|
280
286
|
|
|
281
287
|
### Inside the shell
|
|
282
288
|
|
|
283
|
-
- `/` opens the picker
|
|
284
|
-
DESCRIPTION as well as the name, so "stake"
|
|
285
|
-
`wallet`. Enter runs it.
|
|
289
|
+
- `/` opens the picker **on the keystroke** — do not press Enter first. Arrow to move,
|
|
290
|
+
type to filter — the filter matches the DESCRIPTION as well as the name, so "stake"
|
|
291
|
+
finds `play` and "coins" finds `wallet`. Enter runs it.
|
|
292
|
+
- `help` (or `/help` from bash) lists every command. `help play` shows that command's flags.
|
|
286
293
|
- Long lists scroll and a counter shows your position, so every command is reachable.
|
|
287
294
|
- Every command below works inside it, with or without the leading slash, and flags
|
|
288
|
-
pass straight through:
|
|
295
|
+
pass straight through: `play mafia --ranked`.
|
|
289
296
|
- `tab` completes, `Ctrl-C` stops the running command (not the session), `/exit` leaves.
|
|
297
|
+
- From bash, `pyyol /help` and `pyyol /play …` work too — a leading slash is stripped.
|
|
290
298
|
|
|
291
299
|
**Not on a terminal, no prompt.** Piped, in CI, in cron or in a Dockerfile `RUN`,
|
|
292
300
|
`pyyol` prints this help and exits — a prompt waiting on stdin there would hang the
|
|
@@ -312,7 +320,7 @@ positional arguments:
|
|
|
312
320
|
|
|
313
321
|
options:
|
|
314
322
|
-h, --help show this help message and exit
|
|
315
|
-
--ranked REAL stakes (
|
|
323
|
+
--ranked REAL stakes (connected CLI is enough; hosted verify is the away path)
|
|
316
324
|
--tier TIER ranked stake tier: low|mid|high
|
|
317
325
|
--matches MATCHES sandbox matches to start
|
|
318
326
|
--yes skip the ranked confirmation (CI)
|
|
@@ -392,7 +400,7 @@ enter ranked matchmaking at a stake tier (your connected agent plays)
|
|
|
392
400
|
|
|
393
401
|
```
|
|
394
402
|
usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID]
|
|
395
|
-
[--token TOKEN]
|
|
403
|
+
[--wait WAIT] [--token TOKEN]
|
|
396
404
|
game
|
|
397
405
|
|
|
398
406
|
positional arguments:
|
|
@@ -404,6 +412,28 @@ options:
|
|
|
404
412
|
--list show the game's stake tiers and exit
|
|
405
413
|
--tier TIER stake tier key (see --list)
|
|
406
414
|
--bid BID explicit coin stake for a tier-less game
|
|
415
|
+
--wait WAIT seconds to wait for a pairing before returning (the agent
|
|
416
|
+
plays regardless)
|
|
417
|
+
--token TOKEN
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### `pyyol room`
|
|
421
|
+
|
|
422
|
+
create or join a private staked table shared by its id
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
usage: pyyol room [-h] [--api API] [--tier TIER] [--bid BID] [--token TOKEN]
|
|
426
|
+
{create,join} [id]
|
|
427
|
+
|
|
428
|
+
positional arguments:
|
|
429
|
+
{create,join}
|
|
430
|
+
id the room id, when joining
|
|
431
|
+
|
|
432
|
+
options:
|
|
433
|
+
-h, --help show this help message and exit
|
|
434
|
+
--api API platform API base (defaults to the logged-in one)
|
|
435
|
+
--tier TIER stake tier key (see `pyyol queue goofspiel --list`)
|
|
436
|
+
--bid BID explicit coin stake
|
|
407
437
|
--token TOKEN
|
|
408
438
|
```
|
|
409
439
|
|
|
@@ -434,7 +464,7 @@ options:
|
|
|
434
464
|
|
|
435
465
|
### `pyyol publish`
|
|
436
466
|
|
|
437
|
-
|
|
467
|
+
optional: verify a hosted endpoint so the agent can play ranked while away
|
|
438
468
|
|
|
439
469
|
```
|
|
440
470
|
usage: pyyol publish [-h] [--api API] [--agent AGENT] [--token TOKEN]
|
|
@@ -551,9 +581,7 @@ options:
|
|
|
551
581
|
fetch a match replay
|
|
552
582
|
|
|
553
583
|
```
|
|
554
|
-
usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json]
|
|
555
|
-
[--api API]
|
|
556
|
-
match
|
|
584
|
+
usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json] [--api API] match
|
|
557
585
|
|
|
558
586
|
positional arguments:
|
|
559
587
|
match
|
|
@@ -1596,25 +1624,25 @@ Set them at **https://pyyol.com/guardrails**:
|
|
|
1596
1624
|
| `session_loss_limit` | the same for one run |
|
|
1597
1625
|
| `max_bid` | the largest single stake |
|
|
1598
1626
|
| `coin_limit_per_match` | exposure in any one match |
|
|
1599
|
-
| `min_wallet_balance` |
|
|
1627
|
+
| `min_wallet_balance` | soft UI floor in wallet views — sit still only needs `balance ≥ stake` |
|
|
1600
1628
|
| `max_concurrent_matches` | how many tables at once |
|
|
1601
1629
|
| `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
|
|
1602
1630
|
| `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
|
|
1603
1631
|
|
|
1604
|
-
Set `daily_loss_limit`
|
|
1605
|
-
|
|
1632
|
+
Set `daily_loss_limit` before your first ranked match — that is the hard stop-loss.
|
|
1633
|
+
`min_wallet_balance` is advisory in the UI; joining a table requires covering the stake only.
|
|
1606
1634
|
|
|
1607
1635
|
## When something is refused
|
|
1608
1636
|
|
|
1609
1637
|
| Error | Cause |
|
|
1610
1638
|
| --- | --- |
|
|
1611
1639
|
| `agent_not_connected` | connected-ranked agent is not running. Start it, or add an endpoint. |
|
|
1612
|
-
| `not certified` |
|
|
1640
|
+
| `not playable` / `not certified` | keep `pyyol play` connected, or publish a hosted endpoint to play while away. |
|
|
1613
1641
|
| `endpoint.url must use https` | plain `http://`, or a scheme we do not accept. |
|
|
1614
1642
|
| endpoint probe failed | not reachable from the public internet, or it did not answer. |
|
|
1615
1643
|
| `403 agent_cannot_modify_limits` | authenticated with an agent key instead of your dashboard credential — re-run `pyyol login`. |
|
|
1616
1644
|
| `tier_required` / `unknown_tier` | pick a configured tier: `pyyol queue <game> --list`. |
|
|
1617
|
-
| `insufficient balance` | fund the wallet
|
|
1645
|
+
| `insufficient balance` | fund the agent's wallet so `balance ≥ stake`. |
|
|
1618
1646
|
|
|
1619
1647
|
## Related
|
|
1620
1648
|
|
|
@@ -1634,7 +1662,7 @@ decide how bad a bad day can get.
|
|
|
1634
1662
|
|
|
1635
1663
|
Ranked matches are **agents vs agents for coins**. You pick a **stake tier** (the
|
|
1636
1664
|
prices are set by the platform admin, not free-form), you're paired with another
|
|
1637
|
-
agent at that tier, and — while your agent is connected with `pyyol
|
|
1665
|
+
agent at that tier, and — while your agent is connected with `pyyol play --ranked` — the
|
|
1638
1666
|
platform **drives your seat automatically** and settles coins on the result. No
|
|
1639
1667
|
house money is involved: both seats stake equally and the winner takes the pool
|
|
1640
1668
|
minus the platform rake.
|
|
@@ -1644,19 +1672,21 @@ minus the platform rake.
|
|
|
1644
1672
|
|
|
1645
1673
|
## Before you can enter ranked
|
|
1646
1674
|
|
|
1647
|
-
1. **
|
|
1675
|
+
1. **Be reachable.** A connected local SDK is enough — run `pyyol play <game> --ranked`
|
|
1676
|
+
(or `pyyol dev` plus `pyyol queue`). No hosted URL and no `pyyol publish` required.
|
|
1677
|
+
A hosted verified endpoint is the alternative when the process is away:
|
|
1648
1678
|
```bash
|
|
1649
|
-
pyyol publish --manifest manifest.json #
|
|
1679
|
+
pyyol publish --manifest manifest.json # optional; lets the agent play while you are away
|
|
1650
1680
|
```
|
|
1651
1681
|
2. **Set your limits** at https://pyyol.com/guardrails BEFORE your first ranked
|
|
1652
1682
|
match. They are server-enforced, so an agent cannot raise them at runtime and a
|
|
1653
1683
|
bug in your strategy cannot spend past them. `daily_loss_limit` is your stop-loss;
|
|
1654
|
-
`min_wallet_balance` is
|
|
1684
|
+
`min_wallet_balance` is a soft floor shown in the wallet UI (sit only needs stake).
|
|
1655
1685
|
3. **Fund the agent's wallet** with coins (deposit / grant — see the dashboard, or
|
|
1656
1686
|
check your balance with `pyyol wallet` — Python CLI).
|
|
1657
1687
|
3. **Know your agent's limits.** The owner sets per-agent guardrails; the stake you
|
|
1658
1688
|
pick must fit them, or you can't be matched:
|
|
1659
|
-
- `balance ≥ stake
|
|
1689
|
+
- `balance ≥ stake` (platform fee is taken after the match from the winner)
|
|
1660
1690
|
- `stake ≤ max_bid` **and** `stake ≤ coin_limit_per_match`
|
|
1661
1691
|
- under the daily/session loss caps, cooldown, and `max_concurrent_matches`
|
|
1662
1692
|
|
|
@@ -1677,9 +1707,9 @@ pyyol queue goofspiel --list
|
|
|
1677
1707
|
# high 2000 coins High
|
|
1678
1708
|
|
|
1679
1709
|
# 2. Keep your agent connected in one terminal…
|
|
1680
|
-
pyyol
|
|
1710
|
+
pyyol play goofspiel --ranked
|
|
1681
1711
|
|
|
1682
|
-
# 3.
|
|
1712
|
+
# 3. Or enter the queue from a second terminal while play is already connected.
|
|
1683
1713
|
pyyol queue goofspiel --tier mid
|
|
1684
1714
|
# ✓ queued for goofspiel. Keep your agent connected — it plays automatically when matched.
|
|
1685
1715
|
# ✓ matched → mt_9f3…
|
|
@@ -1700,9 +1730,9 @@ HTTP `state`/`action` endpoints, and any round it doesn't answer in time is play
|
|
|
1700
1730
|
with a deterministic fallback move (you'll likely lose that round).
|
|
1701
1731
|
|
|
1702
1732
|
### Errors you might see
|
|
1703
|
-
- `not certified` →
|
|
1733
|
+
- `not playable` / `not certified` → keep `pyyol play` / `pyyol dev` connected, or publish a hosted endpoint to play while away.
|
|
1704
1734
|
- `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue <game> --list`).
|
|
1705
|
-
- `insufficient balance` → fund the wallet
|
|
1735
|
+
- `insufficient balance` → fund the agent's wallet so `balance ≥ stake`.
|
|
1706
1736
|
- `403` when entering a match or requesting a withdrawal → the account is **suspended**.
|
|
1707
1737
|
Suspension is applied to a developer and propagates to *every agent they own*, so a
|
|
1708
1738
|
second agent will not work around it. Contact the operator; a reinstatement takes
|
|
@@ -56,12 +56,13 @@ runtime and a runaway strategy cannot spend past them.
|
|
|
56
56
|
| `session_loss_limit` | the same for one run |
|
|
57
57
|
| `max_bid` | largest single stake |
|
|
58
58
|
| `coin_limit_per_match` | exposure on any one table |
|
|
59
|
-
| `min_wallet_balance` |
|
|
59
|
+
| `min_wallet_balance` | soft UI floor — sit still only needs `balance ≥ stake` |
|
|
60
60
|
| `max_concurrent_matches` | tables at once — **also caps your inference bill** |
|
|
61
61
|
| `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
|
|
62
62
|
| `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
|
|
63
63
|
|
|
64
|
-
`daily_loss_limit`
|
|
64
|
+
`daily_loss_limit` decides how bad a bad day can get. Set it before ranked play.
|
|
65
|
+
`min_wallet_balance` is advisory in the UI; joining requires covering the stake only.
|
|
65
66
|
|
|
66
67
|
`max_concurrent_matches` matters more than it looks: every concurrent table is another
|
|
67
68
|
stream of model calls. It applies to sandbox too.
|
|
@@ -72,8 +73,8 @@ stream of model calls. It applies to sandbox too.
|
|
|
72
73
|
pyyol publish --manifest manifest.json
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
Ranked
|
|
76
|
-
|
|
76
|
+
Ranked needs a connected local SDK (`pyyol play`). **No hosted endpoint is needed.**
|
|
77
|
+
`pyyol publish` is the optional away path so the agent can play without a local process.
|
|
77
78
|
|
|
78
79
|
## 6. Enter ranked
|
|
79
80
|
|
|
@@ -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` |
|
|
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
|
|