Skip to main content

Shadow-OS Python SDK

The official Python client for Shadow-OS — build, operate and talk to AI agents.

pip install shadow-os
from shadow_os import ShadowOS

with ShadowOS() as client:          # reads $SHADOW_OS_API_KEY
    print(client.chat("Summarise the notes I uploaded yesterday"))
  • Complete — the assistant, conversations, memory, files & workspace, documents & search, the full agent platform, and the automation the agent creates for you.
  • Bring your own model — run everything on your provider and your key (OpenAI, Anthropic, Google, or any OpenAI-compatible service). Your key is never stored.
  • Typed — every response is a dataclass, the package ships py.typed, and unknown server fields stay reachable via .raw.
  • Sync and async — ShadowOS and AsyncShadowOS share one definition per endpoint, so they cannot drift.
  • Resilient — timeouts, exponential backoff with jitter, Retry-After support, and automatic retries for cold starts and 5xx.

Authentication

Two credentials, for two different jobs.

Credential Looks like Use it for
Account key sk-shadow-… Your account: chat, documents, and managing agents you own.
Agent token / share code sk-agent-… / lnk_… Talking to one configured agent, as your end-users do.
client = ShadowOS("sk-shadow-…")            # or set SHADOW_OS_API_KEY
agent  = AgentClient("lnk_…")               # or set SHADOW_OS_AGENT_TOKEN

Create an account key at /developers.


Your account assistant

reply = client.chat("What did we agree with Dana?", session_id="conv-42")
print(reply.answer, reply.usage)

session_id is an isolation boundary, not just a label: memory, uploaded files and search are scoped to it, so one end-user can never see another's. Pass scope="account" to opt into the shared account space (which also has access to connected integrations such as Gmail).

client.documents.upload("handbook.pdf", session_id="conv-42")
for doc in client.documents.list(session_id="conv-42"):
    print(doc.name)

for hit in client.documents.search("refund policy", top_k=5, session_id="conv-42"):
    print(hit.score, hit.source, hit.text[:120])

Quota

u = client.usage()
print(f"{u.used}/{u.quota} used — {u.remaining} left")

Bring your own model

Run the whole platform — tools, memory, knowledge, agents — on your provider and your key.

Provider Constructor effort maps to
Google Gemini Model.google("gemini-2.5-pro", …) thinking_level
OpenAI Model.openai("gpt-4o", …) reasoning_effort
Anthropic Model.anthropic("claude-sonnet-4-5", …) thinking budget
from shadow_os import ShadowOS, Model

client = ShadowOS()

client.chat("Analyse this contract", model=Model.google("gemini-2.5-pro", api_key="AIza..."))
client.chat("Draft the reply",       model=Model.openai("gpt-4o", api_key="sk-..."))
client.chat("Think hard",            model=Model.anthropic("claude-sonnet-4-5",
                                                           api_key="sk-ant-..."), effort="high")

print(client.providers().available)     # what this deployment can actually serve
Self-hosted and OpenAI-compatible endpoints

Anything speaking the OpenAI API also works — Groq, Together, OpenRouter, vLLM, Ollama, your own gateway — via an explicit base_url. Capability varies sharply here: the platform drives a real tool loop, and a small local model will often fail to use tools reliably. For production, the three providers above are the supported path.

client.chat("Summarise", model=Model.compatible(
    "llama-3.3-70b", api_key="gsk-...", base_url="https://api.groq.com/openai/v1"))

Your key is never stored. It is sent with the request that uses it, used for that single turn, and discarded — never written to a database, an agent's config, a log line or a backup. That is why it is passed per call rather than configured once: there is nothing to configure, by design. Model also redacts it from repr, so it cannot leak into a traceback.

It works on agents too, and on AgentClient:

client.agent(agent_id).talk("Draft a reply to Dana")
AgentClient("lnk_...").send("Hi", model=Model.google("gemini-2.5-pro", api_key="AIza..."))

effort is "low" | "medium" | "high" and maps onto whatever the provider calls reasoning depth. It works with or without a model — on its own it sets the depth of the platform model.


Local tools — the agent works on your machine

The agent thinks in the cloud. With tools= it can also act on the computer running your code: your own Python functions, or a real browser with your logins. No server, no open port, no MCP, nothing to deploy — the SDK opens one outbound connection, and only what you list is reachable.

from shadow_os import ShadowOS

def stock(item: str) -> int:
    """How many units of an item we have."""    # the docstring and type hints ARE the tool
    return inventory[item]

client = ShadowOS()
reply = client.chat("Do we have enough flour for Friday?", tools=[stock])
print(reply)               # the answer
print(reply.local_calls)   # what ran here: [{"tool": "stock", "ok": True, "ms": 4}]

A real browser, over several turns (pip install "shadow-os[browser]"; Chromium downloads on first use):

from shadow_os.local import Browser

