norns-sdk
Python SDK for Norns.
pip install norns-sdk
The SDK has two parts. Norns is the worker — it connects to the server, registers your agent, and sits in a loop handling tasks. NornsClient is for sending messages and reading results from application code (your Slack bot, web backend, CLI, etc).
Worker
import os
from norns import Norns, Agent, tool
@tool
def search_docs(query: str) -> str:
"""Search product documentation."""
return db.vector_search(query)
@tool(side_effect=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a customer."""
smtp.send(to=to, subject=subject, body=body)
return f"Email sent to {to}"
agent = Agent(
name="support-bot",
model="claude-sonnet-5",
system_prompt="You are a customer support agent. Look up docs and help customers.",
tools=[search_docs, send_email],
mode="conversation",
on_failure="retry_last_step",
)
norns = Norns("http://localhost:4000", api_key=os.environ["NORNS_API_KEY"])
norns.run(agent) # LLM API keys read from env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
norns.run() connects via WebSocket, registers the agent and tools, then blocks forever handling llm_task and tool_task dispatches. LLM calls go through LiteLLM, so any supported provider works. Norns never sees your API keys — your worker makes all external calls.
Gards
A gard pins all of a run's tool dispatch to one worker — worker affinity for coding agents and other filesystem-bound work. Create one (nornsctl gards create prints the claim token once), then claim it:
norns.run(agent, gard=3, claim_token="tok_...")
A worker in a gard serves only runs bound to that gard, and vice versa. Tool handlers can expose service ports for the dashboard — the gard is inferred from the connection:
norns.register_port(3000, name="react", url="http://localhost:3000")
A fatally rejected claim (bad token, destroyed gard) raises JoinError instead of reconnect-looping; GardDestroyed is raised if the gard is destroyed while the worker is connected.
Shutdown
On SIGTERM or SIGINT the worker drains instead of dying mid-task: it tells Norns to stop sending it work, finishes the tasks it already holds, reports their results, leaves the channel, and run() returns. Tasks still running after shutdown_timeout seconds (default 30, or NORNS_SHUTDOWN_TIMEOUT) are dropped and Norns re-dispatches them. A second signal exits immediately.
norns.run(agent, shutdown_timeout=60)
norns.shutdown() requests the same drain from code — a tool handler or another thread can call it. New work that arrives while a worker drains queues until its replacement connects, so a connector restarted by a supervisor loses nothing.
Client
import os
from norns import NornsClient
client = NornsClient("http://localhost:4000", api_key=os.environ["NORNS_API_KEY"])
# Fire-and-forget
run = client.send_message("support-bot", "Where's my order?")
# run.run_id, run.status == "accepted"
# Wait for completion
result = client.send_message("support-bot", "Where's my order?", wait=True, timeout=30)
print(result.output)
# Multi-turn with a conversation key
result = client.send_message("support-bot", "And the tracking number?",
conversation_key="slack:U01ABC", wait=True)
# Inspect a run
run = client.get_run(42)
events = client.get_events(42)
# Stream events as they happen
for event in client.stream("support-bot", "Research quantum computing"):
if event.type == "completed":
print(event.data.get("output", "")[:80])
break
Human-in-the-loop
An agent can call the built-in ask_human tool to pause and ask a question. The run parks with status "waiting" until someone answers, and survives a restart while parked.
result = client.send_message("support-bot", "Book me a table", wait=True)
if result.is_waiting:
print(result.waiting_for.question) # "7pm or 8pm?"
client.reply(result.run_id, "7pm")
wait=True returns as soon as the agent parks — it's waiting on you, so it won't progress on its own. Sending the agent another message answers the question too, which is usually what a chat or Slack client wants; reply() targets one specific run.
Tools
The @tool decorator infers JSON Schema from type hints. The docstring becomes the tool description the LLM sees.
@tool
def lookup_customer(email: str) -> str:
"""Look up a customer by email."""
customer = db.query("SELECT * FROM customers WHERE email = ?", email)
return f"Found: {customer['name']} ({customer['plan']})"
Mark side-effecting tools so Norns can enforce idempotency on replay:
@tool(side_effect=True)
def charge_card(customer_id: str, amount: float) -> str:
"""Charge a credit card."""
result = stripe.charges.create(customer=customer_id, amount=int(amount * 100))
return f"Charged ${amount}: {result['id']}"
Async handlers work too:
@tool
async def fetch_page(url: str) -> str:
"""Fetch a web page."""
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
Agent options
agent = Agent(
name="my-agent",
model="claude-sonnet-5",
system_prompt="You are helpful.",
tools=[search, send_email],
mode="conversation", # "task" or "conversation"
checkpoint_policy="on_tool_call", # "every_step", "on_tool_call", "manual"
context_window=20,
max_steps=50,
on_failure="retry_last_step", # "stop" or "retry_last_step"
max_tokens=8192, # ceiling on one response
)
max_tokens is the ceiling on a single response, not on the history.
Leave it unset and the worker uses 8192; raise it for an agent whose
turns are long, such as one writing a whole file in a single turn. A
turn that reaches the ceiling comes back truncated — the run completes,
and the llm_response event carries finish_reason: "length" so a
client can say so.
Docs
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file norns_sdk-0.7.0.tar.gz.
File metadata
- Download URL: norns_sdk-0.7.0.tar.gz
- Upload date:
- Size: 193.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60b145e5770f1391653a13b907ac5727919796f10e9759e4abff6df8d76c1872
|
|
| MD5 |
c947a251205bca071dd915528256ff53
|
|
| BLAKE2b-256 |
1995b1de73bb5cf6d10771f918d2a110853087ad6e62d0815d8078215814ed9f
|
Provenance
The following attestation bundles were made for norns_sdk-0.7.0.tar.gz:
Publisher:
release.yml on nornscode/norns-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
norns_sdk-0.7.0.tar.gz -
Subject digest:
60b145e5770f1391653a13b907ac5727919796f10e9759e4abff6df8d76c1872 - Sigstore transparency entry: 2799935526
- Sigstore integration time:
-
Permalink:
nornscode/norns-sdk-python@22506d6937db9f31c39d539b3f5339f0190a29c3 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/nornscode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22506d6937db9f31c39d539b3f5339f0190a29c3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file norns_sdk-0.7.0-py3-none-any.whl.
File metadata
- Download URL: norns_sdk-0.7.0-py3-none-any.whl
- Upload date:
- Size: 20.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e0002e66c9e7bafc68d3dd4366eab3a9373f8fe55c5632128de30e5bd2ae2e3
|
|
| MD5 |
c5e02c4371fe041d53302eb3bbf9e1d6
|
|
| BLAKE2b-256 |
123f568d9cad11a16c6cb4f376f46590a7af7475d6ec7d86c2575c690b9cd792
|
Provenance
The following attestation bundles were made for norns_sdk-0.7.0-py3-none-any.whl:
Publisher:
release.yml on nornscode/norns-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
norns_sdk-0.7.0-py3-none-any.whl -
Subject digest:
9e0002e66c9e7bafc68d3dd4366eab3a9373f8fe55c5632128de30e5bd2ae2e3 - Sigstore transparency entry: 2799935602
- Sigstore integration time:
-
Permalink:
nornscode/norns-sdk-python@22506d6937db9f31c39d539b3f5339f0190a29c3 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/nornscode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22506d6937db9f31c39d539b3f5339f0190a29c3 -
Trigger Event:
push
-
Statement type: