A framework for building AI agents in Python
Project description
lemurian
lemurian is a framework for building AI agents in Python.
Architecture
| Layer | Components | Role |
|---|---|---|
| Orchestration | Swarm | Registers agents, creates handoff tool, manages handoff loop, tracks active agent |
| Execution | Runner | Mediates all transcript access: builds messages for provider, injects system prompt, appends responses and tool results, detects handoffs |
| Interface | Context, Tools, Provider | Tools use Context to access state/session/agent. Provider receives pre-built messages and schemas. |
| Data | Session, State | Session holds the transcript (ground truth). State holds typed application data. |
Tool — A @tool-decorated Python function exposed to an LLM. Schema is generated from the function signature. Receives a Context for accessing state, session, and agent.
Agent — Declarative Pydantic model bundling a system prompt, tools, model name, and provider. Agents don't run themselves — the Runner executes them.
Runner — The agent loop. Builds messages for the provider, dispatches tool calls, serializes results, detects handoffs. The only component that reads or writes the transcript.
Session — A single conversation transcript shared across all agents in a Swarm.
State — Typed Pydantic model for application data. Subclass it to add fields. Mutated in-place through context.state.
Context — Passed automatically to any tool declaring a context parameter. Holds references to session, state, and agent.
Swarm — Multi-agent orchestrator. Creates a dynamic handoff tool with an enum of available agents. Fresh-context handoffs: each agent sees only the handoff message onward.
Provider — Abstraction over LLM APIs. Implementations for OpenAI, OpenRouter, local vLLM, and Modal vLLM.
Defining Tools
Use the @tool decorator on any function. The schema is generated from the type hints and docstring:
from lemurian.tools import tool
@tool
def lookup_customer(email: str):
"""Find a customer by their email address."""
return {"customer_id": "CUST-123", "name": "Jane Doe"}
You can override the name and description:
@tool(name="search", description="Search the knowledge base")
def kb_search(query: str, limit: int = 10):
...
Async functions work the same way:
@tool
async def fetch_data(url: str):
"""Fetch data from a URL."""
...
To access state, session, or the current agent, add a context parameter. It is automatically excluded from the schema and injected at call time:
from lemurian.context import Context
@tool
def update_counter(context: Context, amount: int):
"""Increment the counter in state."""
context.state.counter += amount
return context.state.counter
Single Agent
import asyncio
from lemurian.tools import tool
from lemurian.agent import Agent
from lemurian.runner import Runner
from lemurian.session import Session
from lemurian.state import State
from lemurian.message import Message, MessageRole
from lemurian.provider import OpenAIProvider
@tool
def greet(name: str):
"""Greet someone by name."""
return f"Hello, {name}!"
agent = Agent(
name="greeter",
system_prompt="You are a friendly greeter. Use the greet tool when asked.",
tools=[greet],
model="gpt-4o-mini",
provider=OpenAIProvider(),
)
async def main():
session = Session(session_id="demo")
session.transcript.append(
Message(role=MessageRole.USER, content="Say hi to Alice")
)
result = await Runner().run(agent, session, State())
print(result.last_message.content)
asyncio.run(main())
Multi-Agent Swarm
import asyncio
from lemurian.tools import tool
from lemurian.agent import Agent
from lemurian.swarm import Swarm
from lemurian.state import State
from lemurian.provider import OpenAIProvider
@tool
def check_invoice(invoice_id: str):
"""Check the status of an invoice."""
return {"amount": 49.99, "status": "paid"}
provider = OpenAIProvider()
triage = Agent(
name="triage",
description="Routes customer requests to the right agent",
system_prompt="Route customer requests to the appropriate agent.",
model="gpt-4o-mini",
provider=provider,
)
billing = Agent(
name="billing",
description="Handles billing and invoice questions",
system_prompt="Help customers with billing inquiries.",
tools=[check_invoice],
model="gpt-4o-mini",
provider=provider,
)
async def main():
swarm = Swarm(agents=[triage, billing])
result = await swarm.run("I have a question about my invoice", agent="triage")
print(f"[{result.active_agent}] {result.last_message.content}")
asyncio.run(main())
The Swarm automatically creates a handoff tool for each agent with an enum of available targets. When triage calls handoff(agent_name="billing", message="..."), the Swarm switches context to the billing agent.
Model Providers
OpenAI / OpenRouter
from lemurian.provider import OpenAIProvider, OpenRouter
provider = OpenAIProvider() # uses OPENAI_API_KEY env var
provider = OpenRouter() # uses OPENROUTER_API_KEY env var
Local vLLM
Serve a model with vLLM:
uv run --extra local vllm serve "Qwen/Qwen3-8B" \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--reasoning-parser qwen3
from lemurian.provider import VLLMProvider
provider = VLLMProvider(url="localhost", port=8000)
Refer to the vLLM docs to pair the appropriate tool call parser with your model.
Modal vLLM (Remote/Serverless)
Deploy vLLM to Modal for serverless GPU inference:
uv sync --group modal
modal setup
modal deploy src/scripts/modal_vllm.py
from lemurian.provider import ModalVLLMProvider
provider = ModalVLLMProvider(
endpoint_url="https://your-workspace--lemurian-vllm-vllmserver-serve.modal.run"
)
Customize deployments with environment variables:
| Variable | Default | Description |
|---|---|---|
MODEL_ID |
Qwen/Qwen3-8B |
HuggingFace model ID |
GPU_TYPE |
A100 |
Modal GPU type (A10G, A100, H100) |
GPU_COUNT |
1 |
Number of GPUs for tensor parallelism |
MAX_MODEL_LEN |
8192 |
Maximum sequence length |
Pre-download model weights for faster cold starts:
modal run src/scripts/modal_vllm.py --download
Testing
uv run pytest
uv run pytest --cov=lemurian --cov-report=term-missing
Development
lemurian is work in progress for agent-based experimentation. Feel free to suggest issues or modifications.
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 lemurian-1.0.0.tar.gz.
File metadata
- Download URL: lemurian-1.0.0.tar.gz
- Upload date:
- Size: 213.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f64681ff80437c2b98e02dcb767d9f627ca27095a930b4184fd64f7706edcb7c
|
|
| MD5 |
8bb53857a7c23020143dc1b6c1377ddb
|
|
| BLAKE2b-256 |
331dfe22f97d3d42a20efd54055c6c08ef89bc61a17d3be41bc937a2049ac112
|
Provenance
The following attestation bundles were made for lemurian-1.0.0.tar.gz:
Publisher:
publish.yml on mmargenot/lemurian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lemurian-1.0.0.tar.gz -
Subject digest:
f64681ff80437c2b98e02dcb767d9f627ca27095a930b4184fd64f7706edcb7c - Sigstore transparency entry: 976050600
- Sigstore integration time:
-
Permalink:
mmargenot/lemurian@aa9fb727c0c9f5259decad41db74cc554dd533ad -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/mmargenot
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aa9fb727c0c9f5259decad41db74cc554dd533ad -
Trigger Event:
release
-
Statement type:
File details
Details for the file lemurian-1.0.0-py3-none-any.whl.
File metadata
- Download URL: lemurian-1.0.0-py3-none-any.whl
- Upload date:
- Size: 19.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11fb69abeb50dc96e6edec661f468d65ba1c8dbfcf70e4e6e6f3ddf99c6f63b4
|
|
| MD5 |
3999d35815e6d57525157fa7765eff73
|
|
| BLAKE2b-256 |
1317842d679a885ceaa35245479027abf2d593f4d0b5885be8eeba9b664d425e
|
Provenance
The following attestation bundles were made for lemurian-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on mmargenot/lemurian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lemurian-1.0.0-py3-none-any.whl -
Subject digest:
11fb69abeb50dc96e6edec661f468d65ba1c8dbfcf70e4e6e6f3ddf99c6f63b4 - Sigstore transparency entry: 976050607
- Sigstore integration time:
-
Permalink:
mmargenot/lemurian@aa9fb727c0c9f5259decad41db74cc554dd533ad -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/mmargenot
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aa9fb727c0c9f5259decad41db74cc554dd533ad -
Trigger Event:
release
-
Statement type: