Skip to main content
Version: 1.0.0

SDK Python

Host packages and execution options​

Both SDKs accept a module path or a pkg:organization.package/identifier selector. Declare the module in the installed package's hosts. Installation verifies content; activation is an explicit application choice.

SettingJavaScriptPython
AttestationhumanApprovalHosthuman_approval_host
Agent decisionsagentDecisionProvideragent_decision_provider
Attestation timeout, mshumanApprovalAuthorityTimeoutMshuman_approval_authority_timeout_ms
Pure-function detailtracePureFunctionstrace_pure_functions

The timeout defaults to 5,000 ms, with a range of 1 to 60,000 ms. Detail defaults to full; calls keeps calls and governance with fewer internal events. See packages and traces.

The Python SDK calls the same governed CLI as the JavaScript SDK. Both guides follow the same path, with the same concepts and introductory examples.

Handler callback arguments​

JavaScript calls run({ request, context }); Python calls run(request, context). The declared input signature determines request.

Declared inputsJavaScript requestPython request
None{}{}
One: value as Text"Invoice 42""Invoice 42"
Two: left as Int, right as Int{ left: 2, right: 3 }{"left": 2, "right": 3}

With one input, read the value directly from request. Context contains execution metadata and helpers; business inputs are never siblings of it. Without an explicit input signature, the handler receives the arguments map.

Installation​

From your application root, choose the Python SDK:

touba sdk install python

Touba verifies its bundled wheel, copies it into vendor/touba-sdk/<hash>/, creates or reuses .venv, and installs touba-sdk offline. It checks both ToubaEngine and AsyncToubaEngine imports. Nothing is installed globally. Python 3.11+ is required, with uv or venv and pip.

Choose the package manager​

touba sdk install python --manager uv
touba sdk install python --manager pip

The default auto selects uv when the project has a uv.lock, otherwise pip. Install the selected manager first. With uv and a pyproject.toml, Touba records the local wheel as a dependency and updates uv.lock offline. A later uv sync therefore retains the SDK. Without a declared Python project, Touba prepares only the virtual environment.

Offline resolution of an existing uv project requires its other dependencies to be available in the uv cache. Touba installs only its wheel; you choose when to sync the rest of the project. For Poetry, PDM or another manager, use the verified archive returned by touba sdk path python with your manager.

To select the base interpreter for a new venv:

touba sdk install python --python python3.12

An existing .venv keeps its interpreter. Activate it before running your app:

source .venv/bin/activate
python -c "from touba_sdk import ToubaEngine, AsyncToubaEngine; print('SDK ready')"

On Windows use .venv\Scripts\Activate.ps1, or invoke .venv\Scripts\python.exe directly without activation. Touba must remain on PATH, or pass its absolute path as ToubaEngine(command=...).

Commit vendor/touba-sdk with your dependency declarations, and exclude .venv. In uv projects, commit uv.lock as well.

On Windows, the SDK recognizes the installed touba.cmd launcher and calls the bundled Node executable with the CLI entry point, without a command shell. Paths with spaces and arguments remain literal values. Custom .cmd or .bat scripts are refused: use an executable with separate arguments. This discovery does not verify publisher signatures.

Updates​

After updating Touba, explicitly update the project's SDK:

touba sdk install python

Touba retains the existing venv and verifies the new archive. Other projects' SDKs remain unchanged.

Imports​

from touba_sdk import ToubaEngine, ToubaEngineError, ToubaHandlerError
from touba_sdk.testing import assert_run_succeeded

The installation package is touba-sdk; its import module is touba_sdk. Test helpers are in its testing submodule.

Run a program​

program.tb
module sdk.engine
strategy Review
takes value as Text
gives Text
do
give value
run.py
from touba_sdk import ToubaEngine

engine = ToubaEngine(project_root=".")
run = engine.run(source="program.tb", inputs={"value": "Invoice 42"})
print(run.status, run.reason, run.result)

In an asynchronous application, use AsyncToubaEngine:

import asyncio
from touba_sdk import AsyncToubaEngine

async def main():
engine = AsyncToubaEngine(project_root=".")
run = await engine.run(source="program.tb", inputs={"value": "Invoice 42"})
print(run.status, run.reason, run.result)

