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.

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"
)

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.3.2.tar.gz (185.5 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.3.2-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file norns_sdk-0.3.2.tar.gz.

File metadata

  • Download URL: norns_sdk-0.3.2.tar.gz
  • Upload date:
  • Size: 185.5 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.3.2.tar.gz
Algorithm Hash digest
SHA256 7ef86e2a66f559b30a5c7f547d2a6e58c5e9ed1db1bd440c46f1cd0ada041438
MD5 b74d4a9e5564ec54e95b97d7d2d8e1fb
BLAKE2b-256 01637468cee1a5bd38d34643a7041cdb066fd267f710dd58dc8fd1610beb88ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for norns_sdk-0.3.2.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.3.2-py3-none-any.whl.

File metadata

  • Download URL: norns_sdk-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 16.0 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.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 afd4292ed9ddf70c3a348fd716666636a73138ca3a332774206145ea4fa6bfe9
MD5 57150b03b230e414e558c0ffa44e2e2b
BLAKE2b-256 9858f9ba88157ca6ee1f22be1dda1e7ff7d0dafec741aff11370796ca48f8d90

See more details on using hashes here.

Provenance

The following attestation bundles were made for norns_sdk-0.3.2-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

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

This release

0.3.2 This release

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