lithe
lithe is a reusable, storage-free ReAct agent kernel for Python: the engine, framework, and protocols that drive a tool-calling agent, plus optional capability bundles. The kernel knows nothing about how runs are stored — or even whether they are — so any application (a thesis-writing platform, a coding agent, a support bot) supplies its own tools, system prompt, and persistence, and reuses everything else: the loop, the event stream, undo, and delegation.
Highlights
- Zero-I/O core.
import litheperforms no file or network I/O. Every module in the root package is pure engine + protocol; anything that touches a disk or a socket ships as an optional bundle. - Storage-free by design. Persistence is a pair of host-supplied
interfaces (an
EventSinkto write, aMemoryProviderto replay). Don't want storage? Pass nothing. Want storage?JsonlRunStoreis the zero-database default. - An honest event stream. Streaming
assistant_deltaevents, ausageevent per model call (tokens, cost, context fullness), tool calls announced before they execute, and cancellation that closes the in-flight model stream instead of paying for a response nobody wants. - Undo built in. Every bundled write tool registers a reverter; the
UndoEngineis pure and storage-free like the rest of the core, andAgentHostexposes one-callundo_run. - Delegation without context bloat. Subagents run isolated, are tagged so undo still reverts their work, fan out in parallel with per-delegation budgets, and fold their spend back into the parent run's stats.
- Any OpenAI-compatible endpoint. Chat-completions and Responses
protocols behind one interface, with retries, jittered backoff,
Retry-After-aware 429 handling, and SSE streaming; usage is normalized to one shape across both.
Install
pip install lithe
# local development (editable install + test/lint deps):
pip install -e ".[dev]"
Requires Python 3.10+. The only runtime dependency is httpx.
Quickstart
A minimal host: sandboxed file tools, a JSONL store, and a run.
from lithe import AgentContext, LLMConfig, ToolRegistry
from lithe.bundles import AgentHost, JsonlRunStore
from lithe.bundles.workspace import Workspace, register_file_tools
registry = ToolRegistry()
# workspace_for(ctx) -> Workspace (a sandboxed root per user):
register_file_tools(registry, lambda ctx: Workspace(f"/data/{ctx.user_id}"))
store = JsonlRunStore("/var/lib/myapp/agent") # zero-DB default
host = AgentHost(registry,
LLMConfig(model=..., base_url=..., api_key=...),
store, build_system_prompt=my_prompt_builder)
ctx = AgentContext(run_id=rid, user_id=uid)
async for event in host.run(ctx, task, history=prior_turns):
... # forward run_start / step / tool_call / tool_result / done to your frontend
A host supplies only its tools, system prompt, and (optionally) a
store backend — the engine, persistence, run envelope, undo, and (via
subagents) delegation are all reused.
Architecture
Core — lithe (zero I/O, zero business logic)
| Module | What you get |
|---|---|
runtime |
AgentRuntime, the ReAct loop as an async event stream: streaming deltas; cancellation checked between deltas; run budgets (max_cost / max_total_tokens) ending with status="budget_exceeded" (cost computed from LLMConfig.pricing when the gateway reports none; parallel delegations count their in-flight siblings' live spend against the same ceiling); a repeat-call nudge for stuck models (reset by intervening writes, so read-after-write cycles are never flagged); a forced toolless wrap-up when the step budget is hit; mid-run context trimming that escalates from shrinking old tool outputs to dropping whole old exchanges under an omission note; a steering inbox (inbox= queue of user texts injected at step boundaries); error-isolated sinks; an injectable http_client for connection pooling (a client still on httpx's 5s default is upgraded to cfg.timeout instead of silently failing long calls). Every event is stamped with run_id + a monotonic seq; runs report wall-clock duration_s, tool results carry elapsed_ms, and streaming calls report time-to-first-token. |
llm |
OpenAI-compatible client: retry with jittered backoff, Retry-After-aware 429 handling, fail-fast on fatal 4xx, response-shape validation, and SSE streaming helpers. |
transports |
Chat-completions and Responses transports with usage normalized to prompt_tokens / completion_tokens / total_tokens; reasoning items replayed within the tool loop where the protocol supports it; image_url blocks mapped to input_image on the Responses path (unmappable blocks raise instead of vanishing). LLMConfig.extra_body / default_headers carry vendor fields and gateway headers on every call; LLMConfig.reasoning_effort maps to reasoning_effort (chat) / reasoning.effort (Responses), deep-merged with extra_body["reasoning"] siblings. |
tools |
ToolRegistry: register / unregister / dispatch, READ/WRITE/META category filtering, argument validation (required + top-level types + enums), pre-validation argument transforms (path canonicalization, defaults, redaction), per-tool timeouts, and pre-dispatch middleware for audit, quota, or human-in-the-loop confirmation of write tools. |
actions |
Action + UndoEngine: pure, storage-free undo; reverters may be sync or async. |
memory |
replay_messages / recap_text / window_with_recap / run_timeline + the MemoryProvider protocol; two-sided, window-safe replay reconciliation drops orphaned tool rows instead of sending API-rejected payloads. |
events |
EventSink with separate display + record channels, and SSE serialization (to_sse) that degrades non-JSON values instead of crashing. |
context / modes |
AgentContext plus host-defined modes via register_mode(name, categories); ctx.shared is per-run state shared by reference with subagent contexts — the vehicle for cross-agent coordination. |
Bundles — lithe.bundles (optional capabilities)
| Bundle | What you get |
|---|---|
host |
AgentHost: message assembly, the run_start/done envelope, error funneling, StoreSink, zero-config undo_run, and DictToolAdapter for wrapping dict-based tool systems. Forwards budgets, cancellation, and a shared HTTP client to every run. |
store |
RunStore / ConversationStore / BlobStore Protocols, plus JsonlRunStore — a zero-database JSONL store with content-addressed blob spillover for large action values and optional fsync crash durability. |
subagents |
SubagentEngine + delegate / delegate_parallel tools: isolated worker agents the orchestrator hands subtasks to — tagged for undo, bounded by max_parallel, per-delegation budgets, cancellation propagated from the parent run, live subagent_progress heartbeats. |
admin |
Admin-panel tools: tool_categories, list_tools_admin, list_tool_packages_admin, check_packages. |
patch |
apply_patch: line-oriented multi-file edits via the Codex *** Begin Patch envelope, a four-pass fuzzy matcher, and all-or-nothing application against an in-memory overlay. |
providers |
Vendor presets: known-good LLMConfig fields per OpenAI-compatible endpoint (openai / zai / deepseek / openrouter / qwen / moonshot) as plain data — get_preset / apply_preset merge a preset under host overrides (dict fields per key). Defaults, never truth: credentials and model always come from the caller, model-specific switches stay in notes. Each preset also names the family's document_format dialect (see the documents bundle). |
workspace |
Sandboxed file I/O: read_file / write_file / edit_file / list_files / search_files / glob_files with undo reverters, a stale-file guard that refuses to clobber externally changed files, and symlink-safe directory walks. |
todos |
A per-scope task list the agent plans against: TodoStore + atomic JSON persistence + update_todos / list_todos tools + a todos_block for the system prompt. |
images |
image_info (a stdlib-only header probe: dimensions, dpi, color mode) and analyze_image (one vision-model call, memoized per file hash + question) — image answers never enter the main conversation. |
documents |
document_info (a stdlib-only probe: PDF version, OOXML subtype, best-effort page count) and analyze_document (one model call with the document as a content block, memoized per file hash + question). The block dialect follows the endpoint family: document_format = inline-file (OpenRouter family, incl. self-built same-format routers) / files-api (strict OpenAI two-step upload) / none (probe only); LLMConfig.document_format carries it, provider presets default it, a 400 names the mismatch. |
sandbox |
run_code / run_file: Python execution with bubblewrap or passthrough backends, head+tail output truncation so tracebacks stay visible. WRITE-classified, so invisible in read-only modes. |
command |
run_command: opt-in native shell execution (bash/sh on Unix-like systems, PowerShell/cmd on Windows), bounded by timeout/output limits and started in the host-selected workspace. Commands run with the host user's permissions and are not sandboxed or undoable. |
download |
download_file: streaming HTTP(S) ingress into the workspace — SSRF-guarded (every redirect hop re-validated, all resolved addresses must be globally routable), size-capped, time-budgeted, landed atomically. |
skills |
Markdown skill libraries: flat SkillLibrary, package-aware SkillPackages with remote registry mirrors, and the load_skill tool. |
mcp |
MCPManager + MCPServerConfig: bridge external MCP servers (stdio or streamable-http) into the registry — tools/list pagination followed, a safe env allow-list for spawned servers (never all of os.environ), self-healing sessions. |
from lithe.bundles import MCPManager, MCPServerConfig
manager = MCPManager([MCPServerConfig(
name="zai", command=["npx", "-y", "@z_ai/mcp-server"],
env={"Z_AI_API_KEY": "...", "Z_AI_MODE": "ZHIPU"},
default_category="read")])
await manager.attach(registry) # registry gains zai__* tools
Safety defaults
Long-lived autonomous loops need guardrails on by default:
- Path sandboxing — workspace tools resolve inside the sandbox root;
directory walks never follow symlinks, so code executed by
run_codecannot plant a link to a host file and read it back. - SSRF guard —
download_fileaccepts only globally routable http/https addresses, re-validating every redirect hop (cloud metadata endpoints included in the rejection set). - Secret hygiene — spawned MCP servers get an env allow-list, never
the parent environment; URLs log as
scheme://hostso credentials in query strings never reach a log line. - Shell execution is explicit —
run_commandis an optional host bundle, not a default tool. When enabled, commands run with the host user's permissions; they are not sandboxed and their side effects cannot be undone. - Write serialization — READ tools run in parallel, every WRITE/META tool runs alone in model order: no write races.
- Run budgets —
max_cost/max_total_tokenscut a runaway run short with no further tool execution or model calls.
Storage model
The kernel stores nothing. A host provides:
- an
EventSink(write side) — persists message records however it likes (DB / file / nowhere); - a
MemoryProvider(read side, optional) — replays prior turns; - an
UndoEnginefed from wherever the host kept actions.
More
examples/minimal_host.py— a runnable, offline minimal host (tools → run → events → undo); runs in CI.examples/live_host.py— the same flow against a real OpenAI-compatible endpoint (setLITHE_API_KEY/LITHE_BASE_URL/LITHE_MODEL; no default endpoint, it never spends tokens by accident).CHANGELOG.md— what changed and when.
License
MIT
Metadata
Release files for lithe 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| lithe-0.1.8.tar.gz | 283.1 kB | With provenance | Details for lithe-0.1.8.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| lithe-0.1.8-py3-none-any.whl | Python 3 | none | any | Details for lithe-0.1.8-py3-none-any.whl |
Total release size: 470.3 kB
Release files / lithe-0.1.8.tar.gz
| Download URL | lithe-0.1.8.tar.gz |
|---|---|
| Size | 283.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
61d720bee41d41efee3c9293942951b146a395f46cce2faaee5b498efc8c4f02
|
|
BLAKE2b-256 checksum How to use checksums |
95a1339a6001473af7386f53ca09c70cd0bc39cf6593ff84ecdc8e81b57735fa
|
| 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 6, 2026.
Transparency logRelease files / lithe-0.1.8-py3-none-any.whl
| Download URL | lithe-0.1.8-py3-none-any.whl |
|---|---|
| Size | 187.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d8c5b2851ccc743f85e2376266155e370f35099b5429c4688c4cbb6cadbe02c7
|
|
BLAKE2b-256 checksum How to use checksums |
2abc6f64cdee08862b34dd9a243592b7bd38cda91ef8c5dcc6fbfdcf10b0e52e
|
| 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 6, 2026.
Transparency logSecurity for lithe 0.1.8
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
Both files in this release have been uploaded with provenance metadata.
Attestation for lithe-0.1.8.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 6, 2026.
Transparency logA 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.