asyncio.run(main())

RunOptions provides the full typed option list. Python uses snake_case: entry_ref, manifest, secrets, inputs, resource_bindings, supply_chain_lock, require_supply_chain_lock, effect_journal, agent_decision_provider, run_limits, max_provider_calls, human_approval_host, human_decisions, user_inputs, human_checkpoint_out, human_resume_checkpoint, parallel_checkpoint_out, parallel_resume_checkpoint, trigger_event, trigger_state, skill_packs, active_skill_pack_refs. Relative paths use project_root.

Compile a program​

compiled = engine.compile("program.tb")
run = engine.run(air_json=compiled.air_json, inputs={"value": "Invoice 42"})
# compiled.air_document is the parsed object; air_json also accepts this object.

This extends run.py. compile returns CompileResult with air_json (text) and air_document (object). Supply exactly one input to run: source, air (file), or air_json (text or dictionary). With AsyncToubaEngine, also await the call to compile.

Results and errors​

pendingInteractions (JavaScript) and pending_interactions (Python) expose all currently pending questions as {interaction_ref, prompt} objects, or []. The singular field identifies the first question for user_input_pending. Answer using userInputs / user_inputs keyed by occurrence, then resume with parallelResumeCheckpoint / parallel_resume_checkpoint. Private continuation state is not part of this public list.

RunResult exposes status, reason, result, messages, pending_interaction and trace. Pauses, human refusals and explicit stops remain controlled outcomes. Technical failures and runtime refusals raise ToubaEngineError with phase, code and safe details, without copying raw stderr.

engine.version() identifies the CLI; touba_sdk_metadata() identifies SDK builds by their content hash.

Store execution traces​

run = engine.run(source="program.tb", trace_store=".touba/traces.sqlite")

SQLite stores traces in .touba/traces.sqlite at the engine’s project root by default. The path above can be customized. Each segment remains available after the process exits; traceOut / trace_out also produces a JSON export. See trace retention for archival.

For a long trace, choose trace_mode="reference". Once the journal is finalized, the response contains the business result, trace_reference, and trace set to None. Read events in pages:

trace_store = ".touba/traces.sqlite"
run = engine.run(
source="program.tb", trace_store=trace_store, trace_mode="reference",
)
after = None
while True:
page = engine.read_trace(
trace_store=trace_store, reference=run.trace_reference,
after=after, limit=100,
)
for event in page["events"]:
print(event["kind"])
if page["done"]:
break
after = page["next"]

read_trace uses the read-only Core reader. limit accepts 1 to 1,000 events; event content in each page is capped at 1 MiB. Runtime errors carry their reference in error.details["trace_reference"]. References support reading; execution resumes from its checkpoint. Keep the reference and journal to resume reading after a restart. Start without a cursor to verify the full segment digest; subsequent pages check their events and order.

With AsyncToubaEngine, use await engine.run(...) and await engine.read_trace(...) with the same parameters.

In inline mode, trace contains the complete document. The 32 MiB transport limit applies to SDK responses, including business results and messages. A long trace can be stored and read in pages without increasing that limit. See the trace reference.

Write a handler​

After declaring a tool, select Python and generate its handler:

touba init --language python
touba sdk install python
touba prepare --mode write

prepare generates the .handler.py file, its contract tests and a binding to .venv. Preserve generated constants and the define_tool call; implement run(request, context) with def or async def. One input becomes request directly; zero or multiple inputs form a named dictionary.

define_tool handles JSON process input, create_tool_handler adapts business code and handle_tool_request validates the envelope. Unexpected exceptions are redacted; use ToubaHandlerError for a secret-free public diagnostic. Reserve stdout for the protocol, not logs.

Follow the Python handlers tutorial.

Effects and provenance​

Memory provenance uses an explicit envelope, separate from business data:

from touba_sdk import create_tool_result

return create_tool_result(record, memory_metadata=[
{"memory_ref": "memory:app/Records", "operation": "read"},
])

This excerpt belongs inside a handler; record is the business value. A memory_metadata key in an ordinary dictionary does not create an envelope. The Core checks tool, scope and permission correspondence.

