> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usecompassai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The deterministic loop

> How Compass evaluates positions without an LLM — event-driven, per-account, fully reproducible.

> Compass's hot path is a pure-function Rust evaluator. It runs per account, only when something changes, and produces a structured `EvaluatorThought` that can be replayed bit-for-bit. The LLM is not in this loop.

This is the page where the central design claim of Compass — *"LLM out of
the hot loop"* — becomes concrete. Below is what actually runs every time the
system makes a routing decision.

## Why a deterministic loop at all

A naive AI yield agent calls the LLM every time it considers rebalancing.
That has three costs:

* **Money.** LLM inference is paid per token, per account, per tick. At any
  meaningful scale, this eats yield.
* **Reproducibility.** LLM outputs vary between runs. A decision can't be
  re-audited by re-running it; you have to trust the after-the-fact
  explanation.
* **Auditability.** A "why did the agent do X" answer from the LLM is a
  rationalization, not a trace. There's no way to know whether the same input
  would produce the same output tomorrow.

Compass solves this by making the routing decision a **pure function**. Given
the same inputs (user rules, current positions, current yields), it always
produces the same output. The LLM never enters this path. It enters only
when the user opens chat — under 5% of all evaluations on a typical account.

## Two parts: scheduler + evaluator

The loop is split into a global **scheduler** and a per-account **evaluator**.
They have different jobs and different properties.

| Component     | Scope       | Role                                                         |
| :------------ | :---------- | :----------------------------------------------------------- |
| **Scheduler** | Global      | Decides *when* and *for which account* to run the evaluator. |
| **Evaluator** | Per account | Decides *what to do* for one account, given its state.       |

This split is what makes event-driven execution possible. The scheduler
watches external signals (yield-source updates, price feeds, on-chain events).
When something material changes, it enqueues an evaluation for every account
that could be affected. The evaluator itself doesn't poll — it only runs when
the scheduler wakes it.

### Scheduler

The scheduler is event-driven, not cadence-driven. There is no fixed "every
N seconds." Instead, the scheduler triggers an account's evaluator when:

* A whitelisted yield source on a whitelisted chain publishes a new rate.
* A user's account state changes (deposit, withdrawal, rule update).
* An in-flight cross-chain intent settles or fails.
* A retry condition from a previous tick is met (e.g. indexer lag recovery).

If nothing relevant changes, no tick runs. Idle accounts cost nothing.

### Evaluator

The evaluator is a pure Rust function. Given an account's state and the
current world state, it produces an `EvaluatorThought`. It has no side
effects of its own — execution and audit-writing happen outside it, against
the thought it returned.

Because the evaluator is pure, every tick is reproducible. Re-running an
evaluator with the same recorded inputs produces the same recorded output.

## The five steps of an EvaluatorThought

Every tick produces a structured `EvaluatorThought` with five fields, one per
step. The thought is what gets written to the audit trail — including the
ticks that decide to do nothing.

<img src="https://mintcdn.com/compassai/Qq7UDvjWweIau4hP/images/deterministic-loop-diagram.svg?fit=max&auto=format&n=Qq7UDvjWweIau4hP&q=85&s=0fd1355e7337dc2269578dceb67e399c" alt="deterministic loop diagram" className="rounded-lg" width="1500" height="800" data-path="images/deterministic-loop-diagram.svg" />

Both branches end at the audit trail. A no-op tick is just as much a record
as an executed one — that's how the system can answer *"why didn't the agent
do anything?"* with a deterministic trace.

### 1. `load_state`

Snapshot the account at this moment: current positions across all chains,
USDC balances, the user's risk band, protocol whitelist, chain whitelist, and
caps. Also snapshot any in-flight intents from previous ticks.

This is the "what's true about this account right now" record.

### 2. `fetch_yields`

Read current rates for every whitelisted (protocol, chain) pair. Yields come
from venue-specific adapters — each adapter is small, audited code that
returns a normalized rate.

This is the "what does the world look like right now" record.

### 3. `propose`

A deterministic function over the previous two steps: given the current
positions and current rates, what is the best route? The answer can be:

* **A new route** — e.g. exit a lending position on one L2 and open one on
  another where rates are higher.
* **Stay put** — current allocation is already optimal under the rules.
* **No valid route** — nothing satisfies the rules right now.

`propose` does not call the policy engine yet. It just produces a candidate.

### 4. `check_policy`

Run the candidate through the [policy engine](/architecture/policy-engine).
Every rule attached to the account — whitelists, risk band, per-route cap,
daily cap — is checked. The output is either:

* **Approved** — the candidate becomes a signed call.
* **Rejected** — with a structured reason field naming the rule that failed.

A rejection is a normal outcome, not an error. It just means this tick
produces no on-chain action.

### 5. `emit`

Two possible emissions:

* **A session-key-signed call** if `check_policy` approved.
* **A no-op record** if it didn't.

Either way, the full `EvaluatorThought` — all five fields — is written to the
audit trail. See [Audit trail](/architecture/audit-trail).

## What's in the loop, what's not

The loop is what runs every tick. The LLM is what runs when the user opens
chat. These are different code paths.

| In the deterministic loop | Not in the loop              |
| :------------------------ | :--------------------------- |
| Scheduler event handling  | LLM inference                |
| Yield-source adapters     | Natural-language parsing     |
| `propose` function        | Plan generation from prompts |
| Policy checks             | Conversational explanations  |
| Session key signing       | User-facing chat responses   |

The boundary is strict. A plan that came from the chat agent still has to
pass through `check_policy` and `emit` before anything moves. A tick triggered
by a yield change never calls the LLM at all. See
[Chat agent](/architecture/chat-agent) for how the LLM hands off to the loop.

## Reproducibility in practice

Because the evaluator is pure and the inputs are recorded, any past decision
can be replayed:

1. Pull the `EvaluatorThought` from the audit trail.
2. Feed its `load_state` and `fetch_yields` snapshots back into the same
   evaluator binary version.
3. The output matches bit-for-bit.

This is the property that lets users (and the team) answer *"why did the
agent do that?"* with a deterministic trace, not a post-hoc explanation.

## Retries and indexer lag

Cross-chain settlement via Circle Gateway uses `BurnIntent` signed messages
rather than broadcast transactions. This matters for the loop:

* If an intent settles but the indexer hasn't caught up, the next tick's
  `load_state` may still show the old position. The scheduler holds a
  pending-intent guard so duplicate proposals don't fire.
* If an intent fails or expires, the scheduler enqueues a retry with the
  same parameters. Because the underlying signature is reusable, no
  re-signing or re-prompting is needed.
* Compass uses a 60-second window for intent retries before falling back to
  a paused state and surfacing the issue in the dashboard.

See [Four-step pipeline](/cross-chain/four-step-pipeline).

## Next steps

<CardGroup cols={2}>
  <Card title="Policy engine" icon="shield-check" href="/architecture/policy-engine">
    The gate that every candidate plan passes through.
  </Card>

  <Card title="Audit trail" icon="scroll" href="/architecture/audit-trail">
    Every EvaluatorThought, including no-ops, recorded and replayable.
  </Card>

  <Card title="Chat agent" icon="message-square" href="/architecture/chat-agent">
    The LLM path, and where it hands off to the loop.
  </Card>

  <Card title="System overview" icon="layers" href="/architecture/system-overview">
    Back to the full three-layer picture.
  </Card>
</CardGroup>
