submilli
The Python SDK for submilli-server. It runs TypeScript programs under a Blueprint's policy, on behalf of a user, from any Python harness: an agent framework, a job queue, or a script.
pip install submilli
It needs Python 3.11 or later and submilli-server 0.4.1 or later. For LangChain and
deepagents, langchain-submilli
builds on it.
Quick start
import asyncio
from submilli import AsyncSubmilli
PROGRAM = """
export function main(): string {
return `${1 + 1}`;
}
"""
async def main() -> None:
async with AsyncSubmilli() as client:
session = await client.sessions.create("research", variables={"userId": "u_ada"})
result = await client.sessions.run(session.id, PROGRAM, idempotency_key="call-1")
print(result.result) # "2"
asyncio.run(main())
Submilli has the same API without await:
from submilli import Submilli
with Submilli() as client:
session = client.sessions.create("research", variables={"userId": "u_ada"})
print(client.sessions.run(session.id, PROGRAM).result)
The client reads the server's URL from $SUBMILLI_SERVER (default http://127.0.0.1:8128) and
its API token from $SUBMILLI_SERVER_TOKEN, or from base_url= and token=.
Sessions
A session binds a Blueprint to one user's variables and harness secrets. Programs in it run one after another and share its files.
session = await client.sessions.create(
"research",
variables={"userId": "u_ada"},
secrets={"GITHUB_TOKEN": token}, # the Blueprint's `harness` secrets
)
state = await client.sessions.execute(session.id, code, idempotency_key=call_id)
await client.sessions.rebind(session.id, {"GITHUB_TOKEN": new_token})
last = await client.sessions.last_run(session.id) # the full console, or None
await client.sessions.delete(session.id)
Results, not exceptions. A program that fails or is denied is a result:
result = await client.sessions.run(session.id, code)
if result.denied:
print(result.error.capability, result.error.source) # e.g. "http.get", "policy"
elif result.error:
print(result.error.kind, result.error.message) # e.g. "compile_error"
else:
print(result.result, result.console)
Idempotency. Pass an idempotency_key with every execute that may be retried. When the
same request reaches the server twice, the program runs once and the retry gets the recorded
result. idempotency_key(call_id) turns any id, such as a model's tool call id, into a key the
server accepts.
Your tools in programs
A session can declare the application's own tools. Programs import them as @tools, typed from
their schemas, and the server checks each call against the Blueprint's tools.call capability:
from submilli import PendingCall, ToolResult
QUERY_ORDERS = {
"name": "query_orders",
"description": "Orders of one customer.",
"input_schema": {"type": "object", "properties": {"customer": {"type": "string"}}, "required": ["customer"]},
"output_schema": {"type": "object", "properties": {"count": {"type": "number"}}, "required": ["count"]},
}
async def answer(call: PendingCall) -> ToolResult:
orders = await db.orders(call.args["customer"])
return ToolResult.success(call.call_id, {"count": len(orders)})
session = await client.sessions.create("shop", variables={"userId": "u_ada"}, tools=[QUERY_ORDERS])
result = await client.sessions.run(session.id, PROGRAM, idempotency_key="call-1", on_tool_call=answer)
import tools from "@tools";
function main(): string {
return `${tools.query_orders({ customer: "c_42" }).count} orders`;
}
- The loop. Each call stops the run on the server.
sessions.runhands the call toon_tool_call, sync or async, and resumes the run with the answer, until the program finishes. The time a run waits doesn't count against its time limit. - Answers.
ToolResult.success(call_id, value)returnsvalueto the program.ToolResult.failed(call_id, message)throwsErrorthere.ToolResult.denied(call_id, message)throwsPermissionDeniedError, reported withsource: "harness". - Pausing. If
on_tool_callraises, the exception propagates and the run keeps waiting on the server, up to the Blueprint'stool_call_timeout. That is how a harness pauses for a person's approval. Executing again with the sameidempotency_keyanswers the run's current state, andsessions.runcarries on from the call it waits on. - Declarations.
session.warningslists what programs won't see of the declared tools, such as a Blueprint that grants notools.call, or a tool whose result is typedunknown. The server doesn't resolve$ref, so pass Pydantic schemas throughsubmilli.schema.inline_refs. - Lower level.
runs.resume,runs.retrieve, andruns.completedrive a run yourself. - Restarts. A suspended run is lost if the server restarts, and resuming it raises
UnknownRunError.
Discovery
What a model needs to write programs for a Blueprint:
prompt = await client.blueprints.prompt("research") # tool descriptions for a model
await client.blueprints.search_packages("research", "github")
docs = await client.blueprints.package_docs("research", "@submilli/jina") # docs.found, docs.text
await client.blueprints.builtins("research")
await client.blueprints.builtin_docs("research", ["Map", "Temporal"])
sessions.search_packages and sessions.package_docs answer the same for one session, including
its @tools.
Errors
Every error is a SubmilliError:
| Error | When |
|---|---|
APIConnectionError, APITimeoutError |
The server couldn't be reached, or didn't answer in time |
AuthenticationError |
401: the token is missing or wrong |
UnknownSessionError |
The session ended, usually after its idle timeout. Open a new one |
SessionRequiresSecretsError |
The server asks for the session's harness secrets again. rebind them; .required names them |
IdempotencyError |
The server refused the key and ran nothing. .code says why |
UnknownRunError |
The server no longer holds a suspended run |
RunNotSuspendedError, UnknownCallError |
A resume that no longer applies. runs.complete handles these |
UnknownBlueprintError |
No Blueprint of that name |
BadRequestError, ForbiddenError, NotFoundError, ConflictError, ServiceUnavailableError, InternalServerError |
Other refusals, by status |
Each APIStatusError carries status_code, body, the body's code, and the execution_id
it concerns, when there is one.
Harness secrets are kept encrypted with the server's secret-store key, so sessions keep them
across restarts. A server whose key changed refuses such a session with a ConflictError
("cannot decrypt session secrets").
Retries and timeouts
A connection failure, or a 502, 503, or 504, is retried up to max_retries times (default 2),
with backoff, but only for requests that are safe to repeat: reads, deletes, rebinds, resumes,
and executes that carry an idempotency key. An execute without a key and sessions.create are
never retried. The default timeout is 330 seconds, because an execute may wait up to 300 seconds
for an earlier attempt with the same key.
client = AsyncSubmilli(timeout=60.0, max_retries=0, http_client=httpx.AsyncClient(...))
Development
This package lives in the submilli-python
workspace, next to langchain-submilli. See the repository's README for setup, checks, and
releases. In packages/submilli:
uv run pytest # unit tests, against a fake of the HTTP API
SUBMILLI_BIN=/path/to/bin uv run pytest tests/integration # starts its own submilli-server
uv run python scripts/generate_sync.py # after changing _async_client.py
License
Apache-2.0.
Metadata
Release files for submilli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| submilli-0.1.0.tar.gz | 29.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| submilli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.3 kB
Release files / submilli-0.1.0.tar.gz
| Download URL | submilli-0.1.0.tar.gz |
|---|---|
| Size | 29.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1637d1a64c82b80b5f2de9d8942336c3d4ae1bb5887dc9b591c39329cddcea4b
|
|
BLAKE2b-256 checksum How to use checksums |
b6f3ae545db96b40aa9d12b6af9505b3bdc80f82122b4fed7a50c38153762172
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 15, 2026.
Transparency logRelease files / submilli-0.1.0-py3-none-any.whl
| Download URL | submilli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ab52e1664c7c2c1733d765a57a1d82138e48ac3f694850f99ae8105d32a2df80
|
|
BLAKE2b-256 checksum How to use checksums |
2ad742440bf5e1ac55b0a433d9d9889561b622192b65cd67c987678fd7dfd0dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 15, 2026.
Transparency log