Skip to main content

norns-sdk

CI PyPI Python 3.10+ License: MIT

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

norns_sdk-0.7.0.tar.gz (193.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

norns_sdk-0.7.0-py3-none-any.whl (20.5 kB view details)

Uploaded Python 3

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

Hashes for norns_sdk-0.7.0.tar.gz
Algorithm Hash digest
SHA256 60b145e5770f1391653a13b907ac5727919796f10e9759e4abff6df8d76c1872
MD5 c947a251205bca071dd915528256ff53
BLAKE2b-256 1995b1de73bb5cf6d10771f918d2a110853087ad6e62d0815d8078215814ed9f

See more details on using hashes here.

Provenance

The following attestation bundles were made for norns_sdk-0.7.0.tar.gz:

Publisher: release.yml on nornscode/norns-sdk-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for norns_sdk-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e0002e66c9e7bafc68d3dd4366eab3a9373f8fe55c5632128de30e5bd2ae2e3
MD5 c5e02c4371fe041d53302eb3bbf9e1d6
BLAKE2b-256 123f568d9cad11a16c6cb4f376f46590a7af7475d6ec7d86c2575c690b9cd792

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.8.0

2 files

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page