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

# AI Agent

> Synapse — a two-speed nervous system that decides when to submit, how much to tip, and what to do when a bundle fails.

## Overview

The AI agent — **Synapse** — is a two-speed nervous system. It connects to the sidecar via
Server-Sent Events and wakes on three triggers. Each wake first passes through a deterministic gate
(`decideCycle`) that routes the cycle to one of two tiers: a **Reflex** fast-path that submits with
no model call, or a **Cortex** ReAct loop (the LLM) for anything that needs judgment.

<CardGroup cols={3}>
  <Card title="tx_enqueued" icon="inbox">
    A new user transaction arrived in the queue.
  </Card>

  <Card title="bundle_settled(failed)" icon="triangle-exclamation">
    A bundle terminated with a failure class that requires a retry decision.
  </Card>

  <Card title="30s watchdog" icon="clock">
    Safety net for SSE reconnect gaps — agent checks for any missed work.
  </Card>
</CardGroup>

***

## Two-tier router (Reflex / Cortex)

Most submissions need no judgment: a stable floor with a fresh transaction has one correct move —
bid the known clearing floor — and paying for LLM inference on that path is latency for nothing. So
every wake first hits a gate that routes it to the cheapest tier that can decide it correctly.

<CardGroup cols={2}>
  <Card title="Reflex — System 1" icon="bolt">
    Deterministic fast-path, **no model call (\~0 ms)**. Fires only when every trivial-case check
    holds (fresh tx, stable floor, Jito leader, no unrecovered failure, oracle fresh) and submits at
    the recent clearing floor.
  </Card>

  <Card title="Cortex — System 2" icon="brain">
    The LLM ReAct loop (**\~4.2 s decision**). Any ambiguity escalates here — a retry, a moving floor,
    a near-TTL transaction, a cold start, an unrecovered failure.
  </Card>
</CardGroup>

A misjudged Reflex bid is cheap by construction: it fails at the auction, pays **no tip** (`Invalid`),
and the resulting `bundle_settled(failed)` routes its retry to the Cortex. The cost of an optimistic
fast-path is bounded by one free rejection — never a lamport.

The tier is a **runtime toggle**, not a fork. `cortex` is the default (a bare deployment reasons on
every cycle); flip to `reflex` live with no restart:

```ts theme={null}
await sinker.setMode('reflex');   // fast-path
await sinker.setMode('cortex');   // full reasoning
const { decision_mode } = await sinker.getMode();
```

Or seed the startup tier with `AGENT_DECISION_MODE=reflex`. Each landing records which tier produced
it on the lifecycle entry (`decision_mode`, and `escalated` in reflex mode).

***

## The Cortex cycle (System 2)

When the gate routes a wake to the Cortex, this is the loop that runs (the Reflex
fast-path skips all of it). Data gathering is parallelized out of the LLM hot path. Before the model is
invoked, the outer loop fetches `getState`, `getTip`, `getLifecycle`, and
`getPendingTxs` in a single `Promise.all` and injects the results directly
into the cycle prompt. The agent reads pre-loaded context and goes straight
to the decision — typically two tool calls total.

```
trigger fires
    ↓
preflight: queue depth > 0? inflight guard clear?
    ↓
Promise.all([getState, getTip, getLifecycle, getPendingTxs])  ← parallel, ~50ms
    ↓
inject into prompt as PRE-LOADED CONTEXT block
    ↓
generateText (LLM call with tool set)
    ↓
  [THINK] read pre-loaded slot, tip percentiles, queue items, history
  [THINK] select percentile, form reason string
  [CALL]  submit({ tip_lamports, reason, tx_ids })   ← step 1
  [CALL]  writeTrace({ id, trace })                  ← step 2
    ↓
terminal: submitted / held / error
```

This cuts the typical Cortex cycle from 6+ sequential LLM steps to 2. The
remaining inference still dominates — a measured **\~4.2 s** for the Cortex
decision (the Reflex fast-path skips it entirely, \~0 ms). The agent still reads
the same data; it just doesn't burn a round-trip per field to fetch it.

***

## Tip selection

The agent reads five percentiles on the live Jito tip oracle (`p25` … `p99`) and **opens at the
lowest tip recent history shows landing** — often the observed minimum, not a high percentile —
because a `fee_too_low` rejection pays no tip and is free information. It escalates *only* on that
free failure, climbing one tier per consecutive `fee_too_low`:

| Consecutive `fee_too_low` failures | Tip selected                                  |
| ---------------------------------- | --------------------------------------------- |
| 0                                  | lowest recent clearing tip (probe from below) |
| 1                                  | `p75`                                         |
| 2                                  | `p95`                                         |
| ≥ 3                                | `p99`                                         |

This is not hardcoded logic — the agent reads the lifecycle history and reaches this conclusion through its reasoning trace. The trace is written to the lifecycle entry so every decision is auditable.

**Example reasoning trace from a real cycle:**

> *"fee\_too\_low at p75=5000. Pattern: 2 consecutive fee\_too\_low. Oracle p99=1,000,000. Tx age=34/50 slots near TTL. Escalating to p95=100,000. If this fails: p99."*

***

## The clean boundary

The architectural invariant that makes the system independently upgradeable:

<CardGroup cols={2}>
  <Card title="Rust never calls an LLM" icon="rust">
    All execution — signing, serialization, Jito submission — happens in the
    Rust sidecar. The agent is an external observer that issues commands.
  </Card>

  <Card title="Agent never touches a keypair" icon="key">
    The agent calls `POST /internal/submit` with a tip amount and list of
    `tx_ids`. The sidecar does the rest.
  </Card>
</CardGroup>

This means you can swap the AI provider — GPT-4o, Claude, Grok, or a local Ollama model — without any change to the execution layer.

***

## Configuring the model

The agent model is configured via environment variables in the agent process:

```bash theme={null}
# Use Anthropic Claude
ANTHROPIC_API_KEY=sk-ant-...
MODEL_PROVIDER=anthropic
MODEL_NAME=claude-sonnet-4-5

# Use OpenAI
OPENAI_API_KEY=sk-...
MODEL_PROVIDER=openai
MODEL_NAME=gpt-4o

# Use a local Ollama model (no API key needed)
MODEL_PROVIDER=ollama
MODEL_NAME=llama3.1:8b
OLLAMA_BASE_URL=http://localhost:11434
```

The startup **decision tier** is set by `AGENT_DECISION_MODE` (`cortex` is the default — a bare
deployment reasons on every cycle; set `reflex` for the fast-path). It's runtime-flippable via
`sinker.setMode()` or `POST /internal/mode` — no restart.

***

## Retry race condition

A non-obvious concurrency hazard: the `bundle_settled(failed)` SSE event fires while the original submit cycle is still executing. The agent handles this with a `pendingRetry` field:

```ts theme={null}
if (state.runningCycle && retryContext) {
  state.pendingRetry = { ...retryContext, slot };
  return; // don't start a new cycle yet
}

// ... cycle runs ...

} finally {
  state.runningCycle = false;
  if (state.pendingRetry) {
    const pending = state.pendingRetry;
    state.pendingRetry = null;
    await maybeRunCycle(pending.slot, state, pending); // drain
  }
}
```

This guarantees the retry cycle fires immediately after the current cycle's `finally` block with zero additional delay.
