Agent Experience
The eight agent-first rules every surface in OPB Brain must satisfy
The product bet: an agent with zero prior context should be able to do useful work here on its
first call. These rules are that bet made concrete. They are review gates, not suggestions. A PR
that violates one gets changes_requested.
What is running today
The rules are the design contract. The surfaces they govern are not built yet: there is no MCP
server, no REST API under /api/v1, no GraphQL endpoint, and no domain tables behind them. The
rules already apply to any PR that adds one.
The canonical source is spec/03-agent-experience.md in this repository. This page restates it.
1. Discoverable
- Every surface answers "what can I do here?" without documentation:
GET /api/v1returns a capability manifest, MCPwhoamireturns the agent's own permissions, and every entity response carrieslegal_transitionsandnext_actions. - The specs are served as MCP resources and at
/spec/*.md. The rules the server enforces are the rules the agent can read. - Ambiguity is a bug. If an agent has to guess an enum value, ship the enum in the schema.
Why. An agent that has to be told how the system works has already cost a human the time the system was meant to save.
2. Self-describing errors
RFC 9457 application/problem+json, always:
{
"type": "https://opb.brain/errors/invalid_transition",
"title": "Illegal status transition",
"status": 422,
"code": "invalid_transition",
"detail": "Task tsk_01J is 'in_progress'; 'done' is not reachable from there.",
"hint": "Open a PR and move to 'in_review' first.",
"legal_transitions": ["in_review", "blocked", "cancelled"],
"retryable": false,
"docs_url": "https://opb.brain/spec/domain-model#task-state-machine"
}Stable machine code. retryable on every error. hint says what to do next. Never a bare 500
with a stack trace. Never a 200 carrying an error in the body.
Why. An agent must never have to string-match an English error message to decide what happens next. The error is teaching material: the reason, the legal alternatives, and the exact follow-up call.
3. Safe to retry
- Idempotency keys required on every mutating call. Replays return the original result.
- Optimistic concurrency (
versionplusIf-Match) on every update. - Atomic claims. Leases with TTL. Expired work returns to the queue automatically.
- All writes inside a transaction that also writes the Event. Partial state is never observable.
Why. Two agents editing the same task must not silently clobber each other, and a crashed runner must not leave work locked.
4. Context-efficient
Context window is the scarcest resource in the system. Treat every byte as a cost.
- Default responses are the compact projection. Full objects are opt-in (
?view=full). ?fields=selection and?since=deltas on every list.- No response returns an unbounded array. Everything paginates, default limit 25, max 100.
- Prose fields have documented length budgets. Long bodies are returned as artifact references, not inlined.
- Markdown views (
.md) exist so agents read prose instead of parsing HTML.
Why. Every unused field in a response is paid for in tokens by every agent that reads it.
5. Deterministic
- Same input, same output. Timestamps and ids come from the server, never from client clocks.
- Sortable ULIDs mean "the newest 10 events" is unambiguous.
- Enums, not free strings, for anything the state machine reads.
- Server-side validation is the only validation that counts.
Why. A non-deterministic surface cannot be tested, and an agent cannot verify its own work against one.
6. Provenance and honesty
This is the load-bearing rule. The others make the system usable; this one makes it trustworthy.
- Every row records who wrote it and whether that actor was a human, an agent, or the system.
- Events are append-only and never rewritten. The timeline is the truth.
- The write-back contract requires reporting failure as failure. Any feature that makes it easier to report success than to report a blocker is a design defect.
Why. The whole product depends on remote runners reporting honestly. A failed run reported as failed is worth more than a green lie. Once a run can lie, no record in the system can be trusted, and the database stops being a source of truth.
The same rule governs this documentation. A page never describes state or capability the system does not have. Where a page documents an unbuilt design, it says so in plain language rather than writing in a present tense that implies the thing runs today.
7. Least privilege
- Scoped tokens per agent and per machine. A token can be revoked without touching others.
toolAllowlistper agent, enforced server-side. The MCP server refuses tools the agent lacks, it does not merely hide them.- Destructive and outward-facing actions require an
Approval, and the approval is recorded. - No secret values in the database. Only
secretRefpointers. An agent that needs a credential gets a reference and resolves it on its own machine. - Rate limits per token,
429withRetry-After, and the remaining budget on every response.
Why. Hiding a tool is not access control. An agent is a credential holder on a machine nobody is watching, so the server has to be the thing that says no.
8. Resumable
An agent must be able to crash, restart, and continue with no human help:
whoami → open leases → tasks_get → events_stream?since=<lastEventId> → back to workAny design that requires in-memory state to continue a task is rejected.
Why. Sessions end. The database, not the context window, is the source of truth, so a restart costs a few calls rather than a task.
Review checklist
Before a PR touching any agent-facing surface reaches ready_to_merge:
- New capability is reachable from every door it belongs on (MCP tool, REST route, GraphQL field) and appears in the capability manifest, the OpenAPI document, and the committed SDL
- Input schema uses enums; every field, argument and enum value carries a description
- Mutations accept
Idempotency-Key/idempotencyKeyandIf-Match/expectedVersion - Errors are problem+json (REST) or typed
userErrors(GraphQL) withcode,hint,retryable, never a message an agent must string-match - Lists paginate with cursors (REST
?cursor=, GraphQLfirst/after) and support?fields=/ field selection - GraphQL resolvers use DataLoader; no N+1, and the query stays inside the complexity budget
- State transitions validated server-side and covered by tests, including the illegal ones
- Every mutation writes an Event in the same transaction
- AuthZ tested per scope, including the negative case
- New UI elements carry stable
data-testid - Specs in
spec/updated in the same PR