with client.session(tools=[Browser(visible=True)]) as s:
    s.ask("Open my-kabinet.com and log me in")      # it can pause and let YOU log in
    s.ask("Play one round and tell me the score")

Logins stay in a saved profile (~/.shadow_os/browser/default), so you sign in once. To drive your own Chrome, start it with --remote-debugging-port=9222 and use Browser(attach="http://localhost:9222"). Long browser tasks go best on a strong model — pass model= (see Bring your own model).

Anything else, in one line:

from shadow_os import local

client.chat("...", tools=[
    local.expose(my_module),              # every public function of a module or object
    local.tool(render, timeout=300),      # options when you need them
    stock,                                # async functions work too
])
client.agent("ag_123").talk("Check today's stock", tools=[stock])   # your agent, as its manager

What you can rely on:

  • Every local call prints on your console as it happens (quiet=True on a LocalRunner silences it).
  • Return anything JSON can carry — dataclasses, paths, dates and pydantic models are converted. An exception reaches the agent as an error it can explain; the traceback stays on your machine.
  • Nothing runs twice. A turn with local tools is never retried. If it is cut off after something already ran, you get TurnInterrupted with .local_calls, and your code decides whether to ask again.
  • If the connection drops, the SDK reconnects by itself and keeps the same runner.

Conversations & memory

A conversation loads its own messages, so there is nothing to work out:

for conv in client.conversations.list():
    print(conv.session_id, conv.title)
    for msg in conv.messages():
        print(" ", msg.role, msg.content)

# or, if you already hold the id
for msg in client.conversations.history("thread-id"):
    print(msg.role, msg.content)

client.conversations.rename("thread-id", "Contract review")
print(client.memory.summary())          # what it has learned about you, across everything

The same shape works for an agent's members:

with AgentClient("lnk_...") as agent:
    for conv in agent.conversations(member_key="user-42"):
        for msg in conv.messages():
            print(msg.role, msg.content)

Files & workspace

Two different things, and the difference matters:

client.workspace.index("handbook.pdf")   # extract text → findable by semantic search
client.workspace.add("analysis.py")      # keep the RAW file → the agent can edit and RUN it
client.workspace.list()
client.workspace.download("report.xlsx") # something the agent produced

Automation

You don't build automation through this API — you ask for it. The agent creates and runs recurring work itself, from an ordinary message. These methods exist to see and control what it already made, which is what you need for a dashboard.

client.chat("Every weekday at 08:00, email me a summary of my unread mail")

for t in client.tasks.list():        # scheduled work
    print(t.description, t.target_time, t.status)
client.tasks.reschedule(t.id, "2026-10-01T08:00:00")

for w in client.watches.list():      # monitors it created
    print(w.title, w.status)
client.watches.pause(w.id)

for r in client.recipes.list():      # reusable workflows it saved
    for run in client.recipes.runs(r.id):
        print(run.status, run.started_at)

Creating them directly is possible (client.recipes.create(), client.watches.create()) but it is the harder path: the agent writes better steps from a sentence than you will from a schema.

Notifications & integrations

client.notifications.set_prefs(scheduled_results=True, owner_alerts=False)
client.integrations.google()        # Gmail + Calendar connected?
client.integrations.whatsapp()

Building an agent

from shadow_os import ShadowOS, AgentConfig

client = ShadowOS()

created = client.agents.create(
    name="Nona Pizza",
    template="pizzeria",                       # or persona="..." for full control
    fields={"business_name": "Nona", "hours": "Sun–Thu 11:00–23:00"},
    config=AgentConfig(memory_mode="single", tone="warm and brief"),
)
print(created.id, created.manager_token)       # the token is shown ONCE — store it now

Or start from a draft: the platform reads your website, a document or a few words and proposes the name, instructions, welcome line, hours, prices and FAQ. Nothing is created until you call create.

draft = client.agents.draft(url="https://nona.example", notes="We also cater events")

Browse what's available before you build:

for t in client.templates():
    print(t.id, t.name, [f["id"] for f in t.fields])

print(client.tool_catalog().groups)            # optional tool bundles you can enable per agent

Teaching it

agent = client.agent(created.id)

agent.knowledge.add_file("menu.pdf", description="Full menu with prices")
agent.knowledge.add_file("storefront.jpg")     # images are understood AND sendable to customers
agent.knowledge.add_url("https://nona.example/about")
agent.knowledge.add_text("We deliver within 4km. Closed on holidays.", title="policies")

agent.knowledge.add_sendable("invoice-template.pdf")   # deliverable, but NOT searchable knowledge

Short facts go in the fact book — shown to the agent on every turn, and kept current: a newer version of a fact replaces the old one instead of piling up next to it.

agent.facts.add("Delivery is free above 120 NIS")
agent.facts.add("Delivery is free above 150 NIS")   # replaces the previous fact
for f in agent.facts.list():
    print(f["id"], f["text"])

