--- title: "BDPD v0.8 — Release Notebook" subtitle: "Agent SDK: BDPD as a citable testbed for LLM cooperation experiments" author: "Roberto Brunelli" date: "2026-05-20" release_tag: "v0.8" release_type: "infrastructural" format: html: toc: true code-fold: true embed-resources: true --- # Overview {#sec-overview} v0.8 is the **Agent SDK** release. It turns BDPD into a citable testbed for LLM cooperation experiments, with a stable Python SDK, a refactored middle-man, DeepSeek v4 as the operational default, an eval harness for paired-run pilots, and a parser robustness ladder that closes a silent bug discovered in the eval pilot. Single LLM provider supported and tested: **DeepSeek**, via its OpenAI-compatible API (`deepseek-v4-flash` thinking-off as the default, `deepseek-v4-pro` available for thinking-on workloads). See [Cloud APIs](../llm/cloud-apis.md) for the rationale. The underlying `openai` SDK is generic — any OpenAI-compatible endpoint will physically respond — but pricing tables, model recommendations, and examples target DeepSeek exclusively. # What changed {#sec-what-changed} ## `bdpd_sdk` Python package Six modules in `agents/bdpd_sdk/`: | Module | Surface | |---|---| | `client.py` | `make_client()` factory pointed at DeepSeek; reads `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL` with fall-back to `OPENAI_API_KEY` | | `agent.py` | `BDPDAgent` class: system prompt + per-instance history + `chat(user_message, *, memory=None, response_format=None)`. Rolling history bounded via `deque(maxlen=max_history*2)`. Exposes `last_reasoning_content` and `last_usage` per call | | `memory.py` | `PlayerMemory` dataclass with typed fields. Three writers: deterministic `update_from_observation` (EMA aggressor intensity), LLM-side `apply_updates` (partial merge, strict semantics), and direct caller-side mutation | | `trace.py` | `TraceWriter` appends one JSONL entry per `/decide` turn with a fixed schema. Suitable for post-hoc replay and per-call cache-rate analysis | | `tools.py` | `@tool` decorator with auto-schema from `inspect.signature` + `get_type_hints`. Internal tool-call loop (`max_tool_iterations=4`) with optional `on_tool_call` hook. `parallel_tool_calls=False` enforced | | `bdpd_tools.py` | Two engine-agnostic starter tools wired by the middle-man as closures over per-player `TURN_LOG`: `query_history(turn_start, turn_end)` and `compute_trend(window)` | A logistic-only `simulate_future` was prototyped and dropped — the platform stays engine-agnostic; dynamics belong in the JS engine registry, not in Python tools. ## Middle-man (`bdpd_agent.py`) refactor - `bdpd_agent.py` now delegates `/decide` and `/reset` to per-player `BDPDAgent` instances (`AGENTS` dict). The Flask routes stay thin; all LLM logic lives in the SDK. - `--archetype {aggressive, conservative, adaptive, random}` injects a behavioural nudge into the system prompt; `--trace-file` enables JSONL output; `--no-memory` falls back to bare-number responses. - JSON mode: the model is asked to respond `{"harvest": N, "scratchpad": {...}}` via `response_format={"type":"json_object"}`. Disabled in `--no-memory` mode and on tool-emitting iterations of the tool loop, where DeepSeek rejects JSON mode together with tools. ### Parser robustness (commit `76bac3a`) v4-flash narrates first and emits JSON last in ~100% of `/decide` calls. The legacy parser failed `json.loads` on the prefix prose and fell back to a bare-number regex that grabbed stock percentages — a silent bug found by the eval-harness pilot. `parse_response` now uses a three-step ladder: 1. Full-text `json.loads` 2. `JSONDecoder.raw_decode` scan for embedded JSON 3. Targeted `"harvest": N` regex Pinned by `examples/agents/parse_response_smoke.py` (14 cases including the exact bug patterns). ## DeepSeek v4 migration - `DEFAULT_MODEL` → `deepseek-v4-flash`. All `chat.completions.create` calls pass `extra_body={"thinking":{"type":"disabled"}}` because v4 defaults thinking ON and the tool-call loop requires `reasoning_content` replay across `tool_call` boundaries that v0.8 does not yet plumb. - Probing test (`examples/agents/probing_test.py`) showed thinking is marginal for BDPD's calc-heavy observations; default v0.8 = flash-noThink. Full rationale in `memory/project_v08_thinking_utility`. - All docs and the cards code path migrated from the legacy `deepseek-chat` / `deepseek-reasoner` aliases (deprecated by DeepSeek 2026-07-24) to the explicit v4 names. ## Prompt-cache optimisation - **Perf (commit `08cbd6d`):** `PlayerMemory` snap is now baked into the user message instead of being sent as a separate system message. Reason: the snap changes every turn (EMA refresh), and DeepSeek caches on the longest stable prefix. With the snap inline, the prefix `[system, *history]` stays bit-stable and cache hit rate climbs monotonically with turn count — measured ~89% across turns 1–9 of the pilot (vs ~50% pre-fix). - After turn 9 the rolling-history FIFO eviction invalidates the prefix again. A fix (`max_history` 6/8 → 32) was applied post-tag and folded into v0.9. - **Telemetry (commit `c66bf91`):** `TraceWriter` and `BDPDAgent.last_usage` capture the full `usage` block of every call, including DeepSeek-specific `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`. ## Platform integration - **(commit `07b0558`):** `arena.start()` fires a `POST /reset` at every http-callback agent at kickoff (fire-and-forget, 5 s timeout, errors swallowed as `agent_reset_failed` events). Reset URL is derived by replacing the last path segment of `callbackUrl` (`/decide` → `/reset`, etc.). Guarantees the middle-man's per-player state is purged between arenas without operator intervention. Smoke: `examples/agents/smoke_arena_reset.mjs`. ## Eval harness - **(commit `3852dd3`):** `examples/pilot/run_pilot.py` — Python orchestrator. For each `(scenario, mix)` cell it spawns 3 middle-man processes, wires them as http agents in a generated `sweep.json`, runs `node experiments/experiment.js`, and saves per-cell summary. Scenarios shipped: `hardin_3p` (symmetric 30/30/30) and `olson_3p` (asymmetric 50/10/10). Mixes shipped: `balanced` (a+c+adp), `lone-defender` (2a+c), `lone-defector` (a+2c). Output lands in `data/pilot//`. - **(commit `c252d8a`):** `--n-runs N` on the runner + `compare_runs.py` diff tool that reports per-cell determinism at three levels (outcome / harvest-sequence / response-text). Backs the v0.8 finding: at `temperature=0.4` without an explicit `seed` param, DeepSeek v4-flash is fully non-deterministic — 0/4 cells have identical outcomes between paired runs sharing the same arena seed. Documented in `memory/project_v08_seed_determinism`. - **(commit `3df2a19`):** `examples/agents/probing_test.py` — 10-probe × 3-config aptitude test (flash-noThink, flash-think, pro-think). Empirical finding: thinking helps only on metacognitive probes; on calc-heavy BDPD-style probes it's neutral or worse (truncation). # Pilot findings (preliminary, n=1) {#sec-findings} Three behavioural patterns observed in the v0.8 pilot, recorded in `memory/project_v08_pilot_findings`: - **Cooperation does not scale from n=1.** In the *balanced* mix (1 conservative among 3), conservative restraint collapses after ~7 turns; in *lone-defector* (2 conservatives + 1 aggressive), the conservatives hold steady restraint for all 30 turns. Empirical replica of Janssen & Ostrom (2006) small-group robustness. - **Aggressive self-modulates for survival.** Under the "harvest near full capacity" nudge, the agent ramps up linearly for ~14 turns and then drops to ~50% when the stock plateaus — v4-flash noThink has enough strategic cognition to read `stock_direction = stable` and infer "commons in danger" without thinking mode. - **Safe betrayal under inequality.** In `olson_3p__lone-defector` (1 rich aggressive + 2 poor conservatives), the rich captures 6× more wealth than the two poor combined while the commons survives (ratio 0.34). The rich's restraint at T24-25 keeps the system alive; the poor pay the restraint cost for 23 turns and finish marginal winners only of survival, not of wealth. Findings are n=1 exploratory; promotion to a proper `examples/llm_findings/` showcase is registered as a v0.8.1 candidate. # Provider scope {#sec-scope} v0.8 is built and tested against DeepSeek only. Multi-provider support is out of scope for v0.8 and not on any roadmap before v1.x. # References {#sec-refs} - [LLM Agents — Cloud APIs](../llm/cloud-apis.md) - [LLM Agents — Prompt Design](../llm/prompt-design.md) - [Changelog v0.8](../dev/changelog.md)