Python-pure AI Agent + Workflow SDK. Zero external dependencies.
zeroagent is a minimalist SDK for building AI agents and workflows in Python,
with zero external dependencies. Everything runs on the Python standard library.
Why?
pip install langchainpulls 80+ transitive dependencies. For most use cases, that's unnecessary.- Understanding what actually happens inside an agent loop is impossible when everything is abstracted behind opaque classes.
- Constrained environments (Kaggle, Alpine containers, Raspberry Pi) can't afford that weight.
zeroagent exposes the same primitives β ReAct agent, tool registry, memory, native async,
workflow DAG β in code you can read in an afternoon.
Working today:
- One-shot CLI, interactive chat, token-by-token streaming
- Structured tool calling + full ReAct loop
- Conversational memory (Buffer, Window, Summary)
- Native async support (
arun(),AsyncLLMClient, parallel tool calls) - Workflow DAG (Node, State, Graph, conditional edges, parallel execution)
- Stable public SDK API
No dependencies to install for zeroagent itself:
# 1. Clone
git clone https://github.com/Tahsine/zeroagent.git
cd zeroagent
# 2. Run directly (zero pip install)
python -m zeroagent "Calculate 123 * 456"
# 3. Interactive chat mode
python -m zeroagent --chat
# 4. Token-by-token streaming
python -m zeroagent --chat --stream
# 5. Watch the ReAct loop (thought β action β observation)
python -m zeroagent "Calculate (15 + 27) * 3" --verboseCreate a .env file at the root:
LLM_BASE_URL=https://ollama.com/v1
LLM_MODEL=minimax-m3:cloud
LLM_API_KEY=your_api_keyzeroagent works with any OpenAI-compatible provider: OpenAI, Ollama (local or cloud),
Kaggle LLM Server, Anthropic, and more.
from zeroagent import Agent, tool, LLMClient
@tool(description="Search for information")
def search(query: str) -> str:
return f"Results for: {query}"
llm = LLMClient(base_url="https://api.openai.com/v1", api_key="sk-...")
agent = Agent(llm=llm, tools=[search])
# Sync
result = agent.run("What is the capital of Benin?")
print(result)
# Async
import asyncio
result = asyncio.run(agent.arun("What is the capital of Benin?"))
print(result)# run() β final answer (str), sync
answer = agent.run("question")
# run_full() β full RunResult, sync
result = agent.run_full("question")
print(result.answer) # str
print(result.stop_reason) # StopReason.FINAL_ANSWER | MAX_ITERATIONS | ERROR
print(result.iterations) # int
print(result.tool_calls_made) # int
print(result.thoughts) # list[str]
# arun() β final answer (str), async
answer = await agent.arun("question")
# arun_full() β full RunResult, async
result = await agent.arun_full("question")from zeroagent import BufferMemory, WindowMemory, SummaryMemory
# Full buffer β keeps the entire conversation
agent = Agent(llm=llm, memory=BufferMemory())
# Sliding window β keeps the last N messages
agent = Agent(llm=llm, memory=WindowMemory(k=10))
# Auto-summary β compresses old messages via LLM
agent = Agent(llm=llm, memory=SummaryMemory(llm=llm, max_messages=20, keep_recent=6))agent = Agent(
llm=llm,
tools=[search],
on_thought=lambda thought, i: print(f"[{i}] Thought: {thought}"),
on_action=lambda name, args, i: print(f"[{i}] Action: {name}({args})"),
on_observation=lambda name, obs, ok, i: print(f"[{i}] Obs: {obs}"),
on_final=lambda answer, reason: print(f"Final ({reason}): {answer}"),
)Hooks can be sync functions or async coroutines β arun() detects and awaits them automatically.
# Main entry points
from zeroagent import Agent, tool, LLMClient
# Async
from zeroagent import AsyncLLMClient, AsyncAgentLoop, AsyncExecutor
# LLM types
from zeroagent import Message, LLMResponse, ToolCall
# Registry
from zeroagent import ToolRegistry, ToolSchema
# Memory
from zeroagent import BufferMemory, WindowMemory, SummaryMemory
# Loop control and results
from zeroagent import RunResult, StopReason, RunConfig
# Advanced (hooks, introspection)
from zeroagent import ParsedAction, ActionType, ExecutionResult
# Workflow
from zeroagent import Graph, Node, State, GraphResultfrom zeroagent import Agent, LLMClient, tool
from zeroagent.workflow import Graph, Node, State
@tool(description="Add two numbers")
def add(a: int, b: int) -> int:
return a + b
llm = LLMClient(base_url="...", api_key="...")
agent = Agent(llm=llm, tools=[add])
# Pure functions as nodes
def prepare(state: State) -> State:
state["input"] = f"Calculate {state['a']} + {state['b']}"
return state
def format_output(state: State) -> State:
state["final"] = f"Result: {state['output']}"
return state
# Build the graph
graph = Graph(name="pipeline")
graph.add_node(Node("prepare", fn=prepare))
graph.add_node(Node("agent", fn=agent)) # zeroagent Agent as a node
graph.add_node(Node("format", fn=format_output))
graph.add_edge("prepare", "agent")
graph.add_edge("agent", "format")
graph.set_entry("prepare")
graph.set_finish("format")
# ASCII visualization
print(graph.visualize())
# Graph 'pipeline'
#
# βββββββββββ
# β prepare β (entry)
# βββββββββββ
# β
# βΌ
# βββββββββ
# β agent β
# βββββββββ
# β
# βΌ
# ββββββββββ
# β format β (finish)
# ββββββββββ
# Sync execution
result = graph.run(State({"a": 42, "b": 58}))
print(result.state["final"]) # "Result: 42 + 58 = 100"
print(result.executed) # ['prepare', 'agent', 'format']
# Async execution
result = await graph.arun(State({"a": 42, "b": 58}))def router(state: State) -> str:
return "success" if state.get("ok") else "fallback"
graph.add_conditional_edge("validate", router, {
"success": "process",
"fallback": "error_handler",
})Nodes without dependencies between them run automatically in parallel
via asyncio.gather() during arun():
# branch_a and branch_b run in parallel
graph.add_edge("start", "branch_a")
graph.add_edge("start", "branch_b")
graph.add_edge("branch_a", "merge")
graph.add_edge("branch_b", "merge")from zeroagent.workflow import State
# Basic
state = State({"query": "test", "result": ""})
state["result"] = "done"
state.merge({"step": 2, "ok": True})
# Subclassed with schema and default values
class PipelineState(State):
query: str
result: str = ""
retries: int = 0
state = PipelineState({"query": "test"})
# result and retries are injected automatically
errors = state.validate() # checks required fieldsfrom zeroagent.workflow import Node, State
# Pure function
node = Node("step", fn=my_function)
# With retries and timeout
node = Node("step", fn=my_function, retries=3, timeout=30.0)
# Native async
async def async_step(state: State) -> State:
await asyncio.sleep(0.1)
state["done"] = True
return state
node = Node("async_step", fn=async_step)
# zeroagent Agent (duck-typed: reads state["input"], writes state["output"])
node = Node("research", fn=my_agent)AsyncLLMClient implements pure async HTTP/1.1 via asyncio β zero external dependencies,
same API as LLMClient:
from zeroagent import AsyncLLMClient
from zeroagent.core.llm import Message
async def main():
client = AsyncLLMClient(
base_url="https://api.openai.com/v1",
model="gpt-4o",
api_key="sk-...",
)
# Completion
response = await client.acomplete([
Message(role="user", content="Hello!")
])
print(response.content)
# Streaming
async for chunk in client.astream(messages):
print(chunk, end="", flush=True)
# From an existing LLMClient
async_client = AsyncLLMClient.from_sync(sync_client)Multiple tool calls in the same ReAct iteration run in parallel
via asyncio.gather() inside AsyncAgentLoop.
| Flag | Default | Description |
|---|---|---|
question |
β | One-shot question (positional) |
--base-url |
$LLM_BASE_URL |
LLM provider URL |
--model |
$LLM_MODEL |
Model name |
--api-key |
$LLM_API_KEY |
API key |
--system-prompt |
default | Custom system prompt |
--max-tokens |
1024 |
Max tokens to generate |
--temperature |
0.3 |
Sampling temperature |
--max-iterations |
6 |
Max ReAct iterations |
--no-tools |
β | Disable default tools |
--verbose |
β | Show Thought/Action/Observation |
--quiet |
β | Minimal output (answer only) |
--fail-on-error |
β | Exit code 1 on failure |
--chat |
β | Interactive chat mode (REPL) |
--stream |
β | Token-by-token streaming (chat only) |
--env |
.env |
Custom path to .env file |
src/zeroagent/
βββ __init__.py # Package root β re-exports the full public API
βββ __main__.py # CLI β argparse, colors, one-shot/chat/stream modes
βββ sdk.py # Public entry point β all stable imports
β
βββ core/ # Foundations
β βββ llm.py # LLMClient β pure HTTP, OpenAI-compatible + Anthropic native
β βββ async_llm.py # AsyncLLMClient β pure async HTTP/1.1 (asyncio)
β βββ tools.py # @tool decorator + ToolRegistry (auto JSON Schema)
β βββ memory.py # BufferMemory + WindowMemory
β
βββ harness/ # Full ReAct loop
β βββ agent.py # Agent β public facade (run, arun, run_full, arun_full)
β βββ loop.py # AgentLoop sync β Thought β Action β Observation
β βββ async_loop.py # AsyncAgentLoop + AsyncExecutor (parallel tool calls)
β βββ parser.py # Parses LLM decisions (tool call, final answer, thought)
β βββ executor.py # Executes actions, captures observations
β βββ memory.py # SummaryMemory (auto-summary via LLM)
β
βββ workflow/ # Multi-node DAG
β βββ state.py # State β wrapped dict, merge, deep copy, validation
β βββ node.py # Node β fn(State)->State, sync/async, retries, Agent support
β βββ graph.py # Graph β DAG, edges, conditional, parallel, ASCII viz
β
βββ tests/ # 235 unit tests (stdlib unittest)
βββ test_llm.py
βββ test_tools.py
βββ test_harness.py
βββ test_memory.py
βββ test_async.py
βββ test_workflow.py
A CLI script strictly identical to zeroagent was written with LangChain to enable a side-by-side comparison:
pip install langchain langchain-openai
python langchain-sdk-test.py "Calculate 123 * 456"
python langchain-sdk-test.py --chat --verboseSame flags, same colors, same calculator tool, same .env.
Key differences:
- zeroagent: zero dependencies, readable and auditable code
- LangChain: 40+ packages, black-box internals, but native structured tool calling
- A production framework with checkpointing, observability, and distributed execution β use LangGraph
- A wrapper around a specific API β zeroagent is provider-agnostic
- A LangChain reimplementation β same primitives, different design choices
- Pure HTTP LLMClient (OpenAI-compatible + native Anthropic)
-
@tooldecorator + ToolRegistry (auto JSON Schema) - Full ReAct loop (Thought β Action β Observation)
- Conversational memory (Buffer, Window, Summary)
- CLI one-shot, chat, streaming
- Stable public SDK API (
from zeroagent import Agent, tool, LLMClient) - Native async (
arun(),AsyncLLMClient,AsyncAgentLoop, parallel tool calls) - Workflow DAG (Node, State, Graph, conditional edges, parallel execution)
- PyPI publication
Active development. Public updates on LinkedIn and YouTube.
Architecture decisions are documented in DESIGN.md.
MIT