Skip to content

Configuration

Speculate needs no configuration — the zero-config default is annotated mode with a learner that picks up your own traffic. A config file adds per-server modes, allow/denylists, TTLs, budgets, and declarative prediction rules.

speculate init      # writes a starter config

The file is JSON with comments (JSONC).

speculate.config.json
{
  "mode": "strict",
  "maxPredictionsPerTrigger": 3,
  "log": "stderr",
  "servers": {
    "github": {
      "command": "npx",
      "args": ["tsx", "mock/mock-github.ts"],
      "env": { "SPECULATE_MOCK_LATENCY_MS": "400" },
      "speculation": {
        "defaultTtlMs": 30000,
        "maxPerMinute": 30,
        "maxConcurrent": 2,
        "adaptiveAdmission": true,
        "minExpectedSavedMs": 15
      }
    }
  }
}

Top level

Field Type Default Meaning
mode strict | annotated | off strict Eligibility policy for an explicit config; managed on and wrap default to annotated. See Safety.
maxPredictionsPerTrigger number 3 Per-trigger prediction cap
log stderr | off stderr Decision log destination (JSONL)
servers object Upstream servers, keyed by name
persistence object enabled { enabled?, path?, retentionDays?, maxBytes? } — learned transition model and aggregate evidence

Persistence never holds results

What survives a session is tool names, argument source descriptors, filtered constants, and aggregate evidence. There is no raw call/result archive.

Learning is written to disk automatically, outside the repository by default: $XDG_STATE_HOME/speculate when set to an absolute path, %LOCALAPPDATA%/speculate on Windows, or ~/.local/state/speculate otherwise. State is scoped by workspace, upstream, and account identity.

persistence.retentionDays defaults to 30; persistence.maxBytes defaults to 8388608 (8 MiB per scoped state). Expired evidence is pruned and weaker/older entries are trimmed to fit. Oversized or unreadable state falls back to cold learning. Learner retention uses observation timestamps. Aggregate feedback and latency models use decay-reference timestamps, refreshed when exported; the window is not an absolute maximum age for their underlying observations. The byte cap applies to the entire learned state. Aggregate usage records are separate and can be compacted with speculate stats --compact or removed with speculate memory clear --all.

Recent raw call payloads are retained only within the running session. The added earlier-call history is bounded to eight calls and an estimated 1 MiB per server, separate from the current call and speculation cache. Source descriptors can reuse a value from an earlier call even when other calls intervene. Oversized payloads are skipped for historical reuse without changing real calls. See secret handling and memory commands.

Per-server

Transport

Field Type Meaning
command string Executable to spawn
args string[] Arguments
env object Extra environment variables
Field Type Meaning
url string Upstream endpoint
headers object Extra request headers — how an authenticated remote is reached

${VAR} placeholders in header values are resolved from the environment at load, so the token need not live in the config file.

A hand-set Authorization header and speculate auth are mutually exclusive

Enforced as such. The transport spreads configured headers after the OAuth bearer, so a stale hand-set header would silently shadow a valid token and present as an inexplicable 401.

Eligibility

Field Type Meaning
allowTools string[] Tools the operator vouches for — the whole strict-mode allowlist
denyTools string[] Never speculate on these, regardless of mode
rules rule[] Declarative prediction rules (below)

profile is accepted and ignored

Vetted per-server profiles were removed. The field stays valid so an older config still loads — Speculate warns and drops it rather than failing a working setup over a dead line. See why they went.

speculation

Field Type Meaning
defaultTtlMs number Cache TTL for this server's entries
ttlMsByTool object Per-tool TTL overrides; 0 disables
longHorizonTtlFactor number in (0,1] TTL multiplier for startup ("standing") predictions; learned transitions always target the next call
maxPerMinute number Speculative-call rate budget
maxConcurrent number Speculative-call concurrency budget
adaptiveAdmission boolean Rank by confidence × learned upstream latency and suppress low-utility calls (default true)
minExpectedSavedMs number Minimum expected latency saving to issue a prediction (default 15)

Adaptive admission is most useful for mixed servers: a 400 ms network read can justify a moderate-confidence prefetch while a 5 ms local metadata call often cannot. Set adaptiveAdmission: false to retain confidence-only admission, or raise minExpectedSavedMs when upstream quota matters more than latency.

Read before lowering longHorizonTtlFactor

It defaults to 1 — no shortening — on measured grounds. If you lower it, watch expired and perRule['opener:*'].wasted. The measurement is in §13.19.

Prediction rules

Rules provide explicit predictions from the first matching call. The learner normally builds evidence from repeated transitions, although compatible tool schemas can also support a prediction before a transition has been observed.

"rules": [
  {
    "trigger": "list_pull_requests",     // (1)!
    "predict": [
      {
        "tool": "get_pull_request",
        "forEach": "$parsed",            // (2)!
        "limit": 3,
        "confidence": 0.6,
        "args": {
          "owner": "$args.owner",        // (3)!
          "repo": "$args.repo",
          "pullNumber": "$item.number"   // (4)!
        }
      }
    ]
  }
]
  1. A server-local (unprefixed) tool name. Completed calls to it fire the rule.
  2. A selector that must resolve to an array. Each element binds $item.
  3. $args.* reads the trigger call's arguments.
  4. $item.* reads the current forEach element — and requires forEach on the same predict entry.
Field Required Meaning
trigger yes Tool name whose completed calls fire the rule
predict[].tool yes Tool to prefetch
predict[].args yes Argument template; string values use the selector language
predict[].confidence no Static prior, clamped into [0,1]
predict[].forEach no Selector resolving to an array; binds $item per element
predict[].limit no Max forEach fan-out

Selectors

Selector Resolves to
$args.<path> A value from the trigger call's arguments
$parsed.<path> A value from the trigger call's parsed result
$item.<path> A value from the current forEach element

Selectors fail closed

A selector that doesn't resolve cancels that prediction rather than guessing. $parsed being unavailable — the server answered in non-JSON text — means the same thing.

Non-JSON servers can be learned but not ruled

Rules copy values out of a parsed result; they do not compute them. A server that answers in plain text keeps memorisation across repeats and nothing else.

The starter file is speculate.config.example.json.