---
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/<cell>/`.
- **(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)
