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
| Variable | Required | Default | What it does |
|---|---|---|---|
NOVARCH_SERVER_URL | Recommended | http://localhost:8000 | Base URL of the novarch_server deployment the session connects to. Override per session with server_url= / serverUrl. |
NOVARCH_SDK_KEY | Yes, against a deployed server | none | Authenticates 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_KEY | Only with the LangChain adapter | none | Consumed 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:
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:
- The per-session option (
server_url=in Python,serverUrlin TypeScript). - The
NOVARCH_SERVER_URLenvironment variable. - 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.
| Argument | Default | What it does |
|---|---|---|
tenant_id, team_id, agent_id | none (required) | The hierarchy keys (tenant → team → agent) stamped on every action in the session. |
server_url | None → resolves from env / default | Overrides NOVARCH_SERVER_URL for this session. |
session_id | None → server mints one | Pin an explicit session id (e.g. to correlate with your own run id). |
poll_timeout_s | 300.0 | The longest this process can block on a PAUSED action. The deployment’s hold window can shorten it; see The hold window. |
poll_interval_s | 1.0 | Backoff between retries when a verdict poll hits a transient error. Not the poll cadence itself; see below. |
max_hold_s | poll_timeout_s | What this process declares it can wait when an action is PAUSED. See The hold window. |
resumable | False | Whether 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. |
evidence | None | Declares the fields this agent stamps on its own actions, by action type. See Evidence your agent reports. |
Fields on the SessionConfig passed to withSession. See the
TypeScript SDK for the full interface.
| Option | Default | What it does |
|---|---|---|
tenantId, teamId, agentId | none (required) | The hierarchy keys (tenant → team → agent) stamped on every action in the session. |
serverUrl | resolves from env / default | Overrides NOVARCH_SERVER_URL for this session. Required on Workers. |
sdkKey | NOVARCH_SDK_KEY | Required on Workers, where there is no environment to read. |
sessionId | server mints one | Pin an explicit session id (e.g. to correlate with your own run id). |
pollTimeoutMs | 300000 | The longest this runtime can block on a PAUSED action. The deployment’s hold window can shorten it; see The hold window. |
pollIntervalMs | 1000 | Backoff between retries when a verdict poll hits a transient error. Not the poll cadence itself; see below. |
maxHoldMs | pollTimeoutMs | What this runtime declares it can wait when an action is PAUSED. See The hold window. |
resumable | false | Whether this runtime can park across the wait rather than block through it. |
reasoningProfile | 'provided' | What this agent’s silence means. See Declaring that your agent has no reasoning. |
evidence | none | Declares the fields this agent stamps on its own actions, by action type. See Evidence your agent reports. |
timeoutMs | 30000 | Per-request deadline. Must exceed the 25s long-poll hold. |
signal | none | Caller cancellation, composed with the deadline on the operator long poll. |
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.
| Declaration | What it means |
|---|---|
max_hold_s omitted | Declares poll_timeout_s, which is what a process that has said nothing can survive. |
max_hold_s=augur.ADOPT_DEPLOYMENT_WINDOW | Declares no ceiling of its own and waits as long as the deployment says a hold may wait. |
max_hold_s=3600 | Declares 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: ...| Declaration | What it means |
|---|---|
maxHoldMs omitted | Declares the resolved pollTimeoutMs, which is what a runtime that has said nothing can survive. |
maxHoldMs: null | Declares no ceiling of its own and waits as long as the deployment says a hold may wait. |
maxHoldMs: 3600000 | Declares that ceiling. Milliseconds here, whole seconds on the wire. |
await withSession( { tenantId: 'acme', teamId: 'ap-ops', agentId: 'ap-bot', maxHoldMs: null }, async session => { ... },)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: ...await withSession( { tenantId: 'acme', teamId: 'ap-ops', agentId: 'nightly-sweep', reasoningProfile: 'unavailable', }, async session => { ... },)The declaration says what an absence means, and each action records one of three states:
| State | When |
|---|---|
provided | The write staged reasoning. A blank string counts: the agent explained itself and what it said was nothing. |
unavailable | The session declared unavailable and this write staged nothing. |
missing | The 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: ...await withSession( { tenantId: 'acme', teamId: 'ap-ops', agentId: 'po-agent', evidence: { accept_purchase_order: [ { name: 'variance_cents', kind: 'int', constraints: { min: 0 }, label: 'Variance (cents)', }, ], }, }, async session => { ... },)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
httpxclient uses a 30-second read timeout per request and it is fixed; TypeScript exposes it astimeoutMswith 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
timeoutMsbelow 25s would cut every hold poll short, so keep it above the hold. - SDK version header. Every request carries an
X-Novarch-SDK-Versionheader. If the server considers your SDK’s major.minor incompatible, it returns one explicit400(surfaced as a fail-closedNovarchKillError) instead of failing every action silently. Keep the SDK and server within a compatible minor range.
Next steps
- Quickstart: wire these into a running agent.
- augur decorators: the full Python decorator signatures.
- TypeScript SDK:
withSession,readTool,writeTool. - The fail-closed posture: what the deployment does when it cannot get a judgment, and the two answers that do not raise.
- NovarchKillError: the fail-closed conditions referenced above.