Diva SDK for Python (thin client) — build agents on the Diva platform over a hosted gateway (bearer token, no local engine)
Project description
diva-ai — Diva SDK for Python
Build agents on the Diva platform from Python. A thin client: the agent
engine runs server-side on Diva's hosted gateway — you connect with a bearer
token (sk-diva-…), the engine never runs locally, and all model traffic goes
through the platform.
This is the Python sibling of the TypeScript @diva-ai/sdk
and speaks the same gateway wire protocol (session keys are byte-identical, so a
Python and a TS client can resume the same server-side conversation).
Status: alpha (
0.1.0a1).run/stream/generate, client tools, toolsets, sessions + memory, permissions, hooks, guards, sub-agents, parallel, and stream reconnect are implemented and covered by a live E2E suite. See Feature parity andPORT_PLAN.md.
Install
pip install diva-ai
Requires Python ≥ 3.10. Depends only on websockets and pydantic.
Quickstart
import asyncio
from diva_ai import Agent
async def main() -> None:
agent = Agent(
"diva/deepseek/deepseek-v4-flash",
instructions="You are a concise assistant.",
api_key="sk-diva-...", # or set DIVA_API_KEY
)
result = await agent.run("What is the capital of France?")
print(result.text) # "Paris."
print(result.usage) # token usage
await agent.close()
asyncio.run(main())
Point the client at a gateway with DIVA_GATEWAY_URL (e.g.
ws://localhost:5002/gateway for a local platform) or pass gateway_url= to
Agent. ws:// is allowed only to loopback / private ranges; wss:// always.
Configuration
| Option | Source | Notes |
|---|---|---|
| API key | api_key= or DIVA_API_KEY |
sk-diva-… bearer token |
| Gateway | gateway_url= or DIVA_GATEWAY_URL |
defaults to the hosted endpoint |
| Model | Agent("diva/<family>/<model>") |
namespaced; split into provider+model on the wire |
Model refs
Models are namespaced diva/<family>/<model>. The SDK splits this into a
provider and model on the wire and the platform routes it to the real
backend. A per-turn override is available via run(..., model=...).
Core API
run — one turn
r = await agent.run("Summarize this in one line: ...")
r.text # the reply
r.usage # Usage(input_tokens, output_tokens, total_tokens, cache_*)
r.reasoning # model thinking, when reasoning is enabled (else None)
r.run_id, r.duration_ms, r.stop_reason
stream — token streaming
from diva_ai import DeltaChunk, DoneChunk
async for chunk in agent.stream("Write a haiku about the sea."):
if isinstance(chunk, DeltaChunk):
print(chunk.delta, end="", flush=True)
elif isinstance(chunk, DoneChunk):
print("\n--", chunk.usage)
If the socket drops mid-stream (a transient close after connect), the SDK
transparently resumes via agent.streamEvents replay and fetches the
authoritative terminal — a lost terminal fails loud, never silently truncates.
generate — structured output
from pydantic import BaseModel
class Contact(BaseModel):
name: str
email: str
res = await agent.generate("Extract: John Smith, john@x.com.", Contact)
res.output # Contact(name="John Smith", email="john@x.com")
res.attempts # 1 = one-shot; 2 = repaired on retry
res.repaired
The schema drives the JSON directive and validation; one repair retry runs in a disjoint session so it never pollutes the caller's conversation.
Client tools
Define a tool with a pydantic input schema; the engine calls it and the SDK executes it locally, then returns the result — the model loops until done.
from pydantic import BaseModel
from diva_ai import Agent, tool
class WeatherInput(BaseModel):
city: str
def get_weather(inp: WeatherInput):
return {"city": inp.city, "tempC": 21, "sky": "clear"}
agent = Agent(
"diva/deepseek/deepseek-v4-flash",
instructions="Answer weather questions by calling get_weather.",
tools=[tool(name="get_weather", description="Get weather for a city.",
input_schema=WeatherInput, execute=get_weather)],
)
await agent.run("What is the weather in Lisbon?")
execute may be sync or async. Group related tools with toolsets:
from diva_ai import toolset
agent = Agent(model, toolsets=[toolset("weather", [get_weather_tool])])
Permissions (can_use_tool)
An interactive per-call gate applied client-side before a tool runs (fail-closed):
from diva_ai import Agent, Permissions
async def can_use_tool(name, args):
if name == "delete_all":
return {"behavior": "deny", "message": "not allowed"}
return {"behavior": "allow"}
agent = Agent(model, tools=[...], permissions=Permissions(can_use_tool=can_use_tool,
allow=["get_weather"]))
permissions.mode / deny target engine built-ins the thin client doesn't
expose and raise DivaNotImplementedError — use can_use_tool (+ guard.tool).
Hooks & guards
Lifecycle hooks wrap the turn and tool calls; guards are declarative sugar.
from diva_ai import Agent, Hooks, guard
hooks = Hooks(
before_agent_start=lambda ev: {"replace": ev["message"].strip()},
before_reply=lambda ev: {"replace": ev["text"]} if ok(ev["text"]) else {"block": "unsafe"},
agent_end=lambda ev: log(ev["reply"]),
)
agent = Agent(
model,
hooks=hooks,
guards=[guard.output("password", "secret"), # hard-block the reply on a match
guard.tool("rm -rf", tool="exec")], # soft-block a tool by input
)
Hook outcomes: return None (continue), {"block": reason} (→ DivaGuardTripped),
or {"replace": value} (rewrite message/reply/tool-input/tool-output).
Sub-agents (handoff)
Delegate to another agent as a tool — "just a typed tool transfer", no graphs.
from diva_ai import Agent, handoff
qualifier = Agent(model, instructions="Qualify the lead in one line.")
agent = Agent(model, tools=[handoff(qualifier, name="qualifier",
description="Qualify an inbound sales lead")])
Each handoff is an independent, stateless sub-agent turn. Close sub-agents
yourself — the parent's close() does not cascade.
Skills
Named instruction/knowledge blocks composed into the system prompt every turn.
from diva_ai import Agent, skill, skill_from_dir
agent = Agent(model, skills=[
skill(name="objection-handling", description="How to handle pricing pushback",
body="When the customer says it's too expensive, ..."),
skill_from_dir("./skills/refunds"), # reads ./skills/refunds/SKILL.md
])
Skill content is trusted (you author it). Bodies are size-bounded and duplicate names fail loud.
MCP servers
Connect external MCP servers (stdio or HTTP);
their tools join the agent as client tools named <server>__<tool>. Requires the
mcp extra (pip install 'diva-ai[mcp]'); connections open lazily on the first
turn and close with agent.close().
import sys
from diva_ai import Agent, MCP
agent = Agent(
model,
mcp=[
MCP.stdio("filesystem", "npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/data"]),
MCP.http("weather", "https://mcp.example.com/mcp", headers={"Authorization": "Bearer ..."}),
],
)
Sessions & memory
Multi-turn memory two ways:
# Server-side (default): a stable session id keeps history on the platform.
chat = agent.session("user-42")
await chat.run("My name is Ada.")
await chat.run("What is my name?") # → "Ada"
# Client-side: YOU own the history (in-process or on disk).
from diva_ai import Agent, MemoryStore, FileStore
agent = Agent(model, store=FileStore("./sessions")) # or MemoryStore()
With a store, prior turns are injected as fenced untrusted-data and the server
turn runs stateless. Session keys fold model + instructions + session id and are
byte-identical to the TS client, so cross-client resume works.
Flow (slot-filling)
Guide a conversation with a frame: soft guidance (asks + rules) plus a hard GUARANTEE — a terminal action is blocked until its required slots are filled, and tools can be gated on prerequisites.
from diva_ai import Agent, flow
checkout = (
flow("checkout")
.slot("address", fill_when={"tool_called": ["set_address"]}, ask="Ask for the shipping address")
.gate("place_order", require_prior=["set_address"], block_reason="Set the address first.")
.completion("place_order", requires=["address"])
.build()
)
agent = Agent(model, tools=[set_address_tool, place_order_tool], flow=checkout)
The interpreter runs client-side over the hooks: it tracks slot beliefs from tool
results, blocks a gated tool until its prerequisites are met (up to max_blocks),
and hard-blocks the completion action until required slots are filled.
Parallel fan-out
from diva_ai import parallel
results = await parallel(
[lambda: agent.run(f"Weather in {c}?") for c in ["Berlin", "Tokyo", "Lima"]],
concurrency=4,
)
for r in results:
print(r.value.text if r.status == "fulfilled" else r.reason)
Reasoning
thinking_default (off | minimal | low | medium | high | xhigh |
adaptive) maps to each provider's native reasoning control. When on and the
model emits reasoning, it is surfaced on result.reasoning, kept separate from
result.text.
Errors
All errors subclass DivaError: DivaAuthError (no key/target), DivaHostError
(gateway unreachable), DivaRequestError (turn failed/timed out),
DivaNotImplementedError (unwired feature), DivaHookError, DivaGuardTripped.
Feature parity
| Feature | Status |
|---|---|
run / stream / generate |
✅ |
| Client tools (inline) + toolsets | ✅ |
Permissions / can_use_tool |
✅ |
| Hooks + guards | ✅ |
Sub-agents (handoff) |
✅ |
Sessions + memory (server + MemoryStore/FileStore) |
✅ |
| Parallel, thinking levels, observability | ✅ |
| Stream reconnect (resumable) | ✅ |
| Session-key parity vs TS | ✅ (byte-identical) |
Skills (skill / skill_from_dir, prepend) |
✅ |
| MCP servers (stdio / http) | ✅ (diva-ai[mcp]) |
| Flow (slot-filling frames) | ✅ |
| Park/resume tool mode | roadmap (engine ISKARIOT_RUN_PARK_RESUME; the hosted gateway is inline) |
Testing
pip install -e ".[dev]"
pytest tests/test_unit.py # pure logic, no network
DIVA_GATEWAY_URL=ws://localhost:5002/gateway DIVA_API_KEY=sk-diva-... \
pytest tests/test_e2e_live.py # live E2E (skipped without env)
License
Apache-2.0. The SDK is open; the Diva engine and platform are proprietary.
Project details
Release history Release notifications | RSS feed
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 diva_ai-0.1.0a1.tar.gz.
File metadata
- Download URL: diva_ai-0.1.0a1.tar.gz
- Upload date:
- Size: 49.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a54f7bf1c1303f7579f30dd16958ec788dd2f5ace66276504cef6803e2375f4
|
|
| MD5 |
bd59758915ac16e17e85e55467593d0a
|
|
| BLAKE2b-256 |
aefca8861338a3cb1e543a475bc269e7eda22d8fa0f9562703a1842ebb19820d
|
File details
Details for the file diva_ai-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: diva_ai-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 50.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6fa579e7b4a2a32e4e112712637b815b4cf63b39d5ac2184478cb75852653d5
|
|
| MD5 |
68b2e7d5abe525a002f2334a56833ec6
|
|
| BLAKE2b-256 |
412f4de7aabeef9023343443b9131bac62dc644b140a9d48483a247479106a23
|