Skip to content

Feat/othello#1190

Open
wanderingweights wants to merge 7 commits into
mainfrom
feat/othello
Open

Feat/othello#1190
wanderingweights wants to merge 7 commits into
mainfrom
feat/othello

Conversation

@wanderingweights

Copy link
Copy Markdown
Collaborator

No description provided.

Pure, shared building blocks for the env-tournament memory system: a
fixed-slot memory store and OpenAI-compatible tool schemas/dispatch that
both the validator eval harness and the training rollout will use.

- core/models/pvp_models.py: MemoryArea/MemoryOp enums, MemoryConfig and
  the MemorySlotEdit/GameActionArgs argument models.
- core/pvp/memory.py: SlotMemory — fixed slots, per-slot token budget,
  rewrite (tail-truncate) / append (FIFO front-evict), total no-op-on-bad-
  input semantics, injectable TokenCounter.
- core/pvp/tools.py: tool schemas and dispatch generated from the
  MemoryArea x MemoryOp product; slot fields range-constrained for grammar.
- tests/test_pvp_memory.py: 36 tests covering truncation, eviction,
  bad-op no-ops, generative tool set, dispatch and arg coercion.

Placed in core/ (not validator/evaluation/) so the training rollout can
share the identical implementation without depending on eval code.
Each turn is now an agentic loop: the model is given the game state plus its
memory slots and a tool set, may call memory tools (whose results are fed
back), and ends the turn by committing a legal move via game_action. The
conversation is rebuilt fresh every turn — the only state carried across turns
is SlotMemory, never a growing transcript, so context stays bounded.

Models (core/models/pvp_models.py):
- ToolCall (arguments: dict[str, JsonScalar]), ToolSchema/FunctionSchema
  envelope, ChatMessage extended with tool_calls/tool_call_id + to_openai()
  wire conversion, ChatResult.tool_calls, ChatRole.TOOL, ChatFn(config,
  messages, tools), MemoryArea.persists_across_games.

Logic:
- core/pvp/tools.py returns ToolSchema models; core/pvp/memory.py gains reset().
- chat.py passes tools + tool_choice=auto and parses tool_calls back into
  ToolCall, serialising messages/tools via their models.
- bot.py rewritten: reconstruct-per-turn context, memory tools + game_action,
  illegal move -> error result -> retry, bad memory op -> no-op, bounded by
  PVP_MAX_INNER_STEPS + per-turn SIGALRM. working resets / long_term persists.
- constants: PVP_TURN_TIMEOUT_SECONDS 5 -> 90 (sized for the loop) plus loop
  and slot config.

Tests: new test_pvp_bot_loop.py for the loop; test_pvp_bot.py reduced to
strip-think + timeout/forfeit integration; memory and coverage-gap tests
updated to the tool-calling protocol. Full PvP suite green.

Cross-game persistence and the reflection turn are wired in Phase 3; bots
currently use fresh per-game memories.
game_runner now carries one long-term memory per player across all games of a
matchup, so each side builds an opponent model over the series; working memory
stays fresh per game. After each game both surviving bots run a single-shot
reflection (same memory tools, no game_action) to consolidate long-term memory;
a forfeiting bot is skipped.

- bot.py: add LLMBot.reflect() — best-effort, single generation, swallows
  errors since the game is already decided. Factor the memory render into a
  shared block used by both the turn and reflection prompts.
- game_runner.py: per-matchup long_term_a/long_term_b threaded into each game's
  bot via fresh working + persistent long-term; reflect both players post-game.
- tests: TestReflection (bot-level) and test_pvp_persistence.py (a marker
  written in game 1's reflection appears in a later game's prompt).

Still using the whitespace token counter; the tokenizer-backed counter is a
follow-up. Persisting per-hotkey long-term memory to the DB is Phase 4.
…atch runners

