Skip to main content
The niadra package is the Python SDK: a synchronous client, Niadra, and an asynchronous twin, AsyncNiadra, with the same methods. It is open source under Apache 2.0, needs Python 3.10 or later, and depends only on httpx and pydantic. Every method on this page maps to a route of the API reference.

Install

Each framework’s extra is in Integrations. The niadra command ships with the library.

The client

AsyncNiadra takes the same arguments, with http_client: httpx.AsyncClient. One instance per process is enough, and it is thread-safe. The client also works as a context manager: leaving the block calls close(), and an exit hook flushes the queue for up to two seconds.

Properties

Safe by default, strict on request

Without a key the client warns once and does nothing. Every public method catches and logs its own failures and returns a safe value: an empty Context (check context.error), an empty result, False or None. Logs carry method names, status codes, error codes and request ids, never handles or text. With strict=True the same failures raise the exceptions listed under Errors.

Time budgets

The SDK keeps its own time budget per method, whatever the platform around it allows.
A connection stays open between calls for 120 s, one for each call that was in flight at once. When no idle connection is likely left for a call (every open one is busy, or none answered in that time), it gets connect on top of its budget, for TCP and TLS, which take a few round trips that a 0.3 s read cannot hold from another continent. A turn that reads its context, notes and state at once, with only the connection keep_warm holds, opens two more, and both get it. The calls that start while the first one opens a connection get it too, and then none does until an answer comes, so an outage spends it once. A call that finds an idle connection keeps its budget exact. A voice read never grows past its own budget: on a cold connection it answers empty in time instead of holding the turn. connect=0 never adds it. context and navigation left at their defaults are the API’s time: the client measures the round trip to the region once when it starts (VoiceOptions.probe) and adds it to those two, so an agent far from the region (Sao Paulo is 170 ms from us-east-2) does not lose its read to the network. The measurement also opens the connection the first read uses. A value you set is a ceiling the SDK keeps; when the round trip plus 50 ms exceeds it, the client logs one warning, because every such read would run out of time. While the measurement is on its way and a connection is open, those two get connect on top of the default, so the first read does not run out for want of it; with no connection open, connect already comes with the new connection, never twice. A measurement that failed, or probe off, leaves the defaults as they are. Voice budgets do not change. While a conversation or task is open (created and not ended) and the client was used in the last 10 minutes, it sends GET /healthz every 100 s in which nothing else went out for 90 s, one attempt with a 2 s budget: the connection stays open, and a turn after a long pause does not pay TCP, TLS and often DNS again (from Sao Paulo, a read after 150 s idle took 550 ms instead of 200, and about 900 ms when the DNS entry had also expired). These calls are not the client’s use, and a conversation nobody holds any more stops them. keep_warm=False never sends them. In voice, context_voice (200 ms) is not a round trip to the region: the pinned body comes from the SDK’s memory, and the budget only covers the wait for the read of the turn’s words that a prefetch() already started; past it, the turn goes out with the body and no slots, and the read goes on in the background, within prefetch (1 s). context_voice_start bounds the call’s first read, made by begin() while the phone rings: a cold connection costs three round trips (TCP, TLS and the request) plus the server’s first compile. Outside voice, the first read of a conversation, task or object in a client, and its first agent_memory(), get context_first instead of context while context is at its default: the API compiles the pack on that read (0.41 s measured on the server), and the next reads of it find the pack compiled.
settle is how long the partial transcript must stay the same before the SDK reads the turn with it; min_coverage is the share of the final turn’s words the partial must carry for that read’s slots to answer it; probe measures the round trip to the region once (GET /healthz, twice, the faster; niadra.rtt) when the client starts, or at the first call of an AsyncNiadra built outside a running loop: the read budgets left at their defaults add it, and the log warns when a budget you set, or a voice budget after the first voice read, cannot hold it. probe=False measures nothing. VoiceOptions(enabled=False) sends every voice turn to the API as the other views do. Retries: 5xx answers, 429 and network errors are retried with backoff; 421 (the space is moving between cells) is retried at once on a fresh connection; other 4xx answers are final.

The context cache

Inside a conversation or task (a call with conversation_id or task_id), packs are cached in memory:
  • younger than 10 s: returned without a request;
  • up to 10 minutes older: returned at once while one background request refreshes it;
  • when a request fails: the last good pack, if it is less than 30 minutes old;
  • at most 1,000 packs, the least recently used evicted first;
  • in the voice view, with a conversation_id or task_id: the pinned body is served from memory at once, whatever its age, and revalidated by ETag in the background; each turn’s slots come from the read of its words and are never cached. Ending the conversation drops its packs and what the voice read kept; an answer still on its way is not stored.
