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`
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
)`
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
}`
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)