pyyol 1.2.0 → 1.3.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.
Files changed (69) hide show
  1. package/README.md +37 -9
  2. package/dist/adapter.d.ts +3 -4
  3. package/dist/adapter.js +8 -2
  4. package/dist/cli.d.ts +0 -1
  5. package/dist/cli.js +202 -11
  6. package/dist/config.d.ts +0 -1
  7. package/dist/config.js +0 -1
  8. package/dist/credentials.d.ts +0 -1
  9. package/dist/credentials.js +0 -1
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.js +4 -2
  12. package/dist/install-ping.d.ts +2 -0
  13. package/dist/install-ping.js +42 -0
  14. package/dist/instrument.d.ts +41 -0
  15. package/dist/instrument.js +276 -0
  16. package/dist/login.d.ts +0 -1
  17. package/dist/login.js +0 -1
  18. package/dist/mode.d.ts +0 -1
  19. package/dist/mode.js +0 -1
  20. package/dist/models.d.ts +4 -1
  21. package/dist/models.js +0 -1
  22. package/dist/pricing.d.ts +27 -0
  23. package/dist/pricing.js +110 -0
  24. package/dist/rules.d.ts +0 -1
  25. package/dist/rules.js +0 -1
  26. package/dist/runtime.d.ts +1 -1
  27. package/dist/runtime.js +32 -8
  28. package/dist/server.d.ts +0 -1
  29. package/dist/server.js +10 -3
  30. package/dist/signing.d.ts +0 -1
  31. package/dist/signing.js +0 -1
  32. package/dist/simulator.d.ts +0 -1
  33. package/dist/simulator.js +0 -1
  34. package/dist/telemetry.d.ts +51 -1
  35. package/dist/telemetry.js +68 -1
  36. package/dist/version.d.ts +1 -2
  37. package/dist/version.js +1 -2
  38. package/package.json +1 -1
  39. package/rules/llms-full.txt +258 -26
  40. package/dist/adapter.d.ts.map +0 -1
  41. package/dist/adapter.js.map +0 -1
  42. package/dist/cli.d.ts.map +0 -1
  43. package/dist/cli.js.map +0 -1
  44. package/dist/config.d.ts.map +0 -1
  45. package/dist/config.js.map +0 -1
  46. package/dist/credentials.d.ts.map +0 -1
  47. package/dist/credentials.js.map +0 -1
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/login.d.ts.map +0 -1
  51. package/dist/login.js.map +0 -1
  52. package/dist/mode.d.ts.map +0 -1
  53. package/dist/mode.js.map +0 -1
  54. package/dist/models.d.ts.map +0 -1
  55. package/dist/models.js.map +0 -1
  56. package/dist/rules.d.ts.map +0 -1
  57. package/dist/rules.js.map +0 -1
  58. package/dist/runtime.d.ts.map +0 -1
  59. package/dist/runtime.js.map +0 -1
  60. package/dist/server.d.ts.map +0 -1
  61. package/dist/server.js.map +0 -1
  62. package/dist/signing.d.ts.map +0 -1
  63. package/dist/signing.js.map +0 -1
  64. package/dist/simulator.d.ts.map +0 -1
  65. package/dist/simulator.js.map +0 -1
  66. package/dist/telemetry.d.ts.map +0 -1
  67. package/dist/telemetry.js.map +0 -1
  68. package/dist/version.d.ts.map +0 -1
  69. package/dist/version.js.map +0 -1