create_effect_receipt and create_compensation_receipt produce correlated receipts. Use context helpers create_effect_receipt and create_compensation_receipt after the actual effect. A receipt grants no permissions and does not replace external service idempotency.

Testing​

from touba_sdk.testing import assert_run_succeeded, assert_trace_events

run = engine.run(source="program.tb", inputs={"value": "Invoice 42"})
assert_run_succeeded(run, "Invoice 42")

This extends run.py. touba_sdk.testing also provides create_scripted_decision_provider, assert_run_refused and assert_trace_events. A scripted provider proposes test decisions, never permissions. Generated contract tests check admission; add positive tests for business results and effects.

Resumes and parallelism​

Resume with the operation's checkpoint, the same effect journal and a decision for the pending occurrence. human_checkpoint_out and human_resume_checkpoint track approvals; parallel_checkpoint_out and parallel_resume_checkpoint track parallel groups. The application attests identity through human_approval_host, never a model-provided dictionary.

AsyncToubaEngine supports concurrent calls through asyncio.gather. This does not replace a parallel group declared in Touba, which retains its own limits and resume rules. See parallel processing and approvals.

timeout=30 bounds transport; cancel_event=threading.Event() cancels a synchronous call. With asyncio, cancel the task. No automatic retry occurs. POSIX terminates the process group; Windows terminates the CLI without an equivalent descendant guarantee. An effect may already have happened: inspect the journal before resuming.

Monetary budgets​

Import create_sqlite_provider_budget from touba_sdk.budget.sqlite. It shares the JavaScript SDK's SQLite format and refusal codes, with snake_case options and integer micro-units. Connecting model transports is a separate concern. See the provider cost guide and no-paid-call tutorial.

Additional capabilities​

The Python Engine uses a fresh process per run. Model adapters, conversation history and MCP have Python APIs. Persistent Engine transport across multiple runs remains specific to the JavaScript SDK.

Providers and history​

create_ai_sdk_decision_provider, create_open_ai_decision_provider and create_ollama_decision_provider follow the same decision contracts as their JavaScript counterparts. Call them with await provider(request). decision_interface="structured" is the default; "native" uses native function calls, still admitted by the Core before any effect.

from touba_sdk import create_open_ai_decision_provider
from touba_sdk.history.sqlite import create_sqlite_decision_history_store

store = create_sqlite_decision_history_store(filename="history.sqlite")
decide_agent_capability = create_open_ai_decision_provider(
model="YOUR_MODEL_ID",
decision_interface="native",
history={"store": store, "namespace": "support"},
max_output_tokens=512,
max_retries=0,
)

This configures a provider without making a model call. Select an available model and a host-managed key. Pass the .py module through agent_decision_provider or --agent-decision-provider. The CLI selects the project's .venv; TOUBA_HOST_PYTHON selects an explicit interpreter path. Export decide_agent_capability or decide from the module. An approval module exports attest_human_approval and initiator_ref. These modules are trusted: the model never supplies their paths.

Adapters reuse the Node backend already included with Touba over private stdio. The wheel installs no additional provider SDK. The generic provider accepts Python generate_text, generate_object and json_schema callbacks following the AI SDK contract; raw Python vendor SDK messages/results need explicit adaptation.

Without a json_schema callback, the JSON schema is passed directly to the Python generation function. HTTP adapters also accept fetch and count_fetch: each callback receives {url, method, headers, body} and returns {status, headers, body}, with a text body and string-to-string headers. These exchanges are bounded to 8 MiB and remain private to the host transport. Do not log their headers, which may contain provider credentials.

RAM is the default store. Import create_memory_decision_history_store from touba_sdk for an explicit store. History options include max_bytes, max_messages, max_retained_bytes, max_summary_bytes, summarize and on_event. Structural compaction is local; only the optional host summarize callback produces a summary. Invocation IDs and native messages follow the same rules as JS. SQLite persists records; RAM does not survive module shutdown. Close directly created providers and stores with close().

OpenAI context options include context_window_tokens, safety_margin_tokens and timeout_ms. Context size and monetary spending are separate controls. When both are enabled, token-count requests need their own explicit tariff. A broken channel is never replayed automatically.

