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.
The file is JSON with comments (JSONC).
{
"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)!
}
}
]
}
]
- A server-local (unprefixed) tool name. Completed calls to it fire the rule.
- A selector that must resolve to an array. Each element binds
$item. $args.*reads the trigger call's arguments.$item.*reads the currentforEachelement — and requiresforEachon 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.