Tools the agent built for itself in the Manage chat wait for your approval:

for t in agent.tools.list():
    print(t["name"], t["status"])
agent.tools.approve("weather_now")

Sharing it

link = agent.share_link()                      # persistent, created once, stable forever
print(link.web, link.whatsapp)

staff = agent.access.create_share_link(role="manager", label="Front desk")
api   = agent.access.create_token(role="member", label="Website widget")

Operating it

for c in agent.customers():
    print(c.display_name, "·", c.profile)

for e in agent.escalations():
    agent.answer_escalation(e.id, "Yes — we're open until 23:00 on Sunday.")

print(agent.analytics().raw)
print(agent.appointments())
print(agent.delivery_status())

Campaigns follow up quiet leads and invite customers back — each message written per person:

print(agent.campaigns.preview("quiet_leads"))       # who is due, and a sample message; sends nothing
agent.campaigns.run("quiet_leads")

Talk to it as its manager — it sees your customer roster and acts with your authority:

print(agent.talk("Message Dana that her order is ready"))

Talking to an agent (what you embed)

from shadow_os import AgentClient

with AgentClient("lnk_…") as agent:
    info = agent.info()
    print(info["agent_name"], info["welcome"])

    reply = agent.send("Are you open on Sunday?", member_key="user-7f3c")
    print(reply.answer, reply.files)

member_key is the identity of the person you are speaking for. Give each end-user a stable, unguessable id and each gets private, durable memory and history inside the agent — they can never see each other.

dana = agent.for_member("user-7f3c", member_name="Dana")   # shares the connection pool
for conv in dana.conversations():
    print(conv.session_id, conv.title)
for msg in dana.history(session_id="main"):
    print(msg.role, msg.content)

Async

Identical surface, awaited:

from shadow_os import AsyncShadowOS

async with AsyncShadowOS() as client:
    reply = await client.chat("hello")
    agents = await client.agents.list()
    stats  = await client.agent(agents[0].id).analytics()

Errors

from shadow_os import QuotaExceeded, RateLimited, ShadowOSError

try:
    client.chat("hello")
except QuotaExceeded as e:
    print("out of quota:", e.usage)
except RateLimited as e:
    print("slow down, retry in", e.retry_after)
except ShadowOSError as e:
    print(e.status, e.code, e.message, e.request_id)

Every error carries status, code, message and — when the server sent one — request_id. Quote that id in a support request and the exact call can be found in the logs.

Exception HTTP
BadRequestError 400 / 422
AuthenticationError 401
QuotaExceeded 402
PermissionDeniedError 403
NotFoundError 404
ConflictError 409
PayloadTooLarge 413
RateLimited 429
ServerError / ServiceUnavailable 5xx
APITimeoutError / APIConnectionError no response

Configuration

client = ShadowOS(
    api_key="sk-shadow-…",       # or $SHADOW_OS_API_KEY
    base_url="https://…",        # or $SHADOW_OS_BASE_URL — for self-hosting or staging
    timeout=120.0,               # per request; generous on purpose (the service can cold-start)
    max_retries=3,               # timeouts, connection errors, 429 and 5xx
)

Bring your own httpx client (proxies, custom TLS, shared pools):

import httpx
client = ShadowOS(http_client=httpx.Client(proxies="http://…", timeout=60))

Retries use exponential backoff with full jitter. Retry-After always wins. 4xx responses other than 429 are never retried — they will not become a 200.


Development

pip install -e ".[dev]"
pytest

MIT licensed.

Metadata

Release files for shadow-os 1.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shadow-os 1.4.0
File Size Uploaded Provenance Actions
shadow_os-1.4.0.tar.gz 67.2 kB With provenance Details for shadow_os-1.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for shadow-os 1.4.0
File Interpreter ABI Platform Actions
shadow_os-1.4.0-py3-none-any.whl Python 3 none any Details for shadow_os-1.4.0-py3-none-any.whl

Total release size: 125.4 kB

Release files / shadow_os-1.4.0.tar.gz

Download URL shadow_os-1.4.0.tar.gz
Size 67.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e2933c8e3edf6430bcb0087af23bc8955f32ae1f172ba0ee1a8c9c12fcb7e149
BLAKE2b-256 checksum
How to use checksums
690d8ba23e86223c1fd751b4f6c0ff51409448565e8542db2b6522506f55f457
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Release files / shadow_os-1.4.0-py3-none-any.whl

Download URL shadow_os-1.4.0-py3-none-any.whl
Size 58.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
143a034ec9788dc7eb55434533d596ca41c5ec22b52b15bb87f49747e5bd9362
BLAKE2b-256 checksum
How to use checksums
f3fa00ddf90e53fa5c9c63542d91e94e691f9cdf3a28b0c1fb4ec4e2465a6f94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Security for shadow-os 1.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for shadow_os-1.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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