Skip to content

Configuration

The SDK is configured through a small set of environment variables read when a session opens, plus per-session options. Two environment variables matter for every deployment; everything else has a working default.

Environment variables

VariableRequiredDefaultWhat it does
NOVARCH_SERVER_URLRecommendedhttp://localhost:8000Base URL of the novarch_server deployment the session connects to. Override per session with server_url= / serverUrl.
NOVARCH_SDK_KEYYes, against a deployed servernoneAuthenticates the SDK to the server. Sent as the X-Novarch-Key header on every request; a configured server rejects writes without it. Omit only when pointing at a local dev server that has no key set.
ANTHROPIC_API_KEYOnly with the LangChain adapternoneConsumed by LangChain’s Anthropic chat model to call the agent model. Novarch never reads this directly. It’s the LangChain dependency’s own credential. The judge’s Anthropic key lives on the server, not in your agent’s environment.

A minimal environment

For an agent talking to a managed or self-hosted deployment:

Terminal window
export NOVARCH_SERVER_URL=https://<your-deployment>
export NOVARCH_SDK_KEY=<your-sdk-key>

Resolution order

The server URL resolves in this order. The first one set wins:

  1. The per-session option (server_url= in Python, serverUrl in TypeScript).
  2. The NOVARCH_SERVER_URL environment variable.
  3. The built-in default, http://localhost:8000.

The SDK key resolves from the NOVARCH_SDK_KEY environment variable in Python (there is no decorator argument for it: the key is an environment concern, not a call-site one). TypeScript accepts it as sdkKey as well, because Workers has no environment to read.

Per-session options

Arguments to @augur.session(...), pinned per call-site. See augur decorators for the full signature.

ArgumentDefaultWhat it does
tenant_id, team_id, agent_idnone (required)The hierarchy keys (tenant → team → agent) stamped on every action in the session.
server_urlNone → resolves from env / defaultOverrides NOVARCH_SERVER_URL for this session.
session_idNone → server mints onePin an explicit session id (e.g. to correlate with your own run id).
poll_timeout_s300.0The longest this process can block on a PAUSED action. The deployment’s hold window can shorten it; see The hold window.
poll_interval_s1.0Backoff between retries when a verdict poll hits a transient error. Not the poll cadence itself; see below.
max_hold_spoll_timeout_sWhat this process declares it can wait when an action is PAUSED. See The hold window.
resumableFalseWhether this runtime can park across the wait rather than block through it.
reasoning_profile"provided"What this agent’s silence means. See Declaring that your agent has no reasoning.
evidenceNoneDeclares the fields this agent stamps on its own actions, by action type. See Evidence your agent reports.

The hold window

How long a PAUSED action waits for a person belongs to the deployment. An admin sets hold_window_s in ADMIN → SETTINGS; it ships at 5 minutes and can go up to 7 days.

Your session declares what it can honour when it opens, and the server answers with the wait this session actually gets: the smaller of the two numbers. A resumed run negotiates again when it attaches, so it never inherits last night’s number.

DeclarationWhat it means
max_hold_s omittedDeclares poll_timeout_s, which is what a process that has said nothing can survive.
max_hold_s=augur.ADOPT_DEPLOYMENT_WINDOWDeclares no ceiling of its own and waits as long as the deployment says a hold may wait.
max_hold_s=3600Declares that ceiling, in whole seconds rounded down. A capability rounded up is one the process cannot keep.
from novarch import augur
@augur.session(
tenant_id="acme",
team_id="ap-ops",
agent_id="ap-bot",
max_hold_s=augur.ADOPT_DEPLOYMENT_WINDOW,
)
def nightly_run() -> None:
...

Adopting the window is an opt-in. A process that dies mid-wait turns a hold a person was asked to judge into a fail-closed block, which is rare across 5 minutes and ordinary across 4 hours on a nightly batch, so nobody inherits a multi-hour blocking wait without asking for it.

The window says how long. What happens at the end of it is the deployment’s fail-closed posture. Under the default the action is blocked and your agent gets a NovarchKillError carrying [fail_closed_operator_timeout]. A deployment that has chosen to release instead lets the call through and records a release with no judgment behind it. That only happens while your process is still waiting; a run that has already exited gets neither, and the posture page covers why.

The agent’s own work sits half-finished for the length of the wait. Anything it opened stays open that long, which is a few minutes on the default window and most of a day on a wide one.