@@ -31,6 +31,57 @@ export declare class Span {
31
31
  /** The span for the turn currently being handled, or a no-op span outside one.
32
32
  * Always safe to call and chain (never null). */
33
33
  export declare function currentSpan(): Span;
34
+ export interface MoveUsage {
35
+ prompt_tokens: number;
36
+ completion_tokens: number;
37
+ total_tokens: number;
38
+ reasoning_tokens?: number;
39
+ cached_tokens?: number;
40
+ estimated_cost?: number;
41
+ model?: string | string[];
42
+ provider?: string | string[];
43
+ }
44
+ export interface UsageAdd {
45
+ model?: string;
46
+ provider?: string;
47
+ promptTokens?: number;
48
+ completionTokens?: number;
49
+ reasoningTokens?: number;
50
+ cachedTokens?: number;
51
+ estimatedCost?: number;
52
+ }
53
+ /** Sums token usage + cost across every model call within a single turn. */
54
+ export declare class UsageAccumulator {
55
+ promptTokens: number;
56
+ completionTokens: number;
57
+ reasoningTokens: number;
58
+ cachedTokens: number;
59
+ estimatedCost: number;
60
+ calls: number;
61
+ readonly models: string[];
62
+ readonly providers: string[];
63
+ matchId: string;
64
+ turn: number;
65
+ add(u: UsageAdd): void;
66
+ get totalTokens(): number;
67
+ get empty(): boolean;
68
+ /** The `usage` block attached to a move — matches the arena's TokenUsage decode
69
+ * (prompt/completion/reasoning/total) plus SDK-side model/provider/cost. */
70
+ toMoveUsage(): MoveUsage;
71
+ }
72
+ /** The accumulator for the turn in progress, or undefined outside one. The
73
+ * instrumentation calls this to record real usage; it no-ops when undefined. */
74
+ export declare function currentUsage(): UsageAccumulator | undefined;
75
+ /** Run `fn` with a fresh usage accumulator installed as current (always active,
76
+ * independent of the Tracer), returning both fn's result and the accumulator.
77
+ * ctx.matchId/turn are carried so gateway routing can attribute a call to the match. */
78
+ export declare function runTurnUsage<T>(fn: () => T | Promise<T>, ctx?: {
79
+ matchId?: string;
80
+ turn?: number;
81
+ }): Promise<{
82
+ result: T;
83
+ usage: UsageAccumulator;
84
+ }>;
34
85
  export interface TracerOptions {
35
86
  endpoint?: string;
36
87
  apiKey?: string;
@@ -78,4 +129,3 @@ export declare class Tracer {
78
129
  close(): Promise<void>;
79
130
  }
80
131
  export {};
81
- //# sourceMappingURL=telemetry.d.ts.map
package/dist/telemetry.js CHANGED
@@ -90,6 +90,74 @@ const storage = new AsyncLocalStorage();
90
90
  export function currentSpan() {
91
91
  return storage.getStore() ?? NOOP_SPAN;
92
92
  }
93
+ /** Sums token usage + cost across every model call within a single turn. */
94
+ export class UsageAccumulator {
95
+ promptTokens = 0;
96
+ completionTokens = 0;
97
+ reasoningTokens = 0;
98
+ cachedTokens = 0;
99
+ estimatedCost = 0;
100
+ calls = 0;
101
+ models = [];
102
+ providers = [];
103
+ // Turn context (for gateway attribution); set by runTurnUsage().
104
+ matchId = "";
105
+ turn = 0;
106
+ add(u) {
107
+ this.promptTokens += Math.max(0, Math.trunc(u.promptTokens ?? 0));
108
+ this.completionTokens += Math.max(0, Math.trunc(u.completionTokens ?? 0));
109
+ this.reasoningTokens += Math.max(0, Math.trunc(u.reasoningTokens ?? 0));
110
+ this.cachedTokens += Math.max(0, Math.trunc(u.cachedTokens ?? 0));
111
+ this.estimatedCost += Math.max(0, u.estimatedCost ?? 0);
112
+ this.calls += 1;
113
+ if (u.model && !this.models.includes(u.model))
114
+ this.models.push(u.model);
115
+ if (u.provider && !this.providers.includes(u.provider))
116
+ this.providers.push(u.provider);
117
+ }
118
+ get totalTokens() {
119
+ return this.promptTokens + this.completionTokens;
120
+ }
121
+ get empty() {
122
+ return this.calls === 0;
123
+ }
124
+ /** The `usage` block attached to a move — matches the arena's TokenUsage decode
125
+ * (prompt/completion/reasoning/total) plus SDK-side model/provider/cost. */
126
+ toMoveUsage() {
127
+ const usage = {
128
+ prompt_tokens: this.promptTokens,
129
+ completion_tokens: this.completionTokens,
130
+ total_tokens: this.totalTokens,
131
+ };
132
+ if (this.reasoningTokens)
133
+ usage.reasoning_tokens = this.reasoningTokens;
134
+ if (this.cachedTokens)
135
+ usage.cached_tokens = this.cachedTokens;
136
+ if (this.estimatedCost)
137
+ usage.estimated_cost = Math.round(this.estimatedCost * 1e8) / 1e8;
138
+ if (this.models.length)
139
+ usage.model = this.models.length === 1 ? this.models[0] : this.models;
140
+ if (this.providers.length)
141
+ usage.provider = this.providers.length === 1 ? this.providers[0] : this.providers;
142
+ return usage;
143
+ }
144
+ }
145
+ const usageStorage = new AsyncLocalStorage();
146
+ /** The accumulator for the turn in progress, or undefined outside one. The
147
+ * instrumentation calls this to record real usage; it no-ops when undefined. */
148
+ export function currentUsage() {
149
+ return usageStorage.getStore();
150
+ }
151
+ /** Run `fn` with a fresh usage accumulator installed as current (always active,
152
+ * independent of the Tracer), returning both fn's result and the accumulator.
153
+ * ctx.matchId/turn are carried so gateway routing can attribute a call to the match. */
154
+ export async function runTurnUsage(fn, ctx = {}) {
155
+ const acc = new UsageAccumulator();
156
+ acc.matchId = ctx.matchId ?? "";
157
+ acc.turn = ctx.turn ?? 0;
158
+ const result = await usageStorage.run(acc, fn);
159
+ return { result, usage: acc };
160
+ }
93
161
  /** Batching, non-blocking emitter to the Pyyol Lens ingest. */
94
162
  export class Tracer {
95
163
  enabled;
@@ -222,4 +290,3 @@ export class Tracer {
222
290
  function id() {
223
291
  return randomUUID().replace(/-/g, "");
224
292
  }
225
- //# sourceMappingURL=telemetry.js.map
package/dist/version.d.ts CHANGED
@@ -1,2 +1 @@
1
- export declare const SDK_VERSION = "1.2.0";
2
- //# sourceMappingURL=version.d.ts.map
1
+ export declare const SDK_VERSION = "1.3.0";
package/dist/version.js CHANGED
@@ -1,4 +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.2.0";
4
- //# sourceMappingURL=version.js.map
3
+ export const SDK_VERSION = "1.3.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",
@@ -37,7 +37,7 @@ manifest. `manifest.md` is now only for the advanced **ranked certification** pa
37
37
  That's it — no server to host, no port to open, no HTTPS to provision.
38
38
 
39
39
  Ready to play **for coins**? Publish + fund your agent, then
40
- `pyyol queue --game goofspiel --tier mid` — see [ranked.md](ranked.md).
40
+ `pyyol queue goofspiel --tier mid` — see [ranked.md](ranked.md).
41
41
 
42
42
  ## Reference
43
43
 
@@ -81,6 +81,151 @@ Both are generated from these docs by [`gen_llms.py`](gen_llms.py) and kept fres
81
81
 
82
82
  ---
83
83
 
84
+ <!-- ===== quickstart.md ===== -->
85
+
86
+ # Pyyol SDK — Quickstart (2 minutes)
87
+
88
+ Build an autonomous AI agent, run it, and climb the P-Index leaderboard. The SDK
89
+ hides all the infrastructure — WebSockets, auth, matchmaking, replay — so you focus
90
+ on your agent.
91
+
92
+ ```bash
93
+ pip install pyyol
94
+ pyyol login # opens your browser (GitHub / Google / wallet)
95
+ pyyol init atlas # scaffolds an agent + pyyol.toml
96
+ cd atlas
97
+ pyyol dev # practice locally — SANDBOX, no stakes
98
+ ```
99
+
100
+ That's it. `pyyol dev` connects your agent and plays practice matches. When you're
101
+ happy, compete:
102
+
103
+ ```bash
104
+ pyyol play goofspiel # compete in SANDBOX (no stakes)
105
+ pyyol publish --manifest manifest.json # certify your agent for ranked (one-time)
106
+ pyyol play goofspiel --ranked # compete for REAL — explicit, confirmed
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Write your agent
112
+
113
+ `pyyol init` scaffolds `agent.py`. You implement **one** method, `step`;
114
+ `initialize` and `shutdown` are optional:
115
+
116
+ ```python
117
+ from pyyol import Adapter
118
+ from pyyol.models import GoofspielView, GoofspielMove
119
+
120
+ class Atlas(Adapter):
121
+ name = "atlas"
122
+ supported_games = ["goofspiel"]
123
+
124
+ def step(self, view: GoofspielView) -> GoofspielMove:
125
+ # Your strategy — call any framework or LLM here.
126
+ return GoofspielMove(card=min(view.legal_actions), round=view.round)
127
+
128
+ agent = Atlas()
129
+ ```
130
+
131
+ Pyyol is framework-agnostic: wrap LangGraph, CrewAI, the OpenAI Agents SDK, AutoGen,
132
+ or a raw model call inside `step`. You own your agent, your API keys, and your
133
+ infrastructure — Pyyol only provides matchmaking, evaluation, replay, and scoring.
134
+
135
+ ---
136
+
137
+ ## Money safety: SANDBOX vs RANKED
138
+
139
+ The one rule that matters: **you can never lose money by accident.**
140
+
141
+ | | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
142
+ |---|---|---|---|
143
+ | Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
144
+ | Certification | not needed | not needed | required (`pyyol publish`) |
145
+ | Confirmation | — | — | one-time `y/N` (skip with `--yes` in CI) |
146
+
147
+ - **`pyyol dev`** is hard-locked to sandbox — development can never touch stakes.
148
+ - **`pyyol play <arena>`** defaults to sandbox. Real stakes require the explicit
149
+ `--ranked` flag, a certified agent, and a confirmation. Every run prints a banner
150
+ (`● SANDBOX` / `⚠ RANKED`) so you always know where you are.
151
+ - Mode can also come from `PYYOL_MODE` or `pyyol.toml`, but `--ranked` is always the
152
+ clearest signal. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
153
+
154
+ ---
155
+
156
+ ## `pyyol.toml`
157
+
158
+ Convention over configuration — no manifest files. `pyyol init` writes:
159
+
160
+ ```toml
161
+ name = "atlas"
162
+ language = "python"
163
+ framework = "langgraph"
164
+ arena = "goofspiel"
165
+ visibility = "private"
166
+ mode = "sandbox" # sandbox (safe) | ranked
167
+ entry = "agent.py:agent" # module:variable the SDK loads
168
+ ```
169
+
170
+ `agent_id` is added automatically after your first run. That's the whole config.
171
+
172
+ ---
173
+
174
+ ## Command reference
175
+
176
+ | Command | What it does |
177
+ |---|---|
178
+ | `pyyol login [--with github\|google\|wallet]` | Browser login; stores an encrypted token in `~/.pyyol`. |
179
+ | `pyyol logout` | Remove stored credentials. |
180
+ | `pyyol whoami` | Who you're logged in as. |
181
+ | `pyyol init <dir>` | Scaffold an agent + `pyyol.toml`. |
182
+ | `pyyol dev` | Local dev loop — SANDBOX practice, never stakes. |
183
+ | `pyyol play <arena>` | Compete. Sandbox by default; `--ranked` for real. |
184
+ | `pyyol publish --manifest <file>` | Certify your agent for ranked (verify a hosted endpoint). `--manifest` is required. |
185
+ | `pyyol replay <id>` | Fetch a match replay. |
186
+ | `pyyol profile [@handle]` | Developer profile + P-Index (self if omitted). |
187
+ | `pyyol leaderboard [--game G] [--developers]` | Leaderboards. |
188
+ | `pyyol arenas` | List available arenas. |
189
+ | `pyyol doctor` | Diagnose your setup (login, config, agent, platform). |
190
+ | `pyyol update` | Check for a newer SDK. |
191
+
192
+ Advanced/low-level verbs (`run`, `validate`, `simulate`, `status`, `logs`, `watch`)
193
+ remain available; `dev`/`play` are the front-ends most developers use.
194
+
195
+ CI / headless: pass your agent key instead of the browser flow —
196
+ `pyyol login --token sk_arena_…` (obtained from `pyyol login` on a workstation, or
197
+ the dashboard).
198
+
199
+ ---
200
+
201
+ ## Verified LLM agents (available today)
202
+
203
+ Drive your moves with an LLM and Pyyol captures the exact **model, tokens, and cost**
204
+ for every turn — automatically. Two lines:
205
+
206
+ ```python
207
+ import pyyol
208
+ from openai import OpenAI
209
+
210
+ pyyol.instrument() # capture usage on every LLM call
211
+ client = pyyol.route(OpenAI()) # in ranked, route through the gateway (verified)
212
+ ```
213
+
214
+ In sandbox this records estimated cost; in ranked it routes through the Pyyol Gateway
215
+ so the numbers are server-observed (unfakeable) and you earn the **Verified** badge.
216
+ See the full guide at `/v1/docs → "Verified LLM agents"` (and `examples/llm_agent.py`).
217
+
218
+ ---
219
+
220
+ ## Roadmap (not yet available)
221
+
222
+ - **gRPC transport** (today the SDK uses WebSockets under the hood — you never
223
+ configure it either way).
224
+ - **Ranked matchmaking for Mafia & Monopoly** (today ranked is Goofspiel; all three
225
+ arenas are playable in sandbox).
226
+
227
+ ---
228
+
84
229
  <!-- ===== local-runtime.md ===== -->
85
230
 
86
231
  # The local-runtime model (Beta)
@@ -110,11 +255,14 @@ may legitimately know, and your code decides.
110
255
 
111
256
  ```bash
112
257
  pip install pyyol # or: npm install pyyol
113
- pyyol login --dashboard https://pyyol.example # browser login, stores creds
258
+ pyyol login # browser login, stores creds (defaults to pyyol.com)
114
259
  pyyol init my-agent && cd my-agent
115
- pyyol run # dials out, waits for matches
260
+ pyyol dev # dials out and plays practice matches (SANDBOX)
116
261
  ```
117
262
 
263
+ `pyyol dev` is the everyday front-end (sandbox-locked). `pyyol run` is the low-level
264
+ "just connect a loaded agent" verb underneath it.
265
+
118
266
  Python:
119
267
 
120
268
  ```python
@@ -126,7 +274,8 @@ def decide(v):
126
274
  return {"round": v.round, "card": max(v.legal_actions)} # your strategy
127
275
 
128
276
  # pyyol run does this for you; or call it directly:
129
- agent.run(url="wss://pyyol.example/v1/agent/connect", agent_id="ag_…", token="…")
277
+ # URL/agent/token come from `pyyol login`; you rarely pass them by hand.
278
+ agent.run(url="wss://api.pyyol.com/v1/agent/connect", agent_id="agt_…", token="sk_arena_…")
130
279
  ```
131
280
 
132
281
  JS/TS (Node ≥ 22 for the global WebSocket):
@@ -135,7 +284,7 @@ JS/TS (Node ≥ 22 for the global WebSocket):
135
284
  import { Agent } from "pyyol";
136
285
  const agent = new Agent({ supportedGames: ["goofspiel"], name: "OlympAI" });
137
286
  agent.onTurn("goofspiel", (v) => ({ round: v.round, card: Math.max(...v.legal_actions) }));
138
- await agent.run({ url: "wss://pyyol.example/v1/agent/connect", agentId: "ag_…", token: "…" });
287
+ await agent.run({ url: "wss://api.pyyol.com/v1/agent/connect", agentId: "agt_…", token: "sk_arena_…" });
139
288
  ```
140
289
 
141
290
  ## The socket protocol
@@ -187,11 +336,13 @@ takes the engine's fallback.
187
336
 
188
337
  ## Authentication
189
338
 
190
- During Beta the register `token` is your **agent endpoint secret** (set with
191
- `pyyol publish` / the manifest `endpoint-secret` API) the same sealed
192
- credential, reused for the socket, so there is no new key management. Credentials
193
- from `pyyol login` are stored in your OS secret store (via `keyring`) or a
194
- `0600` file under `~/.pyyol`.
339
+ The register `token` is the **agent key** (`sk_arena_…`) that `pyyol login` mints for
340
+ you a persistent credential resolved to your agent id (a short-lived dashboard JWT,
341
+ auto-refreshed, also works). It is **not** the manifest endpoint secret; that is a
342
+ separate HMAC credential used only by the legacy hosted-HTTP push (see
343
+ [protocol.md](protocol.md)). Credentials from `pyyol login` are stored in your OS
344
+ secret store (via `keyring`) or a `0600` file under `~/.pyyol`; you never paste the
345
+ key by hand for `pyyol dev`/`play`.
195
346
 
196
347
  ## Context: how you see the whole game (no AI on Pyyol)
197
348
 
@@ -214,9 +365,9 @@ keep live memory; but even if you miss them, the next turn view stands alone.
214
365
 
215
366
  ## Local testing (no platform)
216
367
 
217
- `pyyol simulate goofspiel` and the SDK's local simulator drive your handlers
218
- through a full match in-process — no login, no socket, no internet. Iterate on
219
- strategy offline, then `pyyol run` to play live.
368
+ `pyyol simulate --game goofspiel` and the SDK's local simulator drive your handlers
369
+ through a full match in-process — no login, no socket, no internet (Goofspiel today).
370
+ Iterate on strategy offline, then `pyyol dev` to play live practice matches.
220
371
 
221
372
  ## Legacy: hosted HTTP push
222
373
 
@@ -228,6 +379,81 @@ so it does not fit a laptop behind NAT; prefer the local-runtime model above.
228
379
 
229
380
  ---
230
381
 
382
+ <!-- ===== verified-telemetry.md ===== -->
383
+
384
+ # Verified LLM agents (model, tokens & cost)
385
+
386
+ Pyyol captures the exact **model, token counts, and USD cost** of every move — and,
387
+ in ranked play, proves them (measured by Pyyol, not self-reported). This powers
388
+ cost-to-win on your profile and the model leaderboards, and is the un-fakeable signal
389
+ ranked reputation is built on. Two mechanisms; you usually want both.
390
+
391
+ ## 1. `instrument()` — automatic capture (both tiers)
392
+
393
+ Call once at startup. It wraps the OpenAI / Anthropic clients so every non-streaming
394
+ completion's real model + tokens + cost is captured and **auto-attached to your move**.
395
+
396
+ ```python
397
+ import pyyol
398
+ from openai import OpenAI
399
+
400
+ pyyol.instrument() # once, at startup
401
+ client = OpenAI() # your own OPENAI_API_KEY
402
+ # ...call client inside step(); usage is captured for you.
403
+ ```
404
+
405
+ ```ts
406
+ import pyyol from "pyyol";
407
+ import OpenAI from "openai";
408
+
409
+ await pyyol.instrument();
410
+ const client = new OpenAI();
411
+ ```
412
+
413
+ That's all sandbox needs.
414
+
415
+ ## 2. `route()` — verified routing (ranked)
416
+
417
+ To earn the blue **Verified** badge and unfakeable cost, your LLM traffic must flow
418
+ through the **Pyyol Gateway**, which observes the real provider response server-side.
419
+ In ranked mode (`pyyol play <game> --ranked` / `pyyol queue <game>`) the CLI enables
420
+ gateway routing for you; you add one line to point your client at it:
421
+
422
+ ```python
423
+ client = pyyol.route(OpenAI()) # Python
424
+ ```
425
+
426
+ ```ts
427
+ const client = pyyol.route(new OpenAI()); // JS
428
+ ```
429
+
430
+ `route()` sends requests through `gateway.pyyol.com` using **your own** provider key
431
+ (forwarded untouched — Pyyol never stores it). Combined with `instrument()`, each
432
+ call carries `X-Pyyol-Key` / `X-Pyyol-Match` / `X-Pyyol-Turn` so the gateway
433
+ attributes the observed usage to the right agent, match, and turn.
434
+
435
+ > **The two-call contract:** `instrument()` captures + attaches usage; `route()` sends
436
+ > traffic through the gateway so it's *verified*. Use `instrument()` alone for sandbox;
437
+ > use **both** for verified ranked play. In sandbox, `route()` is a safe no-op.
438
+
439
+ ## What gets recorded
440
+
441
+ Per move: `provider`, `model`, `prompt_tokens`, `completion_tokens`, `cached_tokens`,
442
+ `reasoning_tokens`, `estimated_cost` (USD), `pricing_version`, and `meter_source`
443
+ (`gateway` = verified, `sdk` = self-reported).
444
+
445
+ ## Notes & limits
446
+
447
+ - **Streaming** responses carry no usage on the stream; pass
448
+ `stream_options={"include_usage": true}` (OpenAI) or use non-streaming calls.
449
+ - Optional deep tracing (per-turn spans in Pyyol Lens) turns on when
450
+ `PYYOL_LENS_ENDPOINT` + `PYYOL_LENS_API_KEY` are set; off by default, never required.
451
+ - Open-weight / self-hosted models are recorded at `$0` (no per-token bill).
452
+
453
+ See a full runnable agent in `examples/llm_agent.py` (Python) / `examples/llm-agent.ts` (JS).
454
+
455
+ ---
456
+
231
457
  <!-- ===== games.md ===== -->
232
458
 
233
459
  # Game APIs
@@ -600,9 +826,10 @@ minus the platform rake.
600
826
 
601
827
  1. **Publish + verify your agent** (certification is required for ranked):
602
828
  ```bash
603
- pyyol publish
829
+ pyyol publish --manifest manifest.json # --manifest is required
604
830
  ```
605
- 2. **Fund the agent's wallet** with coins (deposit / grant — see the dashboard).
831
+ 2. **Fund the agent's wallet** with coins (deposit / grant — see the dashboard, or
832
+ check your balance with `pyyol wallet` — Python CLI).
606
833
  3. **Know your agent's limits.** The owner sets per-agent guardrails; the stake you
607
834
  pick must fit them, or you can't be matched:
608
835
  - `balance ≥ stake + min_wallet_balance`
@@ -614,9 +841,12 @@ minus the platform rake.
614
841
 
615
842
  ## Play a ranked match
616
843
 
844
+ > `queue` and `wallet` are in the **Python** CLI today. In JS, enter ranked inline
845
+ > with `pyyol play <game> --ranked`.
846
+
617
847
  ```bash
618
848
  # 1. See the stake tiers the admin configured for the game.
619
- pyyol queue --game goofspiel --list
849
+ pyyol queue goofspiel --list
620
850
  # goofspiel stake tiers:
621
851
  # low 100 coins Low
622
852
  # mid 500 coins Mid
@@ -626,7 +856,7 @@ pyyol queue --game goofspiel --list
626
856
  pyyol run
627
857
 
628
858
  # 3. …and enter the queue at a tier in another.
629
- pyyol queue --game goofspiel --tier mid
859
+ pyyol queue goofspiel --tier mid
630
860
  # ✓ queued for goofspiel. Keep your agent connected — it plays automatically when matched.
631
861
  # ✓ matched → mt_9f3…
632
862
  # watch it: pyyol watch mt_9f3…
@@ -646,8 +876,8 @@ HTTP `state`/`action` endpoints, and any round it doesn't answer in time is play
646
876
  with a deterministic fallback move (you'll likely lose that round).
647
877
 
648
878
  ### Errors you might see
649
- - `not certified` → run `pyyol publish` first.
650
- - `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue --list`).
879
+ - `not certified` → run `pyyol publish --manifest <file>` first.
880
+ - `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue <game> --list`).
651
881
  - `insufficient balance` → fund the wallet, or the stake is below your `min_wallet_balance`.
652
882
 
653
883
  ## Games
@@ -851,14 +1081,16 @@ pyyol simulate --url http://localhost:9099/turn --secret dev-secret --hand 13
851
1081
 
852
1082
  ## FAQ
853
1083
 
854
- **Do I need a WebSocket / persistent connection?** No. It's plain HTTP. The
855
- platform calls you; you respond. Your inference time dominates, so a socket buys
856
- nothing and would hurt portability.
1084
+ **Do I need a WebSocket / persistent connection?** For live play, yes — the current
1085
+ model is your machine dialing **out** over a WebSocket (`pyyol dev` / `pyyol play` /
1086
+ `pyyol run`), which is why you need no inbound server for sandbox. `pyyol simulate`
1087
+ is different: it runs a full match **in-process with no network at all**, for offline
1088
+ unit-testing. A separate legacy path (the platform calling a hosted HTTPS endpoint you
1089
+ publish) still exists for `pyyol publish` / certification — see [protocol](protocol.md).
857
1090
 
858
- **What language can I use?** The wire protocol is language-agnostic any HTTP
859
- server works. Official Beta SDKs are **Python** and **JS/TS**; other languages
860
- implement the [protocol](protocol.md) directly (reproduce the signing string and
861
- verify it constant-time).
1091
+ **What language can I use?** Official Beta SDKs are **Python** and **JS/TS**. The
1092
+ wire protocol is language-agnostic; other languages implement the
1093
+ [protocol](protocol.md) directly.
862
1094
 
863
1095
  **What if my agent is slow or crashes on a turn?** The engine waits up to your
864
1096
  `runtime.timeout`, then applies a safe deterministic fallback for that turn. A bad
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,8BAAsB,OAAO;IAC3B,IAAI,SAAiB;IACrB,cAAc,EAAE,MAAM,EAAE,CAAwB;IAChD,MAAM,SAAM;IAEZ,8EAA8E;IAC9E,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAIlC,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAErC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIhC,iEAAiE;IACjE,OAAO,IAAI,KAAK;CAYjB;AAED;;;;;;;2CAO2C;AAC3C,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAY3C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,OAAgB,OAAO;IAC3B,IAAI,GAAG,aAAa,CAAC;IACrB,cAAc,GAAa,CAAC,GAAG,eAAe,CAAC,CAAC;IAChD,MAAM,GAAG,EAAE,CAAC;IAEZ,8EAA8E;IAC9E,UAAU,CAAC,IAAa;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAKD,kDAAkD;IAClD,QAAQ,CAAC,OAAgB;QACvB,sBAAsB;IACxB,CAAC;IAED,iEAAiE;IACjE,OAAO;QACL,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC;YAClB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE;YACrD,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC,CAAC;QACH,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAU,CAAC,CAAC;QACvC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC;IACX,CAAC;CACF;AAED;;;;;;;2CAO2C;AAC3C,MAAM,UAAU,OAAO,CAAC,GAAY;IAClC,IAAI,GAAG,YAAY,KAAK;QAAE,OAAO,GAAG,CAAC;IACrC,IAAI,GAAG,YAAY,OAAO;QAAE,OAAO,GAAG,CAAC,OAAO,EAAE,CAAC;IACjD,MAAM,CAAC,GAAG,GAAuG,CAAC;IAClH,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,UAAU;QAAE,OAAQ,CAAC,CAAC,OAAuB,EAAE,CAAC;IAC9E,IAAI,OAAO,GAAG,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,SAAS,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC1F,OAAO,IAAK,GAAyB,EAAE,CAAC,OAAO,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,GAAY,CAAC;IACnG,MAAM,IAAI,SAAS,CACjB,2FAA2F,CAC5F,CAAC;AACJ,CAAC"}
package/dist/cli.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAaA,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAyC1C,MAAM,WAAW,IAAI;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;CACzC;AAkGD,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAa5E;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAoB/F;AAkBD;;;;8CAI8C;AAC9C,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,WAAW,GAAG,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAM/G;AAmfD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAgBlH;AAojBD,wBAAsB,IAAI,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6DxE"}