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

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 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.
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.
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.
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.
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.
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