An admin can also set a window your runtime cannot honour. Under the default advise mode the session opens and waits the smaller of the two numbers. Under require the session is refused when it opens, and the SDK raises NovarchKillError with a [fail_closed_config] prefix naming configuration as the reason.

Declaring that your agent has no reasoning

Some agents explain their writes and some cannot. A rules engine, a scripted sweep, a black-box vendor loop: none of them produces a rationale, and staging a blank string on their behalf would record a sentence the agent never wrote.

Your session says which kind of agent it is when it opens, and again when it attaches.

from novarch import augur
@augur.session(
tenant_id="acme",
team_id="ap-ops",
agent_id="nightly-sweep",
reasoning_profile="unavailable",
)
def nightly_sweep() -> None:
...

The declaration says what an absence means, and each action records one of three states:

StateWhen
providedThe write staged reasoning. A blank string counts: the agent explained itself and what it said was nothing.
unavailableThe session declared unavailable and this write staged nothing.
missingThe session declared provided and this write staged nothing.

An explicitly staged value always wins, so a mostly-deterministic agent that does explain one write records that write as provided.

The operator sees the state on the crossing, as plain text under the action: “No reasoning — the agent declares it produces none” on an unavailable action, “Reasoning missing — the agent declares it reasons and supplied none” on a missing one. The audit packet prints the same sentence in place of the reasoning section, so a silent action says which silence it was.

One signal reads the agent’s reasoning: agent_reasoning_drift_score. A rule that lists it among the signals it reads is marked not evaluable on an action carrying no reasoning, and the judge is told not to cite it there. A rule that lists no signals is judged as written, so rule text asking for a stated rationale can still block a write that has none.

An unrecognized profile is refused before any request reaches the server, as a fail-closed error carrying [fail_closed_config].

Evidence your agent reports

Some facts about a write only your agent knows: a variance it calculated, a decision it made. If a rule should be able to point at one of those facts, declare the field once, when your session opens.

from novarch import augur
@augur.session(
tenant_id="acme",
team_id="ap-ops",
agent_id="po-agent",
evidence={
"accept_purchase_order": [
{
"name": "variance_cents",
"kind": "int",
"constraints": {"min": 0},
"label": "Variance (cents)",
}
]
},
)
def accept_po() -> None:
...

Put the field itself on the context you build for that write; the declaration only names it, its kind, and its bounds. Names are lowercase letters, digits and underscores starting with a letter, and 5 names are reserved: agent_reasoning, agent_reasoning_status, action_id, action_type, tools_called. The declaration rides create and attach, the same as reasoningProfile; omit evidence and nothing is sent.

An invalid declaration is refused before any request reaches the server, naming the action type, the field, and the reason. A declaration the deployment will not accept never refuses the session: a name a signal Novarch already computes owns, a name your agent already declared under a different kind, or one that would exceed a quota comes back as a warning, and that write runs with the field unbound.

Whether the judge is actually offered a declared field is a deployment setting, Agent-declared evidence on ADMIN → SETTINGS, and it ships at shadow:

  • Shadow. Declarations are accepted and recorded, and RULES → SIGNALS lists them, their proposed labels and any rejections. The judge is not offered them as evidence and cannot cite them.
  • Live. The judge is offered every accepted declaration as evidence and can cite it.

A flip takes effect on the next crossing; it is not retroactive.

Fixed behaviors worth knowing

These shape how the SDK behaves at runtime:

  • HTTP timeout: 30s. Python’s httpx client uses a 30-second read timeout per request and it is fixed; TypeScript exposes it as timeoutMs with the same default. A judge call or hold-poll that exceeds it surfaces as a transient error (and, for the action submit, fails closed).
  • Long-poll holds. While an action is PAUSED, each verdict poll blocks server-side for up to ~25s (kept under the 30s client timeout) rather than busy-polling. The poll timeout bounds the total wait across those polls; the poll interval only governs backoff after a transient failure. Setting TypeScript’s timeoutMs below 25s would cut every hold poll short, so keep it above the hold.
  • SDK version header. Every request carries an X-Novarch-SDK-Version header. If the server considers your SDK’s major.minor incompatible, it returns one explicit 400 (surfaced as a fail-closed NovarchKillError) instead of failing every action silently. Keep the SDK and server within a compatible minor range.

Next steps