Refreshes send the cached ETag, so an unchanged pack costs a not_modified answer instead of the full text, and only one refresh per pack runs at a time. A 401 or 403 is not an outage: the cached packs go (all of them on 401, the one requested on 403), so cutting a vendor’s access also cuts what it had cached. A plain read and a delta read of one conversation share one entry, and each delta is handed out once.

The write queue

track(), action() and handoff() only queue. A background thread (a task, with AsyncNiadra) sends a batch when 15 items are waiting or one second after the first arrived, with three attempts and backoff. A message with a conversation_id leaves at once (turn_interval, 0 by default), taking whatever is waiting along: it is the turn the other agents read in live. One batch is in flight per client, and what is queued while it is answered leaves together in the next one, so a burst of turns costs one request per round trip, never one per turn. A batch that still fails with an error that may pass (a connection, a 5xx, a 429, a 421) goes back to the front of the queue, and the queue pauses, doubling the pause up to 30 s while Niadra does not answer; each item keeps its idempotency key, so it arrives once when Niadra is back. When the queue is full, new items are dropped and counted in dropped; so is a batch the API refuses and each item a 207 refuses alone, by the API’s code in dropped_by_reason.

Handles

Handles identify a subject in a channel or system. The helpers are top-level functions that normalize the value and raise ValueError on input that cannot be valid, so a malformed phone fails where it enters your code. Every method that takes a handle also accepts a Handle model or a mapping with its fields ({"type": "phone_e164", "value": "+14155550123"}). Objects are accepted as "invoice:erp:0823" or as an ObjectRef.

context()

The context pack for a person, an organization or a business object. It maps to POST /v1/context.
Returns a Context: every field of the API response plus what the SDK knows about the call.
Searches the whole history of one subject by keyword and meaning. Maps to POST /v1/history/search.
Returns a SearchResult with items, recurrence (how many times the same category happened and how the last one ended), withheld, as_of, tokens_used, degraded, window (the period when was read as), ignored (the filters the server did not read, such as a when it did not recognize) and, on failure, error.

timeline()

One page of the subject’s history, most recent first. Maps to POST /v1/history/timeline.
limit goes from 1 to 100. Returns a TimelinePage with items, next_cursor, withheld, as_of and, on failure, error. Pass next_cursor as cursor to go on.

open()

Opens one episode or object found by search() or timeline(), by the row’s id (a bare UUID). It maps to POST /v1/history/open: the item id, the level and the conversation_id go in the body, never in a URL. With subject, the server opens only an item of that customer, and the tools() kit sends the bound customer.
Returns an OpenedItem (summary, requested, promises, outcome, resolution, derived, timeline and, when the item has them, versions, oldest first) or None when it is unavailable.

object_state() and object_timeline()

Reads of a business object. They map to GET /v1/objects/{object_type}/{namespace}/{external_id} and its timeline.
ObjectRead is the object as a display state read serves it: ref, as_of, fields (each with v, logic, status, claim_safe, its stamps and source) and whatever else the type declares; a type the registry does not declare reads as one with no rules. The state comes only from what the systems of record reported; an agent’s action counts once a system confirms it. ObjectTimeline carries ref, items, next_cursor and as_of: system events and agent actions, newest first, never what anyone said. Object ids are record ids, not personal data, so they go in the URL; an id that contains a slash cannot be addressed this way.

track()

Queues a message, a system event or an action, or any other batch item, and returns at once. Maps to POST /v1/batch.
Accepts an EventItem or a mapping of its fields. A mapping without channel gets the client’s default channel; idempotency_key (a UUIDv7) and occurred_at (now) are filled in when you leave them out. Use the provider’s message id as idempotency_key when there is one, so a retried batch never duplicates anything. Returns False when the item was dropped: invalid, not serializable, queue full or client disabled.

action()

Records what an agent did in a system of record. Queued, like track().
The action stays declared until the system of record confirms it with its own event. Recording actions needs the act scope on the key.

identify()

States that two or more handles belong to the same subject. Sent at once, outside the SDK queue: a new handle joins the profile in the same response, and the merge of two profiles that already existed runs before the response; if the request fails, the item is queued for the background sender and None is returned.
The method says how much the link is worth. With explicit_identify or declared, a handle joins the profile as provisional: it resolves to the profile, but a read through it stays at V1 (reason: unconfirmed_link) until the company’s systems, a login or a person confirm it, and an identify without a record id never merges two customers that already exist (the pair becomes a suggestion). When your company has the proof, use system_import (your CRM’s record or a load) or login.

verify()

Raises the verification level of one conversation or task, after you proved it. Niadra never infers it. Sent at once, and the cached pack of that conversation is dropped, so the next context() reflects the new level.
method is otp_whatsapp, otp_sms, login, kba, network_attestation or human_agent. A level above the ceiling of the source comes back as a verification_not_allowed item error.

feedback()

Corrects what Niadra derived about a subject. Sent at once and recorded as an event, so it is audited like any other. Maps to POST /v1/feedback.
Each action takes its own fields: fact_id and open_item_id take the id as search and the timeline list it: a bare UUID. A prefixed id is refused with 422. A fact or an open item the subject does not hold, in any status, is refused with 404 not_found (one error per item in a batch), never stored to be skipped; with an account or a partner as the subject, an open item one of its linked contacts noted counts, and resolves under that contact. The correction applies in seconds, after the event is stored. value goes up to 2,000 characters and reason up to 500. Returns None when the correction could not be delivered.

feedback_batch()

Up to 500 corrections in one call, each a FeedbackRequest or a mapping with the same fields as feedback() and its own idempotency_key (one is minted when missing). Maps to POST /v1/feedback/batch. Returns a BatchResponse (accepted, duplicates for replayed keys and one error per refused item), or None when the batch could not be delivered.

context_use()

How the space’s agents used the context they read, as GET /v1/context-use reports it: per bucket, sessions, sessions without context, deliveries, use, repetition, transfers and recontact, with 95% intervals. group_by takes day, source_id (the default), vendor, channel, view and experiment_group; since, until (dates), source_id, channel, view and experiment_group filter. Returns a ContextUseReport with typed buckets and the answer’s other blocks in extra, or None when unavailable. The key of an analyst source with the analytics scope reads every source of the space; a key with admin reads its own.
A system of record that knows who works for whom (a CRM, an HR system) links a person to the account or partner they act for, and ends the link. Maps to POST /v1/identity/links and POST /v1/identity/links/{link_id}/end, with an idempotency key. Needs a key with the identity:link scope (or admin); can_see_contacts=True needs admin. Returns a Link (link_id, role, valid_from, valid_to), or None when the call could not be delivered.

ingest_status()

Whether what was sent for a conversation or task became memory yet. Maps to POST /v1/ingest/status.
Returns state (open, processing, ready, failed or unknown), last_event_at, closed_at, close_reason and extraction (the latest extraction’s status), never content. Call flush() first when the turns went through track(), so they have arrived.

whoami()

What this key authenticates as: space_id, source_id, source_name, vendor, scopes, audience, verification_ceiling, purposes and agent_memory (whether the space has the agents’ notes on). Any key may ask, whatever its scopes; use it to check a key before wiring an agent to it. Maps to GET /v1/sources/me.

niadra.admin

For a key with the admin scope, the governance reads and writes, failing open like the rest of the SDK: find_profiles(query, type=, scope=, limit=), memory(profile_id) (profile memory), fact_history(profile_id, fact_id) (fact history), correct(profile_id, action, ...) and correct_batch(items) (data subject corrections, one or up to 100), forget(...) and forget_status(request_id) (erasure) and export(...) (the data subject’s package). The answers are the models in niadra.models.admin. An agent key lacks that scope: the routes answer 403.

upload_media()

Hands a file to Niadra, such as a call recording, and returns the reference for its event. Media never travels inside an event. Maps to POST /v1/media/uploads.
The method hashes the bytes, reserves an upload, and sends the bytes straight to storage over the short-lived signed URL. It sends exactly the upload_headers the API returned, which are the headers the signature covers, and nothing else: never your key. The URL must be HTTPS, except against the local emulator. Storage checks the body against the declared size and digest. With subject, the file is stored under that person, so erasing them erases it even if no event ever references it. Returns a MediaUpload with media_ref, media_sha256, content_type, size_bytes and expires_at, or None when the upload failed. Each attempt of the transfer has the upload budget, 60 s by default.

handoff()

Records a transfer to a human ("human") or another agent ("agent"). Queued. The context-use measurement reads it to count handoffs where the receiver never read the context.

conversation()

A conversation with one customer, used as a context manager. Leaving the block emits conversation.ended, even when the block raised.
Without a conversation_id the SDK mints one. channel defaults to the client’s. agent_id identifies your agent within the source and is stamped on its turns and actions. The first context() gets the pack the server pins for the conversation. Later reads also ask for the delta, and the conversation keeps every delta it receives, in order, so turn_block carries the live turns, the turn’s slots and all the changes since the pin, in that order. The conversation sends the text of the last customer() as query on every read: the pinned pack is cached as a read without query, and the slots never are. When the server pins a new pack, after verify() for instance, the kept deltas are dropped: the new pack already has them. query= picks this read’s slots by other words than the turn; the pack is the pinned one all the same. Call mark_injected() each time you put the pack in a prompt. It is how Niadra tells a context that arrived after the agent spoke from one the agent had and did not use. The session also keeps context_injected_at and first_agent_turn_at, the first of each, for your own checks.
current_session() returns the conversation or task whose block is running in the current thread or task, if any.

task()

The same for an internal agent (billing, orders, tickets). It emits task.ended on exit.
With an object, the pack is centered on it; use a task view such as task:billing. A task has the same methods as a conversation except handoff().

tools()

The history kit as function-calling tools, bound to one customer. The customer is bound here, outside the model’s reach: the model picks the query, never the profile, so a prompt injection has no argument to switch customers with.
AsyncNiadra.tools() returns an AsyncToolKit, whose call() is awaited. The definitions are also served by GET /v1/history/tools.

subject_token()

Mints a signed 15-minute token that binds an MCP connection to one customer. Call it from your backend. Maps to POST /v1/subject-tokens.
Returns a SubjectToken with token, expires_at and headers, the Niadra-Subject-Token header to send with the source key when the agent opens the connection to mcp_url. With about, the organization is bound like the subject. See MCP with any LLM.

agent_memory(), search_agent_memory() and remember()

The agent’s own memory: its working notes, never about a customer. They correspond to GET /v1/agent-memory/block, POST /v1/agent-memory/search and POST /v1/agent-memory/notes.
agent_memory() returns an AgentMemory: text (the block, for after your instructions and before the customer’s context), notes (the ids of the notes in it), etag, tokens, enabled (False when the space has agent memory off) and source (network, cache or empty). The block is cached per (max_tokens, tags, view) for as long as the context and revalidated by ETag. Inside a conversation or task, agent_memory() uses the session’s view. search_agent_memory() returns the notes that match the query and the tags, best first; empty on failure. remember() is sent at once and returns a RememberResult: note (the saved note), or proposal_id when the space takes agent notes only through a person, or error. A note with personal data comes back with error="personal_data_in_agent_memory" (in strict mode, an UnprocessableEntityError with that code): rewrite it without the data. It needs a key with the agent_memory:write scope. kind is procedure, tool_note, process_note or pitfall; visibility is source (the default), vendor or space.

wrap()

Wraps the OpenAI Python client, or any client with the same shape, so every call inside a conversation or task block gets the context and records the answer.
Inside a block (or for the session you pass), chat.completions.create and chat.completions.parse, sync or async, streaming or not, get the pinned pack as a system message right after your own leading system messages, and the turn block as a system message at the end. The injection is stamped with mark_injected(), and the model’s answer is recorded as the agent’s turn: when a stream ends or is closed and, through with_raw_response, when you call parse(). Outside a block, calls pass through untouched. The wrapper returns a proxy and never modifies your client. Nothing it does can fail your model call: a context it cannot fetch is left out, and a failure to record the answer is logged, without content.

The provider’s prompt cache

The agent’s turn also carries the usage the provider reported for the call, as usage (ModelUsage): the provider, the model, every input token (usage.prompt_tokens), the ones it read from its prompt cache (usage.prompt_tokens_details.cached_tokens) and, through gateways that pass Anthropic’s fields along, the ones it wrote to it. A stream reports usage only when you ask for it, with stream_options={"include_usage": True}; the wrapper never changes your request to get it. Niadra sums the usage per agent, vendor and model, and the Console and GET /v1/usage/prompt-cache show the cache hit rate and the estimated savings. Without wrap(), pass the provider’s response with the agent’s turn. OpenAI responses (chat completions and Responses) and Anthropic ones are read; on Anthropic, usage.input_tokens counts only the uncached rest, and the total is input_tokens + cache_read_input_tokens + cache_creation_input_tokens.
ModelUsage.from_response(response) reads a response on its own. A response without usage is left out, and the turn is recorded either way.

