Skip to main content
An agent’s code keeps a small working state between turns: the step of a form, the offer it showed, the cart it is building, which sub-agent is on which part. Kept by the agent alone, that state is lost when the process restarts and invisible to your company’s governance. Kept as an opaque blob in a memory, it can be neither masked nor erased with proof. Working memory is that state with a declared schema: Niadra does not interpret what it means, but knows its fields, so it can mask, retain, export and erase each one. It differs from agent memory, the working notes about the trade, which never speak of a customer and may enter the prompt. Working memory speaks of a conversation, a task, an object or a customer, and never enters a prompt.

Turning it on

Working memory is the space’s agent_state feature, off by default and turned on in the features document by the security role. The agent’s key needs the agent_state scope. The schema is a type with ownership: agent in the object-types document (Object types): the type named like the agent or, when the registry declares exactly one type of that ownership, that one; without one, no field is declared, and every write is refused with not_declared_field.

The schema and the scope

The type declares its fields like any type, pii, sensitivity and retention included, and an agent_state section:
A state is named by its scope (kind and id: the conversation’s or the task’s id, the object as type:namespace:id, or the canonical form of one of the customer’s handles) and by the agent that writes it. The scope travels in the body, never in a URL, because a conversation id can look like a phone number, and Niadra keeps it only as a keyed hash. The state is written by code, never by a model: Niadra does not offer it as a model’s tool in any protocol, it never enters a context or an extraction, and the Console shows its size, version and dates, never its content (the profile’s working memory tab, and GET /v1/profiles/{profile_id}/agent-state).

Writing

PUT /v1/agent-state takes the scope, the agent, the mode, if_version and body, and subject when the scope does not name the customer, so the erasure and the subject’s data package find the state.
  • Compare-and-swap (cas): body is the whole new state, and the write applies only when the stored version is if_version (0 when the state must not exist yet); otherwise the answer is 412 agent_state_conflict and nothing changes. A field left out of body is gone.
  • Merge by key (merge_by_key): each top-level field of body replaces that field whole, with no deeper merge; a field written as exactly {"$delete": true} is removed; the fields body does not name stay. Sub-agents writing different fields in parallel never lose each other’s writes, and a removed field never comes back unless someone writes it again. With if_version, the merge applies only at that version.
Every stored write adds 1 to the version, which starts at 1. The answer is {stored, version, reason}. A field the schema does not declare is refused (stored: false, reason: not_declared_field), and a pii field is masked at write by the space’s rules. A write that would take the state over the cap is not an error: the previous state stays, and the answer is 200 with stored: false and reason: over_cap, because the state usually arrives after the last byte of the agent’s answer, and failing there would cut the conversation. The check order is the version (412), the declared fields, the cap.

Reading

POST /v1/agent-state/read with the scope and the agent returns body, version and updated_at; a state never written reads as an empty body at version 0. A writer reads its own writes: after a stored: true at version N, every later read of the same scope and agent returns N or a newer version, even across Niadra’s two stores, and a read never waits for the database. The SDK keeps, per scope and agent, the last version it wrote or read, and serves the higher of that and what it reads. With Niadra out of reach, it keeps the version locally and sends the write again later with the same if_version; a compare-and-swap that then conflicts is reported to the code (the conflicts property of agent_state in Python and of agentState in TypeScript), never merged in silence. In a replay, the state starts empty and the writes stay with the runner.

Retention and erasure

A state is kept 30 days after its last write by default (from 1 to 180, in the type’s retention.state), and a field may be kept for less (retention.<field>), counted from its own last write: an expired field is never served, leaves the state with the next write and Niadra removes it from what it stores. Erasing a customer erases the states of their scope and the ones naming them in subject; erasing a conversation erases the states of that conversation’s scope. The subject’s data package carries their states, by declared field.

Next steps

Object types and state

the type with ownership: agent that gives the schema.

Agent memory

the notes about the trade, which enter the prompt and never speak of a customer.

Write working memory

the reference of PUT /v1/agent-state.

Privacy

the erasure and the subject’s data package.