Director
A tension-curve pacing engine in the mold of Left 4 Dead's AI Director. An admin authors a pacing policy - a target intensity curve plus an estimator, per-signal weights, and budgets - and each session runs against it: it ingests weighted signals ("damage taken", "death"), keeps a decaying intensity estimate, and hands back budgeted pacing decisions - spawn a wave, trigger the climax, ease off, hold - by poll or push.
When to reach for it: horror/co-op encounter pacing, adaptive difficulty from a struggle index, notification drip control, supply drops timed to sustain tension, onboarding that slows when friction rises.
When not to: plain scheduling is the Job Queue; feature gating and rollouts belong to Remote Config.
Concepts
- Policies are versioned - creating a policy with an existing id appends a new immutable version; reads return the newest. A session pins the version that was current when it started.
- The target curve describes one build-peak-relax cycle: a shape
(
sawtooth_relax,sine,square,triangle,constant),cycle_seconds, floor/peak intensity (0.0-1.0), and atolerance_bandconsidered "on target". The position in the cycle determines the phase:build_up,peak, orrelax. - Signals are weighted - a signal's
typemust appear in the policy'ssignal_weights; each one addsweight x magnitudeto the estimate (negative weights cool things down). Unknown types are rejected. - The estimate decays lazily - between reads it decays exponentially
toward the curve floor with
decay_half_life_seconds; there is no background tick. - Decisions spend budget - every decision read consumes from per-cycle spawn/event budgets and respects peak cooldowns; budgets replenish each new cycle. See Decisions and delivery.
- Deterministic seeds - each session carries a seed (explicit or
derived) and every decision draw comes from
(seed, decision_index), so decision sequences replay exactly.
Author a policy
Policy authoring requires a platform admin token or the
director_designer capability - without one, create and analytics
fail with 403 insufficient_permissions (verified). Policies are written
as a JSON file:
{
"id": "docs4-policy-survival",
"target_curve": {
"shape": "sawtooth_relax", "cycle_seconds": 180,
"peak_intensity": 0.9, "floor_intensity": 0.2, "tolerance_band": 0.1
},
"estimator": { "decay_half_life_seconds": 30, "clamp": [0.0, 1.0] },
"budgets": {
"spawn_budget_per_cycle": 12, "event_budget_per_cycle": 1,
"min_seconds_between_peaks": 45, "ease_off_min_seconds": 20
},
"signal_weights": { "damage_taken": 0.08, "death": 0.4, "objective_complete": -0.15 }
}snug director policy create --file ./survival.json # -> version 1
snug director policy create --file ./survival.json # same id -> version 2
snug director policy versions --policy-id docs4-policy-survival
snug director policy get --policy-id docs4-policy-survival # newest versionOptional: deterministic.per_session_seed (default true) and a
variant_split for A/B tests (see below). Reading a policy needs no role.
Run a session
snug director session start --policy-id docs4-policy-survival \
--session-id docs4-sess-a --seed 42
snug director session get --session-id docs4-sess-aSession ids are auto-generated (sess_<uuid>) when omitted; supplying one
that already exists fails with 409 session_already_exists (verified).
--scope labels the context (per_session, per_cohort, per_user) and
--variant pins an A/B label. The estimate starts at the curve floor.
Over HTTP (not exposed by the CLI), session creation also accepts a
world_context ({"use_virtual_time": true, "world_id": "..."})
advancing cycle time on an Almanac world clock instead of wall time; an
unresolvable world fails with 424 world_clock_unavailable (verified).
Report signals
snug director signal --session-id docs4-sess-a --type damage_taken --magnitude 2.0On a fresh session this returned an estimate of 0.36: floor 0.2 plus
0.08 x 2.0. A type missing from signal_weights is
400 unknown_signal_type (verified). Over HTTP, signals batch (up to
100), and respond: true returns a decision in the same call:
curl -s -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"signals":[{"type":"damage_taken","magnitude":1.5},{"type":"death","magnitude":1.0}],"respond":true}' \
http://localhost:4000/api/v1/director/sessions/docs4-sess-a/signal{
"status": 200,
"msg": "OK",
"data": {
"session_id": "docs4-sess-a",
"accepted": 2,
"estimate": { "intensity": 0.8667528764184694, "as_of": "2026-08-28T06:31:21.771104Z" },
"budgets_remaining": { "spawn": 12, "event": 1 },
"decision": {
"phase": "build_up",
"current_intensity": 0.8667528764184694,
"target_intensity": 0.45334944444444447,
"action": { "type": "hold", "size": 0, "cost": 0, "cooldown_seconds": 0.0 },
...
}
}
}Ask for a decision
snug director decision --session-id docs4-sess-aPolling compares the decayed estimate to the curve and emits spawn_wave,
trigger_climax, ease_off, or hold with a rationale - and it is not a
pure read: it advances the decision index and can spend budget. The rules,
budgets, determinism, and push delivery over PubSub, webhooks, or
WebSocket: Decisions and delivery.
Replay the arc
snug --output json director curve --session-id docs4-sess-aReturns the target_curve, every trajectory point (realized_intensity
vs target_intensity plus phase, one per signal report or decision
read), and the decision timeline - enough to plot or replay the session.
Outcomes and analytics
Record how sessions ended, then read curve-adherence analytics per policy:
snug director outcome --session-id docs4-sess-a --kind completion
snug director policy analytics --policy-id docs4-policy-survival --variant-breakdownAnalytics (admin/designer only) aggregates time_in_band_ratio,
peak_count, mean realized vs target intensity, and outcome tallies
(completion / rage_quit / abandon), overall and per variant. For A/B
tests, give the policy a variant_split with weighted arms and an
optional Remote Config gate_flag_key: sessions started without an
explicit variant get a sticky assignment - verified: three sessions from
one user all landed in the same arm. Variant-less sessions tally under
default.
Limits and configuration
Policy and session ids are capped at 128 characters, signal batches at 100
per request, and the retained trajectory/decision/outcome series at 10,000
entries each - all tunable via the DIRECTOR_* variables in the
Director CONFIG reference.
Reference
- Director API - every endpoint, callable
- Decisions and delivery - decision rules, budgets, determinism, push channels
- Related: Almanac for world clocks, Remote Config for the A/B gate flag, PubSub and Webhooks for decision push delivery