Integrations

Every voice or agent framework has an adapter in niadra.integrations.<name>, installed as an optional extra (pip install 'niadra[livekit]'): LiveKit Agents, Pipecat, ElevenLabs, Vapi, Retell AI, WhatsApp Cloud API, Twilio, OpenAI Agents SDK, LangChain, LangGraph, Google ADK, Strands Agents, Pydantic AI, LlamaIndex, CrewAI, Agno, Microsoft Agent Framework, Semantic Kernel, Haystack, CAMEL-AI, DSPy, AG2, Anthropic, Amazon Bedrock, Google GenAI and LiteLLM, plus the Langflow components and the Dify plugin. All of them wire the context, the turns, the tools, the verification and the handoff into the framework’s own extension points and take agent_memory=. See Integrations.

flush() and close()

flush() sends everything queued from the calling thread and returns True when nothing is left. One batch is in flight per client: flush() waits for the batch the background sender already sent, even with an empty queue, and only then sends the rest in queue order, so a conversation.ended never lands before the turns queued ahead of it; timeout bounds that wait. close() flushes for up to timeout seconds and releases connections. With AsyncNiadra, both are awaited.

AsyncNiadra

AsyncNiadra has the same methods. The reads, identify(), verify(), feedback(), upload_media(), subject_token(), flush() and close() are awaited; track(), action(), handoff(), conversation(), task() and tools() are not. Conversations and tasks are async context managers.

Turn records, claims, coordination and working memory

Everything in this section depends on the features the space turned on. The SDK reads the SDK profile on first need and revalidates it by ETag; niadra.profile() returns the features on, the claim contract, the summarized type registry and the turn recording mode, and the last profile read stays in use with Niadra out of reach. With a feature off, each call below returns an empty value or goes on without it, never an error.

Turn records

Niadra.build(*, prompts=, corpus_digest=, model=, assembler=, tool_schemas=) makes the build’s pins. conversation.turn() (and task.turn()) opens a turn as a context manager, sync or async; opened inside another, it is a sub-turn. The TurnFrame has turn_id, interact(item) for an interaction, flag(name) for a flag (error, guard_acted, handoff, assertion_failed, synthetic, incomplete, negative_feedback, truncated), tool_call() and model_call() to record a call by hand. @Niadra.tool(name=None, *, provenance=None, ui=None, exclude=(), dry_run=False, binding=None, mask_output=None, on_unknown="pass") records each call of a tool of yours in the turn in progress. With binding, it also measures the constraints block; without one, it uses the binding the profile serves for the tool of that name. mask_output=True keeps the fields the key may not read out of the output before it reaches the model (deny removes, mask masks, by the profile’s field_access); left unset, it follows the served binding’s capabilities.mask_output. on_unknown="block" withholds the whole output when no profile was ever read; "pass", the default, lets it through. niadra.turns.bind(fn) carries the turn to a thread. Niadra(turns=TurnOptions(content_mode=None, max_bytes=64 * 1024 * 1024, max_turns=2000, interval=1.0)) configures the queue, and niadra.turns.store(put=None, *, s3_bucket=None, client=None) puts the values in your bucket (pointer mode). niadra.turns.otel puts the turn on your spans (pip install 'niadra[otel]'). The Google ADK, OpenAI Agents, LangGraph and LangChain adapters take turns=True. See Turn records.

Claims and resolvers

conversation.claims is the claim contract in the agent’s process: check(text, *, context="chat", immutable=False, agent=None) classifies and counts, without changing the text; guard_text(text, *, context="chat", immutable=None, agent=None) and guard(stream, *, context="chat", ...) act as the contract’s actions say. niadra.use_claim_contract(contract) swaps the profile’s contract for your own copy, in CI. conversation.verify_claim(ref, field, value, *, budget=None) (and niadra.verify_claim(..., subject=)) says whether a value may be claimed now and, when it is not safe, reads it again through the type’s resolver, within 300 ms: niadra.resolvers.register(type, fn, *, rate=None) registers a function of yours (ref, fields) -> dict | Resolved, which returns niadra.resolvers.NOT_FOUND when the source no longer has the object; niadra.resolvers.fetch() says why a read brought no object. niadra.content.register(fetch) puts back the text a pointer mode space keeps in your storage. See Claims and The resolver worker.