See the Python provider tutorial and shared history rules.

MCP​

create_mcp_stdio_tool_bridge(mcp_tool_name=..., config=...) returns an async bridge(request, context) handler. Its touba-mcp-bridge-config-v1 format is identical to JavaScript. Python uses a bounded stdio subprocess directly, without another Node server inside the handler. Secret handles, time/size limits and refusals are preserved. Core admission still precedes tool execution.

Supply-chain locking follows static Python imports and package-manager lock files without executing handlers or auditing the installed venv. See supply chain. For a framework-based application, see Python orchestration.

Authority boundary​

Both SDKs call the Touba CLI. The Core checks types, permissions, secrets, scopes, budgets, agent selection, approvals and effects before dispatch, then validates the returned result. The SDK grants no additional authority and is not a second language implementation.

Pass manifests, secrets and resource bindings explicitly. The Engine does not automatically load a .env. Protocol data and references are identical regardless of the SDK.

Switch between SDKs​

Class names and concepts are shared. Functions and options use each language's usual casing. AIR, manifest and trace protocol keys remain unchanged.

JavaScriptPython
ToubaEngine, ToubaEngineError, ToubaHandlerErrorToubaEngine, ToubaEngineError, ToubaHandlerError
projectRoot, entryRef, pendingInteractionproject_root, entry_ref, pending_interaction
pendingInteractionspending_interactions
compile({ source }) → airJson, airDocumentcompile(source) → air_json, air_document
run({ airJson })run(air_json=...)
defineTool, createToolHandler, handleToolRequestdefine_tool, create_tool_handler, handle_tool_request
createToolResult, createEffectReceipt, createCompensationReceiptcreate_tool_result, create_effect_receipt, create_compensation_receipt
toubaSdkMetadatatouba_sdk_metadata
@touba/sdk/testingtouba_sdk.testing
createScriptedDecisionProvider, assertRunSucceeded, assertRunRefused, assertTraceEventscreate_scripted_decision_provider, assert_run_succeeded, assert_run_refused, assert_trace_events

JavaScript uses await; Python provides synchronous ToubaEngine and AsyncToubaEngine with await. Business functions receive run({ request, context }) in JavaScript and run(request, context) in Python. These are calling conventions, not different responsibilities.

Follow the same steps in the JavaScript SDK, Python SDK, JavaScript Engine / Python Engine and JavaScript handlers / Python handlers tutorials.

Typed outputs and provider diagnostics​

Date, format, length, min and max follow the same path in both SDKs: provider schema, response validation, then runtime validation. Invalid data is refused before use.

SettingJavaScriptPython
Schema encodingschemaEncodingschema_encoding
Automatic selection (default)"auto""auto"
Touba representation"canonical""canonical"
Flattened optionals"flat_optionals""flat_optionals"

Automatic mode adapts Optional fields for Anthropic models: it reduces unions in the transmitted schema, validates the response, then restores Touba's {present, value} encoding. Both validations stay active in structured and native interfaces. Malformed values are never converted to absence. Schema limits depend on the chosen model; qualify business types with that model.

In a host module, export initialModes (JS) or initial_modes (Python) to select summary or capabilities per agent reference:

export const initialModes = {
"agent:app.invoices/Reader": "capabilities",
};
initial_modes = {"agent:app.invoices/Reader": "capabilities"}

Without an explicit setting, an agent with no capabilities starts directly with its result contract. When routing by agent, the SDK reports the resolved model; the agent_decision_provider_model_resolved event identifies the provider and model selected for the call.

Token-count failures retain the HTTP status and a bounded diagnostic. Interrupted generation exposes finish_reason and, when supplied, incomplete_reason. Partial results are never executed.

A write handler's public error code is preserved; missing or invalid receipts are reported separately. See External effects.

Route several providers​

from models import providers

async def decide_agent_capability(request):
provider = providers.get(request["agent_ref"])
if provider is None:
raise ValueError("No provider configured for this agent")
return await provider(request)

providers contains adapters initialized once by the host module. The Python bridge propagates invocation context and costs through its private channel, including through this async wrapper. Keep the call in this context; the prompt contains the business request. The JavaScript router explicitly forwards its second execution argument.