The shared system_prompt_template still told the model to "respond with ONLY the
action id (a single number)", which contradicts the tool-calling loop (the bot
separately instructs it to call game_action). Remove that Output Format block —
the bot's tool guidance now stands alone — and add a test asserting the system
prompt no longer demands a bare action id.

Dev/eval tooling for driving the harness without a GPU:
- scripts/pvp_play.py: manual stepper — render the exact per-turn prompt, apply
  tool calls by hand, advance the game (state persisted to /tmp).
- scripts/pvp_anthropic_match.py: a ChatFn adapter bridging the harness to the
  Anthropic Messages API with tool use, so two Claude models can play a matchup.
  Validated end-to-end: cross-game opponent memory drove a real exploit.
…eline

Model prep (a minimal image shipping only core/ + trainer/model_prep/) needs to
run the eval harness to compute a format-aligned baseline, so the shared harness
moves out of validator/ into core/pvp/ — the same shared-code split already used
for memory/tools.

Harness -> core/pvp/ (zero validator imports, verified):
- bot.py, agents.py, chat.py, scoring.py moved; game_eval.py holds the shared
  _AGENT_REGISTRY + timeout/forfeit evaluate_bots wrapper; constants.py holds the
  harness PVP_* constants. validator/core/constants.py re-exports them and
  validator/evaluation/pvp/{bot,agents,chat,scoring}.py are back-compat shims, so
  existing imports keep working.
- game_runner.py keeps the eval-only matchup orchestration, importing from core.

Tokenizer-backed memory budgets:
- core/pvp/tokenizer_counter.py: HFTokenCounter + load_token_counter (whitespace
  fallback for non-repo ids / load failures). game_runner builds a per-player
  counter so slot budgets are real model tokens, not whitespace words.

In-harness MCTS baseline:
- core/pvp/baseline.py: run_mcts_baseline plays the model (tool-calling LLMBot)
  vs a pyspiel MCTSBot, same format as eval, no external MCTS-API server.
- trainer/model_prep/env_stats.py now computes baseline stats via run_mcts_baseline
  instead of POSTing to the external /evaluate server.
- model-prep.dockerfile gains open-spiel + openai (harness runtime deps).

Tests: test_pvp_tokenizer.py, test_pvp_baseline.py; existing tests updated for the
new module paths. Full PvP suite (150) green.
- game_runner no longer re-exports _evaluate_with_timeout / _forfeit_returns;
  the two tests and pvp_play import them from core.pvp.game_eval directly.
- Remove now-unused ENV_EVAL_TASK_TIMEOUT / CONSECUTIVE_FAILURE_LIMIT from
  env_stats (the external-server episode loop they belonged to is gone).
- pvp_play imports LLMBot from core.pvp.bot.

pyflakes clean across the changed surface; full PvP suite (150) green.
Wire othello (OpenSpiel) as a PvP environment across eval, scoring, and
training paths:

- EnvironmentName.OTHELLO + ENVIRONMENT_CONFIGS entry (task_id 400M-499M,
  PVP, MCTS baseline payload)
- OthelloAgent + a setup_initial_state hook on BaseGameAgent. Othello is
  deterministic with no chance nodes, so the hook applies 2-6 seeded random
  opening plies: same seed reproduces the same start, different seeds
  diverge. Paired base/swapped instances share a seed, preserving seat
  fairness.
- Register in _AGENT_REGISTRY; othello_rules in pvp_game_prompts.yml
- Call setup_initial_state in game_runner and baseline after new_initial_state
- Reference trainer rollout fn (dockerfiles/environment_functions/othello.py)
- text_trainer: drive rollout_func wiring off eval_type == PVP instead of a
  hardcoded game tuple, so othello (and future PvP games) are covered
- Add othello to FALLBACK_ENV_IMAGES for consistency

Cleanup: drop the dead BaseGameAgent.generate_user_prompt (the runtime uses
LLMBot._user_prompt) and its test; OthelloAgent relies on the base
format_state rather than re-overriding observation_string.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant