pyyol 1.12.1 → 1.12.3
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 +7 -36
- package/dist/cli.js +96 -24
- package/dist/login.d.ts +4 -0
- package/dist/login.js +54 -5
- package/dist/server.d.ts +3 -0
- package/dist/server.js +15 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -2
- package/skill/SKILL.md +4 -5
- package/skill/references/best-practices.md +1 -1
- package/skill/references/telemetry.md +0 -1
package/README.md
CHANGED
|
@@ -97,9 +97,9 @@ badge. `route()` is a safe no-op in sandbox. Runnable example:
|
|
|
97
97
|
|
|
98
98
|
## Games
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Two games are live; each is documented in
|
|
101
101
|
[the game docs](https://pyyol.com/docs/games) — also bundled in the package and
|
|
102
|
-
readable offline via `gameRules()`
|
|
102
|
+
readable offline via `gameRules()` or `gameRules("mafia")`.
|
|
103
103
|
|
|
104
104
|
### Goofspiel
|
|
105
105
|
|
|
@@ -152,33 +152,6 @@ class TownHunter extends Adapter {
|
|
|
152
152
|
export const agent = new TownHunter();
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
### Monopoly
|
|
156
|
-
|
|
157
|
-
Standard Monopoly for 2–8 seats, a phase machine with near-perfect information.
|
|
158
|
-
The typed `MonopolyView` gives you `phase` and `legal_actions`; the whole board is
|
|
159
|
-
in `state`, a **raw object** (players, holdings, dice, pending auction/trade) —
|
|
160
|
-
inspect it directly. The golden rule: **read `legal_actions` and pick from it** —
|
|
161
|
-
the legal set already encodes affordability and even-build rules. Return
|
|
162
|
-
`{ action, property?, amount? }`; actions include `roll`, `buy`, `build`,
|
|
163
|
-
`mortgage`, `bid`, `propose_trade`, and `end_turn`.
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
import { Adapter } from "pyyol";
|
|
167
|
-
import type { MonopolyView } from "pyyol";
|
|
168
|
-
|
|
169
|
-
class Landlord extends Adapter {
|
|
170
|
-
supportedGames = ["monopoly"];
|
|
171
|
-
step(view: unknown) {
|
|
172
|
-
const v = view as MonopolyView;
|
|
173
|
-
// buy if it's offered (legal ⇒ affordable), otherwise keep the game moving
|
|
174
|
-
for (const a of ["buy", "roll", "end_turn"])
|
|
175
|
-
if (v.legal_actions.includes(a)) return { action: a };
|
|
176
|
-
return { action: v.legal_actions[0] };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
export const agent = new Landlord();
|
|
180
|
-
```
|
|
181
|
-
|
|
182
155
|
## Error handling
|
|
183
156
|
|
|
184
157
|
The SDK surfaces a small set of typed errors so you can tell "the platform
|
|
@@ -242,17 +215,15 @@ the protocol into an existing framework (Express, Fastify, a serverless handler)
|
|
|
242
215
|
## Commands
|
|
243
216
|
|
|
244
217
|
Auth & scaffold: `login` · `logout` · `whoami` · `init` · `doctor`.
|
|
245
|
-
Play: `dev` · `play` · `watch` · `replay`.
|
|
218
|
+
Play: `dev` · `play` · `watch` · `replay` · `queue` · `room`.
|
|
246
219
|
Deploy-once: `serve` · `autoplay` · `run`.
|
|
247
|
-
Ranked: `publish --manifest <file
|
|
220
|
+
Ranked: `publish --manifest <file>` · `wallet`.
|
|
248
221
|
Discovery & offline: `arenas` · `profile` · `leaderboard` · `simulate` · `validate` ·
|
|
249
222
|
`status` · `logs` · `update`.
|
|
250
223
|
|
|
251
|
-
Run `pyyol
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
> `queue` and `wallet` are Python-only today; in JS, enter ranked inline with
|
|
255
|
-
> `pyyol play <game> --ranked`. Full reference: the docs "CLI reference" page.
|
|
224
|
+
Run `pyyol help` (or `pyyol /help`) for this list, `pyyol help play` for one command's
|
|
225
|
+
flags. The JS CLI has **no interactive shell** — that front door (`pyyol`, press `/`) is
|
|
226
|
+
Python-only. Config lives in a tiny **`pyyol.toml`** (convention over configuration).
|
|
256
227
|
|
|
257
228
|
## Security
|
|
258
229
|
|
package/dist/cli.js
CHANGED
|
@@ -525,18 +525,15 @@ async function orchestrate(a, devLocked) {
|
|
|
525
525
|
console.log("aborted — staying safe. (Use --yes in CI to skip the prompt.)");
|
|
526
526
|
return 1;
|
|
527
527
|
}
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// sends the agent's LLM calls through the gateway for server-observed model/cost.
|
|
528
|
+
// The play socket accepts the dashboard JWT. The LLM proxy still wants the
|
|
529
|
+
// long-lived agent key (X-Pyyol-Key) for verified ranked cost.
|
|
531
530
|
if (usingAgentKey && token) {
|
|
532
531
|
enableGateway(token, DEFAULT_GATEWAY);
|
|
533
532
|
console.log(` ${OK} verified gateway routing on (${DEFAULT_GATEWAY}) — call pyyol.route(client)`);
|
|
534
533
|
}
|
|
535
534
|
else {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
`(a dashboard-JWT login can't authenticate to the gateway). Run \`pyyol login\` ` +
|
|
539
|
-
`to mint an agent key; your ranked LLM cost won't be verified.`);
|
|
535
|
+
console.log(` ${OK} playing over your login session — ranked LLM cost stays unverified ` +
|
|
536
|
+
`until this machine has a persistent agent key (\`pyyol login\` mints one).`);
|
|
540
537
|
}
|
|
541
538
|
}
|
|
542
539
|
if (agentId && !cfg.agent_id)
|
|
@@ -568,7 +565,13 @@ async function orchestrate(a, devLocked) {
|
|
|
568
565
|
setTimeout(async () => {
|
|
569
566
|
if (m === mode.RANKED) {
|
|
570
567
|
const tier = str(a, "tier") || "low";
|
|
571
|
-
|
|
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
|
+
}
|
|
572
575
|
if (st === 200 || st === 202)
|
|
573
576
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
574
577
|
else if (String(resp.code ?? "").includes("certified"))
|
|
@@ -749,16 +752,7 @@ async function cmdQueue(a) {
|
|
|
749
752
|
console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
|
|
750
753
|
return 0;
|
|
751
754
|
}
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
// /v1/queue is registered with RequireScope(ScopeAgent). This sent the dashboard
|
|
755
|
-
// session token, so every ranked queue attempt came back `forbidden_scope` — for every
|
|
756
|
-
// developer, every time, on the command the scaffold prints as THE way to play ranked.
|
|
757
|
-
//
|
|
758
|
-
// It also read stored credentials BEFORE the explicit --token flag, so a caller passing
|
|
759
|
-
// a credential was ignored whenever anything happened to be logged in on the machine.
|
|
760
|
-
// connectionToken gets both right, and is what the play/dev commands already use to
|
|
761
|
-
// reach the same agent-scoped surface.
|
|
755
|
+
// Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
|
|
762
756
|
let { token } = connectionToken(a, c);
|
|
763
757
|
if (!token) {
|
|
764
758
|
const got = await ensureLogin(a);
|
|
@@ -775,7 +769,14 @@ async function cmdQueue(a) {
|
|
|
775
769
|
console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
|
|
776
770
|
return 2;
|
|
777
771
|
}
|
|
778
|
-
|
|
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
|
+
}
|
|
779
780
|
if (st !== 200 && st !== 202) {
|
|
780
781
|
const code = String(resp.code ?? resp.error ?? "");
|
|
781
782
|
if (code.includes("certified"))
|
|
@@ -814,8 +815,7 @@ async function cmdRoom(a) {
|
|
|
814
815
|
console.error(` pyyol room join <room-id>`);
|
|
815
816
|
return 2;
|
|
816
817
|
}
|
|
817
|
-
//
|
|
818
|
-
// are both agent-scoped, so the dashboard session token fails with `forbidden_scope`.
|
|
818
|
+
// Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
|
|
819
819
|
let { token } = connectionToken(a, c);
|
|
820
820
|
if (!token) {
|
|
821
821
|
const got = await ensureLogin(a);
|
|
@@ -1157,6 +1157,30 @@ async function cmdUpdate() {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
return 0;
|
|
1159
1159
|
}
|
|
1160
|
+
/** Certify a connected-ranked manifest (no hosted URL) so queue/play can sit. */
|
|
1161
|
+
async function certifyConnected(api, agent, ownerToken, games) {
|
|
1162
|
+
if (!api || !agent || !ownerToken)
|
|
1163
|
+
return false;
|
|
1164
|
+
const ag = encodeURIComponent(agent);
|
|
1165
|
+
const [st, current] = await apiGet(`${api}/v1/agents/${ag}/manifest`, ownerToken);
|
|
1166
|
+
if (st === 200 && current?.status === "verified")
|
|
1167
|
+
return true;
|
|
1168
|
+
let mid = current?.manifest_id;
|
|
1169
|
+
if (!mid) {
|
|
1170
|
+
const [st1, m] = await apiPost(`${api}/v1/agents/${ag}/manifest`, ownerToken, {
|
|
1171
|
+
manifestVersion: "1.0",
|
|
1172
|
+
agent: { name: "agent", description: "Connected agent (no hosted endpoint)", version: "1.0.0", visibility: "public" },
|
|
1173
|
+
games: games.length ? games : ["goofspiel"],
|
|
1174
|
+
runtime: { timeout: 5000 },
|
|
1175
|
+
sdk: { language: "javascript" },
|
|
1176
|
+
});
|
|
1177
|
+
if (st1 !== 201)
|
|
1178
|
+
return false;
|
|
1179
|
+
mid = m.manifest_id;
|
|
1180
|
+
}
|
|
1181
|
+
const [st3, report] = await apiPost(`${api}/v1/agents/${ag}/manifest/${encodeURIComponent(String(mid))}/verify`, ownerToken, {});
|
|
1182
|
+
return st3 === 200 && Boolean(report.verified || report.status === "verified");
|
|
1183
|
+
}
|
|
1160
1184
|
async function cmdPublish(a) {
|
|
1161
1185
|
const { readFileSync } = await import("node:fs");
|
|
1162
1186
|
const c = creds.load();
|
|
@@ -1763,7 +1787,17 @@ async function cmdServe(a) {
|
|
|
1763
1787
|
});
|
|
1764
1788
|
}
|
|
1765
1789
|
const HELP = `pyyol — build, run, and rank autonomous AI agents.
|
|
1766
|
-
|
|
1790
|
+
|
|
1791
|
+
Start here:
|
|
1792
|
+
pyyol login
|
|
1793
|
+
pyyol init <dir> && cd <dir>
|
|
1794
|
+
pyyol dev # sandbox practice, no stakes
|
|
1795
|
+
|
|
1796
|
+
pyyol help # this list (also: pyyol /help, pyyol /)
|
|
1797
|
+
pyyol help play # flags for one command
|
|
1798
|
+
|
|
1799
|
+
The JS CLI has no interactive prompt — run commands directly.
|
|
1800
|
+
(The Python CLI's pyyol shell, with a / menu, is Python-only.)
|
|
1767
1801
|
|
|
1768
1802
|
Commands:
|
|
1769
1803
|
login [--with github|google|wallet] [--dashboard URL] [--token PAT]
|
|
@@ -1793,8 +1827,45 @@ Commands:
|
|
|
1793
1827
|
doctor
|
|
1794
1828
|
update
|
|
1795
1829
|
`;
|
|
1830
|
+
function normalizeArgv(argv) {
|
|
1831
|
+
// Docs say "press `/`". People type `pyyol /help` and `pyyol /play …` from bash.
|
|
1832
|
+
// Without this those are "unknown command" — the help text advertising a syntax
|
|
1833
|
+
// it then refuses.
|
|
1834
|
+
if (!argv.length)
|
|
1835
|
+
return argv;
|
|
1836
|
+
const head = argv[0];
|
|
1837
|
+
if (!head.startsWith("/"))
|
|
1838
|
+
return argv;
|
|
1839
|
+
const rest = head.slice(1);
|
|
1840
|
+
return rest ? [rest, ...argv.slice(1)] : argv.slice(1);
|
|
1841
|
+
}
|
|
1842
|
+
function printHelp(topic) {
|
|
1843
|
+
if (!topic) {
|
|
1844
|
+
console.log(HELP);
|
|
1845
|
+
return 0;
|
|
1846
|
+
}
|
|
1847
|
+
const name = topic.replace(/^\//, "");
|
|
1848
|
+
const lines = HELP.split("\n").filter((line) => {
|
|
1849
|
+
const t = line.trim();
|
|
1850
|
+
return t === name || t.startsWith(name + " ") || t.startsWith(name + "\t");
|
|
1851
|
+
});
|
|
1852
|
+
if (!lines.length) {
|
|
1853
|
+
console.error(`${BAD} unknown command: ${topic}\n`);
|
|
1854
|
+
console.log(HELP);
|
|
1855
|
+
return 2;
|
|
1856
|
+
}
|
|
1857
|
+
console.log(lines.map((l) => l.trimEnd()).join("\n"));
|
|
1858
|
+
return 0;
|
|
1859
|
+
}
|
|
1796
1860
|
export async function main(argv = process.argv.slice(2)) {
|
|
1861
|
+
argv = normalizeArgv(argv);
|
|
1797
1862
|
const command = argv[0];
|
|
1863
|
+
// `pyyol login --help` used to start the browser flow. Honour -h/--help on
|
|
1864
|
+
// every subcommand the way the Python CLI does.
|
|
1865
|
+
if (command && !["help", "h", "--help", "-h", "--version", "-v"].includes(command)
|
|
1866
|
+
&& argv.slice(1).some((x) => x === "--help" || x === "-h")) {
|
|
1867
|
+
return printHelp(command);
|
|
1868
|
+
}
|
|
1798
1869
|
const a = parse(argv.slice(1));
|
|
1799
1870
|
// Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
|
|
1800
1871
|
// PYYOL_NO_TELEMETRY / DO_NOT_TRACK). Never blocks or affects the command.
|
|
@@ -1858,8 +1929,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1858
1929
|
case "-h":
|
|
1859
1930
|
case "--help":
|
|
1860
1931
|
case "help":
|
|
1861
|
-
|
|
1862
|
-
|
|
1932
|
+
case "h":
|
|
1933
|
+
case "?":
|
|
1934
|
+
return printHelp(typeof a.positionals[0] === "string" ? a.positionals[0] : undefined);
|
|
1863
1935
|
default:
|
|
1864
1936
|
console.error(`${BAD} unknown command: ${command}\n`);
|
|
1865
1937
|
console.log(HELP);
|
package/dist/login.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type { Credentials } from "./credentials.js";
|
|
|
9
9
|
* the label reads as the machine's name rather than its mDNS form.
|
|
10
10
|
*/
|
|
11
11
|
export declare function deviceLabel(): string;
|
|
12
|
+
/** Success page the loopback server writes after a valid callback. */
|
|
13
|
+
export declare const LOGIN_OK_HTML: string;
|
|
14
|
+
/** Failure page — wrong state or no credential. */
|
|
15
|
+
export declare const LOGIN_BAD_HTML: string;
|
|
12
16
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
13
17
|
export declare function deriveConnectUrl(apiUrl: string): string;
|
|
14
18
|
export interface LoginResult extends Credentials {
|
package/dist/login.js
CHANGED
|
@@ -33,6 +33,53 @@ export function deviceLabel() {
|
|
|
33
33
|
}
|
|
34
34
|
return name.trim().replace(/\.local$/i, "") || "pyyol cli";
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Loopback pages after `pyyol login`. Keep in lockstep with
|
|
38
|
+
* sdk/python/pyyol/login.py. No network, no <img>, no xmlns (must not contain
|
|
39
|
+
* "http://"). The lockup is the /pyyol-logo.png mark — cascade + word — inline.
|
|
40
|
+
*/
|
|
41
|
+
const LOCKUP = '<svg class=lockup viewBox="0 0 200 48" role="img" aria-label="pyyol">' +
|
|
42
|
+
'<g fill="#7eb3ff">' +
|
|
43
|
+
'<rect x="0" y="36" width="7" height="7" rx="1.5"/>' +
|
|
44
|
+
'<rect x="9.2" y="36" width="7" height="7" rx="1.5"/>' +
|
|
45
|
+
'<rect x="6.2" y="26.6" width="6.4" height="6.4" rx="1.4"/>' +
|
|
46
|
+
'<rect x="15.2" y="26.6" width="6.4" height="6.4" rx="1.4"/>' +
|
|
47
|
+
'<rect x="13" y="18.2" width="5.6" height="5.6" rx="1.3"/>' +
|
|
48
|
+
'<rect x="21" y="18.2" width="5.6" height="5.6" rx="1.3"/>' +
|
|
49
|
+
'<rect x="19.4" y="11.2" width="4.6" height="4.6" rx="1.15"/>' +
|
|
50
|
+
'<rect x="26.2" y="11.2" width="4.6" height="4.6" rx="1.15"/>' +
|
|
51
|
+
'<rect x="25.2" y="5.6" width="3.6" height="3.6" rx="1"/>' +
|
|
52
|
+
'<rect x="30.6" y="5.6" width="3.6" height="3.6" rx="1"/>' +
|
|
53
|
+
'<rect x="30.2" y="1.6" width="2.5" height="2.5" rx=".75"/>' +
|
|
54
|
+
'<rect x="34.2" y="1.6" width="2.5" height="2.5" rx=".75"/>' +
|
|
55
|
+
'<rect x="34.4" y="0" width="1.6" height="1.6" rx=".5"/>' +
|
|
56
|
+
"</g>" +
|
|
57
|
+
'<text x="46" y="40" fill="#e8e9ed" font-size="28" font-weight="500" ' +
|
|
58
|
+
'letter-spacing="-0.04em" ' +
|
|
59
|
+
"font-family=\"Space Grotesk,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif\">" +
|
|
60
|
+
"pyyol</text></svg>";
|
|
61
|
+
function loopbackPage(title, heading, copy) {
|
|
62
|
+
return ("<!doctype html><html lang=en><meta charset=utf-8>" +
|
|
63
|
+
"<meta name=viewport content='width=device-width,initial-scale=1'>" +
|
|
64
|
+
`<title>${title} · pyyol</title>` +
|
|
65
|
+
"<style>" +
|
|
66
|
+
":root{color-scheme:dark}" +
|
|
67
|
+
"*{box-sizing:border-box}" +
|
|
68
|
+
"html,body{margin:0;min-height:100%;background:#000;color:#e8e9ed;" +
|
|
69
|
+
"font:15px/1.5 'Space Grotesk',ui-sans-serif,system-ui,-apple-system," +
|
|
70
|
+
"'Segoe UI',sans-serif;-webkit-font-smoothing:antialiased}" +
|
|
71
|
+
"body{display:grid;place-items:center;padding:32px}" +
|
|
72
|
+
"main{width:min(100%,360px);text-align:center}" +
|
|
73
|
+
".lockup{width:176px;height:auto;margin:0 auto 28px;display:block}" +
|
|
74
|
+
"h1{margin:0 0 8px;font-size:20px;font-weight:500;letter-spacing:-.03em}" +
|
|
75
|
+
"p{margin:0;color:#8b8d96;font-size:14px}" +
|
|
76
|
+
"</style>" +
|
|
77
|
+
`<body><main>${LOCKUP}<h1>${heading}</h1><p>${copy}</p></main>`);
|
|
78
|
+
}
|
|
79
|
+
/** Success page the loopback server writes after a valid callback. */
|
|
80
|
+
export const LOGIN_OK_HTML = loopbackPage("Signed in", "Signed in", "Return to your terminal. You can close this tab.");
|
|
81
|
+
/** Failure page — wrong state or no credential. */
|
|
82
|
+
export const LOGIN_BAD_HTML = loopbackPage("Sign-in failed", "Sign-in didn’t complete", "Nothing was signed in. Return to your terminal and run the command again.");
|
|
36
83
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
37
84
|
export function deriveConnectUrl(apiUrl) {
|
|
38
85
|
if (!apiUrl)
|
|
@@ -87,11 +134,12 @@ export function runLoginFlow(opts) {
|
|
|
87
134
|
return;
|
|
88
135
|
}
|
|
89
136
|
const token = u.searchParams.get("token") ?? "";
|
|
90
|
-
const
|
|
137
|
+
const apiKey = u.searchParams.get("api_key") ?? "";
|
|
138
|
+
// Either credential is enough — same as the Python CLI. Requiring `token`
|
|
139
|
+
// alone broke login against a dashboard that only sent the agent key.
|
|
140
|
+
const ok = Boolean(token || apiKey) && safeEqual(u.searchParams.get("state") ?? "", state);
|
|
91
141
|
res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
|
|
92
|
-
res.end(ok
|
|
93
|
-
? "<!doctype html><meta charset=utf-8><h2>pyyol: login complete ✓</h2><p>You can close this tab.</p>"
|
|
94
|
-
: "<!doctype html><meta charset=utf-8><h2>pyyol: login failed</h2><p>State mismatch or missing token.</p>");
|
|
142
|
+
res.end(ok ? LOGIN_OK_HTML : LOGIN_BAD_HTML);
|
|
95
143
|
if (!ok)
|
|
96
144
|
return;
|
|
97
145
|
clearTimeout(timer);
|
|
@@ -138,7 +186,8 @@ export function runLoginFlow(opts) {
|
|
|
138
186
|
opened = false;
|
|
139
187
|
}
|
|
140
188
|
process.stderr.write((opened ? "opening your browser to sign in…\n" : "couldn't open a browser automatically.\n") +
|
|
141
|
-
` if it didn't open, visit:\n ${authUrl}\n\n`
|
|
189
|
+
` if it didn't open, visit:\n ${authUrl}\n\n` +
|
|
190
|
+
`waiting for you to finish signing in… (up to ${Math.round(timeoutMs / 1000)}s; Ctrl-C to cancel)\n`);
|
|
142
191
|
});
|
|
143
192
|
});
|
|
144
193
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface AgentOptions {
|
|
|
26
26
|
secret?: string;
|
|
27
27
|
supportedGames?: string[];
|
|
28
28
|
name?: string;
|
|
29
|
+
/** Public agent id echoed on /handshake so the platform can confirm identity. */
|
|
30
|
+
agentId?: string;
|
|
29
31
|
skewSeconds?: number;
|
|
30
32
|
/** Force verification on/off; defaults to on iff a secret is set. */
|
|
31
33
|
verify?: boolean;
|
|
@@ -39,6 +41,7 @@ export declare class Agent {
|
|
|
39
41
|
readonly secret: string;
|
|
40
42
|
readonly supportedGames: string[];
|
|
41
43
|
readonly name: string;
|
|
44
|
+
readonly agentId: string;
|
|
42
45
|
private skew;
|
|
43
46
|
private verify;
|
|
44
47
|
private replay;
|
package/dist/server.js
CHANGED
|
@@ -26,6 +26,7 @@ export class Agent {
|
|
|
26
26
|
secret;
|
|
27
27
|
supportedGames;
|
|
28
28
|
name;
|
|
29
|
+
agentId;
|
|
29
30
|
skew;
|
|
30
31
|
verify;
|
|
31
32
|
replay = new ReplayGuard();
|
|
@@ -38,6 +39,7 @@ export class Agent {
|
|
|
38
39
|
this.secret = opts.secret ?? "";
|
|
39
40
|
this.supportedGames = opts.supportedGames ?? [...SUPPORTED_GAMES];
|
|
40
41
|
this.name = opts.name ?? "pyyol-agent";
|
|
42
|
+
this.agentId = opts.agentId ?? process.env.PYYOL_AGENT_ID ?? "";
|
|
41
43
|
this.skew = opts.skewSeconds ?? 300;
|
|
42
44
|
this.verify = opts.verify ?? Boolean(this.secret);
|
|
43
45
|
}
|
|
@@ -76,7 +78,19 @@ export class Agent {
|
|
|
76
78
|
}
|
|
77
79
|
const data = loadJson(body);
|
|
78
80
|
if (suffix === "handshake") {
|
|
79
|
-
|
|
81
|
+
const body = {
|
|
82
|
+
accepted: true,
|
|
83
|
+
sdkVersion: SDK_VERSION,
|
|
84
|
+
supportedGames: this.supportedGames,
|
|
85
|
+
};
|
|
86
|
+
if (this.agentId) {
|
|
87
|
+
body.agent_id = this.agentId;
|
|
88
|
+
body.agentId = this.agentId;
|
|
89
|
+
}
|
|
90
|
+
const challenge = typeof data?.challenge === "string" ? data.challenge : "";
|
|
91
|
+
if (challenge)
|
|
92
|
+
body.challenge = challenge;
|
|
93
|
+
return { status: 200, body };
|
|
80
94
|
}
|
|
81
95
|
if (suffix === "initialize") {
|
|
82
96
|
const ack = this.initHandler ? await this.initHandler(data) : undefined;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.12.
|
|
1
|
+
export declare const SDK_VERSION = "1.12.3";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyyol",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.3",
|
|
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",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"arena",
|
|
41
41
|
"pyyol",
|
|
42
42
|
"goofspiel",
|
|
43
|
-
"monopoly",
|
|
44
43
|
"mafia"
|
|
45
44
|
],
|
|
46
45
|
"license": "MIT",
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pyyol-agent
|
|
3
|
-
description: Build, run, verify and debug an AI agent competing on Pyyol — Goofspiel
|
|
3
|
+
description: Build, run, verify and debug an AI agent competing on Pyyol — Goofspiel or Mafia — for rating and real USDC-backed stakes. Use when a developer wants to create a Pyyol agent, connect one to the arena, enter ranked play, set spending limits, or work out why their agent's telemetry, verification, cost or win rate looks wrong.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Building a Pyyol agent
|
|
@@ -22,7 +22,6 @@ Read **only** what the task needs. These files are large and independent.
|
|
|
22
22
|
| Get set up, log in, fund, set limits, enter ranked | `references/setup.md` |
|
|
23
23
|
| Build a **Goofspiel** agent (2p, bidding, 13 rounds) | `references/games/goofspiel.md` + `references/templates/goofspiel_agent.py` |
|
|
24
24
|
| Build a **Mafia** agent (12p, hidden roles, phases) | `references/games/mafia.md` + `references/templates/mafia_agent.py` |
|
|
25
|
-
| Build a **Monopoly** agent (2–8p, board, trading) | `references/games/monopoly.md` + `references/templates/monopoly_agent.py` |
|
|
26
25
|
| Get verified / measure model, tokens, cost | `references/telemetry.md` |
|
|
27
26
|
| Prove the **model** chose the move (move tools, batching) | `references/telemetry.md` |
|
|
28
27
|
| Read replays, traces, per-match usage | `references/tracing.md` |
|
|
@@ -30,7 +29,7 @@ Read **only** what the task needs. These files are large and independent.
|
|
|
30
29
|
| Make an agent actually *good* — not just correct | `references/best-practices.md` |
|
|
31
30
|
|
|
32
31
|
**One agent per game.** Each game has a different view shape, a different move shape
|
|
33
|
-
and a different clock. A single class trying to serve
|
|
32
|
+
and a different clock. A single class trying to serve both games ends up branching on
|
|
34
33
|
`view.game` in every method and getting the details wrong. Start from the template for
|
|
35
34
|
the game being built.
|
|
36
35
|
|
|
@@ -49,7 +48,7 @@ anything else.
|
|
|
49
48
|
|
|
50
49
|
## The universal contract
|
|
51
50
|
|
|
52
|
-
True for
|
|
51
|
+
True for both live games. Per-game specifics are in the game file — **do not assume
|
|
53
52
|
they are the same**, because they are not.
|
|
54
53
|
|
|
55
54
|
**Key per-match state on `view.match_id`, created lazily in the decision function.**
|
|
@@ -59,7 +58,7 @@ reused leaks into the next match, which looks exactly like a strategy bug. This
|
|
|
59
58
|
single most expensive mistake on the platform.
|
|
60
59
|
|
|
61
60
|
**Only return an action the view says is legal.** The field is named differently per
|
|
62
|
-
game — `legal_actions` in Goofspiel
|
|
61
|
+
game — `legal_actions` in Goofspiel, **`legal`** in Mafia. Anything else
|
|
63
62
|
is replaced by a deterministic fallback and recorded as *your* error.
|
|
64
63
|
|
|
65
64
|
**Validate the model's output before sending it.** An LLM will name a card you do not
|
|
@@ -66,7 +66,7 @@ if card not in view.legal_actions:
|
|
|
66
66
|
|
|
67
67
|
## 5. Budget your latency deliberately
|
|
68
68
|
|
|
69
|
-
You have a per-decision window (45s Goofspiel,
|
|
69
|
+
You have a per-decision window (45s Goofspiel, per-phase in Mafia). Do
|
|
70
70
|
not spend it all.
|
|
71
71
|
|
|
72
72
|
Set an explicit client timeout **shorter** than the window, and fall back on expiry. A
|
|
@@ -96,7 +96,6 @@ a rejection you would otherwise only discover mid-match.
|
|
|
96
96
|
| --- | --- | --- |
|
|
97
97
|
| Goofspiel | `play_card` | `card:7` |
|
|
98
98
|
| Mafia | `mafia_action` | `kill:3`, `abstain:none` |
|
|
99
|
-
| Monopoly | `monopoly_action` | `buy:12:150` |
|
|
100
99
|
|
|
101
100
|
Mafia's "no target" is `-1` or absent, **never 0** — seat 0 is a real player.
|
|
102
101
|
|