Coordination

check(intent, *, purpose, direction="outbound", channel=None, effect_key=None, effect_kind=None, object=None, task=None, gateway_id=None, destination_hash=None, fail_open=None, timeout=None) waits at most 200 ms and, with no answer, decides by the purpose’s direction; it never raises. Every check about an outbound contact keeps the local copy of the suppression list, read again in the background once a minute, and with Niadra down the opt-out holds by it. conversation.declare(kind, **detail) and the shortcuts effect() and contact_made() send in the background. conversation.claim(*, object=None, task=None, kind=None, lease_s=600, level=None, intents=()) returns a Claimed, with held and error. niadra.may_contact(handle, purpose, *, channel=None, at=None, fail_open=None) applies the local copy of the suppression list. The first call reads the copy, the salt and the first page each within Timeouts.navigation; a list longer than a page, or a read that ran out of time, goes on in the background, and the next call has the copy. An entry with a window (the hours the customer asked for, as “don’t call before 9 am”) says no only inside those local hours, at the moment at (now by default), and with Niadra down a check inside the window waits until it ends (contact_window). With no copy and Niadra out of reach, the purpose decides: transactional and service go, the others wait; fail_open overrides that. niadra.contact_gateway(gateway_id, *, space, key, seen=None) returns a ContactGateway with verify(token, *, channel, destination=None, handle=None), which raises ContactTokenError; niadra.coordination.verify_contact_token() checks a token against a fixed key set (pip install 'niadra[gateway]'). See Coordination and Gateways.

Working memory

conversation.agent_state.get() returns a WorkingState (body, version); put(body, *, mode="merge_by_key", if_version=None) returns a StateWrite (stored, version, reason, pending), and the conflicts property lists the resent compare-and-swaps that met another version. See Working memory.

Replay, worker and command line

niadra.replay.Replayer(niadra, agent_factory, *, build=None, read=None).run(scenario_ids, *, runs=5, mode="hermetic_turn", vary=(), paraphrase=None) runs the scenarios in your CI and returns a ReplayRun (run_id, status, verdict, scenarios, regressed); AsyncReplayer for the async client. niadra.replay.Counterfactual runs the tool counterfactual: the recorded calls whose arguments carried an element of the constraints block run again without it, dry when the tool writes state and never without a dry run, and only overlaps and positions go to Niadra; niadra.replay.overlap_at_k() is the depth-weighted overlap the report uses. niadra.cli.worker.ResolverWorker(niadra, *, limit=50, poll=2.0, budget=5.0) serves the refresh requests with run_once(), with watch revalidation: it serves watch_revalidation requests first, pushes each object it read with the request_id it answers and releases what it cannot answer. The niadra command has replay (--agent, --build, --scenario, --runs, --vary; exit 1 on a regression), counterfactual (--tools, --tool, --element, --scenario, --safe, --k, --label), resolver-worker (--resolvers, --once, --limit, --poll), types derive (--dsn, --table, --type, --system, --ownership, --out, and --check with --declaration and --no-send) and contract test (--contract, --corpus, --examples). niadra.api has one method per route (niadra.api.check(...), niadra.api.promote_turns(...), niadra.api.overview(days=7), niadra.api.notifications(cursor=, limit=), niadra.api.release_refresh_request(request_id, body)), which takes the models of niadra.models and raises the API’s error. See Replay in your CI.

Examples

One runnable example per concept, in the SDK repository, each run by the tests:

Errors

With strict=True, or from helpers you call directly such as ApiKey.parse() and the handle functions, the SDK raises: The full code catalog is in Errors. Quote request_id when you contact support.

Local emulator

niadra-mock, installed with the package, is a local in-memory emulator of the same API. In tests, run the SDK against it in-process:
Recent turns become a small pack, deltas are sent once per change, search matches keywords, verification only rises through verify(), objects take their state from system events, feedback becomes a feedback.* event and media uploads land in mock.cell.media. mock.cell also lets you inspect events and inject failures (fail_next, revoke, cut, put_in_holdout). From a shell, niadra-mock --port 8765 serves it over HTTP.

Next steps

TypeScript SDK

The same surface for Node 20 and later.

Quickstart

From a key to the first delivered context.

Context and views

What goes into the pack and why.

Errors

The code catalog and what to do with each code.