Fold Functions
The five fold functions that combine multi-responder answers into one — consensus, best_of, waterfall_first, ensemble_weighted, and the expression escape hatch — each deterministic and pure over a canonical record order.
Overview
Fold is the pure function at the heart of inference. Given a list of responses on a thread and a canonical order, it returns a single answer plus provenance. It has no I/O. It has no retries. It has no state. Calling it twice with the same inputs returns exactly the same output.
That purity is why queries are replayable — re-ingest the thread, re-fold, same answer. It's also why orchestration (the policy layer) has to stay separate; orchestration is where you put retries, escalation, and multi-step state.
The library ships five fold functions. This guide covers each, when to reach for it, the determinism contract, and a worked example.
The five functions
| Function | One-liner | Typical use |
|---|---|---|
consensus | Most agreed-upon answer wins. Trust-weighted vote. | Multi-LLM summaries, classification, structured extraction |
best_of | Highest-trust responder wins. | Named-expert routing, single-winner flows |
waterfall_first | First acceptable response wins. | Cost-minimising cascades (cheap → expensive) |
ensemble_weighted | Weighted aggregation with a custom signal. | Confidence-weighted, recency-weighted, custom-metric fusion |
expression | Full CEL escape hatch over the response list. | Domain-specific aggregation (median, regex, IQR filter…) |
Canonical order — the determinism contract
Every fold function starts with the same line:
canonical_order(responses) = sort by (clock, id) ascendingEvery implementation calls canonical_order before it calls its own combine. This is load-bearing. The pre-implementation proof-of-concept ran 20 random permutations of the same response list through consensus and ensemble_weighted and confirmed identical output for every permutation.
You don't need to implement this yourself — the FoldFunction trait enforces it. But it's useful to know what it means in practice: two responses that tie in canonical order tie for the rest of the pipeline (tiebreak rules kick in there), and inserting a new response between two existing ones only re-orders if the new response has a clock between them.
Fold output
Every fold returns the same envelope:
{
"answer": <the folded value>,
"chosen_response_id": "<record id or null>",
"provenance": ["<id1>", "<id2>", "..."],
"tally": { "<canonical-json-of-answer>": <weight>, "...": <weight> }
}chosen_response_id is a reference (not an embedded copy). The orchestration layer reads it to pull the winner's modifiers (body.fulfills, body.cancels, body.awaits) without breaking fold purity. For fold functions where no single winner exists (expression), it's null.
provenance is every response that contributed. Use this to audit "who weighed in on this KNOW".
tally is present for consensus and ensemble_weighted. It lets you see how far the second-place answer was from first.
Trust floor
Fold shares a trust floor with the relevance scorer — default 0.05. A responder's trust multiplier can never drop below that for weighting purposes. Without the floor, a cold-start responder (zero trust, no history) would contribute nothing to any consensus vote, which makes it impossible to ever accumulate history.
If every contributor is below the floor, the KNOW carries body.fold.cold_start_warning: true. You can route on that in downstream rules.
The floor is configurable via a syncropel.config.fold.v1 record on th_engine_config:
spl config add-fold-rule \
--id default \
--priority 0 \
--status active \
--expression 'true'(Authoring fold rules is covered in the task-management guide; here we just note that the floor lives alongside them.)
Min quorum
Fold won't run if fewer than min_quorum non-error responses came back. Default is 1. If quorum isn't met, the executor emits infer.error.v1 with code: "quorum_not_met" instead of a KNOW.
Raise quorum when you want a minimum of 2 or 3 answers before you believe anything:
"fold": { "function": "consensus", "min_quorum": 2 }Modifier preservation
Some responses carry modifiers on body — fulfills, cancels, awaits. The fold doesn't touch these; it just picks a winner. The executor then looks at chosen_response_id, loads that response's modifiers, and attaches them to the committed KNOW.
The obligation_resolution field in side_effects decides what happens when multiple responses carry conflicting modifiers (e.g., two winners, one fulfills and one cancels):
last_writer_wins(default) — the most-recent response's modifiers apply.fulfills_wins— anyfulfillspreempts anycancels.validation_error— conflict aborts the fold withobligation_conflict.
Function: consensus
Trust-weighted plurality vote. Groups responses by canonical-JSON of the body.answer field, sums the weights per group, picks the highest-sum group. Tiebreak: lexicographic by default (or whatever tie_break is set to).
Weight expression (default): trust * recency * pattern_confidence. All three factors are read from the candidate's metadata at dispatch time. You can override per query:
"fold": { "function": "consensus", "weight_expression": "response.trust" }The floor applies after the full expression evaluates.
When to use: classification, structured extraction, tagging. Anything where responses are naturally comparable as JSON trees and you want the substrate's collective best-guess.
Example — three LLMs agreeing on a sentiment label:
{
"fold": { "function": "consensus", "min_quorum": 2 }
}Response bodies:
- A:
{ "sentiment": "positive", "confidence": 0.9 }(trust 0.8) - B:
{ "sentiment": "positive", "confidence": 0.8 }(trust 0.7) - C:
{ "sentiment": "neutral", "confidence": 0.6 }(trust 0.6)
A and B are keyed identically under canonical-JSON (assuming the bodies are sorted). Weight sum for positive = 0.8 + 0.7 = 1.5. Weight sum for neutral = 0.6. Winner: positive. chosen_response_id is A (higher trust within the group).
Function: best_of
Highest-trust responder wins. Full winner body preserved. Tiebreak uses (clock, id) ascending, then tie_break rule.
When to use: one expert is right about this, you just don't know which one at dispatch time. Code review, medical suggestion, legal reasoning — domains where you'd rather hear the most-trusted voice than a mediocre consensus.
Example — you have two code reviewers of known-different trust:
{
"fold": { "function": "best_of", "tie_break": "highest_trust" }
}If Alice has trust 0.95 and Bob has 0.82, Alice's response wins. The full body — including her confidence and rationale — survives intact into the KNOW.
Function: waterfall_first
First response (in canonical order) that passes accept_fn wins. accept_fn defaults to "non-null answer" but can be a CEL accept_expression.
When to use: you've deliberately ordered responders cheap-to-expensive (or fast-to-slow). You'd rather take the first one that's good enough than wait for the best one.
This is not an orchestration pattern — the orchestration waterfall dispatches sequentially. waterfall_first as a fold takes a batch of responses that already arrived and picks the first-by-order that passes acceptance.
Example — pattern first, then LLM:
Responders: one pattern responder at clock 100, one llm responder at clock 101. If the pattern answer passes accept_expression: "response.body.confidence >= 0.8", fold picks the pattern. Otherwise it moves to the LLM.
{
"fold": {
"function": "waterfall_first",
"expression": "response.body.confidence >= 0.8"
}
}(When you want true sequential dispatch with early termination, use the waterfall orchestration pattern instead — see the orchestration guide.)
Function: ensemble_weighted
Like consensus, but the weight is a CEL expression you write. The result is a weighted answer, not necessarily one of the input responses — the executor picks the highest-weight answer group but the body is the one from the highest-trust responder in that group.
Default weight expression: response.trust. Common overrides:
| Goal | Expression |
|---|---|
| Confidence-weighted | response.trust * response.body.confidence |
| Recency-weighted | response.trust * (1.0 - (now() - response.clock) / 86400) |
| Kind-biased (favour patterns) | response.trust * (response.kind == "pattern" ? 1.5 : 1.0) |
Negative weights clamp to 0 — you can't downweight into oblivion.
When to use: you trust responders differently than their raw trust score suggests, and the difference is a deterministic function of per-response metadata.
Function: expression
The full CEL escape. fold.expression is evaluated with bindings responses (the canonical-ordered list) and tally (always empty for this function), and whatever it returns is the answer. chosen_response_id is always null.
When to use: aggregation logic doesn't fit the first four. Median over numeric answers, inter-quartile-range filtering, regex extraction, custom multi-body merge.
Example — median numeric answer:
{
"fold": {
"function": "expression",
"expression": "responses.map(r, r.body.value).sort()[(responses.size() - 1) / 2]"
}
}Example — concatenate all non-empty string answers:
{
"fold": {
"function": "expression",
"expression": "responses.filter(r, r.body.text != '').map(r, r.body.text).join('\\n---\\n')"
}
}Because expression doesn't produce a chosen_response_id, you can't preserve modifiers through it. If your responders carry fulfills/cancels/awaits and you need to keep them, prefer best_of or consensus.
Choosing a function
| You want | Reach for |
|---|---|
| Most-agreed answer across peers | consensus |
| Most-trusted voice | best_of |
| First acceptable answer (cost cascade) | waterfall_first |
| Custom weight on trust (confidence, recency) | ensemble_weighted |
| Custom aggregation logic (median, regex, merge) | expression |
When in doubt, start with consensus — it's the default, it's the most forgiving, and the tally tells you exactly how close second place was.
See also
- Query Anatomy — Fold — field reference.
- Orchestration Patterns — when you need policy around fold.
- CEL Expressions — syntax for
weight_expressionandexpression. - Trust — how trust feeds
weight_expression.
Query Anatomy
The full infer.query.v1 body shape — every field, every type, every default — with worked examples for single-LLM, consensus ensemble, and verify patterns.
Orchestration Patterns
The six canonical multi-step patterns — single_shot, verify, waterfall, retry_on_low_confidence, escalate, ensemble_with_audit — and how to compose them via nested queries when nothing fits.