Getting started
August 11, 2026 ยท View on GitHub
1. Install
Use Python 3.10 or newer from the repository root:
python -m pip install .
For an editable development install:
python -m pip install -e ".[dev]"
2. Declare a tool
from samsarix_core import samsarix_tool
@samsarix_tool(timeout=5, version="1")
def greet(name: str, excited: bool = False) -> str:
"""Build a greeting for one person."""
suffix = "!" if excited else "."
return f"Hello, {name}{suffix}"
Tool definitions are checked when Python evaluates the decorator. All parameters and the return value must use the supported JSON-compatible type subset.
Use TypedDict when each object field has a different type and should appear by
name in JSON Schema:
from typing import TypedDict
class Greeting(TypedDict):
message: str
excited: bool
@samsarix_tool
def structured_greeting(name: str, excited: bool = False) -> Greeting:
"""Build a structured greeting."""
suffix = "!" if excited else "."
return {"message": f"Hello, {name}{suffix}", "excited": excited}
Unlike dict[str, str | bool], this produces individual message and excited
properties, requires both keys, and rejects additional output keys.
3. Register, inspect, and invoke
import asyncio
import json
from samsarix_core import ToolRuntime
async def main() -> None:
async with ToolRuntime() as runtime:
spec = runtime.register(greet)
print(json.dumps(spec.to_dict(), indent=2))
result = await runtime.invoke("greet", {"name": "Ada", "excited": True})
if result.success:
print(result.output)
else:
print(result.error.to_dict() if result.error else "unknown error")
asyncio.run(main())
runtime.registry.schema_catalog() returns all registered contracts in stable
name order. Catalog data and results are detached JSON-compatible objects.
When one dependency has both a concurrency ceiling and a sustained request quota, configure the exact registration rather than slowing unrelated tools:
from samsarix_core import ToolCircuitBreaker, ToolRateLimit
async with ToolRuntime(max_concurrency=8) as quota_runtime:
quota_runtime.register(
greet,
max_concurrency=2,
rate_limit=ToolRateLimit(calls=30, period_seconds=60, burst=3),
circuit_breaker=ToolCircuitBreaker(
failure_threshold=3,
recovery_timeout_seconds=30,
),
)
result = await quota_runtime.invoke("greet", {"name": "Ada"})
An empty bucket returns the retryable rate_limited result without running the tool.
The bucket is local to this runtime process; see per-tool rate limits.
Three consecutive execution failures open the independent circuit for 30 seconds;
open calls return a retryable circuit_open result without running the tool. One
half-open probe tests recovery. See per-tool circuit breakers.
4. Invoke a bounded batch
from samsarix_core import ToolCall
calls = [ToolCall("greet", {"name": name}) for name in ("Ada", "Grace", "Linus")]
results = await runtime.invoke_many(calls)
Results keep input order. The runtime creates at most max_pending_invocations batch
workers so calls queued on one per-tool bulkhead do not block unrelated tools; global
and per-tool limits still bound execution, and sync functions share a thread pool of
max_concurrency workers.
5. Add a host policy when calls need central admission control
from samsarix_core import ToolPolicyContext, ToolPolicyDecision, ToolRuntime
async def read_only_policy(context: ToolPolicyContext) -> ToolPolicyDecision:
return (
ToolPolicyDecision.ALLOW
if context.spec.read_only
else ToolPolicyDecision.DENY
)
runtime = ToolRuntime(policy=read_only_policy)
The policy runs only for resolved calls with valid, resource-bounded arguments. Its
snapshot is detached from execution, but may contain sensitive input. A denial returns
a normal structured result without running tool code. See
examples/policy_gate.py for request-local scopes.
6. Add content-free lifecycle signals when needed
from collections import deque
from samsarix_core import ToolLifecycleEvent, ToolRuntime
recent_events: deque[ToolLifecycleEvent] = deque(maxlen=1_024)
runtime = ToolRuntime(lifecycle_handler=recent_events.append)
The synchronous callback receives paired start and terminal events but no arguments, outputs, or exception text. This bounded in-memory diagnostic buffer deliberately drops its oldest event at capacity. For durable telemetry, keep the callback non-blocking and hand off to a bounded application-owned processor with an explicit drop or backpressure policy. See the lifecycle observability guide for privacy, cancellation, and OpenTelemetry semantics.
7. Handle boundaries explicitly
- Use
async withor callawait runtime.aclose(). - Treat tool functions as trusted in-process code.
- Add I/O timeouts inside sync tools; the runtime cannot terminate Python threads.
- Leave exception exposure disabled outside a trusted developer environment.
- Check
result.statusorresult.success; caller cancellation still propagates. - Treat host policy as defense in depth, not authentication or a persistent human-approval system.