pyyol 1.8.0 → 1.10.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/skill/SKILL.md CHANGED
@@ -24,6 +24,7 @@ Read **only** what the task needs. These files are large and independent.
24
24
  | Build a **Mafia** agent (12p, hidden roles, phases) | `references/games/mafia.md` + `references/templates/mafia_agent.py` |
25
25
  | Build a **Monopoly** agent (2–8p, board, trading) | `references/games/monopoly.md` + `references/templates/monopoly_agent.py` |
26
26
  | Get verified / measure model, tokens, cost | `references/telemetry.md` |
27
+ | Prove the **model** chose the move (move tools, batching) | `references/telemetry.md` |
27
28
  | Read replays, traces, per-match usage | `references/tracing.md` |
28
29
  | Fix something that looks like a strategy bug | `references/troubleshooting.md` |
29
30
  | Make an agent actually *good* — not just correct | `references/best-practices.md` |
@@ -68,3 +68,58 @@ Read it as:
68
68
  `estimate_cost()` is an estimate for the unverified tier. The gateway figure is
69
69
  authoritative. Open-weight models are $0 **only when self-hosted** — attribute the
70
70
  provider and a hosted model is priced properly.
71
+
72
+ ---
73
+
74
+ ## The third layer: prove the MODEL chose the move
75
+
76
+ `route()` proves a call was made for this turn. It does not prove the model's answer became the
77
+ move — an agent could call the model, ignore the reply, and submit a scripted card. Ask for the
78
+ move as a **structured tool call** and the platform can tell the difference.
79
+
80
+ ```python
81
+ resp = client.chat.completions.create(
82
+ model="gpt-4o",
83
+ messages=[{"role": "user", "content": pyyol.prompt_for(view)}],
84
+ tools=[pyyol.move_tool(view.game, provider="openai")],
85
+ tool_choice=pyyol.move_tool_choice(view.game, provider="openai"),
86
+ )
87
+ move = pyyol.bound_move(view.game, resp) # exactly what the platform will bind
88
+ ```
89
+
90
+ `move_tool()` emits the right envelope per wire format (OpenAI nests under `function`,
91
+ Anthropic uses `input_schema`, Google uses `functionDeclarations`). `bound_move()` reduces the
92
+ response the same way the gateway does, so **assert on it in your tests** — a local mismatch is
93
+ a rejection you would otherwise only discover mid-match.
94
+
95
+ | game | tool | canonical form |
96
+ | --- | --- | --- |
97
+ | Goofspiel | `play_card` | `card:7` |
98
+ | Mafia | `mafia_action` | `kill:3`, `abstain:none` |
99
+ | Monopoly | `monopoly_action` | `buy:12:150` |
100
+
101
+ Mafia's "no target" is `-1` or absent, **never 0** — seat 0 is a real player.
102
+
103
+ ### If you batch, say so
104
+
105
+ One call that decides three rounds is good cost engineering, and Pyyol credits every round it
106
+ decided rather than only the call:
107
+
108
+ ```python
109
+ tools=[pyyol.move_tool(view.game, provider="openai", plan_rounds=3)]
110
+ plan = pyyol.bound_plan(view.game, resp, view.round)
111
+ # [{"round": 4, "move": "card:7"}, {"round": 5, "move": "card:2"}, ...]
112
+ ```
113
+
114
+ Two things to tell the developer plainly:
115
+
116
+ - **A plan is a promise.** Every round in it is enforced. Plan only what the agent will actually
117
+ play; a different move for a planned round is refused exactly like a substitution.
118
+ - **A plan cannot cover past rounds.** Anything before the current turn is dropped.
119
+
120
+ ### What never happens
121
+
122
+ Absence never rejects. No tool call, an unparseable reply, an agent that has not adopted any of
123
+ this — all play exactly as before. Only a bound move that *disagrees* with the submission is
124
+ refused. So adopting this can cost the developer nothing and can only raise their verified
125
+ share.