DEV Community

StareBrain
StareBrain

Posted on

Why StareBrain’s AI Backend Writes Nothing to a Database: A Stateless Agent Architecture

Introduction

Most backend services own state. They write to a database, track sessions, remember where you are in a flow.

StareBrain’s backend does none of that. Every request to /next-step is self-contained. The backend reads what the client sends, decides the next action, and returns it. Nothing is written. Nothing is stored. The session ends when the response leaves.

This is a deliberate architectural choice — and it has real consequences for how the agent handles plans, step history, and recovery.

The Request and Response

The full schema for a single agent step:

`class AgentStepRequest(BaseModel):
    task: str
    screen_description: str
    current_package: Optional[str] = None
    steps_so_far: list[str] = []
    step_count: int
    known_facts: str = ""
    plan: list[str] = []
    current_subgoal: int = 0
    screenshot_b64: Optional[str] = None
    capabilities: list[str] = []

class AgentStepResponse(BaseModel):
    reasoning: Optional[str] = None
    action: str
    target_index: Optional[int] = None
    text_to_type: Optional[str] = None
    app_name: Optional[str] = None
    setting_name: Optional[str] = None
    status_text: str
    final_answer: Optional[str] = None
    remember: Optional[RememberFact] = None
    plan: Optional[list[str]] = None
    advance_subgoal: bool = False
    replan: Optional[list[str]] = None
    requires_confirmation: bool = False
    confirmation_reason: Optional[str] = None
    question: Optional[str] = None
    options: Optional[list[str]] = None`
Enter fullscreen mode Exit fullscreen mode

Notice what’s in the request: steps_so_far, plan, current_subgoal, known_facts. The client sends the entire history of the session on every call. The backend never looks anything up — it receives the full context and decides from that alone.

Why Stateless

Three reasons drove this decision:

1. The client already has the state.
The Android app is executing actions, reading screens, tracking what happened. It has the most accurate picture of session state by definition. Duplicating that in a backend database adds a consistency problem: which one is right when they diverge?

2. Resumability is free.
If the backend crashes mid-session, the client resumes by sending the same request again. There’s no session to recover, no checkpoint to restore. The next call picks up exactly where the last confirmed step left off.

3. Scaling is trivial.
A stateless backend has no affinity — any instance handles any request. No sticky sessions, no shared cache, no distributed state to synchronize.

What the Client Owns

Because the backend is stateless, the client is the source of truth for:

`data class AgentSession(
    val task: String,
    val stepsSoFar: MutableList<String>,
    val plan: MutableList<String>,
    val currentSubgoal: Int,
    val knownFacts: String,
    val stepCount: Int
)`
Enter fullscreen mode Exit fullscreen mode

After each response, the client updates its local session and sends the updated state on the next call. The backend never needs to remember the previous step — it’s in the request.

Plan and Replan

The response can carry a plan (initial plan, set on the first step) or a replan (replacement plan mid-session). The client applies whichever arrives:

`fun applyResponse(session: AgentSession, response: AgentStepResponse) {
    response.plan?.let { session.plan.addAll(it) }
    response.replan?.let {
        session.plan.clear()
        session.plan.addAll(it)
    }
    if (response.advance_subgoal) {
        session.currentSubgoal += 1
    }
    response.remember?.let {
        session.knownFacts += "\n${it.key}: ${it.value}"
    }
    session.stepsSoFar.add(response.action)
    session.stepCount += 1
}`
Enter fullscreen mode Exit fullscreen mode

The backend proposes. The client applies and owns. On the next request, the updated plan goes back up in AgentStepRequest.plan.

The Tradeoff

Stateless is not free. The request payload grows with session length — steps_so_far and known_facts accumulate. Long sessions send more data per call.

The mitigation: steps_so_far is summarized rather than appended verbatim after a threshold, and known_facts is structured as key-value pairs so the backend can read the relevant facts without parsing a growing narrative.

The other tradeoff: if the client loses state (app killed, device rebooted mid-task), the session is gone. There’s no server-side recovery. For the current use case — short agentic tasks on a mobile device — this is acceptable. For long-running tasks that span hours, it would need rethinking.

Series so far:

Part 1: Two-axis state model + DENIED_UNRESOLVED
Part 2: Context propagation + confidence thresholds
Part 3: Late confirmation race + terminal state check
Part 4: Provider capability tiers
Part 5: Stale plan protection with FreshCtx
Part 6: Stateless backend architecture ← today

Top comments (0)