SSyncropel Docs
Inference

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

FunctionOne-linerTypical use
consensusMost agreed-upon answer wins. Trust-weighted vote.Multi-LLM summaries, classification, structured extraction
best_ofHighest-trust responder wins.Named-expert routing, single-winner flows
waterfall_firstFirst acceptable response wins.Cost-minimising cascades (cheap → expensive)
ensemble_weightedWeighted aggregation with a custom signal.Confidence-weighted, recency-weighted, custom-metric fusion
expressionFull 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) ascending

Every 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 — any fulfills preempts any cancels.
  • validation_error — conflict aborts the fold with obligation_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:

GoalExpression
Confidence-weightedresponse.trust * response.body.confidence
Recency-weightedresponse.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 wantReach for
Most-agreed answer across peersconsensus
Most-trusted voicebest_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

On this page